一、背景
在前后端开发过程中,数据校验是一项必须且常见的事,从展示层、业务逻辑层到持久层几乎每层都需要数据校验。如果在每一层中手工实现验证逻辑,既耗时又容易出错。
图片
为了避免重复这些验证,通常的做法是将验证逻辑直接捆绑到领域模型中,通过元数据(默认是注解)去描述模型, 生成校验代码,从而使校验从业务逻辑中剥离,提升开发效率,使开发者更专注业务逻辑本身。
图片
在 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
Bean层面校验
- 变量层面约束
- 属性层面约束
主要为了限制 Setter 方法的只读属性。属性的 Getter 方法打注释,而不是 Setter。
- 容器元素约束
- 类层面约束
@CategoryBrandNotEmptyRecord 是自定义类层面的约束,也可以约束在构造函数上。
- 嵌套约束
嵌套对象需要额外使用 @Valid 进行标注(@Validate 不支持,为什么?请看产生的背景)。
- 手工验证Bean约束
方法层面校验
- 函数参数约束
- 函数返回值约束
- 嵌套约束
嵌套对象需要额外使用 @Valid 进行标注(@Validate 不支持)。
- 在继承中方法约束
Validation 的设计需要遵循里氏替换原则,无论何时使用类型 T,也可以使用 T 的子类型 S,而不改变程序的行为。即子类不能增加约束也不能减弱约束。
子类方法参数的约束与父类行为不一致(错误例子):
方法的返回值可以增加约束(正确例子):
- 手工验证方法约束
方法层面校验使用的是 ExecutableValidator。
分组校验
不同场景复用一个 Model,采用不一样的校验方式。
自定义校验
自定义注解:
自定义校验器:
使用自定义约束:
四、Bean Validation自动执行以及原理
上述 2.6 和 3.5 分别实现了 Bean 和 Method 层面的约束校验,但是每次都主动调用比较繁琐,因此 Spring 在 @RestController 的 @RequestBody 注解中内置了一些自动化校验以及在 Bean 初始化中集成了 AOP 来简化编码。
Validation的常见误解
最常见的应该就是在 RestController 中,校验 @RequestBody 指定参数的约束,使用 @Validated 或者 @Valid(该场景下两者等价)进行约束校验,以至于大部分人理解的 Validation 只要打个注解就可以生效,实际上这只是一种特例。很多人在使用过程中经常遇到约束校验不生效。约束校验生效
Spring-mvc 中在 @RequestBody 后面加 @Validated、@Valid 约束即可生效。
- 约束校验不生效
然而下面这个约束其实是不生效的,想要生效得在 MerchantEntryServiceImpl 类目加上 @Validated 注解。
那么究竟为什么会出现这种情况呢,这就需要对 Spring Validation 的注解执行原理有一定的了解。
Controller自动执行约束校验原理
在 Spring-mvc 中,有很多拦截器对 Http 请求的出入参进行解析和转换,Validation 解析和执行也是类似,其中 RequestResponseBodyMethodProcessor 是用于解析 @RequestBody 标注的参数以及处理 @ResponseBody 标注方法的返回值的。
约束的校验逻辑是在 RequestResponseBodyMethodProcessor.validateIfApplicable 实现的,这里同时兼容了 @Validated 和 @Valid,所以该场景下两者是等价的。
binder.validate() 的实现中使用的 org.springframework.validation.Validator 的接口,该接口的实现为 SpringValidatorAdapter。
在 ValidatorAdapter.validate 实现中,最终调用了 javax.validation.Validator.validate,也就是说最终是调用 JSR 实现,@Validate 只是外层的包装,在这个包装中扩展的分组功能。
targetValidator.validate 就是 javax.validation.Validator.validate 上述 2.6 Bean 层面手工验证一致。
Service自动执行约束校验原理
非Controller的@RequestBody注解,自动执行约束校验,是通过 MethodValidationPostProcessor 实现的,该类继承。
BeanPostProcessor, 在 Spring Bean 初始化过程中读取 @Validated 注解创建 AOP 代理(实现方式与 @Async 基本一致)。该类开头文档注解(JSR 生效必须类层面上打上 @Spring Validated 注解)。
真正执行方法调用时,会走到 MethodValidationInterceptor.invoke,进行约束校验。
execVal.validateParameters 就是 javax.validation.executable.ExecutableValidator.validateParameters 与上述 3.5 方法层面手工验证一致。
五、总结
图片
参考文章:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single