整合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());
     }
 ​
     //处理其它异常。。。
 }

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

相关推荐
why1515 小时前
微服务商城-商品微服务
数据库·后端·golang
結城8 小时前
mybatisX的使用,简化springboot的开发,不用再写entity、mapper以及service了!
java·spring boot·后端
星辰离彬8 小时前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
java·spring boot·后端·sql·mysql·性能优化
q_19132846958 小时前
基于Springboot+Vue的办公管理系统
java·vue.js·spring boot·后端·intellij idea
陪我一起学编程9 小时前
关于nvm与node.js
vue.js·后端·npm·node.js
舒一笑10 小时前
基于KubeSphere平台快速搭建单节点向量数据库Milvus
后端
JavaBuild10 小时前
时隔半年,拾笔分享:来自一个大龄程序员的迷茫自问
后端·程序员·创业
一只叫煤球的猫11 小时前
虚拟线程生产事故复盘:警惕高性能背后的陷阱
java·后端·性能优化
周杰伦fans11 小时前
C#中用于控制自定义特性(Attribute)
后端·c#
Livingbody12 小时前
GitHub小管家Trae智能体介绍
后端