艰难是弹簧,你弱它就强。本文已被 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 反射 APIExecutable#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 |
yourbatman@qq.com | |
微 信 | fsx641385712 |
沉闷平台 |
|
公众号 | BAT 的乌托邦(ID:BAT-utopia) |
常识星球 | BAT 的乌托邦 |
每日文章举荐 | 每日文章举荐 |