Jakarta Bean Validation 规范
1.Bean Validation的前世今生
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
。
![image-20240528104058574](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281040698.png)
![image-20240528104229638](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281042698.png)
重点看一下AbstractBindingResult
即BindingResult
的抽象类
![image-20240528104500920](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281045995.png)
其中objectName是校验Bean的名称,errors就是抛出的异常,进一步看ObjectError
的实现类FieldError
:
![image-20240528105035668](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281050748.png)
field是违约的字段,rejectedValue是该字段显式定义的拒绝的值,bindingFailure表示是否为绑定失败,如果不是的话则说明是校验失败,进一步跟进ObjectError可以看到:
![image-20240528110829904](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281108982.png)
其中objectName为绑定校验的对象名,source为违反约束的实例对象,继续看其父类消息处理类:
![image-20240528111232635](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281112741.png)
其中arguments是处理消息的参数,defaultMessage则是默认消息,会被赋予注解中的message字段值。
4.Bean Validation的使用
4.1 Spring MVC中如何使用
实体类:
@Data
public class Entity {
@NotBlank(message = "名称不能为空")
private String name;
@NotBlank(message = "描述不能为空")
private String description;
@NotNull(message = "类型不能为null")
private Byte type;
}
controller层:
@RestController
@RequestMapping("/demo")
@Validated
public class DemoController {
@PostMapping("create")
public String create(@Valid @RequestBody Entity entity) {
return "ok";
}
}
上述代码涉及到两个注解@Validated和@Valid,其中@Valid是JSR-303规范中的注解,@Validated是由Spring提供的注解,具体区别为:
- 注解使用的位置:@Validated可以用在类型、方法和方法参数上,但是不能用在成员属性上;而@Valid可以用在方法、构造函数、方法参数和成员属性上;
- 分组校验:@Validated提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制;
- 嵌套校验:二者均无法单独提供嵌套校验的功能,但是可以通过在引用属性上加@Valid注解实现对引用属性中的字段校验;
其中分组校验:
// 分组
public interface Group1{
}
public interface Group2{
}
// 实体类
public class Entity {
@NotNull(message = "id不能为null", groups = { Group1.class })
private int id;
@NotBlank(message = "用户名不能为空", groups = { Group2.class })
private String username;
}
// controller层
public String create(@Validated( { Group1.class }) Entity entity, BindingResult result) {
if (result.hasErrors()) {
return "validate error";
}
return "redirect:/success";
}
其中嵌套校验:
@RestController
public class DemoController {
@RequestMapping("/create")
public void create(@Validated Outer outer, BindingResult bindingResult) {
doSomething();
}
}
// 实体类
public class Outer {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@Valid
@NotNull(message = "inner不能为空")
private Inner inner;
}
// 引用属性
public class Inner {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@NotBlank(message = "name不能为空")
private String name;
}
4.2 Dubbo中如何使用
利用dubbo的拦截器扩展点,判断请求参数是否为自定义的Request类型,如果是的话调用validator.validate
方法校验参数,并将结果映射出一个属性路径拼接错误信息的list,最终将校验异常信息封装为失败响应返回。
@Activate(group = Constants.PROVIDER, before = {"DubboExceptionFilter"}, order = -20)
public class ValidatorDubboProviderFilter implements Filter {
private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Object[] arguments = invocation.getArguments();
if (arguments != null && arguments.length > 0 && arguments[0] instanceof Request<?> request) {
Object requestParams = request.getRequestParams();
Set<ConstraintViolation<Object>> errors = validator.validate(requestParams);
List<String> collect = errors.stream().map(error -> error.getPropertyPath() + "," + error.getMessage()).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(collect)) {
OpenApiResponse openApiResponse = new OpenApiResponse();
openApiResponse.setSuccess(false);
openApiResponse.setCode(DddCons.ValidateError);
openApiResponse.setMessage(String.join("||", collect));
if (request.getRequestId() != null) {
openApiResponse.setRequestId(request.getRequestId());
}
return AsyncRpcResult.newDefaultAsyncResult(openApiResponse ,invocation);
}
}
return invoker.invoke(invocation);
}
}
最后别忘了添加META-INF/dubbo/org.apache.dubbo.rpc.Filter
:
ValidatorDubboProviderFilter=com.xxx.infrastructure.tech.validator.dubbo.ValidatorDubboProviderFilter
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接口的实现中。
![image-20240528151130219](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281511348.png)
其中一个非常重要的类就是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 实例。
![image-20240528161416816](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281614934.png)
在 Spring MVC 中,HandlerMethodArgumentResolver
一般会委派HttpMessageConverter
从 HTTP 请求中解析出HandlerMethod
所需要的方法参数值 (有了参数才能反射调用由@RestController
注解标记的方法),然后进行 Bean Validation 操作。RequestResponseBodyMethodProcessor
是极为重要的一个 HandlerMethodArgumentResolver 实现类,因为由@RequestBody
标记的参数就由它解析。
![image-20240528155809865](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281558977.png)
validateIfApplicable方法首先通过determineValidationHints方法判断注解是@Valid还是@Validated,然后调用dataBinder.validate方法校验参数。
![image-20240528160057907](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281600985.png)
![image-20240528161631280](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281616364.png)
最终还是通过SpringValidatorAdapter发送到 jakarta.validation.Validator 的<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups)
方法,(BindingResult继承自Errors)
![image-20240528161838294](https://zhaobo-img.oss-cn-beijing.aliyuncs.com/202405281618384.png)
参考文献:
[1] Spring:全面拥抱 Jakarta Bean Validation 规范.https://cloud.tencent.com/developer/article/2322140
本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。
标签:Spring,校验,Bean,Validator,Jakarta,Validation,public From: https://www.cnblogs.com/zhaobo1997/p/18218342