前言

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

版本约定

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

    正文

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


所有的组件统一通过 ValidatorFactory 暴露出来予以访问:

  1. public interface ValidatorFactory extends AutoCloseable {
  2. ...
  3. MessageInterpolator getMessageInterpolator();
  4. TraversableResolver getTraversableResolver();
  5. ConstraintValidatorFactory getConstraintValidatorFactory();
  6. ParameterNameProvider getParameterNameProvider();
  7. @since 2.0
  8. ClockProvider getClockProvider();
  9. ...
  10. }

MessageInterpolator

消息插值器,按字面意思不好理解,简单的说就是对 message 内容进行格式化,MessageInterpolator 提供了如下两个功能:

  1. 能够对 message 内容进行格式化,若有占位符 {} 或者 el 表达式 ${} 就执行替换和计算,对于语法错误应该尽量的宽容;
  2. 能够处理消息的国际化,消息的 key 是同一个,根据不同的 Locale 展示不同的消息模板。

MessageInterpolator 是 Bean Validation 的标准接口,Hibernate Validator 提供了实现:
Bean Validation 校验器的五大核心组件 - 图1
Hibernate Validator 提供了 ResourceBundleMessageInterpolator 类来既支持参数,也支持 EL 表达式。内部使用了 javax.el.ExpressionFactory API 来支持 EL 表达式的,比如 message 的内容是:must be greater than ${inclusive == true ? 'or equal to ' : ''}{value},它是能够动态计算出 ${inclusive == true ? 'or equal to ' : ''} 这部分值的。

  1. public interface MessageInterpolator {
  2. String interpolate(String messageTemplate, Context context);
  3. String interpolate(String messageTemplate, Context context, Locale locale);
  4. }

接口的方法比较简单,就是根据上下文 Context 填充消息模板 messageTemplate。它的具体工作流程如下图所示:
Bean Validation 校验器的五大核心组件 - 图2
context 上下文里一般是拥有需要被替换的 key 的键值对的,如下图所示:
Bean Validation 校验器的五大核心组件 - 图3
Hibernate 对 Context 的实现中扩展出了如图的两个 Map(非 JSR 标准),可以让你优先于 constraintDescriptor 取值,取不到再 fallback 到标准模式的 ConstraintDescriptor 里取值,也就是注解的属性值。具体取值代码如下:

  1. ParameterTermResolver
  2. private Object getVariable(Context context, String parameter) {
  3. // 先从hibernate扩展出来的方式取值
  4. if (context instanceof HibernateMessageInterpolatorContext) {
  5. Object variable = ((HibernateMessageInterpolatorContext) context).getMessageParameters().get(parameter);
  6. if ( variable != null ) {
  7. return variable;
  8. }
  9. }
  10. // fallback到标准模式:从注解属性里取值
  11. return context.getConstraintDescriptor().getAttributes().get(parameter);
  12. }

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


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

TraversableResolver

该组件是用来确定某个属性是否能够被 ValidationProvider 访问,每次访问某个属性都会通过它来判断,它提供了两个判断方法:

  1. public interface TraversableResolver {
  2. // 是否是可达的
  3. boolean isReachable(Object traversableObject,
  4. Node traversableProperty,
  5. Class<?> rootBeanType,
  6. Path pathToTraversableObject,
  7. ElementType elementType);
  8. // 是否是可级联的(是否标注有@Valid注解)
  9. boolean isCascadable(Object traversableObject,
  10. Node traversableProperty,
  11. Class<?> rootBeanType,
  12. Path pathToTraversableObject,
  13. ElementType elementType);
  14. }

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

ConstraintValidatorFactory

ConstraintValidator 约束校验器我们应该不陌生,每个约束注解都得指定一个或多个约束校验器,比如:@Constraint(validatedBy = { xxx.class })。
image.png
ConstraintValidatorFactory 就是 ConstraintValidator 的工厂类,可以根据 Class 生成 ConstraintValidator 实例。

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

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

接口并没规定你如何生成实例,只是 Hibernate Validator 是使用空构造实现的。

ParameterNameProvider

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

  1. public interface ParameterNameProvider {
  2. List<String> getParameterNames(Constructor<?> constructor);
  3. List<String> getParameterNames(Method method);
  4. }

Hibernate Validator 提供了如下实现类:
Bean Validation 校验器的五大核心组件 - 图5
DefaultParameterNameProvider:基于 Java 反射 API Executable#getParameters() 实现。

  1. @Test
  2. public void test9() {
  3. ParameterNameProvider parameterNameProvider = new DefaultParameterNameProvider();
  4. // 拿到Person的无参构造和有参构造(@NoArgsConstructor和@AllArgsConstructor)
  5. Arrays.stream(Person.class.getConstructors()).forEach(c -> System.out.println(parameterNameProvider.getParameterNames(c)));
  6. }

运行程序,控制台输出:

  1. [arg0, arg1, arg2, arg3]
  2. []

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

ReflectionParameterNameProvider:已过期,使用上面的 default 代替。

ParanamerParameterNameProvider:基于 com.thoughtworks.paranamer.Paranamer 实现参数名的获取,需要额外导入相应的包才能使用。

ClockProvider

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

  1. public class DefaultClockProvider implements ClockProvider {
  2. public static final DefaultClockProvider INSTANCE = new DefaultClockProvider();
  3. private DefaultClockProvider() {
  4. }
  5. // 默认是系统时钟
  6. @Override
  7. public Clock getClock() {
  8. return Clock.systemDefaultZone();
  9. }
  10. }

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

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

加餐:ValueExtractor

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

  1. // T:待提取的容器类型
  2. public interface ValueExtractor<T> {
  3. // 从原始值originalValue提取到receiver里
  4. void extractValues(T originalValue, ValueReceiver receiver);
  5. // 提供一组方法,用于接收ValueExtractor提取出来的值
  6. interface ValueReceiver {
  7. // 接收从对象中提取的值
  8. void value(String nodeName, Object object);
  9. // 接收可以迭代的值,如List、Map、Iterable等
  10. void iterableValue(String nodeName, Object object);
  11. // 接收有索引的值,如List Array
  12. // i:索引值
  13. void indexedValue(String nodeName, int i, Object object);
  14. // 接收键值对的值,如Map
  15. void keyedValue(String nodeName, Object key, Object object);
  16. }
  17. }

Hibernate Validator 提供了如下实现类:
Bean Validation 校验器的五大核心组件 - 图6
举例两个典型实现:

  1. // 提取List里的值 LIST_ELEMENT_NODE_NAME -> <list element>
  2. class ListValueExtractor implements ValueExtractor<List<@ExtractedValue ?>> {
  3. static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor(new ListValueExtractor());
  4. private ListValueExtractor() {
  5. }
  6. @Override
  7. public void extractValues(List<?> originalValue, ValueReceiver receiver) {
  8. for ( int i = 0; i < originalValue.size(); i++) {
  9. receiver.indexedValue( NodeImpl.LIST_ELEMENT_NODE_NAME, i, originalValue.get(i));
  10. }
  11. }
  12. }
  13. // 提取Optional里的值
  14. @UnwrapByDefault
  15. class OptionalLongValueExtractor implements ValueExtractor<@ExtractedValue(type = Long.class) OptionalLong> {
  16. static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor(new OptionalLongValueExtractor());
  17. @Override
  18. public void extractValues(OptionalLong originalValue, ValueReceiver receiver) {
  19. receiver.value(null, originalValue.isPresent() ? originalValue.getAsLong() : null);
  20. }
  21. }

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

如果你需要自定义值提取器,可以自定义一个 ValueExtractor 的实现,通过 ValidatorContext#addValueExtractor() 添加进去即可。

总结

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

转载

打个广告,方便的话,可以关注一下 A哥(YourBatman) 的公众号。
image.png

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/pzmxrs 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。