Bean Validation起源篇----01

x33g5p2x  于2022-07-26 转载在 其他  
字(6.3k)|赞(0)|评价(0)|浏览(222)

为什么需要数据校验

数据校验是Web开发中必不可少的一环,当一个Http请求发出开始,从前端到后端的控制层,再到业务层,再到数据访问层,最终到达数据库,这其中的每一环,都需要数据校验,可见其重要性。

因为,每一层都需要有进行数据校验的要求,但是,我们不可能真的在每一层都进行一遍数据校验,而是会在控制层或者业务层完成需要的数据校验即可。

一般会将请求参数封装为一个Model对象,然后对该对象进行数据校验接口,如果校验没有问题,就可以沿着工作流,不断传递下去,后续也就不需要进行数据校验了。

但是由于大型程序通常都是会分层的,不同的层如果由不同的程序员来开发的话,就不免会写多套数据校验逻辑,这样就会导致代码冗余,出现很多如下所示的重复代码:

public String queryValueByKey(String zhName, String enName, Integer age) {
    checkNotNull(zhName, "zhName must be not null");
    checkNotNull(enName, "enName must be not null");
    checkNotNull(age, "age must be not null");
    validAge(age, "age must be positive");
    ...
}

这其实就是传统的数据校验逻辑,它的缺点如下:

  • 大量校验代码掺杂在了业务代码之中,导致代码可读性降低,维护难度变高,代码臃肿不堪
  • 无法通过一眼看出方法的入参限制要求是什么,需要加以大量注释进行说明
  • 每个程序员做参数验证的方式可能不一样,参数验证抛出的异常也不一样,导致后期几乎没法维护

怎么解决呢?

  • 尝试将数据校验逻辑从业务层剥离出来,将数据校验逻辑和域对象Model绑定

这里先给出一个简单的例子:

public Boolean updateStu(Stu stu){
        stuDao.update(stu);
        return true;
     }

可以看到,上面并没有对stu对象的校验,那么将数据校验逻辑和域对象Model绑定,这是怎么完成的呢?

有很多种方法,这里给出一个最为easy的实现,如下:

@Data
public class Stu {
    Integer num;
    String name;

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        if(num<0){
            throw new IllegalArgumentException("num不能小于0");
        }
        this.num = num;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if(name!=null && !name.isEmpty()){
            throw new IllegalArgumentException("name不能为空");
        }
        this.name = name;
    }
}

只需求确保请求参数绑定到Model对象的过程是通过调用Model对象的setter方法完成的即可。

但是,上面这种写法也存在诸多的问题,例如: 数据校验的异常异常没有统一化,返回的错误结果格式也不统一,那么还能怎么优化呢?

下面就来看看Java 官方为我们提供的Bean Validation数据校验体系吧 !

Bean Validation的前世今生

Jakarta Bean Validation

Jakarta Bean Validation不仅仅是一个规范,它还是一个生态。

之前名为Java Bean Validation,2018年03月之后就得改名叫Jakarta Bean Validation,Bean Validation技术隶属于Java EE规范.

Bean Validation是标准,它的参考实现除了有我们熟悉的Hibernate Validator外还有Apache BVal,但是后者使用非常小众,忘了它吧。实际使用中,基本可以认为Hibernate Validator是Bean Validation规范的唯一参考实现,是对等的。

JSR303

这个JSR提出很早了(2009年),它为 基于注解的 JavaBean验证定义元数据模型和API,通过使用XML验证描述符覆盖和扩展元数据。JSR-303主要是对JavaBean进行验证,如方法级别(方法参数/返回值)、依赖注入等的验证是没有指定的。

作为开山之作,它规定了Java数据校验的模型和API,这就是Java Bean Validation 1.0版本。

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.0.0.GA</version>
</dependency>

该版本提供了常见的校验注解(共计13个):

所有注解均可标注在:方法、字段、注解、构造器、入参等几乎任何地方.

但是,以上所有注解对null是免疫的,也就是说如果你的值是null,是不会触发对应的校验逻辑的(也就说null是合法的),当然@NotNull / @Null除外.

JSR349

该规范是2013年完成的,伴随着Java EE 7一起发布,它就是我们比较熟悉的Bean Validation 1.1。

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>

相较于1.0版本,它主要的改进/优化有如下几点:

  • 标准化了Java平台的约束定义、描述、和验证
  • 支持方法级验证(入参或返回值的验证)
  • Bean验证组件的依赖注入
  • 与上下文和DI依赖注入集成
  • 使用EL表达式的错误消息插值,让错误消息动态化起来(强依赖于ElManager)
  • 跨参数验证。比如密码和验证密码必须相同

它的官方参考实现如下:

可以看到,Java Bean Validation 1.1版本实现对应的是Hibernate Validator 5.x(1.0版本对应的是4.x)

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.3.Final</version>
</dependency>

当你导入了hibernate-validator后,无需再显示导入javax.validation。hibernate-validator 5.x版本基本已停更,只有严重bug才会修复。因此若非特殊情况,不再建议你使用此版本,也就是不建议再使用Bean Validation 1.1版本,更别谈1.0版本喽。

小贴士:Spring Boot1.5.x默认集成的还是Bean Validation 1.1哦,但到了Boot 2.x后就彻底摒弃了老旧版本

JSR380

当下主流版本,也就是我们所说的Java Bean Validation 2.0和Jakarta Bean Validation 2.0版本。关于这两种版本的差异,官方做出了解释:

他俩除了叫法不一样、除了GAV上有变化,其它地方没任何改变。它们各自的GAV如下:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.1</version>
</dependency>

现在应该不能再叫Java EE了,而应该是Jakarta EE。两者是一样的意思,你懂的。Jakarta Bean Validation 2.0是在2019年8月发布的,属于Jakarta EE 8的一部分。它的官方参考实现只有唯一的Hibernate validator了:

此版本具有很重要的现实意义,它主要提供如下亮点:

  1. 支持通过注解参数化类型(泛型类型)参数来验证容器内的元素,如:List<@Positive Integer> positiveNumbers

  2. 更灵活的集合类型级联验证;例如,现在可以验证映射的值和键,如:Map<@Valid CustomerType, @Valid Customer> customersByType

  3. 支持java.util.Optional类型,并且支持通过插入额外的值提取器来支持自定义容器类型

  4. 让@Past/@Future注解支持注解在JSR310时间上

  5. 新增内建的注解类型(共9个):@Email, @NotEmpty, @NotBlank, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent和@FutureOrPresent

  6. 所有内置的约束现在都支持重复标记

  7. 使用反射检索参数名称,也就是入参名,详见这个API:ParameterNameProvider—很明显这是需要Java 8的启动参数支持的

  8. Bean验证XML描述符的名称空间已更改为:
    1.META-INF/validation.xml -> http://xmlns.jcp.org/xml/ns/validation/configuration
    2.mapping files -> http://xmlns.jcp.org/xml/ns/validation/mapping

  9. JDK最低版本要求:JDK 8

Hibernate Validator自6.x版本开始对JSR 380规范提供完整支持,除了支持标准外,自己也做了相应的优化,比如性能改进、减少内存占用等等,因此用最新的版本肯定是没错的,毕竟只会越来越好嘛。

新增注解:

相较于1.x版本,2.0版本在其基础上新增了9个实用注解,总数到了22个。现对新增的9个注解解释如下:

使用示例

导入实现包:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.5.Final</version>
</dependency>

校验Java Bean:

书写JavaBean和校验程序(全部使用JSR标准API哦):

@Data
public class Stu {
    @Min(value = 0)
    Integer num;
    @NotNull
    String name;
}
@Test
    public void testBeanValidator(){
        Stu stu = new Stu();
        stu.setNum(-1);

        // 1、使用【默认配置】得到一个校验工厂  这个配置可以来自于provider、SPI提供
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        // 2、得到一个校验器
        Validator validator = validatorFactory.getValidator();
        // 3、校验Java Bean(解析注解) 返回校验结果
        Set<ConstraintViolation<Stu>> result = validator.validate(stu);

        // 输出校验结果
        result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": "
                + v.getInvalidValue()).forEach(System.out::println);

    }

运行程序,不幸抛错:

Caused by: java.lang.ClassNotFoundException: javax.el.ELManager
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	...

上面说了,从1.1版本起就需要El管理器支持用于错误消息动态插值,因此需要自己额外导入EL的实现。

小贴士:EL也属于Java EE标准技术,可认为是一种表达式语言工具,它并不仅仅是只能用于Web(即使你绝大部分情况下都是用于web的jsp里),可以用于任意地方(类比Spring的SpEL)

这是EL技术规范的API:

<!-- 规范API -->
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>3.0.0</version>
</dependency>

Expression Language 3.0表达式语言规范发版于2013-4-29发布的,Tomcat 8、Jetty 9、GlasshFish 4都已经支持实现了EL 3.0,因此随意导入一个都可(如果是web环境,就需要导入了)。

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.22</version>
</dependency>

添加好后,再次运行程序,控制台正常输出校验失败的消息:

num 最小不能小于0: -1
name 不能为null: null

参考

1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知

相关文章