通用校验框架
背景
在互联网行业中,基于Java开发的业务类系统,不管是服务端还是客户端,业务逻辑代码的更新往往是非常频繁的,这源于功能的快速迭代特性。在一般公司内部,特别是使用Java web技术构建的平台中,不管是基于模块化还是服务化的,业务逻辑都会相对复杂。
这些系统之间、系统内部往往存在大量的API接口,这些接口一般都需要对入参(输入参数的简称)做校验,以保证:
- 核心业务逻辑能够顺利按照预期执行。
- 数据能够正常存取。
- 数据安全性。包括符合约束以及限制,有访问权限控制以及不出现SQL注入等问题。
开发人员在维护核心业务逻辑的同时,还需要为输入做严格的校验。当输入不合法时,能够给caller一个明确的反馈,最常见的反馈就是返回封装了result的对象或者抛出exception。
目标
-
验证逻辑与业务逻辑不再耦合
摒弃原来不规范的验证逻辑散落的现象。 -
校验器各司其职,好维护,可复用,可扩展
一个校验器(Validator)只负责某个属性或者对象的校验,可以做到职责单一,易于维护,并且可复用。 -
使用注解方式验证
可以装饰在属性上,减少硬编码量。 -
易扩展
可定制化开发校验器 -
统一的异常处理机制
对验证过程中发生的错误、异常进行统一的处理。
关键技术
1. Guava的Preconditions类
引入
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
使用
import com.google.common.base.Preconditions;
public class PreconditinosDemo {
public static void main(String[] args) {
boolean demo=5<0;
Preconditions.checkArgument(demo);
}
}
输出:
Exception in thread "main" java.lang.IllegalArgumentException
at com.google.common.base.Preconditions.checkArgument(Preconditions.java:128)
at com.guava.preconditions.PreconditinosDemo.main(PreconditinosDemo.java:15)
- 函数支持
函数 | 描述 | 失败异常 |
---|---|---|
checkArgument(boolean) | 检查boolean是否为真 | IllegalArgumentException |
checkNotNull(T) | 检查value不为null, 直接返回value | NullPointerException |
checkState(boolean) | 检查对象的一些状态,不依赖方法参数 | IllegalStateException |
checkElementIndex(int index, int size) | 检查index是否为在一个长度为size的list,string或array合法的范围 | IndexOutOfBoundsException |
checkPositionIndex(int index, int size) | 检查位置index是否为在一个长度为size的list,string或array合法的范围 | IndexOutOfBoundsException |
checkPositionIndexes(int start, int end, int size) | 检查[start, end)是一个长度为size的list,string或array合法的范围子集 | IndexOutOfBoundsException |
2. Assert
函数支持
函数 | 描述 |
---|---|
Assert.notNull(Object object) | 判断对象非空 |
Assert.isTrue(Object object) | 判断对象为true |
Assert.notEmpty(Collection collection) | 判断集合非空 |
Assert.hasLength(String) | 字符不为null且字符长度不为0 |
Assert.hasText(String text) | text不为null且必须至少包含一个非空的字符 |
Assert.isInstanceOf(Class class, Object object) | object必须为class指定的类 |
3. Bean Validation
Bean Validation2.0是JSR第308号标准,是Java定义的一套基于注解/xml的数据校验规范。目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR380的2.0版本。
它包含两部分Bean Validation API(规范)和HibernateValidator(实现)。Bean Validation 为 JavaBean 验证定义了相应的元数据模型和API。通过使用BeanValidation 或是自己定义的 constraint,例如 @NotNull, @Max, @ZipCode ,就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。BeanValidation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
- 注解支持
注解 | 描述 |
---|---|
@Null | 被注解的元素必须为Null |
@NotNull | 被注解的元素不行不为null |
@AssertTrue | 被注解的元素必须为true |
@AssertFalse | 被注解的元素必须为false |
@Min(value) | 被注解的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注解的元素必须是一个数字,其值必须小于等于指定的最小值 |
@DecimalMin(value) | 被注解的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注解的元素必须是一个数字,其值必须小于等于指定的最小值 |
@size(max, min) | 被注解的元素的大小必须在指定的范围内 |
@Digits(integer, fraction) | 被注解的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注解的元素必须是一个过去日期 |
@Future | 被注解的元素必须是一个将来日期 |
@Pattern(value) | 被注解的元素必须符合指定的正则表达式 |
bean validation 2.0 增加了如下注解:
注解 | 描述 |
---|---|
被注解的元素必须为合法邮箱 | |
@NotEmpty | 被注解的元素必须不为空 |
@NotBlank | 被注解的字符串不为blank |
@Positive | 被注解的元素必须是一个数字,其值必须>0 |
@PositiveOrZero | 被注解的元素必须是一个数字,其值必须>=0 |
@Negative | 被注解的元素必须是一个数字,其值必须<0 |
@NegativeOrZero | 被注解的元素必须是一个数字,其值必须<=0 |
@PastOrPresent | 被注解的元素必须是一个过去或现在日期 |
@FutureOrPresent | 被注解的元素必须是一个将来或现在日期 |
主流框架
Spring-validation
Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。Spring Boot整合JSR-303只需要添加一个starter即可,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
参数校验支持
- 类属性校验
在DTO字段上声明约束注解:
@Data
public class UserDTO {
private Long userId;
@NotNull
@Length(min = 2, max = 10)
private String userName;
@NotNull
@Length(min = 6, max = 20)
private String account;
@NotNull
@Length(min = 6, max = 20)
private String password;
}
在方法参数上声明校验注解:
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
- 方法参数校验
@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
@GetMapping("getByAccount")
public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) {
// 校验通过,才会执行业务逻辑处理
return Result.ok(userDTO);
}
}
- 分组校验
在实际项目中,可能多个方法需要使用同一个DTO类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在DTO类的字段上加约束注解无法解决这个问题。因此,spring-validation支持了分组校验的功能。
比如保存User的时候,UserId是可空的,但是更新User的时候,UserId的值必须>=10000000000000000L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:
约束注解声明适用的分组信息groups:
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {
Save.class, Update.class})
@Length(min = 2, max = 10, groups = {
Save.class, Update.class})
private String userName;
@NotNull(groups = {
Save.class, Update.class})
@Length(min = 6, max = 20, groups = {
Save.class, Update.class})
private String account;
@NotNull(groups = {
Save.class, Update.class})
@Length(min = 6, max = 20, groups = {
Save.class, Update.class})
private String password;
/**
* 保存的时候校验分组
*/
public interface Save {
}
/**
* 更新的时候校验分组
*/
public interface Update {
}
}
@Validated注解上指定校验分组
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
JSR303本身的@Valid并不支持分组校验,但是Spring在其基础提供了一个注解@Validated支持分组校验。@Validated这个注解value属性指定需要校验的分组。
- 嵌套校验
DTO类里面的字段除了基本数据类型,还有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。嵌套校验很简单,只需要在嵌套的实体属性标注@Valid注解,则其中的属性也将会得到校验,否则不会校验。
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {
Save.class, Update.class})
@Length(min = 2, max = 10, groups = {
Save.class, Update.class})
private String userName;
@NotNull(groups = {
Save.class, Update.class})
@Valid
private Job job;
@Data
public static class Job {
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull(groups = {
Save.class, Update.class})
@Length(min = 2, max = 10, groups = {
Save.class, Update