1. 简介

1.1 了解数据校验

在 SpringBoot 日常开发中,后端会接收到前端传递过来的大量数据,这些数据有时我们还需要进行校验,比如姓名不能为空、年龄范围等等,如果在校验时写上大量的 if…else..,无疑是不美观的,所以 Spring 框架已经给我们封装了一套校验组件:validation。

Java API 规范 (JSR303) 定义了 Bean 校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解如@Email、@Length 等。Spring Validation是 对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。

springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入 validation 和 web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了。

1.2 引入依赖

  1. <!--校验组件-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-validation</artifactId>
  5. </dependency>
  6. <!-- 可以不用,用上面的校验组件即可 -->
  7. <dependency>
  8. <groupId>org.hibernate</groupId>
  9. <artifactId>hibernate-validator</artifactId>
  10. <version>6.0.1.Final</version>
  11. </dependency>

1.3 校验注解

  1. @AssertFalse 可以为 null, 如果不为 null 的话必须为 false
  2. @AssertTrue 可以为 null, 如果不为 null 的话必须为 true
  3. @DecimalMax 设置不能超过最大值
  4. @DecimalMin 设置不能超过最小值
  5. @Digits 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内
  6. @Future 日期必须在当前日期的未来
  7. @Past 日期必须在当前日期的过去
  8. @Max 最大不得超过此最大值
  9. @Min 最大不得小于此最小值
  10. @NotNull 不能为 null,可以是空
  11. @Null 必须为 null
  12. @Pattern 必须满足指定的正则表达式
  13. @Size 集合、数组、map 等的 size() 值必须在指定范围内
  14. @Email 必须是 email 格式
  15. @Length 长度必须在指定范围内
  16. @NotBlank 字符串不能为 null, 字符串 trim() 后也不能等于 “”
  17. @NotEmpty 不能为 null,集合、数组、map size() 不能为 0;字符串 trim() 后可以等于 “”
  18. @Range 值必须在指定范围内
  19. @URL 必须是一个 URL

2. 实践入门

2.1 定义实体类

  1. @Data
  2. public class ValidVO {
  3. @Length(min = 6,max = 12,message = "appId 长度必须位于 6 到 12 之间")
  4. private String appId;
  5. @NotBlank(message = "名字为必填项")
  6. private String name;
  7. @Email(message = "请填写正确的邮箱地址")
  8. private String email;
  9. private String sex;
  10. @NotEmpty(message = "级别不能为空")
  11. private String level;
  12. }

2.2 定义Controller

这里我们先定义三个方法 test1,test2,test3,test1 使用了@RequestBody注解,用于接受前端发送的 json 数据,test2 模拟表单提交,test3 模拟单参数提交。注意,当使用单参数校验时需要在 Controller 上加上 @Validated 注解,否则不生效。

  • @Validated 注解:用在类、方法和参数上,表示对后面的参数进行校验。
  1. @Slf4j
  2. @Validated
  3. @RestController
  4. public class ValidVoController {
  5. @PostMapping("/valid/test1")
  6. public String test1(@Validated @RequestBody ValidVO validVO) {
  7. log.info("validEntity is {}", validVO);
  8. return "test1 valid success";
  9. }
  10. @PostMapping(value = "/valid/test2")
  11. public String test2(@Validated ValidVO validVO){
  12. log.info("validEntity is {}", validVO);
  13. return "test2 valid success";
  14. }
  15. @PostMapping(value = "/valid/test3")
  16. public String test3(@Email String email){
  17. log.info("email is {}", email);
  18. return "email valid success";
  19. }
  20. }

2.3 发送请求测试

2.3.1 调用test1

通过 apifox 测试工具,调用 test1 方法,抛出 org.springframework.web.bind.MethodArgumentNotValidException 异常。

  1. {
  2. "appId": "ab1c",
  3. "name": "yxw",
  4. "email": "47693899",
  5. "sex": "man",
  6. "level": "12",
  7. }

IDEA 执行结果如下:

  1. 2022-02-16 10:54:41.288 WARN 33576 --- [nio-9090-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.xuwei.springbootvalidation.controller.ValidVoController.test1(com.xuwei.springbootvalidation.entity.ValidVO) with 2 errors: [Field error in object 'validVO' on field 'email': rejected value [47693899]; codes [Email.validVO.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@79091710,.*]; default message [请填写正确的邮箱地址]] [Field error in object 'validVO' on field 'appId': rejected value [ab1c]; codes [Length.validVO.appId,Length.appId,Length.java.lang.String,Length]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.appId,appId]; arguments []; default message [appId],12,6]; default message [appId 长度必须位于 6 12 之间]] ]

2.3.2 调用test2

调用 test2 方法,提示的是org.springframework.validation.BindException异常。

  1. localhost:9090/valid/test2?name=yxw&level=12&email=476938977&appId=ab1c
  2. 2022-02-16 10:56:58.056 WARN 33576 --- [nio-9090-exec-9] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors<EOL>Field error in object 'validVO' on field 'email': rejected value [476938977]; codes [Email.validVO.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@79091710,.*]; default message [请填写正确的邮箱地址]<EOL>Field error in object 'validVO' on field 'appId': rejected value [ab1c]; codes [Length.validVO.appId,Length.appId,Length.java.lang.String,Length]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.appId,appId]; arguments []; default message [appId],12,6]; default message [appId 长度必须位于 6 12 之间]]

2.3.3 调用test3

调用 test3 方法,提示的是 javax.validation.ConstraintViolationException 异常。

  1. localhost:9090/valid/test3?email=476938977
  2. javax.validation.ConstraintViolationException: test3.email: 不是一个合法的电子邮件地址

2.4 全局自定义异常

这里需要说明下,如果我们使用的 @Validated 注解,但没有添加 BindingResult 这个参数,Spring 会抛出 BindException 级别的异常。所以可以写一个全局异常处理类来统一处理这种校验异常,从而免去重复组织异常信息的代码。

这里总结了三种参数校验时可能发生的异常:

  1. 使用 form data 方式调用接口,校验异常抛出 BindException。
  2. 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException。
  3. 单个参数校验异常抛出 ConstraintViolationException。
  1. @Slf4j
  2. @RestControllerAdvice
  3. public class MyRestExceptionHandler {
  4. // 1.处理 form data方式调用接口校验失败抛出的异常
  5. @ExceptionHandler(value = BindException.class)
  6. public ResultData<List<String>> bindExceptionHandler(BindException e) {
  7. log.error("form data方式调用接口校验失信息 ex={}", e.getMessage(), e);
  8. List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
  9. List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
  10. return ResultData.formError(ReturnCode.RC500.getCode(), e.getMessage(), collect);
  11. }
  12. // 2.处理 json 请求体调用接口校验失败抛出的异常
  13. @ExceptionHandler(MethodArgumentNotValidException.class)
  14. public ResultData methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
  15. List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
  16. List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
  17. return ResultData.formError(ReturnCode.RC500.getCode(), e.getMessage(), collect);
  18. }
  19. // 3.处理单个参数校验失败抛出的异常
  20. @ExceptionHandler(ConstraintViolationException.class)
  21. public ResultData constraintViolationExceptionHandler(ConstraintViolationException e) {
  22. Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
  23. List<String> collect = constraintViolations.stream()
  24. .map(o -> o.getMessage())
  25. .collect(Collectors.toList());
  26. return ResultData.formError(ReturnCode.RC500.getCode(), e.getMessage(), collect);
  27. }
  28. }

再次测试,查看执行结果:
image.png

3. 分组校验

有时我们会遇到一种情况:比如新增用户时用户名称不能为空的,更新用户时用户允许为空,所以这里的校验规则也是不同的,可以使用分组校验。
分组校验有三个步骤:
1、定义两个个分组类 Update 和 Save,或者直接定义一个分组接口。

  1. public interface ValidGroup extends Default {
  2. interface Crud extends ValidGroup {
  3. interface Create extends Crud {
  4. }
  5. interface Update extends Crud{
  6. }
  7. interface Query extends Crud{
  8. }
  9. interface Delete extends Crud{
  10. }
  11. }
  12. }

2、在注解上添加 groups 指定分组。

  1. @Data
  2. public class ValidVO {
  3. @Null(groups = ValidGroup.Crud.Create.class)
  4. @NotNull(groups = ValidGroup.Crud.Update.class, message = "应用 ID 不能为空")
  5. private String appId;
  6. @NotBlank(groups = ValidGroup.Crud.Create.class, message = "名字为必填项")
  7. private String name;
  8. @Email(message = "请填写正确的邮箱地址")
  9. private String email;
  10. private String sex;
  11. @NotEmpty(message = "级别不能为空")
  12. private String level;
  13. }

3、Controller 方法的 @Validated 注解添加分组类。

  1. @RestController
  2. @Slf4j
  3. @Validated
  4. public class ValidVoController {
  5. @PostMapping(value = "/valid/add")
  6. public String add(@Validated(value = ValidGroup.Crud.Create.class) ValidVO validVO) {
  7. log.info("validEntity is {}", validVO);
  8. return "add valid success";
  9. }
  10. @PostMapping(value = "/valid/update")
  11. public String update(@Validated(value = ValidGroup.Crud.Update.class) ValidVO validVO){
  12. log.info("validEntity is {}", validVO);
  13. return "update valid success";
  14. }
  15. }

注意:如果你的 ValidGroup 没有继承 Default 分组,那代码属性上就需要加上@Validated(value = {ValidGroup.Crud.Create.class, Default.class}才能让email字段的校验生效。

4. 递归校验

如果 RequestForm 类增加一个 OrderVO 类的属性,而 OrderVO 中的属性也需要校验,就用到递归校验了,只要在相应属性上增加 @Valid 注解即可实现。
OrderVO类如下

  1. public class OrderVO {
  2. @NotNull
  3. private Long id;
  4. @NotBlank(message = "itemName 不能为空")
  5. private String itemName;
  6. }

在 RequestForm 类中增加一个 OrderVO 类型的属性。

  1. @Data
  2. public class RequestForm {
  3. ...
  4. @Valid
  5. private OrderVO orderVO;
  6. }

5. 自定义校验

Spring Validation 允许用户自定义校验,实现很简单,分两步:
1、自定义校验注解

  1. @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
  2. @Retention(RUNTIME)
  3. @Documented
  4. @Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
  5. public @interface HaveNoBlank {
  6. // 校验出错时默认返回的消息
  7. String message() default "字符串中不能含有空格";
  8. Class<?>[] groups() default { };
  9. Class<? extends Payload>[] payload() default { };
  10. /**
  11. * 同一个元素上指定多个该注解时使用
  12. */
  13. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
  14. @Retention(RUNTIME)
  15. @Documented
  16. public @interface List {
  17. NotBlank[] value();
  18. }
  19. }

2、实现 ConstraintValidator 接口编写约束校验器

  1. public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
  2. @Override
  3. public boolean isValid(String value, ConstraintValidatorContext context) {
  4. // null 不做检验
  5. if (value == null) {
  6. return true;
  7. }
  8. if (value.contains(" ")) {
  9. // 校验失败
  10. return false;
  11. }
  12. // 校验成功
  13. return true;
  14. }
  15. }