Depth understanding of data validation: Bean Validation 2.0 (JSR380)

Each one

> Save the Queen one day of retirement, Haier are courtiers

Spring may be interested in adding wx scan code group: `Java senior engineers, architects' group 3 (the end the two-dimensional code)

Foreword

The previous articles talking about Spring's data binding when repeatedly mentioned data validation. Some people may think that the data check module is not so important, because the hard-coded can do. If think so, it would be wrong -
the previous explanation DataBinder, when a small detail, it is in the package: org.springframework.validationand in the analysis of the source code was only able to see DataBinderit not only able to complete the data binding, also provides data check the support and also save the calibration results.

I data binding DataBinderleads to data validation is prelude this one, is to show its importance. Java abstract became even regarded it JSR标准were put forward, so I think this is compulsory, it is necessary to understand the contents of this chapter.

Why should data validation?

Data check is a very common operation, daily throughout the development of the code in each level from the upper layer to the underlying data View layer.

Here it is necessary to re-emphasize one : in front of that data binding does not belong Spring MVC patent, the same data validation is not only happen in the web layer, it can be at any level , from the back of your example there will be a deeper understanding

At any time, when you have to deal with an application's business logic, data validation is that you 必须have to consider and confront things . Applications must by some means to ensure that the input data is coming from in terms of semantically correct (such as birthdays must be past the age must be> 0 ~ etc.).

We know that under normal circumstances the program must be layered , different layers generally be developed by different people. If you are an experienced programmer, I'm sure you've seen the different layers have appeared in the same check code , which is a sense of junk code.

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");
    ...
}

From this simple method of checking at least the parameters can be found the following problems:

  1. You need to write a lot of code to parameter validation. (This code is even more garbage codes)
  2. Notes need to know what the parameters of each constraint is (or someone else ye can understand)
  3. Each programmer to do parameter validation in a different way, not by throwing parameter validation exceptions are not the same (the latter almost impossible to maintain)

As lead to code redundancy and a number of management problems (the larger the amount of code, and the management more difficult to maintain them) , for example, semantic consistency and so on. To prevent this from happening, it is preferable to verify the corresponding logical domain model (model domain concepts) binding, which is provided herein a new idea (in fact, the idea is to provide JavaEE)

To solve this problem, Bean Validationto JavaBeanverify that defines the corresponding metadata model and API. The default is all kinds of metadata Java Annotations, of course, also supports the xml way and you can also extend -
can be said to Bean Validationbe JavaBeanan expansion, it can be any layer in the layout code is not limited to Web applications or client applications.

Java Bean Validation

JSR is Java Specification Requestsan abbreviation, meaning Java specification proposal. About this data validation, the latest is JSR380, that is, we often say Bean Validation 2.0.

Bean Validation 2.0 by JSR No. 380 standard. The standard connection as follows: HTTPS: //www.jcp.org/en/egc/view the above mentioned id = 380?
Bean Validation homepage: HTTP: //beanvalidation.org
Bean Validation of reference implementations: https: //github.com/ hibernate / hibernate-validator

Bean ValidationIt is a framework to verify the configuration parameters by the annotation, which includes two parts Bean Validation API(specification) and Hibernate Validator(realized).
Bean ValidationIt is defined by a set of Java -based annotation / xml data validation specifications, from now JSR 303upgraded version 1.0 to JSR 349version 1.1, and then to JSR 380version 2.0 (2.0 completed in 2017.08), has gone through three editions (my shots are as follows :)
Here Insert Picture Description
now in fact the vast majority of users are still using the coder Bean Validation 1.1, after all, in general it has been good enough -
this article will introduce Bean Validation 2.0some of the new things to provide practical, after all, Java8 has now become mainstream, can use the ~

Simple Demo sample

To use it, first of all you have to guide package Well - based on experience, and JCache similar Java provides only specifications , and did not provide an implementation, so we can first find it and then import the API packages:

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

In fact, about the differences between the versions of the focus is not described in this article, after all, did a very good 2.0 backward compatible, using them is seamless.
But the department still give a 1.1 and 2.0.1 of shots, a simple compare the difference between the senses:
Here Insert Picture Description
Here Insert Picture Description

Compatibility Table

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>

==小细节:==
Here Insert Picture Description
可以看到,导入了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("[email protected]", "[email protected]", "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的专属配置类
Here Insert Picture Description
先看顶级接口: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:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~==

If interested in technology content can join the group wx exchange: Java高工、架构师3群.
If the group fails two-dimensional code, please add wx number: fsx641385712(or two-dimensional code is scanned beneath wx). And Note: "java入群"the word, will be invited into the group manually

fsx-wx

Guess you like

Origin www.cnblogs.com/fangshixiang/p/11222572.html