• 更简洁的参数校验,使用 SpringBoot Validation 对参数进行校验


    在开发接口时,如果要对参数进行校验,你会怎么写?编写 if-else 吗?虽然也能达到效果,但是不够优雅。

    今天,推荐一种更简洁的写法,使用 SpringBoot Validation 对方法参数进行校验,特别是在编写 Controller 层的方法时,直接使用一个注解即可完成参数校验。

    示例代码:spring-validation-demo: SpringBootValidation Demo (gitee.com)

    🚀引入依赖

    想要完成上述所说的参数校验,我们需要一个核心依赖:spring-boot-starter-validation,此外,为了方便演示,还需要其他依赖。

    依赖如下:

    1.  <dependencies>
    2.      <dependency>
    3.        <groupId>org.springframework.boot</groupId>
    4.        <artifactId>spring-boot-starter-web</artifactId>
    5.      </dependency>
    6.      <dependency>
    7.        <groupId>org.springframework.boot</groupId>
    8.        <artifactId>spring-boot-starter-validation</artifactId>
    9.      </dependency>
    10.      <dependency>
    11.        <groupId>org.projectlombok</groupId>
    12.        <artifactId>lombok</artifactId>
    13.        <optional>true</optional>
    14.      </dependency>
    15.    </dependencies>
    16. 复制代码

    💡 以下部分不是核心内容:

    你在编写下面的示例代码中,会发现主要使用到了javax.validation.constraints 包下的注解,而这个包主要来自于 jakarta.validation-api 这个依赖。

    如果引入依赖的时候直接引入 jakarta.validation-api 是无法实现参数校验功能的,因为它只定义了规范,而没有具体实现。但是 hibernate-validator 实现了这个规范,直接引入 hibernate-validator 也是可以实现参数校验功能的。

    1.  
    2.  <dependency>
    3.      <groupId>jakarta.validationgroupId>
    4.      <artifactId>jakarta.validation-apiartifactId>
    5.  dependency>
    6.  
    7.  
    8.  <dependency>
    9.      <groupId>org.hibernate.validatorgroupId>
    10.      <artifactId>hibernate-validatorartifactId>
    11.  dependency>
    12. 复制代码

    🚀 相关注解说明

    这里罗列出一些主要的注解,这些注解主要来自于包 javax.validation.constraints,有兴趣查看源码的可以去这个包下查看。

    可以先跳过这部分内容,下面的代码如果遇到不清楚作用的注解再回来查阅。

    ✈ 空值检查

    注解说明
    @NotBlank用于字符串,字符串不能为null 也不能为空字符串
    @NotEmpty字符串同上,对于集合(Map,List,Set)不能为空,必须有元素
    @NotNull不能为 null
    @Null必须为 null

    ✈ 数值检查

    注解说明
    @DecimalMax(value)被注释的元素必须为数字,其值必须小于等于指定的值
    @DecimalMin(value)被注释的元素必须为数字,其值必须大于等于指定的值
    @Digits(integer, fraction)被注释的元素必须为数字,其值的整数部分精度为 integer,小数部分精度为 fraction
    @Positive被注释的元素必须为正数
    @PositiveOrZero被注释的元素必须为正数或 0
    @Max(value)被注释的元素必须小于等于指定的值
    @Min(value)被注释的元素必须大于等于指定的值
    @Negative被注释的元素必须为负数
    @NegativeOrZero被注释的元素必须为负数或 0

    ✈ Boolean 检查

    注解说明
    @AssertFalse被注释的元素必须值为 false
    @AssertTrue被注释的元素必须值为 true

    ✈ 长度检查

    注解说明
    @Size(min,max)被注释的元素长度必须在 minmax 之间,可以是 String、Collection、Map、数组

    ✈ 日期检查

    注解说明
    @Future被注释的元素必须是一个将来的日期
    @FutureOrPresent被注释的元素必须是现在或者将来的日期
    @Past被注释的元素必须是一个过去的日期
    @PastOrPresent被注释的元素必须是现在或者过去的日期

    ✈ 其他检查

    注解说明
    @Email被注释的元素必须是电子邮箱地址
    @Pattern(regexp)被注释的元素必须符合正则表达式

    除此之外,org.hibernate.validator.constraints 包下还有其他校验注解,例如 @ISBN 检查一个字符串是否是一个有效地 ISBN 序列号。

    🚀 参数校验

    接下来开始体验 Spring Boot Validation。

    首先,编写一个需要校验的实体类:

    1.  @Data
    2.  public class Student {
    3.      @NotBlank(message = "主键不能为空")
    4.      private String id;
    5.      @NotBlank(message = "名字不能为空")
    6.      @Size(min=2, max = 4, message = "名字字符长度必须为 2~4个")
    7.      private String name;
    8.      @Pattern(regexp = "^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$", message = "手机号格式错误")
    9.      private String phone;
    10.      @Email(message = "邮箱格式错误")
    11.      private String email;
    12.      @Past(message = "生日必须早于当前时间")
    13.      private Date birth;
    14.      @Min(value = 0, message = "年龄必须为 0~100")
    15.      @Max(value = 100, message = "年龄必须为 0~100")
    16.      private Integer age;
    17.      @PositiveOrZero
    18.      private Double score;
    19.  }
    20. 复制代码

    随后编写一个控制层代码,进行测试:

    1.  @RestController
    2.  public class TestController {
    3.  ​
    4.      @GetMapping("/test")
    5.      public Student test(@RequestBody @Validated Student student) {
    6.          return student;
    7.     }
    8.  }
    9. 复制代码

    使用 postman 进行测试,发送一个不带参数的请求,查看结果:

    💡后端控制台日志打印是这样的(显示极度不友好),可以看到校验规则生效了:

    1.  2022-11-23 22:10:13.249 WARN 19840 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.springvalidationdemo.domain.Student com.example.springvalidationdemo.controller.TestController.test(com.example.springvalidationdemo.domain.Student) with 2 errors: [Field error in object 'student' on field 'name': rejected value [null]; codes [NotBlank.student.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.name,name]; arguments []; default message [name]]; default message [名字不能为空]] [Field error in object 'student' on field 'id': rejected value [null]; codes [NotBlank.student.id,NotBlank.id,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.id,id]; arguments []; default message [id]]; default message [主键不能为空]] ]
    2. 复制代码

    🚀 全局异常处理

    查看上面的日志打印,可以看到当参数校验不通过时,会抛出异常 MethodArgumentNotValidException,同时也会打印那些参数没有通过校验,以及该参数校验规则

    为了方便查看,我们可以编写一个全局异常处理,处理这个参数校验异常,并使用统一返回实体返回给前端。

    1.  @ControllerAdvice
    2.  @Slf4j
    3.  public class GlobalExceptionHandler {
    4.  ​
    5.      @ExceptionHandler(MethodArgumentNotValidException.class)
    6.      @ResponseBody
    7.      public ResponseEntity<Object> exception(MethodArgumentNotValidException e, HttpServletRequest request) {
    8.          Map<String, String> result = new HashMap<>();
    9.          BindingResult bindingResult = e.getBindingResult();
    10.          log.error("请求[ {} ] {} 的参数校验发生错误", request.getMethod(), request.getRequestURL());
    11.          for (ObjectError objectError : bindingResult.getAllErrors()) {
    12.              FieldError fieldError = (FieldError) objectError;
    13.              log.error("参数 {} = {} 校验错误:{}", fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage());
    14.              result.put(fieldError.getField(), fieldError.getDefaultMessage());
    15.         }
    16.          // 一般项目都会有自己定义的公共返回实体类,这里直接使用现成的 ResponseEntity 进行返回,同时设置 Http 状态码为 400
    17.          return ResponseEntity.badRequest().body(result);
    18.  ​
    19.     }
    20.  ​
    21.  }
    22. 复制代码

    再次使用 postman 发起测试:

    控制台打印出自定义的日志信息:

    1.  2022-11-23 22:16:37.800 ERROR 19880 --- [nio-8080-exec-2] c.e.s.handler.GlobalExceptionHandler     : 请求[ GET ] http://localhost:8080/test 的参数校验发生错误
    2.  2022-11-23 22:16:37.800 ERROR 19880 --- [nio-8080-exec-2] c.e.s.handler.GlobalExceptionHandler     : 参数 name = null 校验错误:名字不能为空
    3.  2022-11-23 22:16:37.800 ERROR 19880 --- [nio-8080-exec-2] c.e.s.handler.GlobalExceptionHandler     : 参数 id = null 校验错误:主键不能为空
    4.  2022-11-23 22:19:36.594 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler     : 请求[ GET ] http://localhost:8080/test 的参数校验发生错误
    5.  2022-11-23 22:19:36.594 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler     : 参数 email = abc.com 校验错误:邮箱格式错误
    6.  2022-11-23 22:19:36.594 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler     : 参数 score = -20 校验错误:必须是正数或零
    7.  2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler     : 参数 birth = Thu Jan 01 08:00:00 CST 2099 校验错误:生日必须早于当前时间
    8.  2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler     : 参数 phone = 12233 校验错误:手机号格式错误
    9.  2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler     : 参数 age = -40 校验错误:年龄必须为 0~100
    10.  2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler     : 参数 name = 我是很长的名字 校验错误:名字字符长度必须为 2~4
    11.  2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler     : 参数 score = -20 校验错误:需要在09223372036854775807之间
    12. 复制代码

    🚀 传递校验

    我们也可以使用传递校验,即一个参数类中包含了另一个参数类,被包含的参数类也可以被校验。

    在声明一个新的参数类,同时修改 Student 类。

    1.  @Data
    2.  public class ClassInfo {
    3.      @NotBlank(message = "班主任姓名不能为空")
    4.      private String teacher;
    5.      @NotNull(message = "教师不能为空")
    6.      private Integer classroom;
    7.      @NotNull(message = "年级不能为空")
    8.      @Min(value = 1, message = "年级只能是 1-6")
    9.      @Max(value = 6, message = "年级只能是 1-6")
    10.      private Integer grade;
    11.  }
    12.  ​
    13.  @Data
    14.  public class Student {
    15.     //.............
    16.      // 新加的字段,被包含的参数类,使用 @Valid 就能传递校验,如果不使用 @Valid 注解,则无法传递校验。
    17.      @Valid
    18.      private ClassInfo classInfo;
    19.  }
    20. 复制代码

    再使用 postman 测试一次

    🚀 分组校验

    此外还可以使用分组校验,令一组方法对某些字段校验,而令一组方法对其他字段校验,例如:一般情况下,新增实体的接口方法 [POST] 不需要主键 ID,修改实体的接口方法 [PUT] 就需要主键 ID 以便进行修改。

    为注解 @Validated 赋值属性 value,以及为那些校验注解赋值属性 group, 即可达到分组的效果。

    接下来看看如何实现分组校验。

    Student 类中添加两个内部接口 Inteface,同时修改 id 字段的注解,以进行分组

    1.  @Data
    2.  public class Student {
    3.      // id 字段属于 Create 组
    4.      @NotBlank(message = "主键不能为空", groups = {Student.Create.class})
    5.      private String id;
    6.      // .............
    7.      // 更新分组
    8.      public interface Update {}
    9.  ​
    10.      // 创建分组
    11.      public interface Create {}
    12.  }
    13. 复制代码

    在控制层新增两个接口

    1.  @RestController
    2.  public class TestController {
    3.  ​
    4.      // @Validated 注解可以赋值 value 属性进行分组,value 是可以以数组的形式赋值,既可以分配多个组
    5.      @PostMapping("/students")
    6.      public Student create(@RequestBody @Validated(Student.Create.class) Student student) {
    7.          return student;
    8.     }
    9.  ​
    10.      @PutMapping("/students")
    11.      public Student update(@RequestBody @Validated(Student.Update.class) Student student) {
    12.          return student;
    13.     }
    14.  }
    15. 复制代码

    在 postman 上进行测试:

    可以看到分组校验也生效了。

    🚀 总结

    在实际开发中,我们可以使用 Spring Boot Validation 提供的注解进行参数校验,提高代码的可读性,避免编写大量的 if-else 代码块和重复的校验语句。

  • 相关阅读:
    设计模式-行为型设计模式-命令模式
    京东云硬钢阿里云:承诺再低10%
    本地搭建gitlab服务器(Ubuntu)
    管理学精要
    Python学习路线图
    ubuntu20.04 + kiosk + chrome打造一体机系统
    正则系列之断言Assertions
    Java核心知识体系4:AOP原理和切面应用
    DDS数据分发服务——提升汽车领域数据传输效率
    如何用python写 翻译腔?天哪~这实在是太有趣了~
  • 原文地址:https://blog.csdn.net/m0_71777195/article/details/128034145