前言
上篇文章介绍了校验器上下文 ValidatorContext,知道它可以对校验器 Validator 的核心五大组件分别进行定制化设置,那么这些核心组件在校验过程中到底扮演着什么样的角色呢,本文一探究竟。
版本约定
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 内容进行格式化,MessageInterpolator 提供了如下两个功能:
- 能够对 message 内容进行格式化,若有占位符 {} 或者 el 表达式 ${} 就执行替换和计算,对于语法错误应该尽量的宽容;
- 能够处理消息的国际化,消息的 key 是同一个,根据不同的 Locale 展示不同的消息模板。
MessageInterpolator 是 Bean Validation 的标准接口,Hibernate Validator 提供了实现:
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 ' : ''}
这部分值的。
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 就是 ConstraintValidator 的工厂类,可以根据 Class 生成 ConstraintValidator 实例。
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);
}
Hibernate Validator 提供了如下实现类:
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);
}
}
Hibernate Validator 提供了如下实现类:
举例两个典型实现:
// 提取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()
添加进去即可。
总结
本文主要介绍了 Bean Validation 校验器的五大核心组件,Bean Validation 2.0 提供了 ValueExtractor 组件来实现容器内元素的校验,大大简化了对容器元素的校验复杂性,值得点赞。
转载
打个广告,方便的话,可以关注一下 A哥(YourBatman) 的公众号。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/pzmxrs 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。