整合Spring Validation实现更优雅的参数校验

相信每一名后端开发人员一定都在项目中写过参数校验的代码,不知道各位好兄弟们觉得这样一个个手敲校验逻辑方不方便

反正我个人是对此深恶痛绝,参数一多,if判断就开始泛滥成灾了,尤其大部分还都是相似代码,看久了人都要眼花了

一个简单的注册逻辑,你就需要敲一堆的判断逻辑,更别说业务中的那些更复杂的校验了

java 复制代码
 public class ValidateUtil {
     public static Result<Boolean> registerValidate(String userName,String email,String qq,String password,String code) {
         if(StrUtil.isAllBlank(userName,email,qq,password,code)) {
             return Result.fail(ErrorCode.NULL_PARAMS_ERROR, true, "注册信息存在空数据");
         }
         if(RegexUtil.isUserNameInvalid(userName)) {
             return Result.fail(ErrorCode.PARAMS_ERROR, true, "用户名格式不正确");
         }
         if(RegexUtil.isEmailInvalid(email)) {
             return Result.fail(ErrorCode.PARAMS_ERROR, true, "邮箱格式不正确");
         }
         if(RegexUtil.isQQInvalid(qq)) {
             return Result.fail(ErrorCode.PARAMS_ERROR, true, "QQ号格式不正确");
         }
         if(RegexUtil.isPasswordInvalid(password)) {
             return Result.fail(ErrorCode.PARAMS_ERROR, true, "密码格式不正确");
         }
         if(RegexUtil.isCodeInvalid(code)) {
             return Result.fail(ErrorCode.PARAMS_ERROR, true, "验证码格式不正确");
         }
         return Result.ok(false);
     }
 }

于是我就在网上开始浏览有没有更简洁、更方便的方式

事实上还真有,Java官方早就注意到了这个痛点,制定了一系列针对参数校验的一些规范

Bean Validation规范

Bean Validation就是Java官方制定的一项标准,只提供规范不提供实现,规定了一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints包下

更新至今,Bean Validation先后经历了1.0(JSR 303)、1.1(JSR 349)、2.0(JSR 380)这3个版本,目前项目中使用比较多的是Bean Validation 2.0,本篇文章讲解的内容也是基于Bean Validation 2.0版本

Bean Validation的主页:beanvalidation.org

相关版本的兼容性:

Bean Validation Hibernate validation JDK Spring Boot
1.0 (JSR 303) 4.3.1.Final 6+ 1.5.x
1.1 (JSR 349) 5.1.1.Final 7+ 1.5.x
2.0 (JSR 380) 6.0.1.Final 8+ 2.0.x
3.0 7.0.5.Final/8.0.0.Final 9+ 2.0.x

注意:3.0后Bean Validation改名为Jakarta Bean Validation 3.0了。 如果你的项目版本是jdk1.8的,不要使用hibernate-validator 7.0以上的版本,它里面的依赖的jakarta.validation-api:3.0是需要jdk1.9以上版本的部分支持的

Hibernate Validator

既然有了规范,那肯定也必须要有对应的实现方案

Hibernate Validator就是对Bean Validation的参考实现

按照官方的话来说:Hibernate Validator 允许表达和验证应用程序约束。默认元数据源是注释,能够通过使用 XML 进行覆盖和扩展。它不依赖于特定的应用程序层或编程模型,并且可用于服务器和客户端应用程序编程

对应2.0版本的依赖如下:

java 复制代码
 <dependency>
     <groupId>org.hibernate.validator</groupId>
     <artifactId>hibernate-validator</artifactId>
     <version>6.2.5.Final</version>
 </dependency>

使用示例

参照官方示例,最简单的使用方式就是对要校验类的属性添加校验注解,然后使用官方提供的工厂类创建校验对象,校验的结果是一个set结构的对象,该对象大小为0则说明校验无误,反之则需要遍历对象获取错误信息

java 复制代码
 //对要校验的类添加注解
 @Data
 public class Car {
     @NotNull()
     private String manufacturer;
 ​
     @NotNull
     @Size(min = 2, max = 14)
     private String licensePlate;
 ​
     @Min(2)
     private int seatCount;
 ​
     public Car(String manufacturer, String licencePlate, int seatCount) {
         this.manufacturer = manufacturer;
         this.licensePlate = licencePlate;
         this.seatCount = seatCount;
     }
 }
 ​
 //校验测试
 @SpringBootTest
 public class CarTest {
     @Test
     public void testCar() {
         //获取校验类
         ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
         Validator validator = factory.getValidator();
         //校验制造商不能为空
         Car car1 = new Car( null, "DD-AB-123", 4 );
         Set<ConstraintViolation<Car>> constraintViolations1 = validator.validate(car1);
         for (ConstraintViolation<Car> constraintViolation : constraintViolations1) {
             System.out.println(constraintViolation.getMessage());
         }
         //校验车牌在2到14位之间
         Car car2 = new Car( "Morris", "D", 4 );
         Set<ConstraintViolation<Car>> constraintViolations2 = validator.validate(car2);
         for (ConstraintViolation<Car> constraintViolation : constraintViolations2) {
             System.out.println(constraintViolation.getMessage());
         }
     }
 }
 ​
 //校验结果
 不能为null
 个数必须在2和14之间

相关校验注解

在Bean Validation2.0中共提供了22种校验注解,除此之外Hibernate Validator也扩展了几种常用的注解,在本节一起进行说明

分类 注解 说明
空/非空检查 @NULL 限制只能为NULL
@NotNull 限制必须不为NULL
@NotEmpty 验证注解的元素值不为Null且不为空(字符串长度不为0,集合大小不为0)
@NotBlack 验证注解的元素值不为空(不为Null,去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
Boolean值检查 @AssertFalse 限制必须为False
@AssertTrue 限制必须为True
长度检查 @Size(max,min) 限制字符长度必须在min到max之间
@Leanth 限制字符长度必须在min到max之间
日期检查 @Future 限制日期为当前时间之后
@FutureOrPresent 限制日期为当前时间或之后
@Past 限制日期为当前时间之前
@PastOrPresent 限制日期为当前时间或之前
数值检查 @Max(Value) 限制必须为一个不大于指定值的数字
@Min(Value) 限制必须为一个不小于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@DecimalMax(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为小数,且整数部分的位数不能超过Integer,小数部分的位数不能超过fraction
@Negative 限制必须为负整数
@NegativeOrZero(value) 限制必须为负整数或零
@Positive(value) 限制必须为正整数
@PositiveOrZero(value) 限制必须为正整数或零其他检查
其他检查 @Pattern(Value) 限制必须符合指定的正则表达式
@Email 限制必须为email格式

Spring Validation

虽然Hibernate Validator提供的校验方式已经很方便了,但每次还要自己去手动创建对象,手动获取校验结果,在现在这个SpringBoot项目横行的时代,难免还是有些不够优雅

要是能像SpringBoot中使用@Transactional事务注解一样就好了,在想要校验的地方加上注解自动校验,校验一旦出错就去自动获取错误日志

这时候就该Spring Validation出场了,和SpringBoot官方提供的其它starter包一样,完美满足上述需求,达到了真正的开箱即用

对应的依赖:

xml 复制代码
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>
 ​

快速入门

校验请求体

java 复制代码
 //依然是在需要校验类上添加注解
 @Data
 public class User {
     @NotBlank(message = "用户名不能为空字符串")
     private String username;
 ​
     @Size(min = 0,max = 100,message = "年龄范围要在0-100之间")
     private int age;
 ​
     @Email
     private String email;
 }
 //通过添加@Valid注解自动进行校验
 @RestController
 @RequestMapping("valid")
 public class ValidController {
     @PostMapping("/user")
     public String validate(@RequestBody @Valid User user) {
         return "校验通过";
     }
 }

在上面的示例中,@NotBlank 注解用于标记 username 字段需要满足非空条件。

@Valid 注解用于告知 Spring Validation 在数据绑定时进行验证。如果提交的用户数据不满足验证条件,Spring Validation 将会产生验证错误,可以在处理逻辑中捕获并处理这些错误。

通过使用 Spring Validation,你可以更方便地在 Spring Boot 应用程序中进行数据验证,增加了对输入数据有效性的保证

校验请求参数

除了对请求体进行校验,还可以校验被 @PathVariable 以及 @RequestParam 标记的方法参数

和校验请求体不一样,想要对请求参数校验需要额外在类上加上@Validated注解,这样才能开启校验功能

java 复制代码
 @RestController
 @RequestMapping("valid")
 @Validated
 public class ValidController {
     @GetMapping("/user")
     public String validate01(@Valid @RequestParam("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) {
         return "校验通过";
     }
 ​
     @GetMapping("/user/{id}")
     public String validate02(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) {
         return "校验通过";
     }
 }

@Valid 与 @Validated

事实上除了上述示例里的@Valid注解外,还有另外一个校验注解,也就是@Validated,这两个注解都能起到校验的作用,但在一些细节和使用场景上又有一些不同

  1. @Validated 注解:

    • @Validated 是 Spring 框架提供的验证注解,它在 Spring Core 模块中定义。
    • 主要用于验证方法参数和方法返回值,可以用于类级别或方法级别。
    • @Validated 支持分组验证(Group Validation),可以根据不同的验证场景选择不同的验证组。
    • @Validated 在 Spring MVC 控制器或 Spring Boot 的服务类中使用,可以激活 Spring 提供的验证功能,比如 JSR-303/JSR-349 Bean Validation。
    • 支持 Spring EL 表达式和 SpEL 校验,允许在验证逻辑中使用表达式。
  2. @Valid 注解:

    • @Valid 是 Java 标准(JSR-303/JSR-349)中定义的验证注解,用于 Bean Validation。
    • 主要用于验证方法参数、方法返回值、字段等。在 Spring Boot 或 Spring MVC 中,通常用于验证请求体中的对象数据。
    • @Valid 不支持分组验证,只能应用默认验证组。
    • @Valid 会触发标准的 Bean Validation 校验。
    • 在 Spring Boot 中,通常与 @RequestBody 一起使用,用于验证请求体中的数据。

在使用的时候,需要根据实际情况选择使用 @Validated 还是 @Valid

一般来说,如果项目中正在使用 Spring 框架的,并需要一些额外的验证特性(如分组验证、SpEL 表达式),那么可以使用 @Validated。如果只需要进行标准的 Bean Validation,那么可以使用 @Valid

需要注意的是,@Validated 是 Spring 特有的注解,而 @Valid 是标准的 Java Bean Validation 注解。

另外如果你的SpringBoot版本在2.3之前,那么你只需要引入spring-boot-starter-web 依赖就够了

Spring Boot 2.3之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要我们手动加上

自定义注解校验

虽然官方提供给我们的校验注解已经足够多了,但在现实中复杂的业务逻辑下,我们难免需要自己去定义校验规则注解去使用

还是沿用上面的User示例来讲解该如何自定义校验注解吧

假设现在我们有个需求,需要对用户的性别做校验,正常情况下用户性别应该只有男或女?

代码示例:

  1. 首先需要创建一个自定义的校验注解。这个注解需要添加 @Target@Retention@Constraint 这三个注解,指定它可以用于哪些元素(如字段、方法等)、何时有效(运行时、编译时等)、校验逻辑的具体实现类

    java 复制代码
     @Target({ElementType.FIELD})
     @Retention(RetentionPolicy.RUNTIME)
     @Constraint(validatedBy = SexValidator.class) // 指定具体验证逻辑的实现类
     @Documented
     public @interface SexValidation {
         //这部分可以参考官方注解的实现内容
         String message() default "性别错误";
     ​
         Class<?>[] groups() default {};
     ​
         Class<? extends Payload>[] payload() default {};
     }
  2. 根据@Constraint注解中指定的类,创建SexValidator类作为具体验证逻辑的实现类,该类需要实现ConstraintValidator接口

    java 复制代码
     public class GenderValidator implements ConstraintValidator<Gender, String> {
     ​
         @Override
         public void initialize(Gender constraintAnnotation) {
             //初始化逻辑,保证在使用此实例进行验证之前调用此方法
             ConstraintValidator.super.initialize(constraintAnnotation);
         }
     ​
         @Override
         public boolean isValid(String value, ConstraintValidatorContext context) {
             // 定义自定义的验证逻辑,性别只能是 "男" 或 "女"
             return "男".equals(value) || "女".equals(value);
         }
     }
  3. 上述步骤都搞定之后,就可以直接去使用了

    java 复制代码
     @Data
     public class User {
         @Gender(message = "性别必须是男或女")
         String gender;
         
         /...
     }

分组校验

Spring Validation还提供了分组校验的功能,支持在同一个实体类的不同字段在不同的场景下进行不同的校验。这种方式可以在不同的操作或业务场景下对数据进行定制化的校验,以满足不同需求

例如在开发中最常见的一个需求,通常添加用户时ID都是后端生成的,不需要进行校验,但在修改时需要

不使用分组校验也是可以实现的,只不过我们需要为不同的需求去定义不同的User类

代码示例如下:

  1. 创建分组类,在类中定义不同的分组接口

    java 复制代码
     public class GroupType{
     ​
         /**
          * 修改
          */
         public interface UpdateGroup{ };
     ​
         /**
          * 新增
          */
         public interface CreateGroup{ };
     }
  2. 在User类的id校验注解中设置groups属性指定在什么场景下生效

    java 复制代码
     @Data
     public class User {
         @NotNull(groups = GroupType.UpdateGroup.class)
         Integer id;
         
         //。。。
     }
  3. 最后还需要在Controller层使用@Validated注解设定分组

    java 复制代码
     @RestController
     @RequestMapping("valid")
     @Validated
     public class ValidController {
         @PostMapping("/user")
         public String validate(@RequestBody @Validated(GroupType.UpdateGroup.class) User user) {
             return "校验通过";
         }
     }

全局异常处理

还可以再进行优化,可以发现上述校验出错时都是报的异常,出错时报错信息都包裹在异常里面,而现在大多项目返回给前端的都是自己定义的一个结果返回对象,例如Result之类的

所以我们还可以使用@ControllerAdvice+@ExceptionHandler来全局处理参数校验,将校验的结果信息也一并返回给前端

首先依然是创建一个全局异常处理器,并添加@RestControllerAdvice注解

java 复制代码
 @RestControllerAdvice
 public class GlobalExceptionHandler {
     
 }

校验请求体时异常类是MethodArgumentNotValidException,校验请求参数时异常信息是ConstraintViolationException

对这两种异常信息进行拦截,去除message信息返回给前端

java 复制代码
 /**
  * 全局异常处理器
  */
 @Slf4j
 @RestControllerAdvice
 public class GlobalExceptionHandler {
     /**
      * 处理MethodArgumentNotValidException
      *
      * @param e
      * @return
      */
     @ExceptionHandler(MethodArgumentNotValidException.class)
     public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
         log.error("方法参数不正确", e);
         return Result.error(HttpStatus.BAD_REQUEST.value(),
                 e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
     }
 ​
     /**
      * 处理ConstraintViolationException
      *
      * @param e
      * @return
      */
     @ExceptionHandler(ConstraintViolationException.class)
     public Result handleConstraintViolationException(ConstraintViolationException e) {
         log.error("参数错误", e);
         return Result.error(HttpStatus.BAD_REQUEST.value(),
                 e.getConstraintViolations().iterator().next().getMessage());
     }
 ​
     //处理其它异常。。。
 }

如若文章有误,欢迎评论区指正!

相关推荐
鬼火儿4 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin4 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧5 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧5 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧5 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧5 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧6 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧6 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧6 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang6 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构