5. Bean Validation宣告式驗證四大級別:欄位、屬性、容器元素、類
1024,程式碼改變世界。本文已被 https://www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。
目錄
✍前言
你好,我是YourBatman。又一年1024程式設計師節,你快樂嗎?還是在加班上線呢?
上篇文章 介紹了Validator校驗器的五大核心元件,在結合前面幾篇所講,相信你對Bean Validation已有了一個整體認識了。
本文將非常實用,因為將要講述的是Bean Validation在4個層級上的驗證方式,它將覆蓋你使用過程中的方方面面,不信你看。
版本約定
- Bean Validation版本:
2.0.2
- Hibernate Validator版本:
6.1.5.Final
✍正文
Jakarta Bean它的驗證約束是通過宣告式方式(註解)來表達的,我們知道Java註解幾乎可以標註在任何地方(package上都可標註註解你敢信?),那麼Jakarta Bean支援哪些呢?
Jakarta Bean共支援四個級別的約束:
- 欄位約束(Field)
- 屬性約束(Property)
- 容器元素約束(Container Element)
- 類約束(Class)
值得注意的是,並不是所有的約束註解都能夠標註在上面四種級別上。現實情況是:Bean Validation自帶的22個標準約束全部支援1/2/3級別,且全部不支援第4級別(類級別)約束。當然嘍,作為補充的Hibernate-Validator
它提供了一些專門用於類級別的約束註解,如org.hibernate.validator.constraints.@ScriptAssert
就是一常用案例。
說明:為簡化接下來示例程式碼,共用工具程式碼提前展示如下:
public abstract class ValidatorUtil {
public static ValidatorFactory obtainValidatorFactory() {
return Validation.buildDefaultValidatorFactory();
}
public static Validator obtainValidator() {
return obtainValidatorFactory().getValidator();
}
public static ExecutableValidator obtainExecutableValidator() {
return obtainValidator().forExecutables();
}
public static <T> void printViolations(Set<ConstraintViolation<T>> violations) {
violations.stream().map(v -> v.getPropertyPath() + v.getMessage() + ",但你的值是: " + v.getInvalidValue()).forEach(System.out::println);
}
}
1、欄位級別約束(Field)
這是我們最為常用的一種約束方式:
public class Room {
@NotNull
public String name;
@AssertTrue
public boolean finished;
}
書寫測試用例:
public static void main(String[] args) {
Room bean = new Room();
bean.finished = false;
ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(bean));
}
執行程式,輸出:
finished只能為true,但你的值是: false
name不能為null,但你的值是: null
當把約束標註在Field欄位上時,Bean Validation將使用欄位的訪問策略來校驗,不會呼叫任何方法,即使你提供了對應的get/set方法也不會觸碰。
話外音:使用
Field#get()
得到欄位的值
使用細節
- 欄位約束可以應用於任何訪問修飾符的欄位
- 不支援對靜態欄位的約束(static靜態欄位使用約束無效)
若你的物件會被位元組碼增強,那麼請不要使用Field約束,而是使用下面介紹的屬性級別約束更為合適。
原因:增強過的類並不一定能通過欄位反射去獲取到它的值
絕大多數情況下,對Field欄位做約束的話均是POJO,被增強的可能性極小,因此此種方式是被推薦的,看著清爽。
2、屬性級別約束(Property)
若一個Bean遵循Java Bean規範,那麼也可以使用屬性約束來代替欄位約束。比如上例可改寫為如下:
public class Room {
public String name;
public boolean finished;
@NotNull
public String getName() {
return name;
}
@AssertTrue
public boolean isFinished() {
return finished;
}
}
執行上面相同的測試用例,輸出:
finished只能為true,但你的值是: false
name不能為null,但你的值是: null
效果“完全”一樣。
當把約束標註在Property屬性上時,將採用屬性訪問策略來獲取要驗證的值。說白了:會呼叫你的Method來獲取待校驗的值。
使用細節
- 約束放在get方法上優於放在set方法上,這樣只讀屬性(沒有get方法)依然可以執行約束邏輯
- 不要在屬性和欄位上都標註註解,否則會重複執行約束邏輯(有多少個註解就執行多少次)
- 不要既在屬性的get方法上又在set方法上標註約束註解
3、容器元素級別約束(Container Element)
還有一種非常非常常見的驗證場景:驗證容器內(每個)元素,也就驗證引數化型別parameterized type
。形如List<Room>
希望裡面裝的每個Room都是合法的,傳統的做法是在for迴圈裡對每個room進行驗證:
List<Room> beans = new ArrayList<>();
for (Room bean : beans) {
validate(bean);
...
}
很明顯這麼做至少存在下面兩個不足:
- 驗證邏輯具有侵入性
- 驗證邏輯是黑匣子(不看內部原始碼無法知道你有哪些約束),非宣告式
在本專欄第一篇知道了從Bean Validation 2.0開始就支援容器元素校驗了(本專欄使用版本為:2.02
),下面我們來體驗一把:
public class Room {
@NotNull
public String name;
@AssertTrue
public boolean finished;
}
書寫測試用例:
public static void main(String[] args) {
List<@NotNull Room> rooms = new ArrayList<>();
rooms.add(null);
rooms.add(new Room());
Room room = new Room();
room.name = "YourBatman";
rooms.add(room);
ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms));
}
執行程式,沒有任何輸出,也就是說並沒有對rooms立面的元素進行驗證。這裡有一個誤區:Bean Validator是基於Java Bean進行驗證的,而此處你的rooms
僅僅只是一個容器型別的變數而已,因此不會驗證。
其實它是把List當作一個Bean,去驗證List裡面的標註有約束註解的屬性/方法。很顯然,List裡面不可能標註有約束註解嘛,所以什麼都不輸出嘍
為了讓驗證生效,我們只需這麼做:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Rooms {
private List<@Valid @NotNull Room> rooms;
}
public static void main(String[] args) {
List<@NotNull Room> beans = new ArrayList<>();
beans.add(null);
beans.add(new Room());
Room room = new Room();
room.name = "YourBatman";
beans.add(room);
// 必須基於Java Bean,驗證才會生效
Rooms rooms = new Rooms(beans);
ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms));
}
執行程式,輸出:
rooms[0].<list element>不能為null,但你的值是: null
rooms[2].finished只能為true,但你的值是: false
rooms[1].name不能為null,但你的值是: null
rooms[1].finished只能為true,但你的值是: false
rooms[1].finished只能為true,但你的值是: false
從日誌中可以看出,元素的驗證順序是不保證的。
小貼士:在HV 6.0 之前的版本中,驗證容器元素時@Valid是必須,也就是必須寫成這樣:
List<@Valid @NotNull Room> rooms
才有效。在HV 6.0之後@Valid這個註解就不是必須的了
使用細節
- 若約束註解想標註在容器元素上,那麼註解定義的
@Target
裡必須包含TYPE_USE
(Java8新增)這個型別- BV和HV(除了Class級別)的所有註解均能標註在容器元素上
- BV規定了可以驗證容器內元素,HV提供實現。它預設支援如下容器型別:
java.util.Iterable
的實現(如List、Set)java.util.Map
的實現,支援key和valuejava.util.Optional/OptionalInt/OptionalDouble...
- JavaFX的
javafx.beans.observable.ObservableValue
- 自定義容器型別(自定義很重要,詳見下篇文章)
4、類級別約束(Class)
類級別的約束驗證是很多同學不太熟悉的一塊,但它卻很是重要。
其實Hibernate-Validator已內建提供了一部分能力,但可能還不夠,很多場景需要自己動手優雅解決。為了體現此part的重要性,我決定專門撰文描述,當然還有自定義容器型別型別的校驗嘍,我們下文見。
欄位約束和屬性約束的區別
欄位(Field) VS 屬性(Property)本身就屬於一對“近義詞”,很多時候口頭上我們並不做區分,是因為在POJO裡他倆一般都同時存在,因此大多數情況下可以對等溝通。比如:
@Data
public class Room {
@NotNull
private String name;
@AssertTrue
private boolean finished;
}
欄位和屬性的區別
- 欄位具有儲存功能:欄位是類的一個成員,值在記憶體中真實存在;而屬性它不具有儲存功能,屬於Java Bean規範抽象出來的一個叫法
- 欄位一般用於類內部(一般是private),而屬性可供外部訪問(get/set一般是public)
- 這指的是一般情況下的規律
- 欄位的本質是Field,屬性的本質是Method
- 屬性並不依賴於欄位而存在,只是他們一般都成雙成對出現
- 如
getClass()
你可認為它有名為class的屬性,但是它並沒有名為class的欄位
- 如
知曉了欄位和屬性的區別,再去理解欄位約束和屬性約束的差異就簡單了,它倆的差異僅僅體現在待驗證值訪問策略上的區別:
- 欄位約束:直接反射訪問欄位的值 -> Field#get(不會執行get方法體)
- 屬性約束:呼叫屬性get方法 -> getXXX(會執行get方法體)
小貼士:如果你希望執行了驗證就輸出一句日誌,又或者你的POJO被位元組碼增強了,那麼屬性約束更適合你。否則,推薦使用欄位約束
✍總結
嗯,這篇文章還不錯吧,總體瀏覽下來行文簡單,但內容還是挺乾的哈,畢竟1024節嘛,不來點的乾的心裡有愧。
作為此part姊妹篇的上篇,它是每個同學都有必要掌握的使用方式。而下篇我覺得應該更為興奮些,畢竟那裡才能加分。1024,擼起袖子繼續幹。
✔推薦閱讀:
- 1. 不吹不擂,第一篇就能提升你對Bean Validation資料校驗的認知
- 2. Bean Validation宣告式校驗方法的引數、返回值
- 3. 站在使用層面,Bean Validation這些標準介面你需要爛熟於胸
- 4. Validator校驗器的五大核心元件,一個都不能少
♥關注A哥♥
Author | A哥(YourBatman) |
---|---|
個人站點 | www.yourbatman.cn |
yourbatman@qq.com | |
微 信 | fsx641385712 |
活躍平臺 | |
公眾號 | BAT的烏托邦(ID:BAT-utopia) |
知識星球 | BAT的烏托邦 |
每日文章推薦 | 每日文章推薦 |
相關文章
- 6. 自定義容器型別元素驗證,類級別驗證(多欄位聯合驗證)型別
- Scala的類、屬性、物件欄位物件
- Kotlin——中級篇(二): 屬性與欄位詳解Kotlin
- C#屬性與欄位C#
- odoo欄位屬性列舉Odoo
- @Validated和@Valid的區別?校驗級聯屬性(內部類)
- [提問交流]建立模型,新增屬性,欄位型別如何設定2位小數的欄位型別模型型別
- 深入理解JavaScript類與物件:揭秘類欄位和靜態屬性的妙用,js靜態屬性和例項屬性JavaScript物件JS
- 共有的表單欄位屬性
- 獲得某個類的所有宣告的欄位
- 5.7 屬性宣告
- Java Bean ValidationJavaBean
- lambda方法引用獲取欄位屬性
- Laravel-admin form 元件驗證多欄位唯一性LaravelORM元件
- 理解「交叉驗證」(Cross Validation)ROS
- IL角度理解C#中欄位,屬性與方法的區別C#
- Java-Bean Validation後端校驗總結JavaBean後端
- lambda 表示式從集合中獲取某個欄位屬性的集合
- javax.validation包校驗巢狀屬性(List物件)的寫法Java巢狀物件
- mvc中常見的屬性驗證MVC
- CSS 屬性宣告順序CSS
- pydantic 欄位欄位校驗
- Laravel 自定義表單請求驗證忽略某些欄位驗證Laravel
- 通用首部欄位詳解-四大首部欄位之一
- WPF 使用附加屬性宣告 ICommand
- C#快速入門教程(5)——欄位與屬性C#
- 始終使用屬性(Property),而不是欄位(Data Member)
- windows域控裡,屬性和欄位對映表Windows
- python獲取、修改mysql資料庫欄位屬性PythonMySql資料庫
- C#學習筆記-欄位、屬性、索引器C#筆記索引
- 裝備屬性欄位設計和投放詳解
- odoo ORM研究3 - odoo fields常用的欄位屬性OdooORM
- 宣告 NSString 型別的屬性,到底用 strong 還是 copy ?型別
- Python類屬性和例項屬性分別是什麼?Python
- Mybatis Plus實體類屬性與表欄位不一致解決方法MyBatis
- html元素,屬性修改HTML
- golang常用庫:欄位引數驗證庫-validatorGolang
- Laravel 依賴注入方式驗證表單欄位Laravel依賴注入