相信每一名后端开发人员一定都在项目中写过参数校验的代码,不知道各位好兄弟们觉得这样一个个手敲校验逻辑方不方便
反正我个人是对此深恶痛绝,参数一多,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格式 |
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
,这两个注解都能起到校验的作用,但在一些细节和使用场景上又有一些不同
-
@Validated
注解:@Validated
是 Spring 框架提供的验证注解,它在 Spring Core 模块中定义。- 主要用于验证方法参数和方法返回值,可以用于类级别或方法级别。
@Validated
支持分组验证(Group Validation),可以根据不同的验证场景选择不同的验证组。@Validated
在 Spring MVC 控制器或 Spring Boot 的服务类中使用,可以激活 Spring 提供的验证功能,比如 JSR-303/JSR-349 Bean Validation。- 支持 Spring EL 表达式和 SpEL 校验,允许在验证逻辑中使用表达式。
-
@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
示例来讲解该如何自定义校验注解吧
假设现在我们有个需求,需要对用户的性别做校验,正常情况下用户性别应该只有男或女?
代码示例:
-
首先需要创建一个自定义的校验注解。这个注解需要添加
@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 {}; }
-
根据
@Constraint
注解中指定的类,创建SexValidator
类作为具体验证逻辑的实现类,该类需要实现ConstraintValidator
接口javapublic 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); } }
-
上述步骤都搞定之后,就可以直接去使用了
java@Data public class User { @Gender(message = "性别必须是男或女") String gender; /... }
分组校验
Spring Validation
还提供了分组校验的功能,支持在同一个实体类的不同字段在不同的场景下进行不同的校验。这种方式可以在不同的操作或业务场景下对数据进行定制化的校验,以满足不同需求
例如在开发中最常见的一个需求,通常添加用户时ID都是后端生成的,不需要进行校验,但在修改时需要
不使用分组校验也是可以实现的,只不过我们需要为不同的需求去定义不同的User类
代码示例如下:
-
创建分组类,在类中定义不同的分组接口
javapublic class GroupType{ /** * 修改 */ public interface UpdateGroup{ }; /** * 新增 */ public interface CreateGroup{ }; }
-
在User类的id校验注解中设置groups属性指定在什么场景下生效
java@Data public class User { @NotNull(groups = GroupType.UpdateGroup.class) Integer id; //。。。 }
-
最后还需要在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());
}
//处理其它异常。。。
}
如若文章有误,欢迎评论区指正!