從深處去掌握數據校驗@Valid的做用(級聯校驗)

每篇一句

NBA裏有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術

相關閱讀

【小家Java】深刻了解數據校驗:Java Bean Validation 2.0(JSR30三、JSR34九、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】讓Controller支持對平鋪參數執行數據校驗(默認Spring MVC使用@Valid只能對JavaBean進行校驗)
【小家Spring】Spring方法級別數據校驗:@Validated + MethodValidationPostProcessor優雅的完成數據校驗動做java


<center>對Spring感興趣可掃碼加入wx羣:Java高工、架構師3羣(文末有二維碼)</center>spring


前言

關於Bean Validation的基本原理篇完結以後,接下來就是小夥伴最爲關心的乾貨:使用篇
若是說要使用Bean Validation數據校驗,我十分相信小夥伴們都可以使用,但估計大都是有個前提的:Spring MVC環境。我極其簡單的調查了一下,近乎99%的人都是隻把數據校驗使用在Spring MVCController層面的,並且幾乎90%的人都是讓它必須和@RequestBody一塊兒來使用去校驗JavaBean入參~編程

若是這麼去理解Bean Validation的使用,那就有點太過於片面了,畢竟被Spring包裹起來,你其實很難去知道它真正作的事。
熟悉我文章風格的人知道,每篇文章我都會帶你領略一些不同的風景,本章亦不例外,會讓你知道數據校驗在Spring框架以外的一些事~架構

分組校驗

在個人前置原理篇文章,分組校驗實際上是沒太大必要說的,由於使用起來確實很是的簡單。此處仍是給個分組校驗的使用案例吧:框架

@Getter
@Setter
@ToString
public class Person {
    // 錯誤消息message是能夠自定義的
    @NotNull(message = "{message} -> 名字不能爲null", groups = Simple.class)
    public String name;
    @Max(value = 10, groups = Simple.class)
    @Positive(groups = Default.class) // 內置的分組:default
    public Integer age;

    @NotNull(groups = Complex.class)
    @NotEmpty(groups = Complex.class)
    private List<@Email String> emails;
    @Future(groups = Complex.class)
    private Date start;

    // 定義兩個組 Simple組和Complex組
    interface Simple {
    }
    interface Complex {

    }
}

執行分組校驗:ide

public static void main(String[] args) {
        Person person = new Person();
        //person.setName("fsx");
        person.setAge(18);
        // email校驗:雖然是List均可以校驗哦
        person.setEmails(Arrays.asList("fsx@gmail.com", "baidu@baidu.com", "aaa.com"));
        //person.setStart(new Date()); //start 須要是一個未來的時間: Sun Jul 21 10:45:03 CST 2019
        //person.setStart(new Date(System.currentTimeMillis() + 10000)); //校驗經過

        HibernateValidatorConfiguration configure = Validation.byProvider(HibernateValidator.class).configure();
        ValidatorFactory validatorFactory = configure.failFast(false).buildValidatorFactory();
        // 根據validatorFactory拿到一個Validator
        Validator validator = validatorFactory.getValidator();


        // 分組校驗(能夠區分對待Default組、Simple組、Complex組)
        Set<ConstraintViolation<Person>> result = validator.validate(person, Person.Simple.class);
        //Set<ConstraintViolation<Person>> result = validator.validate(person, Person.Complex.class);

        // 對結果進行遍歷輸出
        result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue())
                .forEach(System.out::println);

    }

運行打印:函數

age 最大不能超過10: 18
name {message} -> 名字不能爲null -> 名字不能爲null: null

能夠直觀的看到效果,此處的校驗只執行Person.Simple.class這個Group組上的約束~ui

分組約束在Spring MVC中的使用場景仍是相對比較多的,可是須要注意的是: javax.validation.Valid沒有提供指定分組的,可是 org.springframework.validation.annotation.Validated擴展提供了直接在註解層面指定分組的能力

@Valid註解

咱們知道JSR提供了一個@Valid註解供以使用,在本文以前,絕大多數小夥伴都是在Controller中而且結合@RequestBody一塊兒來使用它,但在本文以後,你定會對它有個全新的認識~this

==該註解用於驗證級聯的屬性方法參數方法返回類型。==
當驗證屬性、方法參數或方法返回類型時,將驗證對象及其屬性上定義的約束,另外:此行爲是遞歸應用的。spa

:::爲了理解@Valid,那就得知道處理它的時機:::

MetaDataProvider

元數據提供者:約束相關元數據(如約束、默認組序列等)的Provider。它的做用和特色以下:

  1. 基於不一樣的元數據:如xml、註解。(還有個編程映射) 這三種類型。對應的枚舉類爲:
public enum ConfigurationSource {
    ANNOTATION( 0 ),
    XML( 1 ),
    API( 2 ); //programmatic API
}
  1. MetaDataProvider只返回直接爲一個類配置的元數據
  2. 它不處理從超類、接口合併的元數據(簡單的說你@Valid放在接口處是無效的
public interface MetaDataProvider {

    // 將**註解處理選項**歸還給此Provider配置。  它的惟一實現類爲:AnnotationProcessingOptionsImpl
    // 它能夠配置好比:areMemberConstraintsIgnoredFor  areReturnValueConstraintsIgnoredFor
    // 也就說能夠配置:讓免於被校驗~~~~~~(開綠燈用的)
    AnnotationProcessingOptions getAnnotationProcessingOptions();
    // 返回做用在此Bean上面的`BeanConfiguration`   若沒有就返回null了
    // BeanConfiguration持有ConfigurationSource的引用~
    <T> BeanConfiguration<? super T> getBeanConfiguration(Class<T> beanClass);
    
}

// 表示源於一個ConfigurationSource的一個Java類型的完整約束相關配置。  包含字段、方法、類級別上的元數據
// 固然還包含有默認組序列上的元數據(使用較少)
public class BeanConfiguration<T> {
    // 三種來源的枚舉
    private final ConfigurationSource source;
    private final Class<T> beanClass;
    // ConstrainedElement表示待校驗的元素,能夠知道它會以下四個子類:
    // ConstrainedField/ConstrainedType/ConstrainedParameter/ConstrainedExecutable
    
    // 注意:ConstrainedExecutable持有的是java.lang.reflect.Executable對象
    //它的兩個子類是java.lang.reflect.Method和Constructor
    private final Set<ConstrainedElement> constrainedElements;

    private final List<Class<?>> defaultGroupSequence;
    private final DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider;
    ... // 它本身並不處理什麼邏輯,參數都是經過構造器傳進來的
}

它的繼承樹:
在這裏插入圖片描述
三個實現類對應着上面所述的三種元數據類型。本文很顯然只須要關注和註解相關的:AnnotationMetaDataProvider

AnnotationMetaDataProvider

這個元數據均來自於註解的標註,而後它是Hibernate Validation的默認configuration source。它這裏會處理標註有@Valid的元素~

public class AnnotationMetaDataProvider implements MetaDataProvider {

    private final ConstraintHelper constraintHelper;
    private final TypeResolutionHelper typeResolutionHelper;
    private final AnnotationProcessingOptions annotationProcessingOptions;
    private final ValueExtractorManager valueExtractorManager;

    // 這是一個很是重要的屬性,它會記錄着當前Bean  全部的待校驗的Bean信息~~~
    private final BeanConfiguration<Object> objectBeanConfiguration;

    // 惟一構造函數
    public AnnotationMetaDataProvider(ConstraintHelper constraintHelper,
            TypeResolutionHelper typeResolutionHelper,
            ValueExtractorManager valueExtractorManager,
            AnnotationProcessingOptions annotationProcessingOptions) {
        this.constraintHelper = constraintHelper;
        this.typeResolutionHelper = typeResolutionHelper;
        this.valueExtractorManager = valueExtractorManager;
        this.annotationProcessingOptions = annotationProcessingOptions;

        // 默認狀況下,它去把Object相關的全部的方法都retrieve:檢索出來放着  我比較費解這件事~~~  
        // 後面才發現:一切爲了效率
        this.objectBeanConfiguration = retrieveBeanConfiguration( Object.class );
    }

    // 實現接口方法
    @Override
    public AnnotationProcessingOptions getAnnotationProcessingOptions() {
        return new AnnotationProcessingOptionsImpl();
    }


    // 若是你的Bean是Object  就直接返回了~~~(大多數狀況下  都是Object)
    @Override
    @SuppressWarnings("unchecked")
    public <T> BeanConfiguration<T> getBeanConfiguration(Class<T> beanClass) {
        if ( Object.class.equals( beanClass ) ) {
            return (BeanConfiguration<T>) objectBeanConfiguration;
        }
        return retrieveBeanConfiguration( beanClass );
    }
}

如上可知,核心解析邏輯在retrieveBeanConfiguration()這個私有方法上。總結一下調用此方法的兩個原始入口(一個構造器,一個接口方法):

  1. ValidatorFactory.getValidator()獲取校驗器的時候,初始化時會本身new一個,調用棧以下圖:

在這裏插入圖片描述

  1. 調用Validator.validate()方法的時候,beanMetaDataManager.getBeanMetaData( rootBeanClass )它會遍歷初始化時全部的metaDataProviders(默認狀況下兩個,沒有xml方式的),拿出全部的BeanConfiguration交給BeanMetaDataBuilder,最終構建出一個屬於此Bean的BeanMetaData。對此有一點注意事項描述以下:

    1. 處理`MetaDataProvider`時會調用`ClassHierarchyHelper.getHierarchy( beanClass ) `方法,不只僅處理本類。拿到本類本身和全部父類後,統一交給`provider.getBeanConfiguration( clazz )`處理(**也就是說任何一個類都會把Object類處理一遍**)

在這裏插入圖片描述

retrieveBeanConfiguration()詳情

這個方法說白了,就是從Bean裏面去檢索屬性、方法、構造器等須要校驗的ConstrainedElement項

private <T> BeanConfiguration<T> retrieveBeanConfiguration(Class<T> beanClass) {
        // 它檢索的範圍是:clazz.getDeclaredFields()  什麼意思:就是蒐集到本類全部的字段  包括private等等  可是不包括父類的全部字段
        Set<ConstrainedElement> constrainedElements = getFieldMetaData( beanClass );
        constrainedElements.addAll( getMethodMetaData( beanClass ) );
        constrainedElements.addAll( getConstructorMetaData( beanClass ) );

        //TODO GM: currently class level constraints are represented by a PropertyMetaData. This
        //works but seems somewhat unnatural
        // 這個TODO頗有意思:當前,類級約束由PropertyMetadata表示。這是可行的,但彷佛有點不天然
        // ReturnValueMetaData、ExecutableMetaData、ParameterMetaData、PropertyMetaData

        // 總之吧:此處就是把類級別的校驗器放進來了(這個set大部分時候都是空的)
        Set<MetaConstraint<?>> classLevelConstraints = getClassLevelConstraints( beanClass );
        if (!classLevelConstraints.isEmpty()) {
            ConstrainedType classLevelMetaData = new ConstrainedType(ConfigurationSource.ANNOTATION, beanClass, classLevelConstraints);
            constrainedElements.add(classLevelMetaData);
        }
        
        // 組裝成一個BeanConfiguration返回
        return new BeanConfiguration<>(ConfigurationSource.ANNOTATION, beanClass,
                constrainedElements, 
                getDefaultGroupSequence( beanClass ),  //此類上標註的全部@GroupSequence註解
                getDefaultGroupSequenceProvider( beanClass ) // 此類上標註的全部@GroupSequenceProvider註解
        );
    }

這一步驟把該Bean上的字段、方法等等須要校驗的項都提取出來。就拿上例中的Demo校驗Person類來講,最終得出的BeanConfiguration以下:(兩個)
在這裏插入圖片描述
在這裏插入圖片描述
這是直觀的結論,能夠看到僅僅是一個簡單的類其實所包含的項是挺多的。

此處說一句:項是有這麼多,可是並非每個都須要走驗證邏輯的。由於畢竟大多數項上面並無約束(註解),大多數 ConstrainedElement.getConstraints()爲空嘛~

總得來講,我我的建議不能光只記憶結論,由於那很容易忘記,因此仍是得稍微深刻一點,讓記憶更深入吧。那就從下面四個方面深刻:

檢索Field:getFieldMetaData( beanClass )
  1. 拿到本類全部字段Fieldclazz.getDeclaredFields()
  2. 把每一個Field都包裝成ConstrainedElement存放起來~~~

    1.  注意:此步驟完成了對每一個`Field`上標註的註解進行了保存
檢索Method:getMethodMetaData( beanClass )
  1. 拿到本類全部的方法Methodclazz.getDeclaredMethods()
  2. 排除掉靜態方法和合成(isSynthetic)方法
  3. 把每一個Method都轉換成一個ConstrainedExecutable裝着~~(ConstrainedExecutable也是個ConstrainedElement)。在此期間它完成了以下事(方法和構造器都複雜點,由於包含入參和返回值):

    1. 找到方法上全部的註解保存起來
        2. 處理入參、返回值(包括自動判斷是做用在入參仍是返回值上)
檢索Constructor:getConstructorMetaData( beanClass )

徹底同處理Method,略

檢索Type:getClassLevelConstraints( beanClass )
  1. 找打標註在此類上的全部的註解,轉換成ConstraintDescriptor
  2. 對已經找到每一個ConstraintDescriptor進行處理,最終都轉換Set<MetaConstraint<?>>這個類型

    1.
  3. Set<MetaConstraint<?>>用一個ConstrainedType包裝起來(ConstrainedType是個ConstrainedElement

==關於級聯校驗此處補充說明一點,處理Type,都會處理級聯校驗狀況,而且仍是遞歸處理:==
也就是這個方法(課件@Valid在此處生效):

// type解釋:分以下N中狀況
    // Field爲:.getGenericType() // 字段的類型
    // Method爲:.getGenericReturnType() // 返回值類型
    // Constructor:.getDeclaringClass() // 構造器所在類

    // annotatedElement:可不必定說必定要有註解才能進來(每一個字段、方法、構造器等都能傳進來)
    private CascadingMetaDataBuilder getCascadingMetaData(Type type, AnnotatedElement annotatedElement, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) {
        return CascadingMetaDataBuilder.annotatedObject( type, annotatedElement.isAnnotationPresent( Valid.class ), containerElementTypesCascadingMetaData, getGroupConversions( annotatedElement ) );
    }

這裏對咱們理解級聯校驗最重要的一句是:annotatedElement.isAnnotationPresent(Valid.class)。也就是說:若元素被此註解標註了,那就證實須要對它進行級聯校驗,這就是JSR定位@Valid的做用~

Spring提高了它???請關注後文Spring對它的應用吧~

ConstraintValidator.isValid()調用處

咱們知道,每一個約束註解都是交給約束校驗器ConstraintValidator.isValid()這個方法來處理的,它被調用(生效)的地方在此(惟一處):

public abstract class ConstraintTree<A extends Annotation> {
    ...
    protected final <T, V> Set<ConstraintViolation<T>> validateSingleConstraint(ValidationContext<T> executionContext,
            ValueContext<?, ?> valueContext,
            ConstraintValidatorContextImpl constraintValidatorContext,
            ConstraintValidator<A, V> validator) {
        ...
        V validatedValue = (V) valueContext.getCurrentValidatedValue();
        isValid = validator.isValid( validatedValue, constraintValidatorContext );
        ...
        // 顯然校驗不經過就返回錯誤消息  不然返回空集合
        if ( !isValid ) {
            return executionContext.createConstraintViolations(valueContext, constraintValidatorContext);
        }
        return Collections.emptySet();
    }
    ...
}

這個方法的調用,會在執行每一個Group的時候

success = metaConstraint.validateConstraint( validationContext, valueContext );

MetaConstraint在上面檢索的時候就已經準備好了,最後經過ConstrainedElement.getConstraints就拿到了每一個元素的校驗器們,繼續調用

// ConstraintTree<A>
boolean validationResult = constraintTree.validateConstraints( executionContext, valueContext );

so,最終就調用到了isValid這個真正作事的方法上了。

==說了這麼多,你可能還雲裏霧裏,那麼就show一把吧:==

Demo Show

上面用一個示例校驗Person這個JavaBean了,可是你會發現示例中咱們全都是校驗的Field屬性。從理論裏咱們知道了Bean Validation它是有校驗方法、構造器、入參甚至遞歸校驗級聯屬性的能力的

校驗屬性Field

校驗Method入參、返回值

校驗Constructor入參、返回值

既校驗入參,同時也校驗返回值

這些是不能直接使用的,須要在運行時進行校驗。具體使用可參考:【小家Spring】讓Controller支持對平鋪參數執行數據校驗(默認Spring MVC使用@Valid只能對JavaBean進行校驗)

級聯校驗

什麼叫級聯校驗,其實就是帶校驗的成員裏存在級聯對象時,也要對它完成校驗。這個在實際應用場景中是比較常見的,好比入參Person對象中,還持有Child對象,咱們不只僅要完成Person的校驗,也依舊還要對Child內的屬性校驗:

@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Positive
    private Integer age;
    @Valid
    @NotNull
    private InnerChild child;

    @Getter
    @Setter
    @ToString
    public static class InnerChild {
        @NotNull
        private String name;
        @NotNull
        @Positive
        private Integer age;
    }

}

校驗邏輯以下:

public static void main(String[] args) {
        Person person = new Person();
        person.setName("fsx");
        Person.InnerChild child = new Person.InnerChild();
        child.setName("fsx-son");
        child.setAge(-1);
        person.setChild(child); // 放進去

        Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false)
                .buildValidatorFactory().getValidator();
        Set<ConstraintViolation<Person>> result = validator.validate(person);

        // 輸出錯誤消息
        result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue())
                .forEach(System.out::println);
    }

運行:

child.age 必須是正數: -1
age 不能爲null: null

child.age這個級聯屬性校驗成功~

總結

本文值得說是深刻了解數據校驗(Bean Validation)了,對於數據校驗的基本使用一直都不是難事,特別是在Spring環境下使用就更簡單了~

知識交流

若文章格式混亂,可點擊原文連接-原文連接-原文連接-原文連接-原文連接

==The last:若是以爲本文對你有幫助,不妨點個讚唄。固然分享到你的朋友圈讓更多小夥伴看到也是被做者本人許可的~==

**若對技術內容感興趣能夠加入wx羣交流:Java高工、架構師3羣
若羣二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。而且備註:"java入羣" 字樣,會手動邀請入羣**

相關文章
相關標籤/搜索