# spring-validation-demo **Repository Path**: Ocean_Tang/spring-validation-demo ## Basic Information - **Project Name**: spring-validation-demo - **Description**: SpringBootValidation Demo - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 1 - **Created**: 2022-11-23 - **Last Updated**: 2024-08-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 使用 SpringBoot Validation 对参数进行校验 在开发接口时,如果要对参数进行校验,你会怎么写?编写 `if-else` 吗?虽然也能达到效果,但是不够优雅。 今天,推荐一种更简洁的写法,使用 SpringBoot Validation 对方法参数进行校验,特别是在编写 Controller 层的方法时,直接使用一个注解即可完成参数校验。 示例代码: ## 🚀引入依赖 想要完成上述所说的参数校验,我们需要一个核心依赖:`spring-boot-starter-validation`,此外,为了方便演示,还需要其他依赖。 依赖如下: ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-validation org.projectlombok lombok true ``` **💡 以下部分不是核心内容:** 你在编写下面的示例代码中,会发现主要使用到了`javax.validation.constraints` 包下的注解,而这个包主要来自于 `jakarta.validation-api` 这个依赖。 如果引入依赖的时候直接引入 `jakarta.validation-api` 是无法实现参数校验功能的,因为它只定义了规范,而没有具体实现。但是 `hibernate-validator` 实现了这个规范,直接引入 `hibernate-validator` 也是可以实现参数校验功能的。 ```xml jakarta.validation jakarta.validation-api org.hibernate.validator hibernate-validator ``` ## 🚀 相关注解说明 这里罗列出一些主要的注解,这些注解主要来自于包 `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)` | 被注释的元素长度必须在 `min` 和 `max` 之间,可以是 String、Collection、Map、数组 | ### ✈ 日期检查 | 注解 | 说明 | | ------------------ | ------------------------------------ | | `@Future` | 被注释的元素必须是一个将来的日期 | | `@FutureOrPresent` | 被注释的元素必须是现在或者将来的日期 | | `@Past` | 被注释的元素必须是一个过去的日期 | | `@PastOrPresent` | 被注释的元素必须是现在或者过去的日期 | ### ✈ 其他检查 | 注解 | 说明 | | ------------------ | ------------------------------ | | `@Email` | 被注释的元素必须是电子邮箱地址 | | `@Pattern(regexp)` | 被注释的元素必须符合正则表达式 | > 除此之外,`org.hibernate.validator.constraints` 包下还有其他校验注解,例如 `@ISBN` 检查一个字符串是否是一个有效地 ISBN 序列号。 ## 🚀 参数校验 接下来开始体验 Spring Boot Validation。 首先,编写一个需要校验的实体类: ```java @Data public class Student { @NotBlank(message = "主键不能为空") private String id; @NotBlank(message = "名字不能为空") @Size(min=2, max = 4, message = "名字字符长度必须为 2~4个") private String name; @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 = "手机号格式错误") private String phone; @Email(message = "邮箱格式错误") private String email; @Past(message = "生日必须早于当前时间") private Date birth; @Min(value = 0, message = "年龄必须为 0~100") @Max(value = 100, message = "年龄必须为 0~100") private Integer age; @PositiveOrZero private Double score; } ``` 随后编写一个控制层代码,进行测试: ```java @RestController public class TestController { @GetMapping("/test") public Student test(@RequestBody @Validated Student student) { return student; } } ``` 使用 `postman` 进行测试,发送一个不带参数的请求,查看结果: ![image-20221123221059039](./assets/image-20221123221059039.png) 💡后端控制台日志打印是这样的(显示极度不友好),可以看到**校验规则生效**了: ```bash 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 [主键不能为空]] ] ``` ## 🚀 全局异常处理 查看上面的日志打印,可以看到当参数校验不通过时,会抛出异常 `MethodArgumentNotValidException`,同时也会打印那些**参数没有通过校验,以及该参数校验规则**。 为了方便查看,我们可以编写一个全局异常处理,处理这个**参数校验异常**,并使用统一返回实体返回给前端。 ```java @ControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public ResponseEntity exception(MethodArgumentNotValidException e, HttpServletRequest request) { Map result = new HashMap<>(); BindingResult bindingResult = e.getBindingResult(); log.error("请求[ {} ] {} 的参数校验发生错误", request.getMethod(), request.getRequestURL()); for (ObjectError objectError : bindingResult.getAllErrors()) { FieldError fieldError = (FieldError) objectError; log.error("参数 {} = {} 校验错误:{}", fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage()); result.put(fieldError.getField(), fieldError.getDefaultMessage()); } // 一般项目都会有自己定义的公共返回实体类,这里直接使用现成的 ResponseEntity 进行返回,同时设置 Http 状态码为 400 return ResponseEntity.badRequest().body(result); } } ``` 再次使用 `postman` 发起测试: ![image-20221123222000215](./assets/image-20221123222000215.png) 控制台打印出自定义的日志信息: ```bash 2022-11-23 22:16:37.800 ERROR 19880 --- [nio-8080-exec-2] c.e.s.handler.GlobalExceptionHandler : 请求[ GET ] http://localhost:8080/test 的参数校验发生错误 2022-11-23 22:16:37.800 ERROR 19880 --- [nio-8080-exec-2] c.e.s.handler.GlobalExceptionHandler : 参数 name = null 校验错误:名字不能为空 2022-11-23 22:16:37.800 ERROR 19880 --- [nio-8080-exec-2] c.e.s.handler.GlobalExceptionHandler : 参数 id = null 校验错误:主键不能为空 2022-11-23 22:19:36.594 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler : 请求[ GET ] http://localhost:8080/test 的参数校验发生错误 2022-11-23 22:19:36.594 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler : 参数 email = abc.com 校验错误:邮箱格式错误 2022-11-23 22:19:36.594 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler : 参数 score = -20 校验错误:必须是正数或零 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 校验错误:生日必须早于当前时间 2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler : 参数 phone = 12233 校验错误:手机号格式错误 2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler : 参数 age = -40 校验错误:年龄必须为 0~100 2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler : 参数 name = 我是很长的名字 校验错误:名字字符长度必须为 2~4个 2022-11-23 22:19:36.595 ERROR 19880 --- [nio-8080-exec-6] c.e.s.handler.GlobalExceptionHandler : 参数 score = -20 校验错误:需要在0和9223372036854775807之间 ``` ## 🚀 传递校验 我们也可以使用传递校验,即一个参数类中包含了另一个参数类,被包含的参数类也可以被校验。 在声明一个新的参数类,同时修改 `Student` 类。 ```java @Data public class ClassInfo { @NotBlank(message = "班主任姓名不能为空") private String teacher; @NotNull(message = "教师不能为空") private Integer classroom; @NotNull(message = "年级不能为空") @Min(value = 1, message = "年级只能是 1-6") @Max(value = 6, message = "年级只能是 1-6") private Integer grade; } @Data public class Student { //............. // 新加的字段,被包含的参数类,使用 @Valid 就能传递校验,如果不使用 @Valid 注解,则无法传递校验。 @Valid private ClassInfo classInfo; } ``` 再使用 `postman` 测试一次 ![image-20221123222955627](./assets/image-20221123222955627.png) ## 🚀 分组校验 此外还可以使用分组校验,令一组方法对某些字段校验,而令一组方法对其他字段校验,例如:一般情况下,新增实体的接口方法 `[POST]` 不需要主键 `ID`,修改实体的接口方法 `[PUT]` 就需要主键 `ID` 以便进行修改。 为注解 `@Validated` 赋值属性 `value`,以及为那些校验注解赋值属性 `group`, 即可达到分组的效果。 接下来看看如何实现分组校验。 在 `Student` 类中添加两个内部接口 `Inteface`,同时修改 id 字段的注解,以进行分组 ```java @Data public class Student { // id 字段属于 Create 组 @NotBlank(message = "主键不能为空", groups = {Student.Create.class}) private String id; // ............. // 更新分组 public interface Update {} // 创建分组 public interface Create {} } ``` 在控制层新增两个接口 ```java @RestController public class TestController { // @Validated 注解可以赋值 value 属性进行分组,value 是可以以数组的形式赋值,既可以分配多个组 @PostMapping("/students") public Student create(@RequestBody @Validated(Student.Create.class) Student student) { return student; } @PutMapping("/students") public Student update(@RequestBody @Validated(Student.Update.class) Student student) { return student; } } ``` 在 postman 上进行测试: ![image-20221123224108363](./assets/image-20221123224108363.png) ![image-20221123224045439](./assets/image-20221123224045439.png) 可以看到分组校验也生效了。 ## 🚀 总结 在实际开发中,我们可以使用 Spring Boot Validation 提供的注解进行参数校验,提高代码的可读性,避免编写大量的 `if-else` 代码块和重复的校验语句。