4. Validator校验器的五大核心组件,一个都不能少

简介: 了解核心组件,就算站稳脚跟

困难是弹簧,你弱它就强。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。

✍前言

你好,我是YourBatman。

[上篇文章]()介绍了校验器上下文ValidatorContext,知道它可以对校验器Validator的核心五大组件分别进行定制化设置,那么这些核心组件在校验过程中到底扮演着什么样的角色呢,本文一探究竟。

作为核心组件,是有必要多探究一分的。以此为基,再扩散开了解和使用其它功能模块便将如鱼得水。但是过程枯燥是真的,所以需要坚持呀。

版本约定

  • Bean Validation版本:2.0.2
  • Hibernate Validator版本:6.1.5.Final

✍正文

Bean Validation校验器的这五大核心组件通过ValidatorContext可以分别设置:若没设置(或为null),那就回退到使用ValidatorFactory默认的组件。

准备好的组件,统一通过ValidatorFactory暴露出来予以访问:

public interface ValidatorFactory extends AutoCloseable {
    ...
    MessageInterpolator getMessageInterpolator();
    TraversableResolver getTraversableResolver();
    ConstraintValidatorFactory getConstraintValidatorFactory();
    ParameterNameProvider getParameterNameProvider();
    @since 2.0
    ClockProvider getClockProvider();
    ...
}

MessageInterpolator

直译为:消息插值器。按字面不太好理解:简单的说就是对message内容进行格式化,若有占位符{}或者el表达式${}就执行替换和计算。对于语法错误应该尽量的宽容。

校验失败的消息模版交给它处理就成为了人能看得懂的消息格式,因此它能够处理消息的国际化:消息的key是同一个,但根据不同的Locale展示不同的消息模版。最后在替换/技术模版里面的占位符即可~

这是Bean Validation的标准接口,Hibernate Validator提供了实现:

Hibernate Validation它使用的是ResourceBundleMessageInterpolator来既支持参数,也支持EL表达式。内部使用了javax.el.ExpressionFactory这个API来支持EL表达式${}的,形如这样:must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}它是能够动态计算出${inclusive == true ? 'or equal to ' : ''}这部分的值的。

public interface MessageInterpolator {
    String interpolate(String messageTemplate, Context context);
    String interpolate(String messageTemplate, Context context,  Locale locale);
}

接口方法直接了当:根据上下文Context填充消息模版messageTemplate。它的具体工作流程我用图示如下:

context上下文里一般是拥有需要被替换的key的键值对的,如下图所示:

Hibernate对Context的实现中扩展出了如图的两个Map(非JSR标准),可以让你优先于 constraintDescriptor取值,取不到再fallback到标准模式的ConstraintDescriptor里取值,也就是注解的属性值。具体取值代码如下:

ParameterTermResolver:

    private Object getVariable(Context context, String parameter) {
        // 先从hibernate扩展出来的方式取值
        if (context instanceof HibernateMessageInterpolatorContext) {
            Object variable = ( (HibernateMessageInterpolatorContext) context ).getMessageParameters().get( parameter );
            if ( variable != null ) {
                return variable;
            }
        }
        // fallback到标准模式:从注解属性里取值
        return context.getConstraintDescriptor().getAttributes().get( parameter );
    }

大部分情况下我们只用得到注解属性里面的值,也就是错误消息里可以使用{注解属性名}这种方式动态获取到注解属性值,给与友好错误提示。

上下文里的Message参数和Expression参数如何放进去的?在后续高级使用部分,会自定义k-v替换参数,也就会使用到本部分的高级应用知识,后文见。

TraversableResolver

能跨越的处理器。从字面是非常不好理解,用粗暴的语言解释为:确定某个属性是否能被ValidationProvider访问,当妹访问一个属性时都会通过它来判断一下子,提供两个判断方法:

public interface TraversableResolver {

    // 是否是可达的
    boolean isReachable(Object traversableObject,
                        Node traversableProperty,
                        Class<?> rootBeanType,
                        Path pathToTraversableObject,
                        ElementType elementType);
                        
    // 是否是可级联的(是否标注有@Valid注解)
    boolean isCascadable(Object traversableObject,
                         Node traversableProperty,
                         Class<?> rootBeanType,
                         Path pathToTraversableObject,
                         ElementType elementType);
}

该接口主要根据配置项来进行判断,并不负责。内部使用,调用者基本无需关心,也不见更改其默认机制,暂且略过。

ConstraintValidatorFactory

约束校验器工厂。ConstraintValidator约束校验器我们应该不陌生:每个约束注解都得指定一个/多个约束校验器,形如这样:@Constraint(validatedBy = { xxx.class })

ConstraintValidatorFactory就是工厂:可以根据Class生成对象实例。

public interface ConstraintValidatorFactory {

    // 生成实例:接口并不规定你的生成方式
    <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key);
    // 释放实例。标记此实例不需要再使用,一般为空实现
    // 和Spring容器集成时 .destroyBean(instance)时会调用此方法
    void releaseInstance(ConstraintValidator<?, ?> instance);
}

Hibernate提供了唯一实现ConstraintValidatorFactoryImpl:使用空构造器生成实例 clazz.getConstructor().newInstance();

小贴士:接口并没规定你如何生成实例,Hibernate Validator是使用空构造这么实现的而已~

ParameterNameProvider

参数名提供器。这个组件和Spring的ParameterNameDiscoverer作用是一毛一样的:获取方法/构造器的参数名

public interface ParameterNameProvider {
    
    List<String> getParameterNames(Constructor<?> constructor);
    List<String> getParameterNames(Method method);
}

提供的实现:

  • DefaultParameterNameProvider:基于Java反射API Executable#getParameters()实现
@Test
public void test9() {
    ParameterNameProvider parameterNameProvider = new DefaultParameterNameProvider();

    // 拿到Person的无参构造和有参构造(@NoArgsConstructor和@AllArgsConstructor)
    Arrays.stream(Person.class.getConstructors()).forEach(c -> System.out.println(parameterNameProvider.getParameterNames(c)));
}

运行程序,输出:

[arg0, arg1, arg2, arg3]
[]

一样的,若你想要打印出明确的参数名,请在编译参数上加上-parameters参数。

  • ReflectionParameterNameProvider已过期。请使用上面的default代替
  • ParanamerParameterNameProvider:基于com.thoughtworks.paranamer.Paranamer实现参数名的获取,需要额外导入相应的包才行。嗯,这里我就不试了哈~

ClockProvider

时钟提供器。这个接口很简单,就是提供一个Clock,给@Past、@Future等阅读判断提供参考。唯一实现为DefaultClockProvider:

public class DefaultClockProvider implements ClockProvider {

    public static final DefaultClockProvider INSTANCE = new DefaultClockProvider();

    private DefaultClockProvider() {
    }

    // 默认是系统时钟
    @Override
    public Clock getClock() {
        return Clock.systemDefaultZone();
    }

}

默认使用当前系统时钟作为参考。若你的系统有全局统一的参考标准,比如统一时钟,那就可以通过此接口实现自己的Clock时钟,毕竟每台服务器的时间并不能保证是完全一样的不是,这对于时间敏感的应用场景(如竞标)需要这么做。

以上就是对Validator校验器的五个核心组件的一个描述,总体上还是比较简单。其中第一个组件:MessageInterpolator插值器我认为是最为重要的,需要理解好了。对后面做自定义消息模版、国际化消息都有用。

加餐:ValueExtractor

值提取器。2.0版本新增一个比较重要的组件API,作用:把值从容器内提取出来。这里的容器包括:数组、集合、Map、Optional等等。

// T:待提取的容器类型
public interface ValueExtractor<T> {

    // 从原始值originalValue提取到receiver里
    void extractValues(T originalValue, ValueReceiver receiver);

    // 提供一组方法,用于接收ValueExtractor提取出来的值
    interface ValueReceiver {
    
        // 接收从对象中提取的值
        void value(String nodeName, Object object);
        // 接收可以迭代的值,如List、Map、Iterable等
        void iterableValue(String nodeName, Object object);
        // 接收有索引的值,如List Array
        // i:索引值
        void indexedValue(String nodeName, int i, Object object);
        // 接收键值对的值,如Map
        void keyedValue(String nodeName, Object key, Object object);
    }
}

容易想到,ValueExtractor的实现类就非常之多(所有的实现类都是内建的,非public的,这就是默认情况下支持的容器类型):

举例两个典型实现:

// 提取List里的值   LIST_ELEMENT_NODE_NAME -> <list element>
class ListValueExtractor implements ValueExtractor<List<@ExtractedValue ?>> {

    static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor( new ListValueExtractor() );

    private ListValueExtractor() {
    }

    @Override
    public void extractValues(List<?> originalValue, ValueReceiver receiver) {
        for ( int i = 0; i < originalValue.size(); i++ ) {
            receiver.indexedValue( NodeImpl.LIST_ELEMENT_NODE_NAME, i, originalValue.get( i ) );
        }
    }
}

// 提取Optional里的值
@UnwrapByDefault
class OptionalLongValueExtractor implements ValueExtractor<@ExtractedValue(type = Long.class) OptionalLong> {

    static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor( new OptionalLongValueExtractor() );

    @Override
    public void extractValues(OptionalLong originalValue, ValueReceiver receiver) {
        receiver.value( null, originalValue.isPresent() ? originalValue.getAsLong() : null );
    }
}

校验器Validator通过它把值从容器内提取出来参与校验,从这你应该就能理解为毛从Bean Validation2.0开始就支持验证容器内的元素了吧,形如这样:List<@NotNull @Valid Person>、Optional<@NotNull @Valid Person>,可谓大大的方便了使用。

若你有自定义容器,需要提取的需求,那么你可以自定义一个ValueExtractor实现,然后通过ValidatorContext#addValueExtractor()添加进去即可

✍总结

本文主要介绍了Validator校验器的五大核心组件的作用,Bean Validation2.0提供了ValueExtractor组件来实现容器内元素的校验,大大简化了对容器元素的校验复杂性,值得点赞。

✔推荐阅读:
目录
相关文章
|
4月前
|
前端开发 安全 Java
Spring Boot项目中VO层设计:选择继承或组合的灵活实践
Spring Boot项目中VO层设计:选择继承或组合的灵活实践
136 0
|
1月前
|
Java 开发者 Spring
灵活扩展Spring:后置处理器的实战技巧与最佳实践
灵活扩展Spring:后置处理器的实战技巧与最佳实践
31 0
|
2月前
|
JSON 前端开发 Java
【SpringBoot实战专题】「开发实战系列」全方位攻克你的技术盲区之Spring定义Jackson转换Null的方法和实现案例
【SpringBoot实战专题】「开发实战系列」全方位攻克你的技术盲区之Spring定义Jackson转换Null的方法和实现案例
46 0
|
5月前
|
编解码 前端开发 Java
JSR 303 验证以及国际化具体使用加案例分析
JSR 303 验证以及国际化具体使用加案例分析
77 0
JSR 303 验证以及国际化具体使用加案例分析
|
5月前
|
XML Java 数据格式
spring的核心技术---bean的生命周期加案例分析详细易懂
spring的核心技术---bean的生命周期加案例分析详细易懂
131 1
|
11月前
|
Java 数据库连接 数据库
高效掌握JDBC技术(三)| 三层架构理念 | 书写符合事务特性的工具类 | JUnit测试框架 | JDBC项目开发步骤(上)
高效掌握JDBC技术(三)| 三层架构理念 | 书写符合事务特性的工具类 | JUnit测试框架 | JDBC项目开发步骤
111 1
|
11月前
|
Java 数据库连接 数据库
高效掌握JDBC技术(三)| 三层架构理念 | 书写符合事务特性的工具类 | JUnit测试框架 | JDBC项目开发步骤(下)
高效掌握JDBC技术(三)| 三层架构理念 | 书写符合事务特性的工具类 | JUnit测试框架 | JDBC项目开发步骤
76 1
|
存储 运维 Dubbo
Dubbo3 源码解读-宋小生-3:框架,应用程序,模块领域模型Model对象的初始化
> Dubbo3 已经全面取代 HSF2 成为阿里的下一代服务框架,2022 双 11 基于 Dubbo3 首次实现了关键业务不停推、不降级的全面用户体验提升,从技术上,大幅提高研发与运维效率的同时地址推送等关键资源利用率提升超 40%,基于三位一体的开源中间件体系打造了阿里在云上的单元化最佳实践和统一标准,同时将规模化实践经验与技术创新贡献开源社区,极大的推动了开源技术与标准的发展。 > 本文
422 0
Dubbo3 源码解读-宋小生-3:框架,应用程序,模块领域模型Model对象的初始化
|
Java Spring 容器
第九篇:SpringBoot 配置高级 ConfigurationProperties注解 宽松绑定/松散绑定
第九篇:SpringBoot 配置高级 ConfigurationProperties注解 宽松绑定/松散绑定
236 0
第九篇:SpringBoot 配置高级 ConfigurationProperties注解 宽松绑定/松散绑定
|
Java 数据库连接 数据库
高效掌握JDBC技术(三)| 三层架构理念 | 书写符合事务特性的工具类 | JUnit测试框架 | JDBC项目开发步骤
高效掌握JDBC技术(三)| 三层架构理念 | 书写符合事务特性的工具类 | JUnit测试框架 | JDBC项目开发步骤
113 0
高效掌握JDBC技术(三)| 三层架构理念 | 书写符合事务特性的工具类 | JUnit测试框架 | JDBC项目开发步骤