@validated註解異常返回JSON值方式
目錄
- @validated註解異常返回ON值
- 使用@Valid註解,對其引數錯誤異常的統一處理
@validated註解異常返回JSON值
@ControllerAdvice public class ValidParamExceptionHandler { @ExceptionHandler(value = Exception.class) @ResponseBody public Map<String,Object> allExceptionHandler(Exception e){ Map<String,Object> map = new HashMap<>(2); if(e instanceof BindException) { BindException ex = (BindException)e; BindingResult bindingResult = ex.getBindingResult(); StringBuilder errMsg = new StringBuilder(bindingResult.getFieldErrors().size() * 16); errMsg.append("Invalid request:"); for (int i = 0 ; i < bindingResult.getFieldErrors().size() ; i++) { if(i > 0) { errMsg.append(","); } FieldError error = bindingResult.getFieldErrors().get(i); errMsg.append(error.getField()+":"+error.getDefaultMessage()); } map.put("errcode",500); map.put("errmsg",errMsg.toString()); } else { map.put("errcode",e.getMessage()); } return map; }
(1)這裡@ControllerAdvice註解標註,@ControllerAdvice是@Controller的增強版,一般與@ExceptionHandler搭配使用。
如果標註@Controller,異常處理只會在當前controller類中的方法起作用,但是使用@ControllerAdvice,則全域性有效。
(2)@ExceptionHandler註解裡面填寫想要捕獲的異常類class物件
使用@Valid註解,對其引數錯誤異常的統一處理
在我們使用springboot作為微服務框架進行敏捷開發的時候,為了保證傳遞資料的安全性,需要對傳遞的資料進行校驗,但是在以往的開發中,開發人員花費大量的時間在繁瑣的if else 等判斷語句來對引數進行校驗,這種方式不但降低了我們的開發速度,而且寫出來的程式碼中帶有很多冗餘程式碼,使得編寫的程式碼不夠優雅,為了將引數的驗證邏輯和程式碼的業務邏輯進行解耦,給我們提供了@Valid註解,用來幫助我們進行引數的校驗,實現了將業務邏輯和引數校驗邏輯在一定程度上的解耦,增加了程式碼的簡潔性和可讀性。
springboot中自帶了spring validation引數校驗框架,其使用上和@valid差不多,在這裡就不累述了,本文主要講解@valid的使用對其引數校驗失敗後的錯誤一樣的統一處理。
首先,簡介對微服務開發中異常的統一處理,spring中的@RestControllerAdvice註解可以獲取帶有@controller註解類的異常,通過@ExceptionHandler(MyException.class)註解來共同完成對異常進行處理。示例如下:
/** * 通用異常攔截處理類(通過切面的方式預設攔截所有的controller異常) */ @Slf4j @RestControllerAdvice public class CommonExceptionHandler { /** * 對執行時異常進行統一異常管理方法 * @param e * @return */ @ExceptionHandler(FlyException.class) // FlyException類繼承於RuntimeException public ResponseEntity<Map<String,Object>> handlerException(FlyException e) { Map<String,Object> result = new HashMap<>(1); result.put("message",e.getMessage()); return ResponseEntity.status(e.getCode()).body(result); }
通過註解@RestControllerAdvice和註解@ExceptionHandler的聯合使用來實現對異常的統一處理,然後在前端以友好的方式顯示。
使用@Valid註解的示例如下:
@PostMapping public ResponseEntity save(@Valid BrandCreateRequestDto dto,BindingResult bindingResult) { // 判斷是否含有校驗不匹配的引數錯誤 if (bindingResult.hasErrors()) { // 獲取所有欄位引數不匹配的引數集合 List<FieldError> fieldErrorList = bindingResult.getFieldErrors(); Map<String,Object> result = new HashMap<>(fieldErrorList.size()); fieldErrorList.forEach(error -> { // 將錯誤引數名稱和引數錯誤原因存於map集合中 result.put(error.getField(),error.getDefaultMessage()); }); return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(result); } brandService.save(dto); return ResponseEntity.status(HttpStatus.CREATED.value()).build(); }
@Valid註解確實將我們原來的引數校驗的問題進行了簡化,但是,如果我們有多個handler需要處理,那我們豈不是每次都要寫這樣的冗餘程式碼。通過檢視@valid的實現機制(這裡就不描述了),當引數校驗失敗的時候,會丟擲MethodArgumentNotValidException異常(當用{@code @Valid}註釋的引數在驗證失敗時,將引發該異常):
/** * Exception to be thrown when validation on an argument annotated with {@code @Valid} fails. * * @author Rossen Stoyanchev * @sin客棧ce 3.1 */ @SuppressWarnings("serial") public class MethodArgumentNotValidException extends Exception { private final MethodParameter parameter; private final BindingResult bindingResult; /** * Constructor for {@link MethodArgumCfAQwAentNotValidException}. * @param parameter the parameter that failed validation * @param bindingResult the results of the validation */ public MethodArgumentNotValidException(MethodParameter parameter,BindingResult bindingResult) { this.parameter = parameter; this.bindingResult = bindingResult; }
按照我們的預想,我們只需要在原來定義的統一異常處理類中,捕獲MethodArgumentNotValidException異常,然後對其錯誤資訊進行分析和處理即可實現通用,程式碼如下:
/** * 對方法引數校驗異常處理方法 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String,Object>> handlerNotValidException(MethodArgumentNotValidException exception) { log.debug("begin resolve argument exception"); BindingResult result = exception.getBindingResult(); Map<String,Object> maps; if (result.hasErrors()) { List<FieldError> fieldErrors = result.getFieldErrors(); maps = new HashMap<>(fieldErrors.size()); fieldErrors.forEach(error -> { maps.put(error.getField(),error.getDefaultMessage()); }); } else { maps = Collections.EMPTY_MAP; } return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps); }
但是經過測試,會發現對該異常進行捕獲然後處理是沒有效CfAQwA果的,這可能是我,也是大家遇到的問題之一,經過對@Valid的執行過程的原始碼進行分析,資料傳遞到spring中的執行過程大致為:前端通過http協議將資料傳遞到spring,spring通過HttpMessageConverter類將流資料轉換成Map型別,然後通過ModelAttributeMethodProcessor類對引數進行繫結到方法物件中,並對帶有@Valid或@Validated註解的引數進行引數校驗,對引數進行處理和校驗的方法為ModelAttributeMethodProcessor.resolveArgument(...),部分原始碼如下所示:
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver,HandlerMethodReturnValueHandler { ... public final Object resolveArgument(MethodParameter parameter,@Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest,@Nullable WebDataBinderFactory binderFactory) throws Exception { ... Object attribute = null; BindingResult bindingResult = null; if (mavContainer.containsAttribute(name)) { attribute = mavContainer.getModel().get(name); } else { // Create attribute instance try { attribute = createAttribute(name,parameter,binderFactory,webRequest); } catch (BindException ex) { if (isBindExceptionRequired(parameter)) { // No BindingResult parameter -> fail with BindException throw ex; } // Otherwise,expose null/empty value and associated BindingResult if (parameter.getParameterType() == Optional.class) { attribute = Optional.empty(); } bindingResult = ex.getBindingResult(); } } //進行引數繫結和校驗 if (bindingResult == null) { // 對屬性物件的繫結和資料校驗; // 使用構造器繫結屬性失敗時跳過. WebDataBinder binder = binderFactory.createBinder(webRequest,attribute,name); if (binder.getTarget() != null) { if (!mavContainer.isBindingDisabled(name)) { bindRequestParameters(binder,webRequest); } // 對繫結引數進行校驗,校驗失敗,將其結果資訊賦予bindingResult物件 validateIfApplicable(binder,parameter); // 如果獲取引數繫結的結果中包含錯誤的資訊則丟擲異常 if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder,parameter)) { throw new BindException(binder.getBindingResult()); } } // Value type adaptation,also covering java.util.Optional if (!parameter.getParameterType().isInstance(attribute)) { attribute = binder.convertIfNecessary(binder.getTarget(),parameter.getParameterType(),parameter); } bindingResult = binder.getBindingResult(); } // Add resolved attribute and BindingResult at the end of the model Map<String,Object> bindingResultModel = bindingResult.getModel(); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); return attribute; }
通過檢視原始碼,當BindingResult中存在錯誤資訊時,會丟擲BindException異常,檢視BindException原始碼如下:
/** * Thrown when binding errors are considered fatal. Implements the * {@link BindingResult} interface (and its super-interface {@link Errors}) * to allow for the direct analysis of binding errors. * * <p>As of Spring 2.0,this is a special-purpose class. Normally,* application code will work with the {@link BindingResult} interface,* or with a {@link DataBinder} that in turn exposes a BindingResult via * {@link org.springframework.validation.DataBinder#getBindingResult()}. * * @author Rod Johnson * @author Juergen Hoeller * @author Rob Harrop * @see BindingResult * @see DataBinder#getBindingResult() * @see DataBinder#close() */ @SuppressWarnings("serial") public class BindException extends Exception implements BindingResult { private final BindingResult bindingResult; /** * Create a new BindException instance for a BindingResult. * @param bindingResult the BindingResult instance to wrap */ public BindException(BindingResult bindingResult) { Assert.notNull(bindingResult,"BindingResult must not be null"); this.bindingResult = bindingResult; }
我們發現BindException實現了BindingResult介面(BindResult是繫結結果的通用介面, BindResult繼承於Errors介面),所以該異常類擁有BindingResult所有的相關資訊,因此我們可以通過捕獲該異常類,對其錯誤結果進行分析和處理。程式碼如下:
/** * 對方法引數校驗異常處理方法 */ @ExceptionHandler(BindException.class) public ResponseEntity<Map<String,Object>> handlerNotValidException(BindException exception) { log.debug("begin resolve argument exception"); BindingResult result = exception.getBindingResult(); Map<String,error.getDefaultMessage()); }); } else { maps = Collections.EMPTY_MAP; } return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps); }
這樣,我們對是content-type型別為form(表單)型別的請求的引數校驗的異常處理就解決了,對於MethodArgumentNotValidException異常不起作用的原因主要是因為跟請求發起的資料格式(content-type)有關係,對於不同的傳輸資料的格式spring採用不同的HttpMessageConverter(http引數轉換器)來進行處理.以下是對HttpMessageConverter進行簡介:
HTTP 請求和響應的傳輸是位元組流,意味著瀏覽器和伺服器通過位元組流進行通訊。但是,使用 Spring,controller 類中的方法返回純 String 型別或其他 Java 內建物件。如何將物件轉換成位元組流進行傳輸?
在報文到達SpringMVC和從SpringMVC出去,都存在一個位元組流到java物件的轉換問題。在SpringMVhttp://www.cppcns.comC中,它是由HttpMessageConverter來處理的。
當請求報文來到java中,它會被封裝成為一個ServletInputStream的輸入流,供我們讀取報文。響應報文則是通過一個ServletOutputStream的輸出流,來輸出響應報文。http請求與相應的處理過程如下:
針對不同的資料格式,springmvc會採用不同的訊息轉換器進行處理,以下是springmvc的內建訊息轉換器:
由此我們可以看出,當使用json作為傳輸格式時,springmvc會採用MappingJacksonHttpMessageConverter訊息轉換器, 而且底層在對引數進行校驗錯誤時,丟擲的是MethodArgumentNotValidException異常,因此我們需要對BindException和MethodArgumentNotValidException進行統一異常管理,最終程式碼演示如下所示:
/** * 對方法引數校驗異常處理方法(僅對於表單提交有效,對於以json格式提交將會失效) * 如果是表單型別的提交,則spring會採用表單資料的處理類進行處理(進行引數校驗錯誤時會丟擲BindException異常) */ @ExceptionHandler(BindException.class) public ResponseEntity<Map<String,Object>> handlerBindException(BindException exception) { return handlerNotValidException(exception); } /** * 對方法引數校驗異常處理方法(前端提交的方式為json格式出現異常時會被該異常類處理) * json格式提交時,spring會採用json資料的資料轉換器進行處理(進行引數校驗時錯誤是丟擲MethodArgumentNotValidException異常) */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String,Object>> handlerArgumentNotValidException(MethodArgumentNotValidException exception) { return handlerNotValidException(exception); } public ResponseEntity<Map<String,Object>> handlerNotValidException(Exception e) { log.debug("begin resolve argument exception"); BindingResult result; if (e instanceof BindException) { BindException exception = (BindException) e; result = exception.getBindingResult(); } else { MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e; result = exception.getBindingResult(); } Map<String,Object> maps; if (result.hasErrors()) { List<FieldError> fieldErrors = result.getFieldErrors(); maps = new HashMap<>(fieldErrors.size()); fieldErrors.forEach(error -> { maps.put(error.getField(),error.getDefaultMessage()); }); } else { maps = Collections.EMPTY_MAP; } return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps); }
這樣就完美解決了我們對引數校驗異常的統一處理。
在這裡我僅僅是針對引數校驗的異常進行了統一處理,也就是返回給前端的響應碼是400(引數格式錯誤),對於自定義異常或者其他的異常都可以採用這種方式來對異常進行統一處理。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支援我們。