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組件來實現容器內元素的校驗,大大簡化了對容器元素的校驗複雜性,值得點贊。

✔推薦閱讀:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章