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

喬丹是我聽過的籃球之神,科比是我親眼見過的籃球之神。本文已被 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標準作爲切入點進行講解,是希望理論和實踐能結合起來學習,畢竟理論的指導作用不可或缺。有了理論鋪墊的基石,後面實踐將更加流暢,正所謂着地走路更加踏實嘛。

✔推薦閱讀:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章