天天看點

SpringBoot 如何進行優雅的資料校驗

在程式進行資料處理之前,對資料進行準确性校驗是我們必須要考慮的事情。盡早發現資料錯誤,不僅可以防止錯誤向核心業務邏輯蔓延,而且這種錯誤非常明顯,容易發現解決。

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

帶批注的元素必須是一個在可接受範圍内的數字

@Email

顧名思義

@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進行真正的校驗處理。

SpringBoot 如何進行優雅的資料校驗

繼續閱讀