1 前言
Spring 的验证框架为我们提供了强大的验证功能,我们不但要会使用它,更要知道它工作的原理,这一文将简要点出 验证的基础基础流程,包括
- spring 如果确定入参需要参与验证
- spring 如何决定是抛出各种验证错误,还是将错误信息传递给开发人员
- spring 如何为表单验证与 JSON 请求体验证产生的不同类型的错误
- spring 如何调用我们编写的自定义全局错误处理器
最后改写默认的全局错误处理器以尽可能覆盖各种错误
2 从表单验证方式说起
spring boot 提供了我们多种验证方式, 通常我们遵循 spring mvc 提供的思想,在控制层写下如下代码:
1 @PostMapping("register") 2 public Object register(@Validated RegisterForm form, BindiningResult result, HttpServletResponse response) { 3 if (result.hasErrors()) { 4 Map<String, String> map = new HashMap<>(4); 5 result.getFieldErrors().forEach(e -> map.put(e.getObjectName(), e.getDefaultMessage())); 6 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 7 return map; 8 } 9 return userService.register(form) ? "success" : "fail"; 10 }
在实际的开发中如果采用这种方式,太多重复性劳动的方式会使得开发变得枯燥且不好维护。
2.1 抛弃 BindiningResult
让我们抛弃 BindingResult,改写方法成下方这一简单的形式,然后提交一个表单
1 @PostMapping("register") 2 public Object register(@Validated RegisterForm form) { 3 return userService.register(form) ? "success" : "fail"; 4 }
然后观察其抛出的错误:
2.2 RestControllerAdvice 捕捉 BindException 错误
上边的返回是不可读的,不可能直接交付给前端,下边是一个适用于 RestController 的全局错误处理器,没有什么特别的
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 @RestControllerAdvice(annotations = RestController.class)
6 public class ExceptionResolver { 7 8 @ExceptionHandler(BindException.class) 9 @ResponseStatus(HttpStatus.BAD_REQUEST) 10 public Map<String, String> bindException(@Nonnull BindException ex) { 11 Map<String, String> map = new HashMap<>(4); 12 ex.getFieldErrors().forEach(e -> map.put(e.getField(), e.getDefaultMessage())); 13 return map; 14 } 15 }
2.3 BindException 错误的产生来源
在 spring 调用 ModelAttributeMethodProcessor#resolveArgument 方法处理方法参数的时候,
会调用 validateIfApplicable 尝试对每一个有 Validated 或者 Valid 注解的参数进行验证,
如果验证的结果存在错误,就会检查是否调用的方法是否入参存在 Errors 的实现,即我们传进去的 BindingResult,如果有则将错误信息放进去 BindingResult , 否则抛出一个 BindException 错误。
2.4 RestControllerAdvice 的查找与调用
spring 调用 ExceptionHandlerExceptionResolver#getExceptionHandlerMethod 方法查找我们配置的 ExceptionHandler
并且测试是否我们配置的 Handler 是否支持这个方法,在这里我们配置了支持注解 @RestController,需要注意的是,如果 @ExceptionHandler 注解上没有任何属性,将会直接匹配到
2.4.1 ReponseStatus 的查找
3 JSON 请求体验证
与表单验证类似,但是验证是在 RequestResponseBodyMethodProcessor#resolveArgument 方法中进行的,抛出的类型也不同,是:MethodArgumentNotValidException。
3.1 验证代码准备
@PostMapping("register") public Object register(@RequestBody @Validated RegisterForm form) { return userService.register(form) ? "success" : "fail"; }
3.2 RestControllerAdvice 捕捉 BindException 错误
与表单验证不同,我们这次要捕捉 MethodArgumentNotValidException 错误:
1 @ExceptionHandler(MethodArgumentNotValidException.class) 2 @ResponseStatus(HttpStatus.BAD_REQUEST) 3 public Map<String, String> methodArgumentNotValidException(@Nonnull MethodArgumentNotValidException ex) { 4 Map<String, String> map = new HashMap<>(4); 5 ex.getBindingResult().getFieldErrors().forEach(e -> map.put(e.getField(), e.getDefaultMessage())); 6 return map; 7 }
4 预设的全局错误处理
作为一个普通的开发人员,总会有很多遗漏,此时,让我们的 全局错误处理器 继承 ResponseEntityExceptionHandler 是个不错的选择,相应的,我们只要重写对应的表单验证与 JSON 请求验证的相关逻辑:
1 package cn.pancc.springboot.examples.security.global; 2 3 import org.springframework.http.HttpHeaders; 4 import org.springframework.http.HttpStatus; 5 import org.springframework.http.ResponseEntity; 6 import org.springframework.validation.BindException; 7 import org.springframework.web.bind.MethodArgumentNotValidException; 8 import org.springframework.web.bind.annotation.RestController; 9 import org.springframework.web.bind.annotation.RestControllerAdvice; 10 import org.springframework.web.context.request.WebRequest; 11 import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 12 13 import java.util.HashMap; 14 import java.util.Map; 15 16 /** 17 * @author pancc 18 * @version 1.0 19 */ 20 @RestControllerAdvice(annotations = RestController.class) 21 public class ErrorHandlerExceptionResolver extends ResponseEntityExceptionHandler { 22 23 @Override 24 protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { 25 Map<String, String> errors = new HashMap<>(4); 26 ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage())); 27 return new ResponseEntity<>(errors, status); 28 } 29 30 @Override 31 protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { 32 Map<String, String> errors = new HashMap<>(4); 33 ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage())); 34 return new ResponseEntity<>(errors, status); 35 } 36 }