在程式進行資料處理之前,對資料進行準确性校驗是我們必須要考慮的事情。盡早發現資料錯誤,不僅可以防止錯誤向核心業務邏輯蔓延,而且這種錯誤非常明顯,容易發現解決。
JSR303 規範(Bean Validation 規範)為 JavaBean 驗證定義了相應的中繼資料模型和 API。在應用程式中,通過使用 Bean Validation 或是你自己定義的 constraint,例如 @NotNull, @Max, @ZipCode , 就可以確定資料模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,類或者接口上面。對于一些特定的需求,使用者可以很容易的開發定制化的 constraint。Bean Validation 是一個運作時的資料驗證架構,在驗證之後驗證的錯誤資訊會被馬上傳回。
關于 JSR 303 – Bean Validation 規範,可以參考官網
對于 JSR 303 規範,Hibernate Validator 對其進行了參考實作 . Hibernate Validator 提供了 JSR 303 規範中所有内置 constraint 的實作,除此之外還有一些附加的 constraint。如果想了解更多有關 Hibernate Validator 的資訊,請檢視官網。
Constraint
詳細資訊
@AssertFalse
被注釋的元素必須為 false
@AssertTrue
同@AssertFalse
@DecimalMax
被注釋的元素必須是一個數字,其值必須小于等于指定的最大值
@DecimalMin
同DecimalMax
@Digits
帶批注的元素必須是一個在可接受範圍内的數字
顧名思義
@Future
将來的日期
@FutureOrPresent
現在或将來
@Max
@Min
被注釋的元素必須是一個數字,其值必須大于等于指定的最小值
@Negative
帶注釋的元素必須是一個嚴格的負數(0為無效值)
@NegativeOrZero
帶注釋的元素必須是一個嚴格的負數(包含0)
@NotBlank
同StringUtils.isNotBlank
@NotEmpty
同StringUtils.isNotEmpty
@NotNull
不能是Null
@Null
元素是Null
@Past
被注釋的元素必須是一個過去的日期
@PastOrPresent
過去和現在
@Pattern
被注釋的元素必須符合指定的正規表達式
@Positive
被注釋的元素必須嚴格的正數(0為無效值)
@PositiveOrZero
被注釋的元素必須嚴格的正數(包含0)
@Szie
帶注釋的元素大小必須介于指定邊界(包括)之間
被注釋的元素必須是電子郵箱位址
@Length
被注釋的字元串的大小必須在指定的範圍内
被注釋的字元串的必須非空
@Range
被注釋的元素必須在合适的範圍内
CreditCardNumber
被注釋的元素必須符合信用卡格式
Hibernate Validator 不同版本附加的 Constraint 可能不太一樣,具體還需要你自己檢視你使用版本。Hibernate 提供的 Constraint在org.hibernate.validator.constraints這個包下面。
一個 constraint 通常由 annotation 和相應的 constraint validator 組成,它們是一對多的關系。也就是說可以有多個 constraint validator 對應一個 annotation。在運作時,Bean Validation 架構本身會根據被注釋元素的類型來選擇合适的 constraint validator 對資料進行驗證。
有些時候,在使用者的應用中需要一些更複雜的 constraint。Bean Validation 提供擴充 constraint 的機制。可以通過兩種方法去實作,一種是組合現有的 constraint 來生成一個更複雜的 constraint,另外一種是開發一個全新的 constraint。
Spring Validation 對 hibernate validation 進行了二次封裝,可以讓我們更加友善地使用資料校驗功能。這邊我們通過 Spring Boot 來引用校驗功能。
如果你用的 Spring Boot 版本小于 2.3.x,spring-boot-starter-web 會自動引入 hibernate-validator 的依賴。如果 Spring Boot 版本大于 2.3.x,則需要手動引入依賴:
有時候接口的參數比較少,隻有一個活着兩個參數,這時候就沒必要定義一個DTO來接收參數,可以直接接收參數。
下面是統一異常處理類
調用結果
實體類DTO校驗
定義一個DTO
接收參數時使用@Validated進行校驗
統一異常處理
對Service層方法參數校驗
個人不太喜歡這種校驗方式,一半情況下調用service層方法的參數都需要在controller層校驗好,不需要再校驗一次。這邊列舉這個功能,隻是想說 Spring 也支援這個。
分組校驗
有時候對于不同的接口,需要對DTO進行不同的校驗規則。還是以上面的UserDTO為列,另外一個接口可能不需要将age限制在18~50之間,隻需要大于18就可以了。
這樣上面的校驗規則就不适用了。分組校驗就是來解決這個問題的,同一個DTO,不同的分組采用不同的校驗政策。
使用方式
使用Group1分組進行校驗,因為DTO中,Group1分組對name屬性沒有校驗,是以這個校驗将不會生效。
分組校驗的好處是可以對同一個DTO設定不同的校驗規則,缺點就是對于每一個新的校驗分組,都需要重新設定下這個分組下面每個屬性的校驗規則。
分組校驗還有一個按順序校驗功能。
考慮一種場景:一個bean有1個屬性(假如說是attrA),這個屬性上添加了3個限制(假如說是@NotNull、@NotEmpty、@NotBlank)。預設情況下,validation-api對這3個限制的校驗順序是随機的。也就是說,可能先校驗@NotNull,再校驗@NotEmpty,最後校驗@NotBlank,也有可能先校驗@NotBlank,再校驗@NotEmpty,最後校驗@NotNull。
那麼,如果我們的需求是先校驗@NotNull,再校驗@NotBlank,最後校驗@NotEmpty。@GroupSequence注解可以實作這個功能。
嵌套校驗
前面的示例中,DTO類裡面的字段都是基本資料類型和String等類型。
但是實際場景中,有可能某個字段也是一個對象,如果我們需要對這個對象裡面的資料也進行校驗,可以使用嵌套校驗。
假如UserDTO中還用一個Job對象,比如下面的結構。需要注意的是,在job類的校驗上面一定要加上@Valid注解。
測試結果
嵌套校驗可以結合分組校驗一起使用。還有就是嵌套集合校驗會對集合裡面的每一項都進行校驗,例如List字段會對這個list裡面的每一個Job對象都進行校驗。這個點
在下面的@Valid和@Validated的差別章節有詳細講到。
如果請求體直接傳遞了json數組給背景,并希望對數組中的每一項都進行參數校驗。此時,如果我們直接使用java.util.Collection下的list或者set來接收資料,參數校驗并不會生效!我們可以使用自定義list集合來接收參數:
包裝List類型,并聲明@Valid注解
調用方法
會抛出NotReadablePropertyException異常,需要對這個異常做統一處理。這邊代碼就不貼了。
在Spring中自定義校驗器非常簡單,分兩步走。
自定義限制注解
實作ConstraintValidator接口編寫限制校驗器
程式設計式校驗
上面的示例都是基于注解來實作自動校驗的,在某些情況下,我們可能希望以程式設計方式調用驗證。這個時候可以注入
javax.validation.Validator對象,然後再調用其api。
快速失敗(Fail Fast)配置
Spring Validation預設會校驗完所有字段,然後才抛出異常。可以通過一些簡單的配置,開啟Fali Fast模式,一旦校驗失敗就立即傳回。
校驗資訊的國際化
Spring 的校驗功能可以傳回很友好的校驗資訊提示,而且這個資訊支援國際化。
這塊功能暫時暫時不常用,具體可以參考這篇文章
首先,@Validated和@Valid都能實作基本的驗證功能,也就是如果你是想驗證一個參數是否為空,長度是否滿足要求這些簡單功能,使用哪個注解都可以。
但是這兩個注解在分組、注解作用的地方、嵌套驗證等功能上兩個有所不同。下面列下這兩個注解主要的不同點。
@Valid注解是JSR303規範的注解,@Validated注解是Spring架構自帶的注解;
@Valid不具有分組校驗功能,@Validate具有分組校驗功能;
@Valid可以用在方法、構造函數、方法參數和成員屬性(字段)上,@Validated可以用在類型、方法和方法參數上。但是不能用在成員屬性(字段)上,兩者是否能用于成員屬性(字段)上直接影響能否提供嵌套驗證的功能;
@Valid加在成員屬性上可以對成員屬性進行嵌套驗證,而@Validate不能加在成員屬性上,是以不具備這個功能。
這邊說明下,什麼叫嵌套驗證。
我們現在有個實體叫做Item:
Item帶有很多屬性,屬性裡面有:pid、vid、pidName和vidName,如下所示:
屬性這個實體也有自己的驗證機制,比如pid和vid不能為空,pidName和vidName不能為空等。
現在我們有個ItemController接受一個Item的入參,想要對Item進行驗證,如下所示:
在上圖中,如果Item實體的props屬性不額外加注釋,隻有@NotNull和@Size,無論入參采用@Validated還是@Valid驗證,Spring Validation架構隻會對Item的id和props做非空和數量驗證,不會對props字段裡的Prop實體進行字段驗證,也就是@Validated和@Valid加在方法參數前,都不會自動對參數進行嵌套驗證。也就是說如果傳的List中有Prop的pid為空或者是負數,入參驗證不會檢測出來。
為了能夠進行嵌套驗證,必須手動在Item實體的props字段上明确指出這個字段裡面的實體也要進行驗證。由于@Validated不能用在成員屬性(字段)上,但是@Valid能加在成員屬性(字段)上,而且@Valid類注解上也說明了它支援嵌套驗證功能,那麼我們能夠推斷出:@Valid加在方法參數時并不能夠自動進行嵌套驗證,而是用在需要嵌套驗證類的相應字段上,來配合方法參數上@Validated或@Valid來進行嵌套驗證。
我們修改Item類如下所示:
然後我們在ItemController的addItem函數上再使用@Validated或者@Valid,就能對Item的入參進行嵌套驗證。此時Item裡面的props如果含有Prop的相應字段為空的情況,Spring Validation架構就會檢測出來,bindingResult就會記錄相應的錯誤。
現在我們來簡單分析下Spring校驗功能的原理。
所謂的方法級别的校驗就是指将@NotNull和@NotEmpty這些限制直接加在方法的參數上的。
比如
或者
都屬于方法級别的校驗。這種方式可用于任何Spring Bean的方法上,比如Controller/Service等。
其底層實作原理就是AOP,具體來說是通過MethodValidationPostProcessor動态注冊AOP切面,然後使用MethodValidationInterceptor對切點方法織入增強。
接着看一下MethodValidationInterceptor:
DTO級别的校驗
這種屬于DTO級别的校驗。在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的參數以及處理@ResponseBody标注方法的傳回值的。顯然,執行參數校驗的邏輯肯定就在解析參數的方法resolveArgument()中。
可以看到,resolveArgument()調用了validateIfApplicable()進行參數校驗。
看到這裡,大家應該能明白為什麼這種場景下@Validated、@Valid兩個注解可以混用。我們接下來繼續看WebDataBinder.validate()實作。
最終發現底層最終還是調用了Hibernate Validator進行真正的校驗處理。