乐趣区

关于数据校验: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 组件来实现 容器内 元素的校验,大大简化了对容器元素的校验复杂性,值得点赞。

✔举荐浏览:
  • 1. 不吹不擂,第一篇就能晋升你对 Bean Validation 数据校验的认知
  • 2. Bean Validation 申明式校验办法的参数、返回值
  • 3. 站在应用层面,Bean Validation 这些标准接口你须要烂熟于胸

♥关注 A 哥♥

Author A 哥(YourBatman)
集体站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
沉闷平台
公众号 BAT 的乌托邦(ID:BAT-utopia)
常识星球 BAT 的乌托邦
每日文章举荐 每日文章举荐

退出移动版