SpringBoot使用Valid验证

Bean Validation是Java定义的一套基于注解的数据校验规范。Hibernate Validation是Bean Validation的一个实现。
Bean Validation官网:https://beanvalidation.org/
Hibernate Validation官网:http://hibernate.org/validator/

在SpringBoot项目中使用BeanValidate校验参数需要的依赖

  1. <dependency>
  2. <groupId>javax.validation</groupId>
  3. <artifactId>validation-api</artifactId>
  4. <version>2.0.1.Final</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.hibernate</groupId>
  8. <artifactId>hibernate-validator</artifactId>
  9. <version>6.0.1.Final</version>
  10. </dependency>

内置注解

validator内置注解:
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value)被注释的元素必须符合指定的正则表达式
Hibernate Validator 附加的注解:
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内
@NotBlank 验证字符串非null,且长度必须大于0

参数校验简单使用

参数校验可以分为两种场景:

  1. 单个参数校验
  2. 实体类参数校验

单个参数校验

  1. @PostMapping("/get/{id}")
  2. @Validate
  3. public String add(@PathVariable @NotNull(message = "不能为空") @Min(value = 0, message = "不能小于0") Integer id)
  4. {
  5. //
  6. }

当对单个参数进行校验时,直接在参数前添加对应的约束注解即可,value为约束值,message为错误信息。之后介绍参数校验的异常处理。

当使用单个参数校验时,需要将@Validate注解放置在上。

实体参数校验

参数传输对象

  1. public class Person
  2. {
  3. @NotNull
  4. @NotBlank
  5. private String name;
  6. @NotNull
  7. @Min(value=0, message="年龄不能小于0岁")
  8. private Integer age;
  9. }

直接将参数放置在对象的属性上即可,多个注解可以叠加使用

需要进行参数校验时,只需在参数前面加上@Validated注解就可以完成校验

  1. @PostMapping("/add")
  2. public String add(@RequestBody @Validated Person person)
  3. {
  4. //
  5. }

@Validated与@Valid的简单对比说明

@Valid注解与@Validated注解功能大部分类似;两者的不同主要在于:

  • @Valid属于javax下的,而@Validated属于spring下;
  • @Valid支持嵌套校验、而@Validated不支持
  • @Validated支持分组,而@Valid不支持。

从它们的源码也可以看出@Validate不能使用在属性域上,所有无法进行嵌套校验。

  1. @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Validated {
  5. Class<?>[] value() default {};
  6. }
  1. @Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Valid {
  5. }

嵌套校验

如果一个参数模型类中包含包含另一个需要校验的模型类:

  1. @Data
  2. public class Person
  3. {
  4. @NotNull
  5. @NotBlank
  6. private String name;
  7. @NotNull
  8. @Min(value=0, message="年龄不能小于0岁")
  9. private Integer age;
  10. @Valid
  11. @NotNull // 当pets不为空后,将对Pet类进行属性的校验
  12. private List<Pet> pets;
  13. // private List<@Valid Pet> pets;
  14. }
  15. @Data
  16. public class Pet
  17. {
  18. @NotBlak(message="Pet名称不能为空")
  19. private String name;
  20. }

校验方式与以前一样

  1. @PostMapping("/add")
  2. public String add(@RequestBody @Validated Person person)
  3. {
  4. //
  5. }

分组校验

只有在校验参数时使用@Validate时才能进行分组校验。

参数实体类如下:

  1. @Data
  2. public class Person
  3. {
  4. @NotNull(groups = Update.class, message="手机号错误")
  5. private Long id;
  6. @NotNull
  7. @NotBlank
  8. private String name;
  9. @NotNull
  10. @Min(value=0, message="年龄不能小于0岁")
  11. private Integer age;
  12. }

id属性上添加@NotNull注解,并且指定groups=Update.class,Update类是一个自定义的接口,用来充当标识作用。注意只能使用接口作为分组的标识。

  1. public interface Update
  2. {
  3. }

分组校验使用:

  1. @PostMapping("/add")
  2. public String add(@RequestBody @Validated Person person)
  3. {
  4. System.out.println(person);
  5. return "ADD OK";
  6. }
  7. @PutMapping("/update")
  8. public String update(@RequestBody @Validated(Update.class) Person person)
  9. {
  10. System.out.println(person);
  11. return "UPDATE OK";
  12. }

在更新方法中,给@Validate注解也添加了Update.class的值,从前面的源码可看到,@Validate注解只有一个属性值,而这个值就是用来进行分组校验的,并且它是一个数组,也就是说可以放入多个分组标识。

上面的update方法将对Person类中的id属性进行校验,而add方法则不会对id属性进行校验。

实际上,每一个参数校验默认都有一个分组为DefaultDefault组和无参构造机制类似,当没有指定分组时,会默认当前校验属于Default组,但是一旦主动给当前校验指定了分组那么就不会再额外指定属于Default组了。我们也可以让自定义的标识接口继承Default接口,使得它同时也属于Default组。

在上面的校验中,因为add方法没有明确使用分组,所有用了Default分组,而id属性的校验属于Update分组,所以不会对id进行为空校验。而当Update接口继承Default后:

  1. public interface Update extends Default
  2. {
  3. }

此时id属性的校验既是Update分组也是Default分组,add方法的Person参数也会对id属性进行校验。

自定义参数检验注解

虽然Bean Validation和Hibernate Validator已经提供了非常丰富的校验注解,但是在实际业务中,难免会碰到一些现有注解不足以校验的情况;这时,我们可以考虑自定义Validation注解。

1.创建自定义注解

  1. @Target({METHOD, FIELD, TYPE, CONSTRUCTOR, PARAMETER})
  2. @Documented
  3. @Retention(RUNTIME)
  4. @Constraint(validatedBy = {PhoneValidator.class})
  5. //@ReportAsSingleViolation
  6. public @interface Phone
  7. {
  8. String message() default "手机号格式不正确"; // 默认错误提示信息
  9. Class<?>[] groups() default {};
  10. Class<? extends Payload>[] payload() default {};
  11. @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
  12. @Retention(RUNTIME)
  13. @Documented
  14. @interface List
  15. {
  16. Phone[] value();
  17. }
  18. }

@Constraint(validatedBy = {PhoneValidator.class})标注了处理该自定义校验注解的类。

  1. public class PhoneValidator implements ConstraintValidator<Phone, String>
  2. {
  3. private static final String TEMPLATE = "手机号不能为空";
  4. @Override
  5. public void initialize(Phone constraintAnnotation)
  6. {
  7. }
  8. @Override
  9. public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext)
  10. {
  11. if (StringUtils.isBlank(s))
  12. {
  13. // 禁用默认提示信息
  14. constraintValidatorContext.disableDefaultConstraintViolation();
  15. // 添加新的错误提示信息
  16. constraintValidatorContext.buildConstraintViolationWithTemplate(TEMPLATE).addConstraintViolation();
  17. return false;
  18. }
  19. String regex = "^1[3|4|5|7|8][0-9]\\d{4,8}$";
  20. Pattern pattern = Pattern.compile(regex);
  21. return pattern.matcher(s).matches();
  22. }
  23. }

参数校验的异常处理

方式一:使用BindingResult类来容纳异常信息

当校验不通过时,不影响正常程 序往下走。我们只需要处理BindingResult中的异常信息即可。

  1. @PostMapping("/add")
  2. public String add(@RequestBody @Validated Person person, BindingResult result)
  3. {
  4. if (result.hasErrors())
  5. {
  6. List<ObjectError> allErrors = result.getAllErrors();
  7. for (ObjectError error : allErrors)
  8. {
  9. System.out.println(error.getDefaultMessage());
  10. }
  11. return "FAILURE";
  12. }
  13. System.out.println(person);
  14. return "ADD OK";
  15. }

方式二:通过SpringMVC全局异常处理器来处理异常。

如果不采用BindingResult来容纳异常信息时,那么异常会被向外抛出。注解校验不通过时,可能抛出的异常有BindException异常、ValidationException异常(或其子类异常)、MethodArgumentNotValidException异常。

  • 单个参数校验出错会抛出ConstraintViolationException异常。
  • 实体类模型参数出错会排除抛出MethodArgumentNotValidException异常

ConstraintViolationException异常是ViolationException异常的子异常

加入全局异常处理

  1. @Slf4j
  2. @RestControllerAdvice
  3. public class ExceptionController
  4. {
  5. @ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
  6. public String handleParameterVerificationException(Exception e)
  7. {
  8. log.error(" handleParameterVerificationException has been invoked", e);
  9. String msg = null;
  10. /// BindException
  11. if (e instanceof BindException)
  12. {
  13. // getFieldError获取的是第一个不合法的参数(P.S.如果有多个参数不合法的话)
  14. FieldError fieldError = ((BindException) e).getFieldError();
  15. if (fieldError != null)
  16. {
  17. msg = fieldError.getDefaultMessage();
  18. }
  19. /// MethodArgumentNotValidException
  20. } else if (e instanceof MethodArgumentNotValidException)
  21. {
  22. BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
  23. // getFieldError获取的是第一个不合法的参数(P.S.如果有多个参数不合法的话)
  24. FieldError fieldError = bindingResult.getFieldError();
  25. if (fieldError != null)
  26. {
  27. msg = fieldError.getDefaultMessage();
  28. }
  29. /// ValidationException 的子类异常ConstraintViolationException
  30. } else if (e instanceof ConstraintViolationException)
  31. {
  32. /*
  33. * ConstraintViolationException的e.getMessage()形如
  34. * {方法名}.{参数名}: {message}
  35. * 这里只需要取后面的message即可
  36. */
  37. msg = e.getMessage();
  38. if (msg != null)
  39. {
  40. int lastIndex = msg.lastIndexOf(':');
  41. if (lastIndex >= 0)
  42. {
  43. msg = msg.substring(lastIndex + 1).trim();
  44. }
  45. }
  46. /// ValidationException 的其它子类异常
  47. } else
  48. {
  49. msg = "处理参数时异常";
  50. }
  51. return msg;
  52. }
  53. }