乐趣区

深入了解数据校验Bean-Validation-20JSR380

每篇一句

吾皇一日不退役,尔等都是臣子


<center> 对 Spring 感兴趣可扫码加入 wx 群:Java 高工、架构师 3 群(文末有二维码)</center>


前言

前几篇文章在讲 Spring 的数据绑定 的时候,多次提到过数据校验。可能有人认为数据校验模块并不是那么的重要,因为硬编码都可以做。若是这么想的话,那就大错特错了~
前面讲解 DataBinder 的时候一个小细节,它所在的包是:org.springframework.validation,并且在分析源码的时候能看到 DataBinder 它不仅能够完成数据绑定,也提供了对数据校验的支持且还保存了校验结果。

我以数据绑定 DataBinder 为引子引出了数据校验这一块,是想表明它的重要性。连 Java 都把它抽象成了 JSR 标准 进行提出,so 我认为这块是必修课,有必要了解本章的内容。

为什么要有数据校验?

数据校验 是非常常见的工作,在日常的开发中贯穿于代码的各个层次,从上层的 View 层到底层的数据层。

在此处有必要 再强调一句 :前面说了数据绑定并不属于 Spring MVC 的专利,同样的数据校验也不是只会发生在 web 层, 它可以在任意一层,从后面的示例中你会有更深的理解

在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你 必须 要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的(比如生日必须是过去时,年龄必须 >0 等等~)。

我们知道通常情况下程序肯定是 分层的 ,不同的层一般由不同的人来开发。若你是一个有经验的程序员,我相信你肯定见过在 不同的层了都出现了相同的校验代码,这就是某种意义上的垃圾代码。

public String queryValueByKey(String parmTemplateCode, String conditionName, String conditionKey, String resultName) {checkNotNull(parmTemplateCode, "parmTemplateCode not null");
    checkNotNull(conditionName, "conditionName not null");
    checkNotNull(conditionKey, "conditionKey not null");
    checkNotNull(resultName, "resultName not null");
    ...
}

从这个简单的方法入参校验至少能发现如下问题:

  1. 需要写 大量的 代码来进行参数验证。(这种代码多了就算垃圾代码)
  2. 需要通过注释 来知道每个入参的约束是什么(否则别人咋看得懂)
  3. 每个程序员做参数验证的方式不一样,参数验证不通过抛出的异常也不一样(后期几乎没法维护)

如上会导致代码冗余和一些 管理的问题(代码量越大,管理起来维护起来就越困难),比如说 语义的一致性 等。为了避免这样的情况发生,最好是将验证逻辑与相应的 域模型(领域模型的概念)进行绑定,这就是本文提供的一个新思路(其实是 JavaEE 提供的思路)

为了解决这个问题,Bean ValidationJavaBean 验证定义了相应的元数据模型和 API。默认的元数据是 各种 Java Annotations,当然也支持 xml 方式并且你也可以扩展~
可以说 Bean ValidationJavaBean的一个拓展,它可以布局于任意一层代码,不局限于 Web 应用还是端应用。

Java Bean Validation

JSR 是 Java Specification Requests 的缩写,意思是 Java 规范提案。关于数据校验这块,最新的是JSR380,也就是我们常说的Bean Validation 2.0

Bean Validation 2.0 是 JSR 第 380 号标准。该标准连接如下:https://www.jcp.org/en/egc/vi…
Bean Validation 的主页:http://beanvalidation.org
Bean Validation 的参考实现:https://github.com/hibernate/…

Bean Validation是一个通过配置注解来验证参数的框架,它包含两部分 Bean Validation API(规范)和Hibernate Validator(实现)。
Bean Validation 是 Java 定义的一套 基于注解 /xml的数据校验规范,目前已经从 JSR 303 的 1.0 版本升级到 JSR 349 的 1.1 版本,再到 JSR 380 的 2.0 版本(2.0 完成于 2017.08),已经经历了三个版本 (我截图如下:)

现在绝大多数 coder 使用者其实都还在使用 Bean Validation 1.1,毕竟一般来说它已经够用了~
本文会介绍 Bean Validation 2.0 提供的一些实用的新东西,毕竟 Java8 现在已成为主流,完全可以使用了~

简单 Demo 示例

要想使用它,首先就得导包嘛~ 根据经验,和 JCache 类似 Java 只提供了规范,并没有提供实现,所以我们可以先找到它的 API 包然后导入:

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

关于版本之间的差异其实不是本文说明的重点,毕竟 2.0 做到了很好的向下兼容,使用起来是无缝的。
但是本处还是给个 1.1 版本和 2.0.1 的截图,感官上简单对比一下区别:

兼容性表格

Bean Validation Hibernate Validation JDK Spring Boot
1.1 5.4 + 6+ 1.5.x
2.0 6.0 + 8+ 2.0.x

关于 Bean Validation 2.0 的关注点(新特性)

因为 2.0 推出的时间 确实不算长,so 此处我把一些重要的关注点列举如下:

  1. 对 Java 的最低版本要求是 Java 8
  2. 支持 容器的校验,通过 TYPE_USE 类型的注解实现对容器内容的约束:List<@Email String>
  3. 支持日期 / 时间的校验,@Past@Future
  4. 拓展元数据(新增注解):@Email,@NotEmpty,@NotBlank,@Positive,@PositiveOrZero,@Negative,@NegativeOrZero,@PastOrPresent 和 @FutureOrPresent

        1. 像 `@Email、@NotEmpty、@NotBlank` 之前是 Hibernate 额外提供的,2.0 标准后 hibernate 自动退位让贤并且标注为过期了
  5. Bean Validation 2.0的唯一实现为Hibernate Validator。(其实还有Apache BVal,但是你懂的,forget it)
  6. 对于Hibernate Validator,它自己也扩展了一些注解支持。

        1. **6.0 以上 ** 版本新增(对应标准 2.0 版本):`@UniqueElements、@ISBN、@CodePointLength`
        2. **6.0 以下 ** 版本可以使用的:`@URL、@ScriptAssert、@SafeHtml、@Range、@ParameterScriptAssert、@Mod11Check、@Mod10Check、@LuhnCheck、@Length、@EAN、@Currency、@CreditCardNumber、@ConstraintComposition、`
        3. `Hibernate Validator` 默认会校验完所有的属性,然后 ** 返回所有的 ** 验证 ` 失败信息 `。开启 fail fast mode 后,只要有一个验证失败,则返回验证失败信息。

so,对于 Java Bean Validation 的实现落地产品就没啥好选的,导入 Hibernate Validator(最新版本) 吧:

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

== 小细节:==

可以看到,导入了 hibernate-validator 就必要再自己导入 Java Bean ValidationAPI 了, 因此建议不用再手动导入 API,交给内部来管理依赖。

定义一个待校验的普通 JavaBean:

@Getter
@Setter
@ToString
public class Person {

    // 错误消息 message 是可以自定义的
    @NotNull(message = "名字不能为 null")
    public String name;
    @Positive
    public Integer age;

    @NotNull
    @NotEmpty
    private List<@Email String> emails;
    @Future
    private Date start;

}

书写测试用例:

    public static void main(String[] args) {Person person = new Person();
        //person.setName("fsx");
        person.setAge(-1);
        // email 校验:虽然是 List 都可以校验哦
        person.setEmails(Arrays.asList("fsx@gmail.com", "baidu@baidu.com", "aaa.com"));
        //person.setStart(new Date()); //start 需要是一个将来的时间: Sun Jul 21 10:45:03 CST 2019
        //person.setStart(new Date(System.currentTimeMillis() + 10000)); // 校验通过

        // 对 person 进行校验然后拿到结果(显然使用时默认的校验器)会保留下校验失败的消息
        Set<ConstraintViolation<Person>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(person);
        // 对结果进行遍历输出
        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)
...

可以看到运行必须依赖于 javax.el 这个包。(其实我是比较费解的,为何校验框架非得依赖它呢?有小伙伴可以帮忙解释一下吗?)

那行,导入依赖 javax.el 以及它的实现:

<!-- 注意这里导入的是 Apr, 2013 发布的 el3.x 的版本,但是 glassfish 并没有对此版本进行支持了  当然 tomcat 肯定是支持的 -->
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>3.0.1-b06</version>
</dependency>
<!-- servlet 容器大都对 el 有实现(支持 jsp 的都对此有实现),比如 tomcat/glassfish 等 -->
<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>javax.el</artifactId>
    <version>2.2.6</version>
</dependency>

需要注意的是,网上大都建议导入 org.glassfish.web 包。但是 EL3.0 后它并没有再提供支持了,因此我个人是不建议使用它,而是使用下面 tomcat 的实现的~

关于 EL 的实现此处啰嗦一句:JavaEE 并没有提供 el 的实现,需要容器自行提供,比如上面你想要导入最为流行的tomcat,你可以导入如下 jar 即可:

<!-- 嵌入式的 tomcat -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.22</version>
</dependency>
<!-- 传统的 tomcat(需要注意的是:传统的 tomcat 这种 jar 是不需要你手动导入的,tomcat 自带的)-->
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper-el</artifactId>
    <version>9.0.22</version>
    <scope>provided</scope>
</dependency>

此处还需要说明一点的是:嵌入式 tomcat(比如 SpringBoot 环境)若要使用时需要显示导入的。但是传统 tomcat 中你若要使用是不用自己导入的(tomcat 自带此 jar)。

但是,但是,但是自从 tomcat8.5 后不再自带 jsper-el 的包了,需要手动导入。(tomcat7 还是有的~

== 最佳实践:==
一般来说,javax.el-api以及 validation-api 都是没有必要单独导入的,第三方包都会自带。所以绝大数情况下,我们只需要这么导入即可正常 work,形如下面这样非常赶紧整洁:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.17.Final</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.22</version>
</dependency>
此处可能有伙伴会问:为何自己在使用的时候从来都没有导入过 EL 相关 Jar 包,也能正常数据校验呢?

答:那是因为绝大多数情况下 你使用 @Valid 是使用在 Spring MVC 上,它是不依赖于 EL 方式的,下篇文章会详细说明关于数据校验在 Spring 上的使用。而本文主要还是讲解 API 的方式~



经过一番导包后,再次运行打印如下(方式一、方式二结果一致):

name 名字不能为 null: null //  此处错误消息是自己的自定义内容
age 必须是正数: -1
emails[2].<list element> 不是一个合法的电子邮件地址: aaa.com

这样通过 API 调用的方式就完成了对这个 JavaBean 的属性校验~

核心 API 分析

Validation

官方给它的定义为:This class is the entry point for Bean Validation.它作为校验的入口,有三种方式来启动它:

  1. 最简单方式:使用默认的ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); 虽然是默认的单也会有如下 2 种情况:

        1. 若使用了 xml 配置了一个 provider,那就会使用这个 provider 来提供 Factory 
        2. 若没有 xml 或者 xml 力没有配置 provider,那就是用默认的 `ValidationProviderResolver` 实现类来处理
  2. 方式二:选择自定义的 ValidationProviderResolver 来跟 XML 配置逻辑选出一个 ValidationProvider 来。大致代码如下:
Configuration configuration = Validation.byDefaultProvider()
        .providerResolver(new MyResolverStrategy()) // 自定义一个 ValidationProviderResolver 的实现类
        .configure();
ValidatorFactory factory = configuration.buildValidatorFactory();
  1. 第三种方式就更加自由了:你可以 直接提供 一个 类型安全 ValidationProvider实现。比如 HibernateValidator 就是一个 ValidationProvider 的实现:
HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class)
        // .providerResolver(...) // 因为制定了 Provider,这个参数就可选了
        .configure()
        .failFast(false);
ValidatorFactory validatorFactory = configuration.buildValidatorFactory();

这三种初始化方式,在源码处就是对应提供的三个 public static 方法:

public class Validation {

    // 方式一
    public static ValidatorFactory buildDefaultValidatorFactory() {return byDefaultProvider().configure().buildValidatorFactory();
    }
    // 方式二
    public static GenericBootstrap byDefaultProvider() {return new GenericBootstrapImpl();
    }
    // 方式三
    public static <T extends Configuration<T>, U extends ValidationProvider<T>> ProviderSpecificBootstrap<T> byProvider(Class<U> providerType) {return new ProviderSpecificBootstrapImpl<>( providerType);
    }
    ...
}

对于若你想使用 xml 文件独立配置校验规则,可以使用 Configuration.addMapping(new FileInputStream(validationFile));,现在很少这么使用,略~
使用注意事项:ValidatorFactory被创建后应该缓存起来再提供使用,因为它是县城安全的。

因为现在都会使用 Hibernate-Validation 来处理校验,因此此处只关心方式三~

HibernateValidatorConfiguration

此接口表示配置,继承自标注接口 javax.validation.Configuration。很明显,它是HibernateValidator 的专属配置类

先看顶级接口:javax.validation.Configuration,为构建 ValidatorFactory 的配置类。默认情况下,它会读取配置文件 META-INF/validation.xml,Configuration 提供的 API 方法是覆盖 xml 配置文件项的。若没有找到validation.xml,就会使用默认的ValidationProviderResolver 也就是:DefaultValidationProviderResolver

public interface Configuration<T extends Configuration<T>> {
    // 该方法调用后就不会再去找 META-INF/validation.xml 了
    T ignoreXmlConfiguration();
    // 消息内插器  它是个狠角色,关于它的使用场景,后续会有详解(包括 Spring 都实现了它来做事)// 它的作用是:插入给定的约束冲突消息
    T messageInterpolator(MessageInterpolator interpolator);
    // 确定 bean 验证提供程序是否可以访问属性的协定。对每个正在验证或级联的属性调用此约定。(Spring 木有实现它)// 对每个正在验证或级联的属性都会调用此约定
    // Traversable:可移动的
    T traversableResolver(TraversableResolver resolver);
    // 创建 ConstraintValidator 的工厂
    // ConstraintValidator:定义逻辑以验证给定对象类型 T 的给定约束 A。(A 是个注解类型)
    T constraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory);
    // ParameterNameProvider:提供 Constructor/Method 的方法名们
    T parameterNameProvider(ParameterNameProvider parameterNameProvider);
    // java.time.Clock 用作判定 @Future 和 @Past(默认取值当前时间)// 若你希望他是个逻辑实现,提供一个它即可
    // @since 2.0
    T clockProvider(ClockProvider clockProvider);
    // 值提取器。这是 add 哦~ 负责从 Optional、List 等这种容器里提取值~
    // @since 2.0
    T addValueExtractor(ValueExtractor<?> extractor);
    // 加载 xml 文件
    T addMapping(InputStream stream);
    // 添加特定的属性给 Provider 用的。此属性等效于 XML 配置属性。// 此方法通常是框架自己分析 xml 文件得到属性值然后放进去,调用者一般不使用(当然也可以用)T addProperty(String name, String value);
    
    // 下面都是 get 方法喽
    MessageInterpolator getDefaultMessageInterpolator();
    TraversableResolver getDefaultTraversableResolver();
    ConstraintValidatorFactory getDefaultConstraintValidatorFactory();
    ParameterNameProvider getDefaultParameterNameProvider();
    ClockProvider getDefaultClockProvider();
    BootstrapConfiguration getBootstrapConfiguration(); // 整个配置也可返回出去

    // 上面都是工作,这个方法才是最终需要调用的:得到一个 ValidatorFactory
    ValidatorFactory buildValidatorFactory();}

该接口提供了一些标准的配置项。在实际应用中都是使用Hibernate Validation,所以再看看这个具体的子接口:

public interface HibernateValidatorConfiguration extends Configuration<HibernateValidatorConfiguration> {

    // 这批属性,证明直接可以通过 System 属性值来控制,大大地方便~
    // 这个机制快速失败机制:true 检查完一个有错误就返回,false 全部检查完把错误消息一起返回   默认 false
    String FAIL_FAST = "hibernate.validator.fail_fast"; 
    String ALLOW_PARAMETER_CONSTRAINT_OVERRIDE = "hibernate.validator.allow_parameter_constraint_override";
    String ALLOW_MULTIPLE_CASCADED_VALIDATION_ON_RESULT = "hibernate.validator.allow_multiple_cascaded_validation_on_result";
    String ALLOW_PARALLEL_METHODS_DEFINE_PARAMETER_CONSTRAINTS = "hibernate.validator.allow_parallel_method_parameter_constraint";
    // @since 5.2
    @Deprecated
    String CONSTRAINT_MAPPING_CONTRIBUTOR = "hibernate.validator.constraint_mapping_contributor";
    // @since 5.3
    String CONSTRAINT_MAPPING_CONTRIBUTORS = "hibernate.validator.constraint_mapping_contributors";
    // @since 6.0.3
    String ENABLE_TRAVERSABLE_RESOLVER_RESULT_CACHE = "hibernate.validator.enable_traversable_resolver_result_cache";
    // @since 6.0.3  ScriptEvaluatorFactory:执行脚本
    @Incubating
    String SCRIPT_EVALUATOR_FACTORY_CLASSNAME = "hibernate.validator.script_evaluator_factory";
    // @since 6.0.5 comparing date/time in temporal constraints. In milliseconds.
    @Incubating
    String TEMPORAL_VALIDATION_TOLERANCE = "hibernate.validator.temporal_validation_tolerance";

    // ResourceBundleMessageInterpolator 用于 load resource bundles
    ResourceBundleLocator getDefaultResourceBundleLocator();
    // 创建一个 ConstraintMapping:通过编程 API 配置的约束映射
    // 设置映射后,必须通过 addMapping(constraintmapping)将其添加到此配置中。ConstraintMapping createConstraintMapping();
    // 拿到所有的值提取器  @since 6.0
    @Incubating
    Set<ValueExtractor<?>> getDefaultValueExtractors();

    // 往下就开始配置了~~~~~~~~~~
    HibernateValidatorConfiguration addMapping(ConstraintMapping mapping);
    HibernateValidatorConfiguration failFast(boolean failFast);
    // used for loading user-provided resources:
    HibernateValidatorConfiguration externalClassLoader(ClassLoader externalClassLoader);
    // true:表示允许覆盖约束的方法。false 表示不予许(抛出异常)默认值是 false
    HibernateValidatorConfiguration allowOverridingMethodAlterParameterConstraint(boolean allow);
    // 定义是否允许对返回值标记多个约束以进行级联验证。默认是 false
    HibernateValidatorConfiguration allowMultipleCascadedValidationOnReturnValues(boolean allow);
    // 定义约束的 ** 并行方法 ** 是否应引发 ConstraintDefinitionException
    HibernateValidatorConfiguration allowParallelMethodsDefineParameterConstraints(boolean allow);
    // 是否允许缓存 TraversableResolver  默认值是 true
    HibernateValidatorConfiguration enableTraversableResolverResultCache(boolean enabled);
    // 设置一个脚本执行器
    @Incubating
    HibernateValidatorConfiguration scriptEvaluatorFactory(ScriptEvaluatorFactory scriptEvaluatorFactory);
    // 允许在时间约束中比较日期 / 时间时设置可接受的误差范围
    // 比如 @Past @PastOrPresent @Future @FutureOrPresent
    @Incubating
    HibernateValidatorConfiguration temporalValidationTolerance(Duration temporalValidationTolerance);
    // 允许设置将传递给约束验证器的有效负载。如果多次调用该方法,则只传播最后传递的有效负载。@Incubating
    HibernateValidatorConfiguration constraintValidatorPayload(Object constraintValidatorPayload);
}

关于此接口的唯一实现类:ConfigurationImpl,这里就不用再做分析了,因为对于 Validation 这块,咱们 面向接口编程 是完全没有问题的~

准备好了 Configuration 后,下一步显然就是 configuration.buildValidatorFactory() 来得到一个 ValidatorFactory 喽,关于 ValidatorFactory 这块的内容,请听下文分解~

总结

该文讲解是关于 Bean Validation 数据校验,在现在 Spring 的高度封装下,越来越少的人能够主动去发现 Java 实现 / 标准了~
实际上 Spring 的强大并不是自己创造了多少轮子,而是它主要是带来了 更为简单的抽象,从而减少样板代码、促进解耦、提高可单测性。因此对于有些常用的功能还是建议稍微了解多一点,做到心中有数,运用起来也才会更加的游刃有余

知识交流

若文章格式混乱,可点击:原文链接 - 原文链接 - 原文链接 - 原文链接 - 原文链接

==The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被 作者本人许可的~==

** 若对技术内容感兴趣可以加入 wx 群交流:Java 高工、架构师 3 群
若群二维码失效,请加 wx 号:fsx641385712(或者扫描下方 wx 二维码)。并且备注:"java 入群" 字样,会手动邀请入群 **

退出移动版