1. 程式人生 > >解決多欄位聯合邏輯校驗問題【享學Spring MVC】

解決多欄位聯合邏輯校驗問題【享學Spring MVC】

每篇一句

不要像祥林嫂一樣,天天抱怨著生活,日日思考著辭職。得罪點說一句:“淪落”到要跟這樣的人共事工作,難道自己身上就沒有原因?

前言

本以為洋洋灑灑的把Java/Spring資料(繫結)校驗這塊說了這麼多,基本已經算完結了。但今天中午一位熱心小夥伴在使用Bean Validation做資料校驗時上遇到了一個稍顯特殊的case,由於此校驗場景也比較常見,因此便有了本文對資料校驗補充。

關於Java/Spring中的資料校驗,我有理由堅信你肯定遇到過這樣的場景需求:在對JavaBean進行校驗時,b屬性的校驗邏輯是依賴於a屬性的值的;換個具象的例子說:當且僅當屬性a的值=xxx時,屬性b的校驗邏輯才生效。這也就是我們常說的多欄位聯合校驗邏輯~
因為這個校驗的case比較常見,因此促使了我記錄本文的動力,因為它會變得有意義和有價值。當然對此問題有的小夥伴說可以自己用if else

來處理呀,也不是很麻煩。本文的目的還是希望對資料校驗一以貫之的做到更清爽、更優雅、更好擴充套件而努力。

需要有一點堅持:既然用了Bean Validation去簡化校驗,那就(最好)不要用得四不像,遇到問題就解決問題~

熱心網友問題描述

為了更真實的還原問題場景,我貼上聊天截圖如下:

待校驗的請求JavaBean如下:

校需求描述簡述如下:

這位網友描述的真實生產場景問題,這也是本文講解的內容所在。
雖然這是在Spring MVC條件的下使用的資料校驗,但按照我的習慣為了更方便的說明問題,我會把此部分功能單摘出來,說清楚了方案和原理,再去實施解決問題本身(文末)~

方案和原理

對於單欄位的校驗、級聯屬性校驗等,通過閱讀我的系列文章,我有理由相信小夥伴們都能駕輕就熟

了的。本文給出一個最簡單的例子簡單"複習"一下:

@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Range(min = 10, max = 40)
    private Integer age;

    @NotNull
    @Size(min = 3, max = 5)
    private List<String> hobbies;

    // 級聯校驗
    @Valid
    @NotNull
    private Child child;
}

測試:

public static void main(String[] args)  {
    Person person = new Person();
    person.setName("fsx");
    person.setAge(5);
    person.setHobbies(Arrays.asList("足球","籃球"));
    person.setChild(new Child());

    Set<ConstraintViolation<Person>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(person);

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

執行,列印輸出:

child.name 不能為null: null
age 需要在10和40之間: 5
hobbies 個數必須在3和5之間: [足球,籃球]

結果符合預期,(級聯)校驗生效。

通過使用@Valid可以實現遞迴驗證,因此可以標註在List上,對它裡面的每個物件都執行校驗


問題來了,針對上例,現在我有如下需求:

  1. 若20 <= age < 30,那麼hobbiessize需介於1和2之間
  2. 若30 <= age < 40,那麼hobbiessize需介於3和5之間
  3. age其餘值,hobbies無校驗邏輯

    實現方案

    Hibernate Validator提供了非標準的@GroupSequenceProvider註解。本功能提供根據當前物件例項的狀態,動態來決定載入那些校驗組進入預設校驗組。

為了實現上面的需求達到目的,我們需要藉助Hibernate Validation提供給我們的DefaultGroupSequenceProvider介面來處理。

// 該介面定義了:動態Group序列的協定
// 要想它生效,需要在T上標註@GroupSequenceProvider註解並且指定此類為處理類
// 如果`Default`組對T進行驗證,則實際驗證的例項將傳遞給此類以確定預設組序列(這句話特別重要  下面用例子解釋)
public interface DefaultGroupSequenceProvider<T> {
    // 合格方法是給T返回預設的組(多個)。因為預設的組是Default嘛~~~通過它可以自定指定
    // 入參T object允許在驗證值狀態的函式中動態組合預設組序列。(非常強大)
    // object是待校驗的Bean。它可以為null哦~(Validator#validateValue的時候可以為null)

    // 返回值表示預設組序列的List。它的效果同@GroupSequence定義組序列,尤其是列表List必須包含型別T
    List<Class<?>> getValidationGroups(T object);
}

注意:

  1. 此介面Hibernate並沒有提供實現
  2. 若你實現請必須提供一個空的建構函式以及保證是執行緒安全的

按步驟解決多欄位組合驗證的邏輯:
1、自己實現DefaultGroupSequenceProvider介面(處理Person這個Bean)

public class PersonGroupSequenceProvider implements DefaultGroupSequenceProvider<Person> {

    @Override
    public List<Class<?>> getValidationGroups(Person bean) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();
        defaultGroupSequence.add(Person.class); // 這一步不能省,否則Default分組都不會執行了,會拋錯的

        if (bean != null) { // 這塊判空請務必要做
            Integer age = bean.getAge();
            System.err.println("年齡為:" + age + ",執行對應校驗邏輯");
            if (age >= 20 && age < 30) {
                defaultGroupSequence.add(Person.WhenAge20And30Group.class);
            } else if (age >= 30 && age < 40) {
                defaultGroupSequence.add(Person.WhenAge30And40Group.class);
            }
        }
        return defaultGroupSequence;
    }
}

2、在待校驗的javaBean裡使用@GroupSequenceProvider註解指定處理器。並且定義好對應的校驗邏輯(包括分組)

@GroupSequenceProvider(PersonGroupSequenceProvider.class)
@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Range(min = 10, max = 40)
    private Integer age;

    @NotNull(groups = {WhenAge20And30Group.class, WhenAge30And40Group.class})
    @Size(min = 1, max = 2, groups = WhenAge20And30Group.class)
    @Size(min = 3, max = 5, groups = WhenAge30And40Group.class)
    private List<String> hobbies;

    /**
     * 定義專屬的業務邏輯分組
     */
    public interface WhenAge20And30Group {
    }
    public interface WhenAge30And40Group {
    }
}

測試用例同上,做出簡單修改:person.setAge(25),執行列印輸出:

年齡為:25,執行對應校驗邏輯
年齡為:25,執行對應校驗邏輯

沒有校驗失敗的訊息(就是好訊息),符合預期。
再修改為person.setAge(35),再次執行列印如下:

年齡為:35,執行對應校驗邏輯
年齡為:35,執行對應校驗邏輯
hobbies 個數必須在3和5之間: [足球, 籃球]

校驗成功,結果符合預期。
從此案例可以看到,通過@GroupSequenceProvider我完全實現了多欄位組合校驗的邏輯,並且程式碼也非常的優雅、可擴充套件,希望此示例對你有所幫助。

本利中的provider處理器是Person專用的,當然你可以使用Object+反射讓它變得更為通用,但本著職責單一原則,我並不建議這麼去做。

使用JSR提供的@GroupSequence註解控制校驗順序

上面的實現方式是最佳實踐,使用起來不難,靈活度也非常高。但是我們必須要明白它是Hibernate Validation提供的能力,而不費JSR標準提供的。
@GroupSequence它是JSR標準提供的註解(只是沒有provider強大而已,但也有很適合它的使用場景)

// Defines group sequence.  定義組序列(序列:順序執行的)
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
public @interface GroupSequence {
    Class<?>[] value();
}

顧名思義,它表示Group組序列。預設情況下,不同組別的約束驗證是無序的
在某些情況下,約束驗證的順序是非常的重要的,比如如下兩個場景:

  1. 第二個組的約束驗證依賴於第一個約束執行完成的結果(必須第一個約束正確了,第二個約束執行才有意義)
  2. 某個Group組的校驗非常耗時,並且會消耗比較大的CPU/記憶體。那麼我們的做法應該是把這種校驗放到最後,所以對順序提出了要求

一個組可以定義為其他組的序列,使用它進行驗證的時候必須符合該序列規定的順序。在使用組序列驗證的時候,如果序列前邊的組驗證失敗,則後面的組將不再給予驗證。

給個栗子:

public class User {

    @NotEmpty(message = "firstname may be empty")
    private String firstname;
    @NotEmpty(message = "middlename may be empty", groups = Default.class)
    private String middlename;
    @NotEmpty(message = "lastname may be empty", groups = GroupA.class)
    private String lastname;
    @NotEmpty(message = "country may be empty", groups = GroupB.class)
    private String country;


    public interface GroupA {
    }
    public interface GroupB {
    }
    // 組序列
    @GroupSequence({Default.class, GroupA.class, GroupB.class})
    public interface Group {
    }
}

測試:

public static void main(String[] args)  {
    User user = new User();
    // 此處指定了校驗組是:User.Group.class
    Set<ConstraintViolation<User>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class);

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

執行,控制檯列印:

middlename middlename may be empty: null
firstname firstname may be empty: null

現象:只有Default這個Group的校驗了,序列上其它組並沒有執行校驗。更改如下:

        User user = new User();
        user.setFirstname("f");
        user.setMiddlename("s");

執行,控制檯列印:

lastname lastname may be empty: null

現象:Default組都校驗通過後,執行了GroupA組的校驗。但GroupA組校驗木有通過,GroupB組的校驗也就不執行了~
@GroupSequence提供的組序列順序執行以及短路能力,在很多場景下是非常非常好用的。

針對本例的多欄位組合邏輯校驗,若想借助@GroupSequence來完成,相對來說還是比較困難的。但是也並不是不能做,此處我提供參考思路:

  1. 多欄位之間的邏輯、“通訊”通過類級別的自定義校驗註解來實現(至於為何必須是類級別的,不用解釋吧~)
  2. @GroupSequence用來控制組執行順序(讓類級別的自定義註解先執行)
  3. 增加Bean級別的第三屬性來輔助校驗~

當然嘍,在實際應用中不可能使用它來解決如題的問題,所以我此處就不費篇幅了。我個人建議有興趣者可以自己動手試試,有助於加深你對資料校驗這塊的理解。


這篇文章裡有說過:資料校驗註解是可以標註在Field屬性、方法、構造器以及Class類級別上的。那麼關於它們的校驗順序,我們是可控的,並不是網上有些文章所說的無法抉擇~

說明:順序只能控制在分組級別,無法控制在約束註解級別。因為一個類內的約束(同一分組內),它的順序是Set<MetaConstraint<?>> metaConstraints來保證的,所以可以認為同一分組內的校驗器是木有執行的先後順序的(不管是類、屬性、方法、構造器...)

所以網上有說:校驗順序是先校驗欄位屬性,在進行類級別校驗不實,請注意辨別。


原理解析

本文中,我藉助@GroupSequenceProvider來解決了平時開發中多欄位組合邏輯校驗的痛點問題,總的來說還是使用簡單,並且程式碼也夠模組化,易於維護的。
但對於上例的結果輸出,你可能和我一樣至少有如下疑問:

  1. 為何必須有這一句:defaultGroupSequence.add(Person.class)
  2. 為何if (bean != null)必須判空
  3. 為何年齡為:35,執行對應校驗邏輯被輸出了兩次(在判空裡面還出現了兩次哦~),但校驗的失敗資訊卻只有符合預期的一次

帶著問題,我從validate校驗的執行流程上開始分析:
1、入口:ValidatorImpl.validate(T object, Class<?>... groups)

ValidatorImpl:
    @Override
    public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
        Class<T> rootBeanClass = (Class<T>) object.getClass();
        // 獲取BeanMetaData,類上的各種資訊:包括類上的Group序列、針對此類的預設分組List們等等
        BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
        ...
    }

2、beanMetaDataManager.getBeanMetaData(rootBeanClass)得到待校驗Bean的元資訊

請注意,此處只傳入了Class,並沒有傳入Object。這是為啥要加!= null判空的核心原因(後面你可以看到傳入的是null)。

BeanMetaDataManager:
    public <T> BeanMetaData<T> getBeanMetaData(Class<T> beanClass) {
        ...
        // 會呼叫AnnotationMetaDataProvider來解析約束註解元資料資訊(當然還有基於xml/Programmatic的,本文略) 
        // 注意:它會遞迴處理父類、父介面等拿到所有類的元資料

        // BeanMetaDataImpl.build()方法,會new BeanMetaDataImpl(...) 這個建構函式裡面做了N多事
        // 其中就有和我本例有關的defaultGroupSequenceProvider
        beanMetaData = createBeanMetaData( beanClass );
    }

3、new BeanMetaDataImpl( ... )構建出此Class的元資料資訊(本例為Person.class

BeanMetaDataImpl:
    public BeanMetaDataImpl(Class<T> beanClass,
                            List<Class<?>> defaultGroupSequence, // 如果沒有配置,此時候defaultGroupSequence一般都為null
                            DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider, // 我們自定義的處理此Bean的provider
                            Set<ConstraintMetaData> constraintMetaDataSet, // 包含父類的所有屬性、構造器、方法等等。在此處會分類:按照屬性、方法等分類處理
                            ValidationOrderGenerator validationOrderGenerator) {
        ... //對constraintMetaDataSet進行分類
        // 這個方法就是篩選出了:所有的約束註解(比如6個約束註解,此處長度就是6  當然包括了欄位、方法等上的各種。。。)
        this.directMetaConstraints = getDirectConstraints();

        // 因為我們Person類有defaultGroupSequenceProvider,所以此處返回true
        // 除了定義在類上外,還可以定義全域性的:給本類List<Class<?>> defaultGroupSequence此欄位賦值
        boolean defaultGroupSequenceIsRedefined = defaultGroupSequenceIsRedefined();
        
        // 這是為何我們要判空的核心:看看它傳的啥:null。所以不判空的就NPE了。這是第一次呼叫defaultGroupSequenceProvider.getValidationGroups()方法
        List<Class<?>> resolvedDefaultGroupSequence = getDefaultGroupSequence( null );
        ... // 上面拿到resolvedDefaultGroupSequence 分組資訊後,會放到所有的校驗器裡去(包括屬性、方法、構造器、類等等)
        // so,預設組序列還是灰常重要的(注意:預設組可以有多個哦~~~)
    }


    @Override
    public List<Class<?>> getDefaultGroupSequence(T beanState) {
        if (hasDefaultGroupSequenceProvider()) {
            // so,getValidationGroups方法裡請記得判空~
            List<Class<?>> providerDefaultGroupSequence = defaultGroupSequenceProvider.getValidationGroups( beanState );
            // 最重要的是這個方法:getValidDefaultGroupSequence對預設值進行分析~~~
            return getValidDefaultGroupSequence( beanClass, providerDefaultGroupSequence );
        }
        return defaultGroupSequence;
    }

    private static List<Class<?>> getValidDefaultGroupSequence(Class<?> beanClass, List<Class<?>> groupSequence) {
        List<Class<?>> validDefaultGroupSequence = new ArrayList<>();
        boolean groupSequenceContainsDefault = false; // 標誌位:如果解析不到Default這個組  就丟擲異常

        // 重要
        if (groupSequence != null) {
            for ( Class<?> group : groupSequence ) {
                // 這就是為何我們要`defaultGroupSequence.add(Person.class)`這一句的原因所在~~~ 因為需要Default生效~~~
                if ( group.getName().equals( beanClass.getName() ) ) {
                    validDefaultGroupSequence.add( Default.class );
                    groupSequenceContainsDefault = true;
                } 
                // 意思是:你要新增Default組,用本類的Class即可,而不能顯示的新增Default.class哦~
                else if ( group.getName().equals( Default.class.getName() ) ) { 
                    throw LOG.getNoDefaultGroupInGroupSequenceException();
                } else { // 正常新增進預設組
                    validDefaultGroupSequence.add( group );
                }
            }
        }
        // 若找不到Default組,就丟擲異常了~
        if ( !groupSequenceContainsDefault ) {
            throw LOG.getBeanClassMustBePartOfRedefinedDefaultGroupSequenceException( beanClass );
        }
        return validDefaultGroupSequence;
    }

到這一步,還僅僅在初始化BeanMetaData階段,就執行了一次(首次)defaultGroupSequenceProvider.getValidationGroups(null),所以判空是很有必要的。並且把本class add進預設組也是必須的(否則報錯)~
到這裡BeanMetaData<T> rootBeanMetaData建立完成,繼續validate()的邏輯~

4、determineGroupValidationOrder(groups)從呼叫者指定的分組裡確定組序列(組的執行順序)

ValidatorImpl:
    @Override
    public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
        ...
        BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
        ...
        ... // 準備好ValidationContext(持有rootBeanMetaData和object例項)
        
        // groups是呼叫者傳進來的分組陣列(對應Spring MVC中指定的Group資訊~)
        ValidationOrder validationOrder = determineGroupValidationOrder(groups);
        ... // 準備好ValueContext(持有rootBeanMetaData和object例項)

        // 此時還是Bean級別的,開始對此bean執行校驗
        return validateInContext( validationContext, valueContext, validationOrder );
    }

    private ValidationOrder determineGroupValidationOrder(Class<?>[] groups) {
        Collection<Class<?>> resultGroups;
        // if no groups is specified use the default
        if ( groups.length == 0 ) {
            resultGroups = DEFAULT_GROUPS;
        } else {
            resultGroups = Arrays.asList( groups );
        }
        // getValidationOrder()主要邏輯描述。此時候resultGroups 至少也是個[Default.class]
        // 1、如果僅僅只是一個Default.class,那就直接return
        // 2、遍歷所有的groups。(指定的Group必須必須是介面)
        // 3、若遍歷出來的group標註有`@GroupSequence`註解,特殊處理此序列(把序列裡的分組們新增進來)
        // 4、普通的Group,那就new Group( clazz )新增進`validationOrder`裡。並且遞迴插入(因為可能存在父介面的情況)
        return validationOrderGenerator.getValidationOrder( resultGroups );
    }

到這ValidationOrder(實際為DefaultValidationOrder)儲存著呼叫者呼叫validate()方法時傳入的Groups們。分組序列@GroupSequence在此時會被解析。
到了validateInContext( ... )就開始拿著這些Groups分組、元資訊開始對此Bean進行校驗了~

5、validateInContext( ... )在上下文(校驗上下文、值上下文、指定的分組裡)對此Bean進行校驗

ValidatorImpl:
    private <T, U> Set<ConstraintViolation<T>> validateInContext(ValidationContext<T> validationContext, ValueContext<U, Object> valueContext, ValidationOrder validationOrder) {
        if ( valueContext.getCurrentBean() == null ) { // 相容整個Bean為null值
            return Collections.emptySet();
        }
        // 如果該Bean頭上標註了(需要defaultGroupSequence處理),那就特殊處理一下
        // 本例中我們的Person肯定為true,可以進來的
        BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
        if ( beanMetaData.defaultGroupSequenceIsRedefined() ) {

            // 注意此處又呼叫了beanMetaData.getDefaultGroupSequence()這個方法,這算是二次呼叫了
            // 此處傳入的Object喲~這就解釋了為何在判空裡面的 `年齡為:xxx`被列印了兩次的原因
            // assertDefaultGroupSequenceIsExpandable方法是個空方法(預設情況下),可忽略
            validationOrder.assertDefaultGroupSequenceIsExpandable( beanMetaData.getDefaultGroupSequence( valueContext.getCurrentBean() ) );
        }

        // ==============下面對於執行順序,就很重要了===============
        // validationOrder裝著的是呼叫者指定的分組(解析分組序列來保證順序~~~)
        // 需要特別注意:光靠指定分組,是無序的(不能保證校驗順序的) 所以若指定多個分組需要小心求證
        Iterator<Group> groupIterator = validationOrder.getGroupIterator();
        // 按照呼叫者指定的分組(順序),一個一個的執行分組校驗。
        while ( groupIterator.hasNext() ) {
            Group group = groupIterator.next();
            valueContext.setCurrentGroup(group.getDefiningClass()); // 設定當前正在執行的分組

            // 這個步驟就稍顯複雜了,也是核心的邏輯之一。大致過程如下:
            // 1、拿到該Bean的BeanMetaData
            // 2、若defaultGroupSequenceIsRedefined()=true  本例Person標註了provder註解,所以有指定的分組序列的
            // 3、根據分組序列的順序,挨個執行分組們(對所有的約束MetaConstraint都順序執行分組們)
            // 4、最終完成所有的MetaConstraint的校驗,進而完成此部分所有的欄位、方法等的校驗
            validateConstraintsForCurrentGroup( validationContext, valueContext );
            if ( shouldFailFast( validationContext ) ) {
                return validationContext.getFailingConstraints();
            }
        }
        
        ... // 和上面一樣的程式碼,校驗validateCascadedConstraints
        
        // 繼續遍歷序列:和@GroupSequence相關了
        Iterator<Sequence> sequenceIterator = validationOrder.getSequenceIterator();
        ...

        // 校驗上下文的錯誤訊息:它會把本校驗下,所有的驗證器上下文ConstraintValidatorContext都放一起的
        // 注意:所有的校驗註解之間的上下文ConstraintValidatorContext是完全獨立的,無法互相訪問通訊
        return validationContext.getFailingConstraints();
    }

that is all. 到這一步整個校驗就完成了,若不快速失敗,預設會拿到所有校驗失敗的訊息。


真正執行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) {
            
        boolean isValid;
        // 解析出value值
        V validatedValue = (V) valueContext.getCurrentValidatedValue(); 
        // 把value值交給校驗器的isValid方法去校驗~~~
        isValid = validator.isValid(validatedValue,constraintValidatorContext);
        ...
        if (!isValid) {
            // 校驗沒通過就使用constraintValidatorContext校驗上下文來生成錯誤訊息
            // 使用上下文是因為:畢竟錯誤訊息可不止一個啊~~~
            // 當然此處藉助了executionContext的方法~~~內部其實呼叫的是constraintValidatorContext.getConstraintViolationCreationContexts()這個方法而已
            return executionContext.createConstraintViolations(valueContext, constraintValidatorContext);
        }
    }
}

至於上下文ConstraintValidatorContext怎麼來的,是new出來的:new ConstraintValidatorContextImpl( ... ),每個欄位的一個校驗註解對應一個上下文(一個屬性上可以標註多個約束註解哦~),所以此上下文是有很強的隔離性的。

ValidationContext<T> validationContextValueContext<?, Object> valueContext它哥倆是類級別的,直到ValidatorImpl.validateMetaConstraints方法開始一個一個約束器的校驗~

自定義註解中只把ConstraintValidatorContext上下文給呼叫者使用,而並沒有給validationContextvalueContext,我個人覺得這個設計是不夠靈活的,無法方便的實現dependOn的效果~


解決網友的問題

我把這部分看似是本文最重要的引線放到最後,是因為我覺得我的描述已經解決這一類問題,而不是隻解決了這一個問題。

回到文首截圖中熱心網友反應的問題,只要你閱讀了本文,我十分堅信你已經有辦法去使用Bean Validation優雅的解決了。如果各位沒有意見,此處我就略了~

總結

本文講述了使用@GroupSequenceProvider來解決多欄位聯合邏輯校驗的這一類問題,這也許是曾經很多人的開發痛點,希望本文能幫你一掃之前的障礙,全面擁抱Bean Validation吧~
本文我也傳達了一個觀點:相信流行的開源東西的優秀,不是非常極端的case,深入使用它能解決你絕大部分的問題的。

相關閱讀

【小家Spring】@Validated和@Valid的區別?教你使用它完成Controller引數校驗(含級聯屬性校驗)以及原理分析
【小家Spring】Bean Validation完結篇:你必須關注的邊邊角角(約束級聯、自定義約束、自定義校驗器、國際化失敗訊息...)
【小家Java】深入瞭解資料校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例

知識交流

==The last:如果覺得本文對你有幫助,不妨點個讚唄。當然分享到你的朋友圈讓更多小夥伴看到也是被作者本人許可的~==

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

若有圖裂問題/排版問題,請點選:原文連結-原文連結-原文連結

==若對Spring、SpringBoot、MyBatis等原始碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起