@validated注解異常返回JSON值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@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" , 500 ); map.put( "errmsg" , e.getMessage()); } return map; } |
(1)這里@ControllerAdvice注解標(biāo)注,@ControllerAdvice是@Controller的增強(qiáng)版,一般與@ExceptionHandler搭配使用。
如果標(biāo)注@Controller,異常處理只會(huì)在當(dāng)前controller類中的方法起作用,但是使用@ControllerAdvice,則全局有效。
(2)@ExceptionHandler注解里面填寫想要捕獲的異常類class對象
使用@Valid注解,對其參數(shù)錯(cuò)誤異常的統(tǒng)一處理
在我們使用springboot作為微服務(wù)框架進(jìn)行敏捷開發(fā)的時(shí)候,為了保證傳遞數(shù)據(jù)的安全性,需要對傳遞的數(shù)據(jù)進(jìn)行校驗(yàn),但是在以往的開發(fā)中,開發(fā)人員花費(fèi)大量的時(shí)間在繁瑣的if else 等判斷語句來對參數(shù)進(jìn)行校驗(yàn),這種方式不但降低了我們的開發(fā)速度,而且寫出來的代碼中帶有很多冗余代碼,使得編寫的代碼不夠優(yōu)雅,為了將參數(shù)的驗(yàn)證邏輯和代碼的業(yè)務(wù)邏輯進(jìn)行解耦,Java給我們提供了@Valid注解,用來幫助我們進(jìn)行參數(shù)的校驗(yàn),實(shí)現(xiàn)了將業(yè)務(wù)邏輯和參數(shù)校驗(yàn)邏輯在一定程度上的解耦,增加了代碼的簡潔性和可讀性。
springboot中自帶了spring validation參數(shù)校驗(yàn)框架,其使用上和@valid差不多,在這里就不累述了,本文主要講解@valid的使用對其參數(shù)校驗(yàn)失敗后的錯(cuò)誤一樣的統(tǒng)一處理。
首先,簡介對微服務(wù)開發(fā)中異常的統(tǒng)一處理,spring中的@RestControllerAdvice注解可以獲取帶有@controller注解類的異常,通過@ExceptionHandler(MyException.class)注解來共同完成對異常進(jìn)行處理。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/** * 通用異常攔截處理類(通過切面的方式默認(rèn)攔截所有的controller異常) */ @Slf4j @RestControllerAdvice public class CommonExceptionHandler { /** * 對運(yùn)行時(shí)異常進(jìn)行統(tǒng)一異常管理方法 * @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的聯(lián)合使用來實(shí)現(xiàn)對異常的統(tǒng)一處理,然后在前端以友好的方式顯示。
使用@Valid注解的示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@PostMapping public ResponseEntity save( @Valid BrandCreateRequestDto dto, BindingResult bindingResult) { // 判斷是否含有校驗(yàn)不匹配的參數(shù)錯(cuò)誤 if (bindingResult.hasErrors()) { // 獲取所有字段參數(shù)不匹配的參數(shù)集合 List<FieldError> fieldErrorList = bindingResult.getFieldErrors(); Map<String, Object> result = new HashMap<>(fieldErrorList.size()); fieldErrorList.forEach(error -> { // 將錯(cuò)誤參數(shù)名稱和參數(shù)錯(cuò)誤原因存于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注解確實(shí)將我們原來的參數(shù)校驗(yàn)的問題進(jìn)行了簡化,但是,如果我們有多個(gè)handler需要處理,那我們豈不是每次都要寫這樣的冗余代碼。通過查看@valid的實(shí)現(xiàn)機(jī)制(這里就不描述了),當(dāng)參數(shù)校驗(yàn)失敗的時(shí)候,會(huì)拋出MethodArgumentNotValidException異常(當(dāng)用{@code @Valid}注釋的參數(shù)在驗(yàn)證失敗時(shí),將引發(fā)該異常):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/** * Exception to be thrown when validation on an argument annotated with {@code @Valid} fails. * * @author Rossen Stoyanchev * @since 3.1 */ @SuppressWarnings ( "serial" ) public class MethodArgumentNotValidException extends Exception { private final MethodParameter parameter; private final BindingResult bindingResult; /** * Constructor for {@link MethodArgumentNotValidException}. * @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; } |
按照我們的預(yù)想,我們只需要在原來定義的統(tǒng)一異常處理類中,捕獲MethodArgumentNotValidException異常,然后對其錯(cuò)誤信息進(jìn)行分析和處理即可實(shí)現(xiàn)通用,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/** * 對方法參數(shù)校驗(yàn)異常處理方法 */ @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); } |
但是經(jīng)過測試,會(huì)發(fā)現(xiàn)對該異常進(jìn)行捕獲然后處理是沒有效果的,這可能是我,也是大家遇到的問題之一,經(jīng)過對@Valid的執(zhí)行過程的源碼進(jìn)行分析,數(shù)據(jù)傳遞到spring中的執(zhí)行過程大致為:前端通過http協(xié)議將數(shù)據(jù)傳遞到spring,spring通過HttpMessageConverter類將流數(shù)據(jù)轉(zhuǎn)換成Map類型,然后通過ModelAttributeMethodProcessor類對參數(shù)進(jìn)行綁定到方法對象中,并對帶有@Valid或@Validated注解的參數(shù)進(jìn)行參數(shù)校驗(yàn),對參數(shù)進(jìn)行處理和校驗(yàn)的方法為ModelAttributeMethodProcessor.resolveArgument(...),部分源代碼如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
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(); } } //進(jìn)行參數(shù)綁定和校驗(yàn) if (bindingResult == null ) { // 對屬性對象的綁定和數(shù)據(jù)校驗(yàn); // 使用構(gòu)造器綁定屬性失敗時(shí)跳過. WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null ) { if (!mavContainer.isBindingDisabled(name)) { bindRequestParameters(binder, webRequest); } // 對綁定參數(shù)進(jìn)行校驗(yàn),校驗(yàn)失敗,將其結(jié)果信息賦予bindingResult對象 validateIfApplicable(binder, parameter); // 如果獲取參數(shù)綁定的結(jié)果中包含錯(cuò)誤的信息則拋出異常 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; } |
通過查看源碼,當(dāng)BindingResult中存在錯(cuò)誤信息時(shí),會(huì)拋出BindException異常,查看BindException源代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/** * 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; } |
我們發(fā)現(xiàn)BindException實(shí)現(xiàn)了BindingResult接口(BindResult是綁定結(jié)果的通用接口, BindResult繼承于Errors接口),所以該異常類擁有BindingResult所有的相關(guān)信息,因此我們可以通過捕獲該異常類,對其錯(cuò)誤結(jié)果進(jìn)行分析和處理。代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/** * 對方法參數(shù)校驗(yàn)異常處理方法 */ @ExceptionHandler (BindException. class ) public ResponseEntity<Map<String, Object>> handlerNotValidException(BindException 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); } |
這樣,我們對是content-type類型為form(表單)類型的請求的參數(shù)校驗(yàn)的異常處理就解決了,對于MethodArgumentNotValidException異常不起作用的原因主要是因?yàn)楦埱蟀l(fā)起的數(shù)據(jù)格式(content-type)有關(guān)系,對于不同的傳輸數(shù)據(jù)的格式spring采用不同的HttpMessageConverter(http參數(shù)轉(zhuǎn)換器)來進(jìn)行處理.以下是對HttpMessageConverter進(jìn)行簡介:
HTTP 請求和響應(yīng)的傳輸是字節(jié)流,意味著瀏覽器和服務(wù)器通過字節(jié)流進(jìn)行通信。但是,使用 Spring,controller 類中的方法返回純 String 類型或其他 Java 內(nèi)建對象。如何將對象轉(zhuǎn)換成字節(jié)流進(jìn)行傳輸?
在報(bào)文到達(dá)SpringMVC和從SpringMVC出去,都存在一個(gè)字節(jié)流到j(luò)ava對象的轉(zhuǎn)換問題。在SpringMVC中,它是由HttpMessageConverter來處理的。
當(dāng)請求報(bào)文來到j(luò)ava中,它會(huì)被封裝成為一個(gè)ServletInputStream的輸入流,供我們讀取報(bào)文。響應(yīng)報(bào)文則是通過一個(gè)ServletOutputStream的輸出流,來輸出響應(yīng)報(bào)文。http請求與相應(yīng)的處理過程如下:
針對不同的數(shù)據(jù)格式,springmvc會(huì)采用不同的消息轉(zhuǎn)換器進(jìn)行處理,以下是springmvc的內(nèi)置消息轉(zhuǎn)換器:
由此我們可以看出,當(dāng)使用json作為傳輸格式時(shí),springmvc會(huì)采用MappingJacksonHttpMessageConverter消息轉(zhuǎn)換器, 而且底層在對參數(shù)進(jìn)行校驗(yàn)錯(cuò)誤時(shí),拋出的是MethodArgumentNotValidException異常,因此我們需要對BindException和MethodArgumentNotValidException進(jìn)行統(tǒng)一異常管理,最終代碼演示如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
/** * 對方法參數(shù)校驗(yàn)異常處理方法(僅對于表單提交有效,對于以json格式提交將會(huì)失效) * 如果是表單類型的提交,則spring會(huì)采用表單數(shù)據(jù)的處理類進(jìn)行處理(進(jìn)行參數(shù)校驗(yàn)錯(cuò)誤時(shí)會(huì)拋出BindException異常) */ @ExceptionHandler (BindException. class ) public ResponseEntity<Map<String, Object>> handlerBindException(BindException exception) { return handlerNotValidException(exception); } /** * 對方法參數(shù)校驗(yàn)異常處理方法(前端提交的方式為json格式出現(xiàn)異常時(shí)會(huì)被該異常類處理) * json格式提交時(shí),spring會(huì)采用json數(shù)據(jù)的數(shù)據(jù)轉(zhuǎn)換器進(jìn)行處理(進(jìn)行參數(shù)校驗(yàn)時(shí)錯(cuò)誤是拋出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); } |
這樣就完美解決了我們對參數(shù)校驗(yàn)異常的統(tǒng)一處理。
在這里我僅僅是針對參數(shù)校驗(yàn)的異常進(jìn)行了統(tǒng)一處理,也就是返回給前端的響應(yīng)碼是400(參數(shù)格式錯(cuò)誤),對于自定義異常或者其他的異常都可以采用這種方式來對異常進(jìn)行統(tǒng)一處理。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持服務(wù)器之家。
原文鏈接:https://blog.csdn.net/syilt/article/details/91388947