Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode, 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
常规使用方式
- 引入pom
1. `<dependency>`
2. `<groupId>org.springframework.boot</groupId>`
3. `<artifactId>spring-boot-starter-validation</artifactId>`
4. `</dependency>`
5. `<dependency>`
6. `<groupId>org.springframework.boot</groupId>`
7. `<artifactId>spring-boot-starter-web</artifactId>`
8. `<exclusions>`
9. `<exclusion>`
10. `<groupId>org.springframework.boot</groupId>`
11. `<artifactId>spring-boot-starter-tomcat</artifactId>`
12. `</exclusion>`
13. `</exclusions>`
14. `</dependency>`
其中在spring-boot-starter-web中有hibernate-validater的依赖。
- 在bean上直接使用注解:
1. `@Data`
2. `@EqualsAndHashCode(callSuper = false)`
3. `@Accessors(chain = true)`
4. `public class Medicine implements Serializable {`
5.
6. `private static final long serialVersionUID = 1L;`
7.
8. `/**`
9. `* 主键`
10. `*/`
11. `@TableId(value = "id", type = IdType.AUTO)`
12. `private Long id;`
13.
14. `/**`
15. `* 序号,药品编号`
16. `*/`
17. `private Long medicineCode;`
18.
19. `/**`
20. `* 简码`
21. `*/`
22. `@NotBlank(message = "拼音简码不能为空")`
23. `private String simpleCode;`
24.
25. `/**`
26. `* 条码`
27. `*/`
28. `@NotBlank(message = "商品条码不能为空")`
29. `private String tiaoCode;`
30.
31. `/**`
32. `* 药品名称`
33. `*/`
34. `@NotBlank(message = "药品名称不能为空")`
35. `private String medicineName;`
36.
37. `/**`
38. `* 剂型`
39. `*/`
40. `@NotBlank(message = "剂型不能为空")`
41. `private String jxType;`
42.
43. `/**`
44. `* 通用名`
45. `*/`
46. `private String commonName;`
47.
48. `/**`
49. `* 规格`
50. `*/`
51. `private String guiType;`
52.
53. `/**`
54. `* 生产厂家`
55. `*/`
56. `private String productor;`
57.
58. `/**`
59. `* 批准文号`
60. `*/`
61. `private String permitCode;`
62.
63. `/**`
64. `* 包装单位`
65. `*/`
66. `@NotBlank(message = "包装单位不能为空")`
67. `private String wrapUnit;`
68.
69. `/**`
70. `* 最小单位`
71. `*/`
72. `@NotBlank(message = "最小单位不能为空")`
73. `private String minUnit;`
- 在controller中添加@Valid注解
1. `@PostMapping(value = "/save")`
2. `@RequiresPermissions("medic:add")`
3. `@AddSysLog(descrption = "保存药品信息")`
4. `@LoginedUser`
5. `@ApiOperation(value = "保存药品信息",notes = "新增药品相关接口")`
6. `public R save(@CurrentUser@ApiIgnore SysUser sysUser, @RequestBody @Valid Medicine medicine) {`
- 普通的String 类型的
1. `@PostMapping(value = "/save")`
2. `@RequiresPermissions("medic:add")`
3. `@AddSysLog(descrption = "查询药品相关接口")`
4. `@LoginedUser`
5. `@ApiOperation(value = "查询药品相关接口",notes = "查询药品相关接口")`
6. `public R queryByName(@CurrentUser@ApiIgnore SysUser sysUser, @NotNull(message = "查询条件不能为空") String medicineName) {`
- 如需要国际化
1. `@PostMapping(value = "/save")`
2. `@RequiresPermissions("medic:add")`
3. `@AddSysLog(descrption = "查询药品相关接口")`
4. `@LoginedUser`
5. `@ApiOperation(value = "查询药品相关接口",notes = "查询药品相关接口")`
6. `public R queryByName(@CurrentUser@ApiIgnore SysUser sysUser, @NotNull(message = "medicine.message.notnull") String medicineName) {`
在messagezhCN.properties中
1. `medicine
.
message
.
notnull
=药品名称不能为空`
在messageenUS.properties中
1. `medicine
.
message
.
notnull
=
medicine name can
not
be
null`
- 默认使用spring validator如使用hibernate validator:
1. `@Configuration`
2. `public class ValidatorConfig {`
3.
4. `@Bean`
5. `public Validator validator(){`
6. `ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()`
7. `.failFast(true).buildValidatorFactory();`
8. `return validatorFactory.getValidator();`
9. `}`
10. `}`
- 异常统一捕获处理,省去每个@Valid后都跟着处理BindingResult
1. `/**`
2. `* 数据校验处理`
3. `* @param e`
4. `* @return`
5. `*/`
6. `@ExceptionHandler({BindException.class, ConstraintViolationException.class})`
7. `public String validatorExceptionHandler(Exception e) {`
8. `String msg = e instanceof BindException ? msgConvertor(((BindException) e).getBindingResult())`
9. `: msgConvertor(((ConstraintViolationException) e).getConstraintViolations());`
10.
11. `return msg;`
12. `}`
13.
14. `/**`
15. `* 参数不合法异常`
16. `*`
17. `* @param ex`
18. `* @return`
19. `* @Description`
20. `* @author`
21. `*/`
22. `@ExceptionHandler(MethodArgumentNotValidException.class)`
23. `@ResponseBody`
24. `public R handleException(MethodArgumentNotValidException ex) {`
25. `BindingResult a = ex.getBindingResult();`
26. `List<ObjectError> list = a.getAllErrors();`
27. `String errorMsg = ErrorEnum.PARAMETER_ERR.getErrorMsg();`
28. `if (CollectionUtils.isNotEmpty(list)) {`
29. `errorMsg = list.get(0).getDefaultMessage();`
30. `}`
31. `return R.error(ErrorEnum.PARAMETER_ERR.getErrorCode(),errorMsg);`
32. `}`
- spring validator分组处理 为什么要有分组这一说呢?因为,举个例子,添加的时候不需要校验id,而修改的时候id不能为空,有了分组以后,就可以添加的时候校验用组A,修改的时候校验用组B。 两个分组的接口,一个是添加的组,一个是修改的组:
实体类中:
controller中:
1. `@PostMapping(value = "/update")`
2. `@RequiresPermissions("medic:update")`
3. `@AddSysLog(descrption = "修改药品信息")`
4. `@ApiOperation(value = "修改药品信息",notes = "修改药品相关接口")`
5. `// @ApiImplicitParams({`
6. `// @ApiImplicitParam(name = "medicine", value = "药品json数据", required = true, paramType = "body", dataType = "medicine")})`
7. `@LoginedUser`
8. `public R update(@CurrentUser@ApiIgnore SysUser sysUser,@RequestBody @Validated(value = {MedicineGroupEdit.class}) Medicine medicine) {`
见:https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/core.html#validation
9.hibernate validator自定义validator 见:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html\_single/#validator-customconstraints
自定义方法:
- 创建注解
2. 创建validator
3. 使用方式
在需要校验的bean上添加:
注意点
- JSR 303 – Bean Validation 规范 http://jcp.org/en/jsr/detail?id=303
- Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。如果想了解更多有关 Hibernate Validator 的信息,请查看 http://www.hibernate.org/subprojects/validator.html
- 一个 constraint 通常由 annotation 和相应的 constraint validator 组成,它们是一对多的关系。也就是说可以有多个 constraint validator 对应一个 annotation。在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证
- BindingResult必须跟在被校验参数之后,若被校验参数之后没有BindingResult对象,将会抛出BindException
- 不要使用 BindingResult 接收String等简单对象的错误信息(也没有特别的错,只是 result 是接不到值。)。简单对象校验失败,会抛出 ConstraintViolationException。
SpringMVC 在进行方法参数的注入(将 Http请求参数封装成方法所需的参数)时,不同的对象使用不同的解析器注入对象。注入实体对象时使用ModelAttributeMethodProcessor而注入 String 对象使用AbstractNamedValueMethodArgumentResolver。而正是这个差异导致了BindingResult无法接受到简单对象(简单的入参参数类型)的校验信息。
HandlerMethodArgumentResolverComposite#resolveArgument():
1. `public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,`
2. `NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {`
3. `// 获取 parameter 参数的解析器`
4. `HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);`
5. `// 调用解析器获取参数`
6. `return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);`
7. `}`
8.
9. `// 获取 parameter 参数的解析器`
10. `private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {`
11. `// 从缓存中获取参数对应的解析器`
12. `HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);`
13. `for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {`
14. `// 解析器是否支持该参数类型`
15. `if (methodArgumentResolver.supportsParameter(parameter)) {`
16. `result = methodArgumentResolver;`
17. `this.argumentResolverCache.put(parameter, result);`
18. `break;`
19. `}`
20. `}`
21. `return result;`
22. `}`
注入 String 参数时,在AbstractNamedValueMethodArgumentResolver#resolveArgument()中,不会抛出BindException/ConstraintViolationException异常、也不会将 BindingResult 传入到方法中。
抛出BindException的地方
注入对象时在ModelAttributeMethodProcessor#resolveArgument():154 行的 validateIfApplicable(binder, parameter)语句,进行了参数校验,校验不通过并且实体对象后不存在BindingResult对象,则会在this#resolveArgument():156抛出BindException。
1. `public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,`
2. `NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {`
3.
4. `// bean 参数绑定和校验`
5. `WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);`
6.
7. `// 参数校验`
8. `validateIfApplicable(binder, parameter);`
9. `// 校验结果包含错误,并且该对象后不存在 BindingResult 对象,就抛出异常`
10. `if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {`
11. `throw new BindException(binder.getBindingResult());`
12. `}`
13.
14. `// 在对象后注入 BindingResult 对象`
15. `Map<String, Object> bindingResultModel = bindingResult.getModel();`
16. `mavContainer.removeAttributes(bindingResultModel);`
17. `mavContainer.addAllAttributes(bindingResultModel);`
18. `}`
抛出ConstraintViolationException的地方
InvocableHandlerMethod#invokeForRequest()的doInvoke(args)方法中Mehtod.invoke() 对应的CglibAopProxy$CglibMethodInvocation的父类ReflectiveMethodInvocation,在 ReflectiveMethodInvocation#process()方法的最后一行:
1. `return
((
MethodInterceptor
)
interceptorOrInterceptionAdvice
).
invoke
(
this
);`
这里的 Methodnterceptor 接口的真身是 MethodValidationInterceptor:
1. `public Object invoke(MethodInvocation invocation) throws Throwable {`
2. `ExecutableValidator execVal = this.validator.forExecutables();`
3. `// 校验参数`
4. `try {`
5. `result = execVal.validateParameters(`
6. `invocation.getThis(), methodToValidate, invocation.getArguments(), groups);`
7. `}`
8. `catch (IllegalArgumentException ex) {`
9. `// 解决参数错误异常、再次校验`
10. `methodToValidate = BridgeMethodResolver.findBridgedMethod(`
11. `ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));`
12. `result = execVal.validateParameters(`
13. `invocation.getThis(), methodToValidate, invocation.getArguments(), groups);`
14. `}`
15. `if (!result.isEmpty()) {`
16. `throw new ConstraintViolationException(result);`
17. `}`
18.
19. `// 执行结果`
20. `Object returnValue = invocation.proceed();`
21.
22. `// 校验返回值`
23. `result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);`
24. `if (!result.isEmpty()) {`
25. `throw new ConstraintViolationException(result);`
26. `}`
27.
28. `return returnValue;`
29. `}`
关于MethodArgumentNotValidException异常的抛出
通常采取处理BindException:
1. `@RestControllerAdvice`
2. `@Slf4j`
3. `public class ExceptionAdvice {`
4.
5. `@ExceptionHandler(BindException.class)`
6. `public Object validExceptionHandler(BindException e){`
7. `FieldError fieldError = e.getBindingResult().getFieldError();`
8. `assert fieldError != null;`
9. `log.error(fieldError.getField() + ":" + fieldError.getDefaultMessage());`
10. `// 将错误的参数的详细信息封装到统一的返回实体`
11. `...`
12. `return ...;`
13. `}`
14. `}`
但是, 如果你使用了@RequestBody @Valid 来封装参数并校验, 这个时候这个异常处理器又不起作用了,需要添加MethodArgumentNotValidException异常处理器:
1. `@ExceptionHandler(MethodArgumentNotValidException.class)`
2. `@ResponseBody`
3. `public R handleException(MethodArgumentNotValidException ex) {`
4. `BindingResult a = ex.getBindingResult();`
5. `List<ObjectError> list = a.getAllErrors();`
6. `String errorMsg = ErrorEnum.PARAMETER_ERR.getErrorMsg();`
7. `if (CollectionUtils.isNotEmpty(list)) {`
8. `errorMsg = list.get(0).getDefaultMessage();`
9. `}`
10. `return R.error(ErrorEnum.PARAMETER_ERR.getErrorCode(),errorMsg);`
11. `}`
原因见 https://github.com/spring-projects/spring-framework/issues/14790
1. `These are actually intentionally different exceptions. @ModelAttribute, which is assumed by default if no other annotation is present,`
2. `goes through data binding and validation, and raises BindException to indicate a failure with binding request properties or validating`
3. `the resulting values. @RequestBody, on the other hand converts the body of the request via HttpMessageConverter, validates it and raises`
4. `various conversion related exceptions or a MethodArgumentNotValidexception if validation fails.`
5. `In most cases a MethodArgumentNotValidException can be handled generically (e.g. via @ExceptionHandler method) while BindException is very`
6. `often handled individually in each controller method.`
- 若没有手动配置Validator对象,自然需要从 Spring 容器中获取校验器对象,注入使用。
- 关于校验模式,默认会校验完所有属性,然后将错误信息一起返回,但很多时候不需要这样,一个校验失败了,其它就不必校验了
1. `@Configuration`
2. `public class ValidatorConfig {`
3.
4. `@Bean`
5. `public Validator validator(){`
6. `ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory();`
7. `return validatorFactory.getValidator();`
8. `}`
9. `}`
- 注解的使用方式
1. `@Null`
2. `被注释的元素必须为 null`
3.
4. `@NotNull`
5. `被注释的元素必须不为 null`
6.
7. `@AssertTrue`
8. `被注释的元素必须为 true`
9.
10. `@AssertFalse`
11. `被注释的元素必须为 false`
12.
13. `@Min(value)`
14. `被注释的元素必须是一个数字,其值必须大于等于指定的最小值`
15.
16. `@Max(value)`
17. `被注释的元素必须是一个数字,其值必须小于等于指定的最大值`
18.
19. `@DecimalMin(value)`
20. `被注释的元素必须是一个数字,其值必须大于等于指定的最小值`
21.
22. `@DecimalMax(value)`
23. `被注释的元素必须是一个数字,其值必须小于等于指定的最大值`
24.
25. `@Size(max=, min=)`
26. `被注释的元素的大小必须在指定的范围内`
27.
28. `@Digits(integer, fraction)`
29. `被注释的元素必须是一个数字,其值必须在可接受的范围内`
30.
31. `@Past`
32. `被注释的元素必须是一个过去的日期`
33.
34. `@Future`
35. `被注释的元素必须是一个将来的日期`
36.
37. `@Pattern(regex=, flag=)`
38. `被注释的元素必须符合指定的正则表达式`
39.
40. `@NotBlank(message =)`
41. `验证字符串非null,且长度必须大于0`
42.
43. `以下为hibernate Validator附加的`
44. `@Email`
45. `被注释的元素必须是电子邮箱地址`
46.
47. `@Length(min=, max=)`
48. `被注释的字符串的大小必须在指定的范围内`
49.
50. `@NotEmpty`
51. `被注释的字符串的必须非空`
52.
53. `@Range(min=, max=, message=)`
54. `被注释的元素必须在合适的范围内`
-
@Valid与@Validated的区别: