一、背景
在前后端开发过程中,数据校验是一项必须且常见的事,从展示层、业务逻辑层到持久层几乎每层都需要数据校验。如果在每一层中手工实现验证逻辑,既耗时又容易出错。
为了避免重复这些验证,通常的做法是将验证逻辑直接捆绑到领域模型中,通过元数据(默认是注解)去描述模型, 生成校验代码,从而使校验从业务逻辑中剥离,提升开发效率,使开发者更专注业务逻辑本身。
在 Spring 中,目前支持两种不同的验证方法:Spring Validation 和 JSR-303 Bean Validation,即 @Validated(org . springframework.validation.annotation.Validated)和 @Valid(javax.validation.Valid)。两者都可以通过定义模型的约束来进行数据校验,虽然两者使用类似,在很多场景下也可以相互替换,但实际上却完全不同,这些差别长久以来对我们日常使用产生了较大疑惑,本文主要梳理其中的差别、介绍 Validation 的使用及其实现原理,帮助大家在实践过程中更好使用 Validation 功能。
二、Bean Validation简介
什么是JSR?
JSR 是 Java Specification Requests 的缩写,意思是 Java 规范提案。是指向 JCP(Java Community Process) 提出新增一个标准化技术规范的正式请求,以向 Java 平台增添新的 API 和服务。JSR 已成为 Java 界的一个重要标准。
JSR-303定义的是什么标准?
JSR-303 是用于 Bean Validation 的 Java API 规范,该规范是 Jakarta EE and JavaSE 的一部分,Hibernate Validator 是 Bean Validation 的参考实现。Hibernate Validator 提供了 JSR 303 规范中所有内置 Constraint 的实现,除此之外还有一些附加的 Constraint。(最新的为 JSR-380 为 Bean Validation 3.0)
常用的校验注解补充:
@NotBlank 检查约束字符串是不是 Null 还有被 Trim 的长度是否大于,只对字符串,且会去掉前后空格。
@NotEmpty 检查约束元素是否为 Null 或者是 Empty。
@Length 被检查的字符串长度是否在指定的范围内。
@Email 验证是否是邮件地址,如果为 Null,不进行验证,算通过验证。
@Range 数值返回校验。
@IdentityCardNumber 校验身份证信息。
@UniqueElements 集合唯一性校验。
@URL 验证是否是一个 URL 地址。
Spring Validation的产生背景
上文提到 Spring 支持两种不同的验证方法:Spring Validation 和 JSR-303 Bean Validation(下文使用@Validated和@Valid替代)。
为什么会同时存在两种方式?
Spring 增加 @Validated 是为了支持分组校验,即同一个对象在不同的场景下使用不同的校验形式。比如有两个步骤用于提交用户资料,后端复用的是同一个对象,第一步验证姓名,电子邮件等字段,然后在后续步骤中的其他字段中。这时候分组校验就会发挥作用。
为什么不合入到 JSR-303 中?
之所以没有将它添加到 @Valid 注释中,是因为它是使用 Java 社区过程(JSR-303)标准化的,这需要时间,而 Spring 开发者想让人们更快地使用这个功能。
@Validated 的内置自动化校验
Spring 增加 @Validated 还有另一层原因,Bean Validation 的标准做法是在程序中手工调用 Validator 或者 ExecutableValidator 进行校验,为了实现自动化,通常通过 AOP、代理等方法拦截技术来调用。而 @Validated 注解就是为了配合 Spring 进行 AOP 拦截,从而实现 Bean Validation 的自动化执行。
@Validated 和 @Valid 的区别
@Valid 是 JSR 标准 API,@Validated 扩展了 @Valid 支持分组校验且能作为 SpringBean 的 AOP 注解,在 SpringBean 初始化时实现方法层面的自动校验。最终还是使用了 JSR API 进行约束校验。
三、Bean Validation的使用
引入POM
// 正常应该引入hibernate-validator,是JSR的参考实现
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
// Spring在stark中集成了,所以hibernate-validator可以不用引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Bean层面校验
- 变量层面约束
public class EntryApplicationInfoCmd {
/**
* 用户ID
*/
@NotNull(message = "用户ID不为空")
private Long userId;
/**
* 证件类型
*/
@NotEmpty(message = "证件类型不为空")
private String certType;
}
- 属性层面约束
主要为了限制 Setter 方法的只读属性。属性的 Getter 方法打注释,而不是 Setter。
public class EntryApplicationInfoCmd {
public EntryApplicationInfoCmd(Long userId, String certType) {
this.userId = userId;
this.certType = certType;
}
/**
* 用户ID
*/
private Long userId;
/**
* 证件类型
*/
private String certType;
@NotNu