解決多字段聯合邏輯校驗問題【享學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,手動邀請你入群一起飛


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM