摘要:如果說要使用數(shù)據(jù)校驗,我十分相信小伙伴們都能夠使用,但估計大都是有個前提的環(huán)境。具體使用可參考小家讓支持對平鋪參數(shù)執(zhí)行數(shù)據(jù)校驗?zāi)J使用只能對進行校驗級聯(lián)校驗什么叫級聯(lián)校驗,其實就是帶校驗的成員里存在級聯(lián)對象時,也要對它完成校驗。
每篇一句
NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術(shù)相關(guān)閱讀
【小家Java】深入了解數(shù)據(jù)校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】讓Controller支持對平鋪參數(shù)執(zhí)行數(shù)據(jù)校驗(默認Spring MVC使用@Valid只能對JavaBean進行校驗)
【小家Spring】Spring方法級別數(shù)據(jù)校驗:@Validated + MethodValidationPostProcessor優(yōu)雅的完成數(shù)據(jù)校驗動作
關(guān)于Bean Validation的基本原理篇完結(jié)之后,接下來就是小伙伴最為關(guān)心的干貨:使用篇。
如果說要使用Bean Validation數(shù)據(jù)校驗,我十分相信小伙伴們都能夠使用,但估計大都是有個前提的:Spring MVC環(huán)境。我極其簡單的調(diào)查了一下,近乎99%的人都是只把數(shù)據(jù)校驗使用在Spring MVC的Controller層面的,而且?guī)缀?b>90%的人都是讓它必須和@RequestBody一起來使用去校驗JavaBean入?yún)
如果這么去理解Bean Validation的使用,那就有點太過于片面了,畢竟被Spring包裹起來,你其實很難去知道它真正做的事。
熟悉我文章風(fēng)格的人知道,每篇文章我都會帶你領(lǐng)略一些不一樣的風(fēng)景,本章亦不例外,會讓你知道數(shù)據(jù)校驗在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) // 內(nèi)置的分組: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 { } }
執(zhí)行分組校驗:
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(); // 根據(jù)validatorFactory拿到一個Validator Validator validator = validatorFactory.getValidator(); // 分組校驗(可以區(qū)分對待Default組、Simple組、Complex組) Set> result = validator.validate(person, Person.Simple.class); //Set > result = validator.validate(person, Person.Complex.class); // 對結(jié)果進行遍歷輸出 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
運行打印:
age 最大不能超過10: 18 name {message} -> 名字不能為null -> 名字不能為null: null
可以直觀的看到效果,此處的校驗只執(zhí)行Person.Simple.class這個Group組上的約束~
分組約束在Spring MVC中的使用場景還是相對比較多的,但是需要注意的是:javax.validation.Valid沒有提供指定分組的,但是org.springframework.validation.annotation.Validated擴展提供了直接在注解層面指定分組的能力@Valid注解
我們知道JSR提供了一個@Valid注解供以使用,在本文之前,絕大多數(shù)小伙伴都是在Controller中并且結(jié)合@RequestBody一起來使用它,但在本文之后,你定會對它有個全新的認識~
==該注解用于驗證級聯(lián)的屬性、方法參數(shù)或方法返回類型。==
當驗證屬性、方法參數(shù)或方法返回類型時,將驗證對象及其屬性上定義的約束,另外:此行為是遞歸應(yīng)用的。
:::為了理解@Valid,那就得知道處理它的時機:::
MetaDataProvider元數(shù)據(jù)提供者:約束相關(guān)元數(shù)據(jù)(如約束、默認組序列等)的Provider。它的作用和特點如下:
基于不同的元數(shù)據(jù):如xml、注解。(還有個編程映射) 這三種類型。對應(yīng)的枚舉類為:
public enum ConfigurationSource { ANNOTATION( 0 ), XML( 1 ), API( 2 ); //programmatic API }
MetaDataProvider只返回直接為一個類配置的元數(shù)據(jù)
它不處理從超類、接口合并的元數(shù)據(jù)(簡單的說你@Valid放在接口處是無效的)
public interface MetaDataProvider { // 將**注解處理選項**歸還給此Provider配置。 它的唯一實現(xiàn)類為:AnnotationProcessingOptionsImpl // 它可以配置比如:areMemberConstraintsIgnoredFor areReturnValueConstraintsIgnoredFor // 也就說可以配置:讓免于被校驗~~~~~~(開綠燈用的) AnnotationProcessingOptions getAnnotationProcessingOptions(); // 返回作用在此Bean上面的`BeanConfiguration` 若沒有就返回null了 // BeanConfiguration持有ConfigurationSource的引用~BeanConfiguration super T> getBeanConfiguration(Class beanClass); } // 表示源于一個ConfigurationSource的一個Java類型的完整約束相關(guān)配置。 包含字段、方法、類級別上的元數(shù)據(jù) // 當然還包含有默認組序列上的元數(shù)據(jù)(使用較少) public class BeanConfiguration { // 三種來源的枚舉 private final ConfigurationSource source; private final Class beanClass; // ConstrainedElement表示待校驗的元素,可以知道它會如下四個子類: // ConstrainedField/ConstrainedType/ConstrainedParameter/ConstrainedExecutable // 注意:ConstrainedExecutable持有的是java.lang.reflect.Executable對象 //它的兩個子類是java.lang.reflect.Method和Constructor private final Set constrainedElements; private final List > defaultGroupSequence; private final DefaultGroupSequenceProvider super T> defaultGroupSequenceProvider; ... // 它自己并不處理什么邏輯,參數(shù)都是通過構(gòu)造器傳進來的 }
它的繼承樹:
三個實現(xiàn)類對應(yīng)著上面所述的三種元數(shù)據(jù)類型。本文很顯然只需要關(guān)注和注解相關(guān)的:AnnotationMetaDataProvider
這個元數(shù)據(jù)均來自于注解的標注,然后它是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
如上可知,核心解析邏輯在retrieveBeanConfiguration()這個私有方法上。總結(jié)一下調(diào)用此方法的兩個原始入口(一個構(gòu)造器,一個接口方法):
ValidatorFactory.getValidator()獲取校驗器的時候,初始化時會自己new一個,調(diào)用棧如下圖:
調(diào)用Validator.validate()方法的時候,beanMetaDataManager.getBeanMetaData( rootBeanClass )它會遍歷初始化時所有的metaDataProviders(默認情況下兩個,沒有xml方式的),拿出所有的BeanConfiguration交給BeanMetaDataBuilder,最終構(gòu)建出一個屬于此Bean的BeanMetaData。對此有一點注意事項描述如下:
1. 處理`MetaDataProvider`時會調(diào)用`ClassHierarchyHelper.getHierarchy( beanClass ) `方法,不僅僅處理本類。拿到本類自己和所有父類后,統(tǒng)一交給`provider.getBeanConfiguration( clazz )`處理(**也就是說任何一個類都會把Object類處理一遍**)
這個方法說白了,就是從Bean里面去檢索屬性、方法、構(gòu)造器等需要校驗的ConstrainedElement項。
privateBeanConfiguration retrieveBeanConfiguration(Class beanClass) { // 它檢索的范圍是:clazz.getDeclaredFields() 什么意思:就是搜集到本類所有的字段 包括private等等 但是不包括父類的所有字段 Set 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 > 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如下:(兩個)
這是直觀的結(jié)論,可以看到僅僅是一個簡單的類其實所包含的項是挺多的。
此處說一句:項是有這么多,但是并不是每一個都需要走驗證邏輯的。因為畢竟大多數(shù)項上面并沒有約束(注解),大多數(shù)ConstrainedElement.getConstraints()為空嘛~
總得來說,我個人建議不能光只記憶結(jié)論,因為那很容易忘記,所以還是得稍微深入一點,讓記憶更深刻吧。那就從下面四個方面深入:
拿到本類所有字段Field:clazz.getDeclaredFields()
把每個Field都包裝成ConstrainedElement存放起來~~~
1. 注意:此步驟完成了對每個`Field`上標注的注解進行了保存
拿到本類所有的方法Method:clazz.getDeclaredMethods()
排除掉靜態(tài)方法和合成(isSynthetic)方法
把每個Method都轉(zhuǎn)換成一個ConstrainedExecutable裝著~~(ConstrainedExecutable也是個ConstrainedElement)。在此期間它完成了如下事(方法和構(gòu)造器都復(fù)雜點,因為包含入?yún)⒑头祷刂?/strong>):
1. 找到方法上所有的注解保存起來 2. 處理入?yún)ⅰ⒎祷刂担òㄗ詣优袛嗍亲饔迷谌雲(yún)⑦€是返回值上)
完全同處理Method,略
找打標注在此類上的所有的注解,轉(zhuǎn)換成ConstraintDescriptor
對已經(jīng)找到每個ConstraintDescriptor進行處理,最終都轉(zhuǎn)換Set
1.
把Set
==關(guān)于級聯(lián)校驗此處補充說明一點,處理Type,都會處理級聯(lián)校驗情況,并且還是遞歸處理:==
也就是這個方法(課件@Valid在此處生效):
// type解釋:分如下N中情況 // Field為:.getGenericType() // 字段的類型 // Method為:.getGenericReturnType() // 返回值類型 // Constructor:.getDeclaringClass() // 構(gòu)造器所在類 // annotatedElement:可不一定說一定要有注解才能進來(每個字段、方法、構(gòu)造器等都能傳進來) private CascadingMetaDataBuilder getCascadingMetaData(Type type, AnnotatedElement annotatedElement, Map, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) { return CascadingMetaDataBuilder.annotatedObject( type, annotatedElement.isAnnotationPresent( Valid.class ), containerElementTypesCascadingMetaData, getGroupConversions( annotatedElement ) ); }
這里對我們理解級聯(lián)校驗最重要的一句是:annotatedElement.isAnnotationPresent(Valid.class)。也就是說:若元素被此注解標注了,那就證明需要對它進行級聯(lián)校驗,這就是JSR定位@Valid的作用~
Spring提升了它???請關(guān)注后文Spring對它的應(yīng)用吧~ConstraintValidator.isValid()調(diào)用處
我們知道,每個約束注解都是交給約束校驗器ConstraintValidator.isValid()這個方法來處理的,它被調(diào)用(生效)的地方在此(唯一處):
public abstract class ConstraintTree { ... protected finalSet > validateSingleConstraint(ValidationContext executionContext, ValueContext, ?> valueContext, ConstraintValidatorContextImpl constraintValidatorContext, ConstraintValidator validator) { ... V validatedValue = (V) valueContext.getCurrentValidatedValue(); isValid = validator.isValid( validatedValue, constraintValidatorContext ); ... // 顯然校驗不通過就返回錯誤消息 否則返回空集合 if ( !isValid ) { return executionContext.createConstraintViolations(valueContext, constraintValidatorContext); } return Collections.emptySet(); } ... }
這個方法的調(diào)用,會在執(zhí)行每個Group的時候
success = metaConstraint.validateConstraint( validationContext, valueContext );
MetaConstraint在上面檢索的時候就已經(jīng)準備好了,最后通過ConstrainedElement.getConstraints就拿到了每個元素的校驗器們,繼續(xù)調(diào)用
// ConstraintTree boolean validationResult = constraintTree.validateConstraints( executionContext, valueContext );
so,最終就調(diào)用到了isValid這個真正做事的方法上了。
==說了這么多,你可能還云里霧里,那么就show一把吧:==
Demo Show上面用一個示例校驗Person這個JavaBean了,但是你會發(fā)現(xiàn)示例中我們?nèi)际切r灥?b>Field屬性。從理論里我們知道了Bean Validation它是有校驗方法、構(gòu)造器、入?yún)⑸踔吝f歸校驗級聯(lián)屬性的能力的:
校驗屬性Field略
校驗Method入?yún)ⅰ⒎祷刂?/b> 校驗Constructor入?yún)ⅰ⒎祷刂?/b> 既校驗入?yún)ⅲ瑫r也校驗返回值這些是不能直接使用的,需要在運行時進行校驗。具體使用可參考:【小家Spring】讓Controller支持對平鋪參數(shù)執(zhí)行數(shù)據(jù)校驗(默認Spring MVC使用@Valid只能對JavaBean進行校驗)
級聯(lián)校驗什么叫級聯(lián)校驗,其實就是帶校驗的成員里存在級聯(lián)對象時,也要對它完成校驗。這個在實際應(yīng)用場景中是比較常見的,比如入?yún)?b>Person對象中,還持有Child對象,我們不僅僅要完成Person的校驗,也依舊還要對Child內(nèi)的屬性校驗:
@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> result = validator.validate(person); // 輸出錯誤消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
運行:
child.age 必須是正數(shù): -1 age 不能為null: null
對child.age這個級聯(lián)屬性校驗成功~
總結(jié)本文值得說是深入了解數(shù)據(jù)校驗(Bean Validation)了,對于數(shù)據(jù)校驗的基本使用一直都不是難事,特別是在Spring環(huán)境下使用就更簡單了~
知識交流若文章格式混亂,可點擊:原文鏈接-原文鏈接-原文鏈接-原文鏈接-原文鏈接
==The last:如果覺得本文對你有幫助,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~==
**若對技術(shù)內(nèi)容感興趣可以加入wx群交流:Java高工、架構(gòu)師3群。
若群二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。并且備注:"java入群" 字樣,會手動邀請入群**
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/75698.html
摘要:和上標注的約束都會被執(zhí)行注意如果子類覆蓋了父類的方法,那么子類和父類的約束都會被校驗。 每篇一句 沒有任何技術(shù)方案會是一種銀彈,任何東西都是有利弊的 相關(guān)閱讀 【小家Java】深入了解數(shù)據(jù)校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【小家Spring】Spring方法級別數(shù)據(jù)校...
摘要:畢竟永遠相信本文能給你帶來意想不到的收獲使用示例關(guān)于數(shù)據(jù)校驗這一塊在中的使用案例,我相信但凡有點經(jīng)驗的程序員應(yīng)該沒有不會使用的,并且還不乏熟練的選手。 每篇一句 NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術(shù) 相關(guān)閱讀 【小家Java】深入了解數(shù)據(jù)校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validati...
摘要:添加依賴,如果使用了,則不需要引用任何依賴,因為包中已經(jīng)包含了依賴在類的屬性上加上對應(yīng)的注解核查結(jié)果中文不能為空檢查結(jié)果中文級聯(lián)校驗不能超過個嫌疑人集合被注釋的元素必須為被注釋的元素必須不為被注釋的元素必須為被注釋的元素必須為被 1.添加 Hibernate-Validator 依賴,如果使用了springboot,則不需要引用任何依賴,因為spring-boot-starter-we...
摘要:就這樣借助相關(guān)約束注解,就非常簡單明了,語義清晰的優(yōu)雅的完成了方法級別入?yún)⑿r灧祷刂敌r灥男r灐5热羰欠祷刂敌r瀳?zhí)行了即使是失敗了,方法體也肯定被執(zhí)行了只能哪些類型上提出這個細節(jié)的目的是約束注解并不是能用在所有類型上的。 每篇一句 在《深度工作》中作者提出這么一個公式:高質(zhì)量產(chǎn)出=時間*專注度。所以高質(zhì)量的產(chǎn)出不是靠時間熬出來的,而是效率為王 相關(guān)閱讀 【小家Java】深入了解數(shù)據(jù)校...
摘要:和一起使用參照博文從原理層面掌握的使用一起學(xué)。至于具體原因,可以移步這里輔助理解從原理層面掌握的使用核心原理篇一起學(xué)再看下面的變種例子重要訪問。 每篇一句 每個人都應(yīng)該想清楚這個問題:你是祖師爺賞飯吃的,還是靠老天爺賞飯吃的 前言 上篇文章 描繪了@ModelAttribute的核心原理,這篇聚焦在場景使用上,演示@ModelAttribute在不同場景下的使用,以及注意事項(當然有些...
閱讀 3699·2021-10-13 09:40
閱讀 3161·2021-10-09 09:53
閱讀 3559·2021-09-26 09:46
閱讀 1860·2021-09-08 09:36
閱讀 4254·2021-09-02 09:46
閱讀 1323·2019-08-30 15:54
閱讀 3188·2019-08-30 15:44
閱讀 1031·2019-08-30 11:06