Jakarta Bean Validation 漫谈
Bean Validation 漫谈
1.Bean Validation的前世今生
Jakarta Bean Validation 规范最早在 Oracle Java EE 下维护。2017 年 11 月,Oracle 将 Java EE 移交给 Eclipse 基金会。 2018 年 3 月 5 日,Eclipse 基金会宣布 Java EE (Enterprise Edition) 被更名为 Jakarta EE。随着JSR-303
、JSR-349
和JSR-380
提案的相继问世(分别对应Bean Validation 1.0、Bean Validation 1.1和Bean Validation 2.0)。
2020年7月发布的 Jakarta Bean Validation 3.0 在 Jakarta Bean Validation 2.0 的基础上,彻底将包命名空间迁移到 jakarta.validation,而不再是 javax.validation。当前版本为Jakarta Bean Validation 3.1,Maven坐标为:
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.0</version>
</dependency>
2.Bean Validation的介绍
Bean Validation 是一套Java的规范,它可以
- 通过使用注解的方式在对象模型上表达约束;
- 以扩展的方式编写自定义约束;
- 提供了用于验证对象和对象图的API;
- 提供了用于验证方法和构造方法的参数和返回值的API;
- 报告违反约定的集合;
在 Jakarta Bean Validation 规范中,有一些核心 API 如下:
Validator
,用于校验常规 Java Bean,同时支持分组校验;ExecutableValidator
,用于校验方法参数与方法返回值,同样支持分组校验。方法参数和方法返回值往往并不是一个常规 Java Bean,可能是一种容器,比如:List、Map 和 Optional 等;Java 8 针对ElementType
新增了一个 TYPE_USE 枚举实例,这让容器元素的校验变得简单,Jakarta Bean Validation API 中内置的注解式约束的头上均有 TYPE_USE 的身影。ConstraintValidator
,如果 Jakarta Bean Validation API 中内置的注解式约束不能满足实际的需求,则需要自定义注解式约束,同时还需要为自定义约束指定校验器,这个校验器需要实现 ConstraintValidator 接口。ValueExtractor
,容器并不仅仅指的是 JDK 类库中的 List、Map 和 Set 等,也可以是一些包装类,比如ResponseEntity
;如果要想校验 ResponseEntity 容器中的 body,那么就需要通过实现 ValueExtractor 接口来自定义一个容器元素抽取器,然后通过Configuration
的addValueExtractor()
方法注册自定义 ValueExtractor。
其内置 Jakarta Bean Validation 3.0的内置约束有:
Constraint | 描述 | 支持类型 |
---|---|---|
@Null |
被注释的元素必须为 null |
任意类型 |
@NotNull |
被注释的元素必须不为 null |
任意类型 |
@AssertTrue |
被注释的元素必须为 true |
boolean 、Boolean |
@AssertFalse |
被注释的元素必须为 false |
boolean 、Boolean |
@Min(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | BigDecimal 、BigInteger 、byte 、short 、int 、long 以及各自的包装类 |
@Max(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | BigDecimal 、BigInteger 、byte 、short 、int 、long 以及各自的包装类 |
@DecimalMin(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | BigDecimal 、BigInteger 、CharSequence 、byte 、short 、int 、long 以及各自的包装类 |
@DecimalMax(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | BigDecimal 、BigInteger 、CharSequence 、byte 、short 、int 、long 以及各自的包装类 |
@Size(max, min) |
被注释的元素的大小必须在指定的范围内 | CharSequence 、Collection 、Map 、Array |
@Negative |
被标注元素必须为是一个严格意义上的负数(即0被认为是无效的) | BigDecimal 、BigInteger 、byte 、short 、int 、long 、float 、double 以及各自的包装类 |
@NegativeOrZero |
被标注元素必须为是负数或者0 | BigDecimal 、BigInteger 、byte 、short 、int 、long 、float 、double 以及各自的包装类 |
@Positive |
被标注元素必须为是一个严格意义上的正数(即0被认为是无效的) | BigDecimal 、BigInteger 、byte 、short 、int 、long 、float 、double 以及各自的包装类 |
@Positive OrZero |
被标注元素必须为是正数或者0 | BigDecimal 、BigInteger 、byte 、short 、int 、long 、float 、double 以及各自的包装类 |
@Digits |
被标注元素必须是在可接受范围内的数字 | BigDecimal 、BigInteger 、CharSequence 、byte 、short 、int 、long 以及各自的包装类 |
@Pattern |
被标注的CharSequence 必须匹配指定的正则表达式,该正则表达式遵循Java的正则表达式规定 |
CharSequence |
@NotEmpty |
被标注元素必须不为null 或者空 |
CharSequence 、Collection 、Map 、Array |
@NotBlank |
被标注元素必须不为null ,并且必须包含至少一个非空格的字符 |
CharSequence |
@Email |
字符串必须是符合正确格式的电子邮件地址 | CharSequence |
详见: Jakarta Bean Validation 3.0
Jakarta Bean Validation 规范的唯一实现为 Hibernate Validator(官网:Hibernate Validator),会附加一些第三方的的约束,详见:2.3.2. Additional constraints
3.Bean Validation的违约处理
上述注解作用于字段或方法上时,对于违反约定的请求参数,Bean Validation会抛出异常:MethodArgumentNotValidException
。
重点看一下AbstractBindingResult
即BindingResult
的抽象类
其中objectName是校验Bean的名称,errors就是抛出的异常,进一步看ObjectError
的实现类FieldError
:
field是违约的字段,rejectedValue是该字段显式定义的拒绝的值,bindingFailure表示是否为绑定失败,如果不是的话则说明是校验失败,进一步跟进ObjectError可以看到:
其中objectName为绑定校验的对象名,source为违反约束的实例对象,继续看其父类消息处理类:
其中arguments是处理消息的参数,defaultMessage则是默认消息,会被赋予注解中的message字段值。
4.Bean Validation的使用
5.Spring Validator 与 Bean Validation的关系
5.1 Spring的Validator接口
在Spring Framework中有自己的Validator
接口,但是其API 设计的比较简陋,而且需要编写大量 Validator 实现类,与javax bean validation的注解式校验相比略显复杂,于是在Spring 3.0版本开始,Spring Validator将其所有校验请求转发至Jakarta Bean Validation接口的实现中。
其中一个非常重要的类就是SpringValidatorAdapter
,不仅实现了 SmartValidator
接口,同时也实现了jakarta.validation.Validator接口。(SmartValidator接口继承自spring的Validator接口)顾名思义,xxxAdapter就是适配层,所以SpringValidatorAdapter
的核心作用就是将校验请求转发给jakarta.validation.Validator
public class SpringValidatorAdapter implements SmartValidator, Validator {
@Nullable
private Validator targetValidator;
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
this.processConstraintViolations(this.targetValidator.validate(target, new Class[0]), errors);
}
}
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
this.processConstraintViolations(this.targetValidator.validate(target, this.asValidationGroups(validationHints)), errors);
}
}
}
//---------------------------------------------------------------------
// Implementation of JSR-303 Validator interface
//---------------------------------------------------------------------
public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
Assert.state(this.targetValidator != null, "No target Validator set");
return this.targetValidator.validate(object, groups);
}
5.2 Spring MVC 是如何进行 Bean 校验的
LocalValidatorFactoryBean
继承自 SpringValidatorAdapter,负责构建与配置 jakarta.validation.Validator 实例,并且实现了InitializingBean
接口,在后者 afterPropertiesSet() 方法内进行构建与配置 jakarta.validation.Validator 实例,然后通过 setTargetValidator() 方法为 SpringValidatorAdapter 注入 Bean Validation 实例。
在 Spring MVC 中,HandlerMethodArgumentResolver
一般会委派HttpMessageConverter
从 HTTP 请求中解析出HandlerMethod
所需要的方法参数值 (有了参数才能反射调用由@RestController
注解标记的方法),然后进行 Bean Validation 操作。RequestResponseBodyMethodProcessor
是极为重要的一个 HandlerMethodArgumentResolver 实现类,因为由@RequestBody
标记的参数就由它解析。
validateIfApplicable方法首先通过determineValidationHints方法判断注解是@Valid还是@Validated,然后调用dataBinder.validate方法校验参数。
最终还是通过SpringValidatorAdapter发送到 jakarta.validation.Validator 的<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups)
方法,(BindingResult继承自Errors)
参考文献:
[1] Spring:全面拥抱 Jakarta Bean Validation 规范.https://cloud.tencent.com/developer/article/2322140
本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。