Java Bean校验工具Hibernate Validator

2,236 阅读5分钟

Java Bean校验工具Hibernate Validator

为什么要进行数据校验

为了系统的鲁棒性,进行防御性编程,让系统更加的健壮,同时防止脏数据的产生。

而且使用参数校验可以让问题在一开始就被抛出来,符合fail-fast的理念,可以帮助我们更快的定位、解决异常和BUG,举个🌰可以看下这位老哥的遭遇👉Why Methods Should Check Their Arguments,使用构造方法创建一个对象的时候写错了两个String入参的位置,但是没有及时进行参数校验,将问题给隐藏了下来。最终导致其他地方使用到这个对象的代码有异常出现,而不是在创建这个对象的地方出抛出异常,增加了问题定位的链路和难度。

如何正确的进行数据校验

image-20210531083854113

数据校验是贯穿所有应用层(从表示层到持久化层)的一项常见任务。通常类似或者相同的数据校验逻辑会被实现在每一层中,这样既耗时又容易出错。比如像下面这种数据校验的代码,四处散落在自己的项目中,时间久了绝对不是一件好事:

if(a.size > 10 && a.size < 100){
    Result result = Reuslt.fail("非法参数size , 请检查输入!") ;
    return result;
}

if(xxx)
    return xxx ;

为了避免重复的校验代码(遵循DRY原则),比较推荐的数据校验方式是将对数据的约束逻辑直接放到数据的领域对象上。

image-20210531090051519

这样子的好处是每次校验数据的时候,不需要重复编写对数据的约束逻辑,而且约束逻辑内聚到领域对象中,也方便代码的维护。

引用Java Bean Validation官网首页的一句话“Constrain Once, validate everywhere”,即“只要配置一次约束,就可以在任何地方使用配置了的约束来进行校验。”👇

image-20210530223525222

Java Bean Validation正是遵循上述的原则而制定的一个Java官方的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08),已经经历了三个版本。

Java Bean Validation规范对应的jar包是jakarta.validation:jakarta.validation-api,包里面定义了用于实体和方法验证的注解和API,但是API没有具体的实现,它只是提供了一种数据校验的规范,需要第三方来实现它的API,来进行数据校验。

有哪些数据校验工具

  1. Hibernate Validator(JBoss)
  2. Apache BVal(Apache)

数据校验工具很多,在网上检索了下,满足有“大厂”背书,同时又实现了最新的Bean Validation2.0规范的数据校验工具无外乎Hibernate Validator和Apache BVal两个(可能还有满足条件的其他数据校验工具,只是我没找到。其他不是有名的“大厂”开发的数据校验工具找到了个Oval。Oval没有按照Bean Validation规范来实现数据校验,但是也是通过使用注解的方式来配置数据约束,而且提供了方式可以兼容Bean Validation的注解,有兴趣可以去看看它的文档,写的很详细,比Apache BVal详细🐶)。

这个时候问题来了,我们应该使用那个数据校验工具?

我们从“性能”、“社区活跃度与文档”两个方面来比较下上述的两款工具。

首先最重要的“性能”,Hiberante Validator(下文简述Hibernate Validator)官方在2018年发布了一组针对 Hibernate Validator6.0.9-SNAPSHOT、Hibernate Validator6.0.4.Final、Hibernate Validator 5.4.2.Final、Apache BVal 1.1.2的基准测试,结果如下:

image-20210531215234155

image-20210531215246460

从上述基准测试的结果可以得出两个最直观的结论:

  1. Hibernate Validator的性能比BVal高出了一大截。
  2. Hibernate Validator的性能随着版本的迭代在不断的增强。

感兴趣的同学可以看下这次基准测试的详细报告Bean Validation benchmark (re)revisited,相关的用于此次基准测试的代码在这个github仓库中hibernate/beanvalidation-benchmark

然后在“社区活跃度与文档”这个方面来说,感觉Hibernate Validator也是一马当先与BVal,Hibernate Validator Github仓库中的PR活跃度、贡献者数量、star数量、fork数量都远远高于BVal。平时我们在搜索引擎中搜索参数校验工具时,首先映入眼帘的也基本上都是Hibernate Validator。就连Springboot的默认的参数校验工具也是Hibernate Validator。

至此回到上面的那个问题:我们应该使用那个数据校验工具?

答案已经呼之欲出了🐶。

Hibernate Validator提供了哪些特性

  1. 基于注解(Bean Validation规范的注解和Hibernate Validator内建的注解看这里)的 约束配置与校验机制。
  2. 支持Class、Method(入参、返回值)(包括构造方法)、Field、Property级别的注解约束配置。
  3. 如果一个Method有多个入参,可以校验多个入参之间的关系是否符合约束。传送门🚪
  4. 支持校验嵌套约束,且支持循环依赖的场景。传送门🚪
  5. 支持校验容器类(Set、List、Map、java.util.Optional等)内部的对象,且可扩展(支持自己写的容器类)。传送门🚪
  6. 约束定义可以通过继承父类或者实现接口传递给子类或接口的实现类(对方法约束的override需要符合李氏替换原则)传送门🚪
  7. 支持自定义约束。传送门🚪
  8. 支持设置约束组。传送门🚪
  9. 支持约束编排。传送门🚪
  10. 约束规则支持使用脚本语言(只要项目中有引入对应的脚本引擎,通过实现这两个接口来实现扩展AbstractCachingScriptEvaluatorFactory,ScriptEvaluator,)传送门🚪
  11. 校验不通过时灵活的提示语配置方式。传送门🚪
  12. 丰富的可供扩展的HOOK TODO 要提供传送门

以上只是点出了Hibernate Validator的部分基础、有趣的特性,想了解更多的话去看👉它的官方文档吧。

在参数校验的过程中,最重要的就是要将验证不通过的约束稳准狠的提示出来,特别这个提示语是给用户看的时候话,所以下一章节会介绍下Hiberbate Validator支持的提示语配置方式。

Hibernate Validator配置校验不通过时的提示语

在Resource Bundle中配置提示语

Bean Validation的约束注解上,有一个message属性,用来配置这个注解的约束校验不通过时的提示语。比如@NotNull:

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {

	String message() default "{javax.validation.constraints.NotNull.message}";

	Class<?>[] groups() default { };

	Class<? extends Payload>[] payload() default { };

	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	@interface List {

		NotNull[] value();
	}
}

当使用@NotNull注解时,默认的提示语是"{javax.validation.constraints.NotNull.message}",Hibernate Validator会从自己内置的resource bundle中,找到这个花括号中的key对应的value,来作为提示语,而且也是支持国际化的: image-20210602082708705

除此之外,我们可以在自己项目中的classpath下添加自定义的resource bundle,默认的文件名为ValidationMessages.properties。如果想要支持国际化的话,可以添加其他语言对应的配置文件,比如ValidationMessages_en_US.properties。Hibernate Validator内部默认通过Locale#getDefault()方法的返回值来决定使用哪个语言的配置文件。

动态的提示语表达式

除了使用resource bundle来配置提示语,Hibernate Validator还支持动态的提示语表达式,形式如下:

    @Min(
            value = 2,
            message = "There must be at least {value} seat${value > 1 ? 's' : ''}"
    )
    private int seatCount;

{}中可以是如下的值:

  1. Resource bundle中的key(来获取key对应的value)。
  2. 注解中的属性名(来获取属性名对应的值)。

${}中的是符合 JSR 341规范的Unified Expression Language(EL表达式),在EL表达式中获取相关变量的方式有:

  1. 使用注解中的属性名(来获取属性名对应的值)
  2. 通过validatedValue来读取当前被校验的对象的值:${validatedValue}。
  3. 使用formatter.format(String format, Object… args)来格式化,效果类似于在java代码中调用这个方法java.util.Formatter.format(String format, Object… args)

举个例子:

public class Car {

    @NotNull
    private String manufacturer;

    @Size(
            min = 2,
            max = 14,
            message = "The license plate '${validatedValue}' must be between {min} and {max} characters long"
    )
    private String licensePlate;

    @Min(
            value = 2,
            message = "There must be at least {value} seat${value > 1 ? 's' : ''}"
    )
    private int seatCount;

    @DecimalMax(
            value = "350",
            message = "The top speed ${formatter.format('%1$.2f', validatedValue)} is higher " +
                    "than {value}"
    )
    private double topSpeed;

    @DecimalMax(value = "100000", message = "Price must not be higher than ${value}")
    private BigDecimal price;

    public Car(
            String manufacturer,
            String licensePlate,
            int seatCount,
            double topSpeed,
            BigDecimal price) {
        this.manufacturer = manufacturer;
        this.licensePlate = licensePlate;
        this.seatCount = seatCount;
        this.topSpeed = topSpeed;
        this.price = price;
    }

    //getters and setters ...
}

自定义提示语生成器

如果上述的两种配置生成提示语的方式,都无法满足你的要求,那么你可以来自定义提示语生成器。

自定义提示语生成器,需要实现Java Bean Validation规范的javax.validation.MessageInterpolator接口,然后在bootstrap(启动)javax.validation.ValidatorFactory的过程中将自己自定义实现的MessageInterpolator设置进去,就可以生效了。

import javax.validation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.bootstrap.GenericBootstrap;
import javax.validation.executable.ExecutableValidator;

GenericBootstrap genericBootstrap = Validation.byDefaultProvider();
Configuration<?> configure = genericBootstrap.configure();
configure.messageInterpolator(new MyMessageInterpolator() ) // 自定义实现的MessageInterpolator
ValidatorFactory factory = configure.buildValidatorFactory();

// 校验Java Bean 要用 validator
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<Car>> constraintViolationExceptions = validator.validate(car);

// 校验Method 要用 executableValidator 
ExecutableValidator executableValidator = validator.forExecutables();

除了可以自定义提示语生成器之外,Java Bean Validation和Hibernate Validation中还有很多接口可以让我们自定义实现,来干涉参数校验的整个过程,可以参见两者的这两个接口:javax.validation.Configuration,org.hibernate.validator.BaseHibernateValidatorConfiguration,以及Hibernate Validation官方文档的这一章节Bootstrapping

额外补充说明一点,要在项目中使用Hibernate Validation的话,除了要往项目中引入Hibernate Validation的包,还要往项目中引入Java Bean Validation的规范包。在实际校验对象编写代码的时候,我们只需要操作Java Bean Validation提供的规范API就能够使用Hibernate Validation去校验Java Bean,就像上面👆的代码一样,而不需要去直接操作Hibernate Validation的API。这是通过Java的SPI(Service Provider Interface)机制来实现的,通过SPI可以对已知的服务进行实现并以类似插件的方式进行加载(Hibernate Validation),供服务定义方(Bean Validation)进行调用。使用SPI的场景很多,比如Spring boot、Dubbo、JDBC、SLF4J等,感兴趣的同学可以去深入了解下。

Hibernate Validator与其他框架集成

Hibernate Validator可以通过AOP的方式与其他框架进行集成,比如它和Spring boot集成时,默认的方式是通过Spring的AOP实现。Spring AOP是通过运行时动态代理的方式来实现的。如果希望使用编译时字节码织入的方式来实现AOP,也可以使用AspectJ。两者本质都差不多,都是使用“代理”的思想,让使用者不用手动调用Bean Validator/Hibernat Validation的API来进行参数校验,由框架帮你封装好。

参考

Why Methods Should Check Their Arguments

Hibernate Validator 6.0.22.Final - JSR 380 Reference Implementation: Reference Guide(Hibernate Validator官方文档)

参数校验优雅实践

Bean Validation benchmark (re)revisited

Validating Form Input

SpringBoot源码解析之集成hibernate-validator验证框架

Java SPI机制详解

SLF4J实现原理(简单分析)

Dubbo SPI与自适应机制

Spring AOP——Spring 中面向切面编程

👍👍👍原生AspectJ用法分析以及Spring-AOP原理分析 我打你妈的,写的太好了

Intro to AspectJ

面试官:什么是AOP?Spring AOP和AspectJ的区别是什么?