首页 > 其他分享 >Jakarta Bean Validation 规范介绍及其API使用以及与Spring Validator之间的关系

Jakarta Bean Validation 规范介绍及其API使用以及与Spring Validator之间的关系

时间:2024-05-28 16:33:53浏览次数:18  
标签:Spring 校验 Bean Validator Jakarta Validation public

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-303JSR-349JSR-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的规范,它可以

  1. 通过使用注解的方式在对象模型上表达约束;
  2. 以扩展的方式编写自定义约束;
  3. 提供了用于验证对象和对象图的API;
  4. 提供了用于验证方法和构造方法的参数和返回值的API;
  5. 报告违反约定的集合;

在 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 接口来自定义一个容器元素抽取器,然后通过ConfigurationaddValueExtractor()方法注册自定义 ValueExtractor。

其内置 Jakarta Bean Validation 3.0的内置约束有:

Constraint 描述 支持类型
@Null 被注释的元素必须为 null 任意类型
@NotNull 被注释的元素必须不为 null 任意类型
@AssertTrue 被注释的元素必须为 true booleanBoolean
@AssertFalse 被注释的元素必须为 false booleanBoolean
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 BigDecimalBigIntegerbyteshortintlong 以及各自的包装类
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 BigDecimalBigIntegerbyteshortintlong 以及各自的包装类
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包装类
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包装类
@Size(max, min) 被注释的元素的大小必须在指定的范围内 CharSequenceCollectionMapArray
@Negative 被标注元素必须为是一个严格意义上的负数(即0被认为是无效的) BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包装类
@NegativeOrZero 被标注元素必须为是负数或者0 BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包装类
@Positive 被标注元素必须为是一个严格意义上的正数(即0被认为是无效的) BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包装类
@Positive OrZero 被标注元素必须为是正数或者0 BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包装类
@Digits 被标注元素必须是在可接受范围内的数字 BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包装类
@Pattern 被标注的CharSequence必须匹配指定的正则表达式,该正则表达式遵循Java的正则表达式规定 CharSequence
@NotEmpty 被标注元素必须不为null或者空 CharSequenceCollectionMapArray
@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 image-20240528104229638

重点看一下AbstractBindingResultBindingResult的抽象类

image-20240528104500920

其中objectName是校验Bean的名称,errors就是抛出的异常,进一步看ObjectError的实现类FieldError

image-20240528105035668

field是违约的字段,rejectedValue是该字段显式定义的拒绝的值,bindingFailure表示是否为绑定失败,如果不是的话则说明是校验失败,进一步跟进ObjectError可以看到:

image-20240528110829904

其中objectName为绑定校验的对象名,source为违反约束的实例对象,继续看其父类消息处理类:

image-20240528111232635

其中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提供的注解,具体区别为:

  1. 注解使用的位置:@Validated可以用在类型、方法和方法参数上,但是不能用在成员属性上;而@Valid可以用在方法、构造函数、方法参数和成员属性上;
  2. 分组校验:@Validated提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制;
  3. 嵌套校验:二者均无法单独提供嵌套校验的功能,但是可以通过在引用属性上加@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

其中一个非常重要的类就是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

在 Spring MVC 中,HandlerMethodArgumentResolver一般会委派HttpMessageConverter从 HTTP 请求中解析出HandlerMethod所需要的方法参数值 (有了参数才能反射调用由@RestController注解标记的方法),然后进行 Bean Validation 操作。RequestResponseBodyMethodProcessor是极为重要的一个 HandlerMethodArgumentResolver 实现类,因为由@RequestBody标记的参数就由它解析。

image-20240528155809865

validateIfApplicable方法首先通过determineValidationHints方法判断注解是@Valid还是@Validated,然后调用dataBinder.validate方法校验参数。

image-20240528160057907 image-20240528161631280

最终还是通过SpringValidatorAdapter发送到 jakarta.validation.Validator<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups)方法,(BindingResult继承自Errors)

image-20240528161838294

参考文献:

[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

相关文章

  • Springboot计算机毕业设计学生考勤管理微信小程序【附源码】开题+论文+mysql+程序+部
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展,高校教学管理日益向数字化、智能化方向转变。传统的考勤管理方式不仅效率低下,而且容易出现误差,已无法满足现代高校管理的需求......
  • SpringBoot修改内置的Tomcat版本
    springboot内置tomcat各版本漏洞及修复情况参考链接:https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core打开项目,找到pom.xml文件找到对应节点,按以下步骤修改:1、pom添加tomcat版本信息   <properties>       <java.version>1.8</java.ver......
  • 基于SpringBoot+Vue+uniapp的互助学习的详细设计和实现(源码+lw+部署文档+讲解等)
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......
  • 基于SpringBoot+Vue+uniapp的考研论坛的详细设计和实现(源码+lw+部署文档+讲解等)
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......
  • 一次spring boot干净结构设计纪录
    前言其实以前对于java写出来的代码还是颇有微辞,特别是当写http服务的时候,写出来的代码难免有许多重复编写却又不得不写的代码,并且一些代码少了一些spring的味道,比如结果处理、日志纪录等,即使说得再明白,每一位同事写出来的都多多少少会偏离,失败和异常的界限很多时候......
  • springboot~封装依赖引用包jar还是pom,哪种更规范
    将多个第三方包封装成一个项目后,如果你的目的是让其他开发人员可以直接引用这些依赖,一般来说有两种常见的方式:打成JAR包:将封装好的项目编译打包成JAR文件,其他开发人员可以将这个JAR文件添加到他们的项目中,并在项目的构建工具(比如Maven)中配置该JAR作为依赖。这样做的好处是简单......
  • SpringBoot系列---【线程池优雅停机,避免消费数据丢数的问题】
    1.问题项目中通过kafka来对接上游,在项目中写一个listener监听topTopic队列,for循环消费records,在for循环中处理成存储到es的对象,一次拉50条,使用自定义线程池esThreadPool异步推送到es中,但是每次停机就会丢数据,例:kafka消费了1000条,但是往es中存储比较慢,优雅停机的时候,esThreadPool......
  • 基于java中的springboot框架实现医药管理系统项目演示【内附项目源码+论文说明】
    基于java中的springboot框架实现医药管理系统项目演示【内附项目源码+LW说明】摘要计算机网络发展到现在已经好几十年了,在理论上面已经有了很丰富的基础,并且在现实生活中也到处都在使用,可以说,经过几十年的发展,互联网技术已经把地域信息的隔阂给消除了,让整个世界都可以即......
  • 基于java中的springboot框架实现秒杀系统项目演示【内附项目源码+论文说明】
    基于java中的springboot框架实现秒杀系统项目演示【内附项目源码+LW说明】摘要社会发展日新月异,用计算机应用实现数据管理功能已经算是很完善的了,但是随着移动互联网的到来,处理信息不再受制于地理位置的限制,处理信息及时高效,备受人们的喜爱。本次开发一套基于SpringBoo......
  • DefaultListableBeanFactory+ GenericBeanDefinition
    定义与用途:GenericBeanDefinition:它是Spring框架中用于定义通用Bean的一个类。它继承自抽象类AbstractBeanDefinition,并增加了一个成员属性parentName。这个类主要用于存储Bean的配置信息,包括Bean的类名、作用域、属性等。DefaultListableBeanFactory:它是SpringIoC容器的一个......