1. 不吹不擂,第一篇就能提升你對Bean Validation資料校驗的認知

YourBatman發表於2020-09-01

喬丹是我聽過的籃球之神,科比是我親眼見過的籃球之神。本文已被 https://www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。

✍前言

你好,我是YourBatman。

作為一個開發者,聊起資料校驗(Bean Validation),不管是前、中、後端都耳熟能詳,並且心裡暗爽:so easy。

的確,對資料做校驗是一個程式設計師的基本素質,它不難但發生在我們程式的幾乎每個角落,就像下面這幅圖所示:每一層都需要做校驗

如果你真的這麼去寫程式碼的話(每一層都寫一份),肯定是不太合適的,良好的狀態應該如下圖所示:

作為一個Java開發者,在Spring大行其道的今天,很多小夥伴瞭解資料校驗來自於Spring MVC場景,甚至止步於此。殊不知,Java EE早已把它抽象成了JSR標準技術,並且Spring還是藉助整合它完成了自我救贖呢。

在我看來,按Spring的3C戰略標準來比,Bean Validation資料校驗這塊是沒有能夠完成對傳統Java EE的超越,自身設計存在過重、過度設計等特點。

本專欄命名為Bean Validation(資料校驗),將先從JSR標準開始,再逐漸深入到具體實現Hibernate Validation、整合Spring使用場景等等。因此本專欄將讓你將得到一份系統資料校驗的知識。

✍正文

在任何時候,當你要處理一個應用程式的業務邏輯,資料校驗是你必須要考慮和麵對的事情。應用程式必須通過某種手段來確保輸入進來的資料從語義上來講是正確的,比如生日必須是過去時,年齡必須>0等等。

為什麼要有資料校驗?

資料校驗是非常常見的工作,在日常的開發中貫穿於程式碼的各個層次,從上層的View層到後端業務處理層,甚至底層的資料層。

我們知道通常情況下程式肯定是分層的,不同的層可能由不同的人來開發或者呼叫。若你是一個有經驗的程式設計師,我相信你肯定見過在不同的層了都出現了相同的校驗程式碼,這就是某種意義上的垃圾程式碼

public String queryValueByKey(String zhName, String enName, Integer age) {
    checkNotNull(zhName, "zhName must be not null");
    checkNotNull(enName, "enName must be not null");
    checkNotNull(age, "age must be not null");
    validAge(age, "age must be positive");
    ...
}

從這個簡單的方法入參校驗至少能發現如下問題:

  1. 需要寫大量的程式碼來進行引數基本驗證(這種程式碼多了就算垃圾程式碼)
  2. 需要通過文字註釋來知道每個入參的約束是什麼(否則別人咋看得懂)
  3. 每個程式設計師做引數驗證的方式可能不一樣,引數驗證丟擲的異常也不一樣,導致後期幾乎沒法維護

如上會導致程式碼冗餘和一些管理的問題(程式碼量越大,管理起來維護起來就越困難),比如說語義的一致性問題。為了避免這樣的情況發生,最好是將驗證邏輯與相應的域模型進行繫結,這就是本文將要提供的一個新思路:Bean Validation

關於Jakarta EE

2018年03月, Oracle 決定把 JavaEE 移交給開源組織 Eclipse 基金會,並且不再使用Java EE這個名稱。這是它的新logo:

對應的名稱修改還包括:

舊名稱 新名稱
Java EE Jakarta EE
Glassfish Eclipse Glassfish
Java Community Process (JCP) Eclipse EE.next Working Group (EE.next)
Oracle development management Eclipse Enterprise for Java (EE4J) 和 Project Management Committee (PMC)

JCP 將繼續支援 Java SE社群。 但是,Jakarta EE規範自此將不會在JCP下開發。Jakarta EE標準大概由Eclipse Glassfish、Apache TomEE、Wildfly、Oracle WebLogic、JBoss、IBM、Websphere Liberty等組織來制定

遷移

既然名字都改了,那接下來就是遷移嘍,畢竟Java EE這個名稱(javax包名)不能再用了嘛。Eclipse接手後釋出的首個Enterprise Java將是 Jakarta EE 9,該版本將以Java EE 8作為其基準版本(最低版本要求是Java8)。

有個意思的現象是:Java EE 8是2019.09.10釋出的,但實際上官方名稱是Jakarta EE 8了。很明顯該版本並非由新組織設計和制定的,不是它們的產物。但是,彼時平臺已更名為Jakarta有幾個月了,因此對於一些Jar你在maven市場上經常能看見兩種座標:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.1</version>
</dependency>

雖然座標不一樣,但是內容是100%一樣的(包名均還為javax.*),很明顯這是更名的過度期,為後期全面更名做準備呢。

嚴格來講:只要大版本號(第一個數字)還一樣,包名是不可能變化的,因此一般來說均具有向下相容性

既然Jakarta釋放出了更名訊號,那麼下一步就是徹徹底底的改變嘍。果不其然,這些都在Jakarta EE 9裡得到實施。

Jakarta EE 9

2020.08.31,Jakarta後的第一個企業級平臺Jakarta EE 9正式釋出。如果說Jakarta EE 8只是冠了個名,那麼這個就名正言順了。

小貼士:我寫本文時還沒到2020.08.31呢,這個時間是我在官網趴來的,因此肯定準確

這次企業平臺的升級最大的亮點是:

  1. 把旗下30於種技術的大版本號全部+1(Jakarta RESTful Web Services除外)
  2. 包名全部javax.*化,全部改為jakarta.*
  3. JavaSE基準版本要求依舊保持為Java 8(而並非Java9哦)

可以發現本次升級的主要目的並著眼於功能點,仍舊是名字的替換。雖然大家對Java EE的javax有較深的情節,但舊的不去新的不來。我們以後開發過中遇到jakarta.*這種包名就不用再感到驚訝了,提前準備總是好的。

Jakarta Bean Validation

Jakarta Bean Validation不僅僅是一個規範,它還是一個生態。

之前名為Java Bean Validation,2018年03月之後就得改名叫Jakarta Bean Validation
嘍,這不官網早已這麼稱呼了:

Bean Validation技術隸屬於Java EE規範,期間有多個JSR(Java Specification Requests)支援,截止到稿前共有三次JSR標準釋出:

說明:JCP這個組織就是來定義Java標準的,在Java行業鼎鼎有名的公司大都是JCP的成員,可以共同參與Java標準的制定,影響著世界。包括掌門人Oracle以及Eclipse、Redhat、JetBrains等等。值得天朝人自豪的是:2018年5月17日阿里巴巴作為一員正式加入JCP組織,成為唯一一家中國公司

Bean Validation是標準,它的參考實現除了有我們熟悉的Hibernate Validator外還有Apache BVal,但是後者使用非常小眾,忘了它吧。實際使用中,基本可以認為Hibernate Validator是Bean Validation規範的唯一參考實現,是對等的。

小貼士:Apache BVal勝在輕量級上,只有不到1m空間所以非常輕量,有些選手還是忠愛的(此專案還在發展中,並未停更哦,有興趣你可以自己使用試試)

JSR303

這個JSR提出很早了(2009年),它為 基於註解的 JavaBean驗證定義後設資料模型和API,通過使用XML驗證描述符覆蓋和擴充套件後設資料。JSR-303主要是對JavaBean進行驗證,如方法級別(方法引數/返回值)、依賴注入等的驗證是沒有指定的。

作為開山之作,它規定了Java資料校驗的模型和API,這就是Java Bean Validation 1.0版本

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.0.0.GA</version>
</dependency>

該版本提供了常見的校驗註解(共計13個):

註解 支援型別 含義 null值是否校驗
@AssertFalse bool 元素必須是false
@AssertTrue bool 元素必須是true
@DecimalMax Number的子型別(浮點數除外)以及String 元素必須是一個數字,且值必須<=最大值
@DecimalMin 同上 元素必須是一個數字,且值必須>=最大值
@Max 同上 同上
@Min 同上 同上
@Digits 同上 元素構成是否合法(整數部分和小數部分)
@Future 時間型別(包括JSR310) 元素必須為一個將來(不包含相等)的日期(比較精確到毫秒)
@Past 同上 元素必須為一個過去(不包含相等)的日期(比較精確到毫秒)
@NotNull any 元素不能為null
@Null any 元素必須為null
@Pattern 字串 元素需符合指定的正規表示式
@Size String/Collection/Map/Array 元素大小需在指定範圍中

所有註解均可標註在:方法、欄位、註解、構造器、入參等幾乎任何地方

可以看到這些註解均為平時開發中比較常用的註解,但是在使用過程中有如下事項你仍舊需要注意:

  1. 以上所有註解對null是免疫的,也就是說如果你的值是null,是不會觸發對應的校驗邏輯的(也就說null是合法的),當然嘍@NotNull / @Null除外
  2. 對於時間型別的校驗註解(@Future/@Past),是開區間(不包含相等)。也就是說:如果相等就是不合法的,必須是大於或者小於
    1. 這種case比較容易出現在LocalDate這種只有日期上面,必須是將來/過去日期,當天屬於非法日期
  3. @Digits它並不規定數字的範圍,只規定了數字的結構。如:整數位最多多少位,小數位最多多少位
  4. @Size規定了集合型別的範圍(包括字串),這個範圍是閉區間
  5. @DecimalMax和@Max作用基本類似,大部分情況下可通用。不同點在於:@DecimalMax設定最大值是用字串形式表示(只要合法都行,比如科學計數法),而@Max最大值設定是個long值
    1. 我個人一般用@Max即可,因為夠用了~

另外可能有人會問:為毛沒看見@NotEmpty、@Email、@Positive等常用註解?那麼帶著興趣和疑問,繼續往下看吧~

JSR349

該規範是2013年完成的,伴隨著Java EE 7一起釋出,它就是我們比較熟悉的Bean Validation 1.1。

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>

相較於1.0版本,它主要的改進/優化有如下幾點:

  1. 標準化了Java平臺的約束定義、描述、和驗證
  2. 支援方法級驗證(入參或返回值的驗證)
  3. Bean驗證元件的依賴注入
  4. 與上下文和DI依賴注入整合
  5. 使用EL表示式的錯誤訊息插值,讓錯誤訊息動態化起來(強依賴於ElManager)
  6. 跨引數驗證。比如密碼和驗證密碼必須相同

小貼士:註解個數上,相較於1.0版本並沒新增~

它的官方參考實現如下:

可以看到,Java Bean Validation 1.1版本實現對應的是Hibernate Validator 5.x(1.0版本對應的是4.x)

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.3.Final</version>
</dependency>

當你匯入了hibernate-validator後,無需再顯示匯入javax.validation。hibernate-validator 5.x版本基本已停更,只有嚴重bug才會修復。因此若非特殊情況,不再建議你使用此版本,也就是不建議再使用Bean Validation 1.1版本,更別談1.0版本嘍。

小貼士:Spring Boot1.5.x預設整合的還是Bean Validation 1.1哦,但到了Boot 2.x後就徹底摒棄了老舊版本

JSR380

當下主流版本,也就是我們所說的Java Bean Validation 2.0Jakarta Bean Validation 2.0版本。關於這兩種版本的差異,官方做出瞭解釋:

他倆除了叫法不一樣、除了GAV上有變化,其它地方沒任何改變。它們各自的GAV如下:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.1</version>
</dependency>

現在應該不能再叫Java EE了,而應該是Jakarta EE。兩者是一樣的意思,你懂的。Jakarta Bean Validation 2.0是在2019年8月釋出的,屬於Jakarta EE 8的一部分。它的官方參考實現只有唯一的Hibernate validator了:

此版本具有很重要的現實意義,它主要提供如下亮點:

  1. 支援通過註解引數化型別(泛型型別)引數來驗證容器內的元素,如:List<@Positive Integer> positiveNumbers
    1. 更靈活的集合型別級聯驗證;例如,現在可以驗證對映的值和鍵,如:Map<@Valid CustomerType, @Valid Customer> customersByType
    2. 支援java.util.Optional型別,並且支援通過插入額外的值提取器來支援自定義容器型別
  2. 讓@Past/@Future註解支援註解在JSR310時間上
  3. 新增內建的註解型別(共9個):@Email, @NotEmpty, @NotBlank, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent和@FutureOrPresent
  4. 所有內建的約束現在都支援重複標記
  5. 使用反射檢索引數名稱,也就是入參名,詳見這個API:ParameterNameProvider
    1. 很明顯這是需要Java 8的啟動引數支援的
  6. Bean驗證XML描述符的名稱空間已更改為:
    1. META-INF/validation.xml -> http://xmlns.jcp.org/xml/ns/validation/configuration
    2. mapping files -> http://xmlns.jcp.org/xml/ns/validation/mapping
  7. JDK最低版本要求:JDK 8

Hibernate Validator自6.x版本開始對JSR 380規範提供完整支援,除了支援標準外,自己也做了相應的優化,比如效能改進、減少記憶體佔用等等,因此用最新的版本肯定是沒錯的,畢竟只會越來越好嘛。

新增註解

相較於1.x版本,2.0版本在其基礎上新增了9個實用註解,總數到了22個。現對新增的9個註解解釋如下:

註解 支援型別 含義 null值是否校驗
@Email 字串 元素必須為電子郵箱地址
@NotEmpty 容器型別 集合的Size必須大於0
@NotBlank 字串 字串必須包含至少一個非空白的字元
@Positive 數字型別 元素必須為正數(不包括0)
@PositiveOrZero 同上 同上(包括0)
@Negative 同上 元素必須為負數(不包括0)
@NegativeOrZero 同上 同上(包括0)
@PastOrPresent 時間型別 在@Past基礎上包括相等
@FutureOrPresent 時間型別 在@Futrue基礎上包括相等

@Email、@NotEmpty、@NotBlank之前是Hibernate額外提供的,2.0標準後hibernate自動退位讓賢並且標註為過期了。Bean Validation 2.0的JSR規範制定負責人就職於Hibernate,所以這麼做就很自然了。就是他:

小貼士:除了JSR標準提供的這22個註解外,Hibernate Validator還提供了一些非常實用的註解,這在後面講述Hibernate Validator時再解釋吧

使用示例

匯入實現包:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.5.Final</version>
</dependency>

校驗Java Bean

書寫JavaBean和校驗程式(全部使用JSR標準API哦):

@ToString
@Setter
@Getter
public class Person {

    @NotNull
    public String name;
    @NotNull
    @Min(0)
    public Integer age;
}
public static void main(String[] args) {
    Person person = new Person();
    person.setAge(-1);

    // 1、使用【預設配置】得到一個校驗工廠  這個配置可以來自於provider、SPI提供
    ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
    // 2、得到一個校驗器
    Validator validator = validatorFactory.getValidator();
    // 3、校驗Java Bean(解析註解) 返回校驗結果
    Set<ConstraintViolation<Person>> result = validator.validate(person);

    // 輸出校驗結果
    result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}

執行程式,不幸拋錯:

Caused by: java.lang.ClassNotFoundException: javax.el.ELManager
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	...

上面說了,從1.1版本起就需要El管理器支援用於錯誤訊息動態插值,因此需要自己額外匯入EL的實現。

小貼士:EL也屬於Java EE標準技術,可認為是一種表示式語言工具,它並不僅僅是隻能用於Web(即使你絕大部分情況下都是用於web的jsp裡),可以用於任意地方(類比Spring的SpEL)

這是EL技術規範的API:

<!-- 規範API -->
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>3.0.0</version>
</dependency>

Expression Language 3.0表示式語言規範發版於2013-4-29釋出的,Tomcat 8、Jetty 9、GlasshFish 4都已經支援實現了EL 3.0,因此隨意匯入一個都可(如果你是web環境,根本就不用自己手動匯入這玩意了)。

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.22</version>
</dependency>

新增好後,再次執行程式,控制檯正常輸出校驗失敗的訊息:

age 最小不能小於0: -1
name 不能為null: null

校驗方法/校驗構造器

請移步下文詳解。

加餐:Bean Validation 3.0

伴隨著Jakarta EE 9的釋出,Jakarta Bean Validation 3.0也正式公諸於世。

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>3.0.0</version>
</dependency>

它最大的改變,甚至可以說唯一的改變就是包名的變化:

至此不僅GAV上實現了更名,對程式碼執行有重要影響的包名也徹徹底底的去javax.*化了。因為實際的類並沒有改變,因此仍舊可以認為它是JSR380的實現(雖然不再由JCP組織制定標準了)。

參考實現

毫無疑問,參考實現那必然是Hibernate Validator。它的步伐也跟得非常的緊,退出了7.x版本用於支援Jakarta Bean Validation 3.0。雖然是大版本號的升級,但是在新特性方面你可認為是

✍總結

本文著眼於講解JSR規範、Bean Validation校驗標準、官方參考實現Hibernate Validator,把它們之間的關係進行了關聯,並且對差異進行了鑑別。我認為這篇文章對一般讀者來說是能夠重新整理對資料校驗的認知的。

wow,資料校驗背後還有這麼廣闊的天地

資料校驗是日常工組中接觸非常非常頻繁的一塊知識點,我認為掌握它並且熟練運用於實際工作中,能起到事半功倍的效果,讓程式碼更加的優雅,甚至還能實現別人加班你加薪呢。所以又是一個投出產出比頗高的小而美專欄在路上......

作為本專欄的第一篇文章以JSR標準作為切入點進行講解,是希望理論和實踐能結合起來學習,畢竟理論的指導作用不可或缺。有了理論鋪墊的基石,後面實踐將更加流暢,正所謂著地走路更加踏實嘛。

✔推薦閱讀:

相關文章