1. 程式人生 > >4. Validator校驗器的五大核心元件,一個都不能少

4. Validator校驗器的五大核心元件,一個都不能少

> 困難是彈簧,你弱它就強。本文已被 [**https://www.yourbatman.cn**](https://www.yourbatman.cn) 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的**專欄**供以免費學習。關注公眾號【**BAT的烏托邦**】逐個擊破,深入掌握,拒絕淺嘗輒止。 [TOC] ![](https://img-blog.csdnimg.cn/2020090218282143.png#pic_center) # ✍前言 你好,我是YourBatman。 [上篇文章]()介紹了校驗器上下文ValidatorContext,知道它可以對校驗器Validator的核心五大元件分別進行定製化設定,那麼這些核心元件在校驗過程中到底扮演著什麼樣的角色呢,本文一探究竟。 作為核心元件,是有必要多探究一分的。以此為基,再擴散開了解和使用其它功能模組便將如魚得水。但是過程枯燥是真的,所以需要堅持呀。 ## 版本約定 - Bean Validation版本:`2.0.2` - Hibernate Validator版本:`6.1.5.Final` # ✍正文 Bean Validation校驗器的這五大核心元件通過ValidatorContext可以分別設定:若沒設定(或為null),那就回退到使用ValidatorFactory預設的元件。 準備好的元件,統一通過ValidatorFactory暴露出來予以訪問: ```java 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提供了實現: ![](https://img-blog.csdnimg.cn/20200901214251873.png#pic_center) Hibernate Validation它使用的是ResourceBundleMessageInterpolator來既支援引數,也支援EL表示式。內部使用了**javax.el.ExpressionFactory**這個API來支援EL表示式`${}`的,形如這樣:`must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}`它是能夠動態計算出`${inclusive == true ? 'or equal to ' : ''}`這部分的值的。 ```java public interface MessageInterpolator { String interpolate(String messageTemplate, Context context); String interpolate(String messageTemplate, Context context, Locale locale); } ``` 介面方法直接了當:根據上下文Context填充訊息模版messageTemplate。它的具體工作流程我用圖示如下: ![](https://img-blog.csdnimg.cn/20200902103551768.png#pic_center) `context`上下文裡一般是擁有需要被替換的key的鍵值對的,如下圖所示: ![](https://img-blog.csdnimg.cn/20200902104023147.png#pic_center) Hibernate對Context的實現中**擴展出**瞭如圖的兩個Map(非JSR標準),可以讓你**優先於** constraintDescriptor取值,取不到再fallback到標準模式的`ConstraintDescriptor`裡取值,也就是註解的屬性值。具體取值程式碼如下: ```java 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訪問**,當妹訪問一個屬性時都會通過它來判斷一下子,提供兩個判斷方法: ```java 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生成物件例項。 ```java public interface ConstraintValidatorFactory { // 生成例項:介面並不規定你的生成方式 > T getInstance(Class key); // 釋放例項。標記此例項不需要再使用,一般為空實現 // 和Spring容器整合時 .destroyBean(instance)時會呼叫此方法 void releaseInstance(ConstraintValidator instance); } ``` Hibernate提供了唯一實現ConstraintValidatorFactoryImpl:使用空構造器生成例項 `clazz.getConstructor().newInstance();`。 > 小貼士:介面並沒規定你如何生成例項,Hibernate Validator是使用空構造這麼實現的而已~ ## ParameterNameProvider 引數名提供器。這個元件和Spring的`ParameterNameDiscoverer`作用是一毛一樣的:獲取方法/構造器的**引數名**。 ```java public interface ParameterNameProvider { List getParameterNames(Constructor constructor); List getParameterNames(Method method); } ``` 提供的實現: ![](https://img-blog.csdnimg.cn/20200902163234854.png#pic_center) - `DefaultParameterNameProvider`:基於Java反射API `Executable#getParameters()`實現 ```java @Test public void test9() { ParameterNameProvider parameterNameProvider = new DefaultParameterNameProvider(); // 拿到Person的無參構造和有參構造(@NoArgsConstructor和@AllArgsConstructor) Arrays.stream(Person.class.getConstructors()).forEach(c -> System.out.println(parameterNameProvider.getParameterNames(c))); } ``` 執行程式,輸出: ```java [arg0, arg1, arg2, arg3] [] ``` 一樣的,若你想要打印出**明確的**引數名,請在編譯引數上加上`-parameters`引數。 - `ReflectionParameterNameProvider`:**已過期**。請使用上面的default代替 - `ParanamerParameterNameProvider`:基於`com.thoughtworks.paranamer.Paranamer`實現引數名的獲取,需要額外匯入相應的包才行。嗯,這裡我就不試了哈~ ## ClockProvider 時鐘提供器。這個介面很簡單,就是提供一個Clock,給`@Past、@Future`等閱讀判斷提供參考。唯一實現為DefaultClockProvider: ```java 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等等。 ```java // T:待提取的容器型別 public interface ValueExtractor { // 從原始值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的,這就是預設情況下支援的容器型別): ![](https://img-blog.csdnimg.cn/20200902165457234.png#pic_center) 舉例兩個典型實現: ```java // 提取List裡的值 LIST_ELEMENT_NODE_NAME -> class ListValueExtractor implements ValueExtractor> { 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資料校驗的認知](https://mp.weixin.qq.com/s/g04HMhrjbvbPn1Mb9JYa5g) - [2. Bean Validation宣告式校驗方法的引數、返回值](https://mp.weixin.qq.com/s/-KeOCq2rsXCvrqD8HYHSpQ) - [3. 站在使用層面,Bean Validation這些標準介面你需要爛熟於胸](https://mp.weixin.qq.com/s/MQjXG0cg8domRtwf