天天看點

4. Validator校驗器的五大核心元件,一個都不能少✍前言✍正文✍總結

困難是彈簧,你弱它就強。本文已被 https://www.yourbatman.cn 收錄,裡面一并有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以免費學習。關注公衆号【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗辄止。
4. Validator校驗器的五大核心元件,一個都不能少✍前言✍正文✍總結

✍前言

你好,我是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提供了實作:

4. 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。它的具體工作流程我用圖示如下:

4. Validator校驗器的五大核心元件,一個都不能少✍前言✍正文✍總結

context

上下文裡一般是擁有需要被替換的key的鍵值對的,如下圖所示:

4. Validator校驗器的五大核心元件,一個都不能少✍前言✍正文✍總結

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);
}           

提供的實作:

4. 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);
    }
}           

容易想到,ValueExtractor的實作類就非常之多(所有的實作類都是内建的,非public的,這就是預設情況下支援的容器類型):

4. 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()

添加進去即可

✍總結

本文主要介紹了Validator校驗器的五大核心元件的作用,Bean Validation2.0提供了ValueExtractor元件來實作容器内元素的校驗,大大簡化了對容器元素的校驗複雜性,值得點贊。

✔推薦閱讀: