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 MVC
的Controller
層面的,並且幾乎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
擴展提供了直接在註解層面指定分組的能力
咱們知道JSR
提供了一個@Valid
註解供以使用,在本文以前,絕大多數小夥伴都是在Controller
中而且結合@RequestBody
一塊兒來使用它,但在本文以後,你定會對它有個全新的認識~this
==該註解用於驗證級聯的屬性、方法參數或方法返回類型。==
當驗證屬性、方法參數或方法返回類型時,將驗證對象及其屬性上定義的約束,另外:此行爲是遞歸應用的。
spa
:::爲了理解@Valid
,那就得知道處理它的時機:::
元數據提供者:約束相關元數據(如約束、默認組序列等)的Provider
。它的做用和特色以下:
public enum ConfigurationSource { ANNOTATION( 0 ), XML( 1 ), API( 2 ); //programmatic API }
MetaDataProvider
只返回直接爲一個類配置的元數據簡單的說你@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
這個元數據均來自於註解的標註,而後它是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()
這個私有方法上。總結一下調用此方法的兩個原始入口(一個構造器,一個接口方法):
ValidatorFactory.getValidator()
獲取校驗器的時候,初始化時會本身new
一個,調用棧以下圖:
調用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
:clazz.getDeclaredFields()
把每一個Field
都包裝成ConstrainedElement
存放起來~~~
1. 注意:此步驟完成了對每一個`Field`上標註的註解進行了保存
Method
:clazz.getDeclaredMethods()
把每一個Method都轉換成一個ConstrainedExecutable
裝着~~(ConstrainedExecutable
也是個ConstrainedElement
)。在此期間它完成了以下事(方法和構造器都複雜點,由於包含入參和返回值):
1. 找到方法上全部的註解保存起來 2. 處理入參、返回值(包括自動判斷是做用在入參仍是返回值上)
徹底同處理Method,略
ConstraintDescriptor
對已經找到每一個ConstraintDescriptor
進行處理,最終都轉換Set<MetaConstraint<?>>
這個類型
1.
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
一把吧:==
上面用一個示例校驗Person
這個JavaBean
了,可是你會發現示例中咱們全都是校驗的Field
屬性。從理論裏咱們知道了Bean Validation
它是有校驗方法、構造器、入參甚至遞歸校驗級聯屬性的能力的:
略
這些是不能直接使用的,須要在運行時進行校驗。具體使用可參考:【小家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入羣"
字樣,會手動邀請入羣**