Categories
程式開發

5. Bean Validation聲明式驗證四大級別:字段、屬性、容器元素、類


1024,代碼改變世界。本文已被 https://www.yourbatman.cn” 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,

✍前言

你好,我是YourBatman。又一年1024程序員節,你快樂嗎?還是在加班上線呢?

上篇文章” 介紹了Validator校驗器的五大核心組件,在結合前面幾篇所講,相信你對Bean Validation已有了一個整體認識了。

本文將非常實用,因為將要講述的是Bean Validation在4個層級上的驗證方式,它將覆蓋你使用過程中的方方面面,不信你看。

版本約定

Bean Validation版本:2.0.2Hibernate 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它提供了一些專門用於類級別的約束註解,如[email protected]就是一常用案例。

說明:為簡化接下來示例代碼,共用工具代碼提前展示如下:

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 void printViolations(Set 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都是合法的,傳統的做法是在for循環裡對每個room進行驗證:

List 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 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 rooms;
}

public static void main(String[] args) {
List 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].不能为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 rooms才有效。在HV 6.0之後@Valid這個註解就不是必須的了

使用細節

若約束註解想標註在容器元素上,那麼註解定義的@Target裡必須包含TYPE_USE(Java8新增)這個類型

1. BV和HV(除了Class級別)的所有註解均能標註在容器元素上

BV規定了可以驗證容器內元素,HV提供實現。它默認支持如下容器類型:

1. java.util.Iterable的實現(如List、Set)

2. java.util.Map的實現,支持key和value

3. java.util.Optional / OptionalInt / OptionalDouble ...

4. JavaFX的javafx.beans.observable.ObservableValue

5. 自定義容器類型(自定義很重要,詳見下篇文章)

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)

1. 這指的是一般情況下的規律

字段的本質是Field,屬性的本質是Method屬性並不依賴於字段而存在,只是他們一般都成雙成對出現

1. 如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哥♥

作者| 阿哥(你的蝙蝠俠)

-------- | -----

個人站點 | www.yourbatman.cn

郵箱| [email protected]

微信 | fsx641385712

公眾號 | BAT的烏托邦(ID:BAT-utopia)

知識星球 | BAT的烏托邦

每日文章推薦 | 每日文章推薦

5. Bean Validation聲明式驗證四大級別:字段、屬性、容器元素、類 1