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
(Java8新增)這個類型TYPE_USE
- BV和HV(除了Class級别)的所有注解均能标注在容器元素上
- BV規定了可以驗證容器内元素,HV提供實作。它預設支援如下容器類型:
-
的實作(如List、Set)java.util.Iterable
-
的實作,支援key和valuejava.util.Map
-
java.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
- 屬性并不依賴于字段而存在,隻是他們一般都成雙成對出現
- 如
你可認為它有名為class的屬性,但是它并沒有名為class的字段getClass()
- 如
知曉了字段和屬性的差別,再去了解字段限制和屬性限制的差異就簡單了,它倆的差異僅僅展現在待驗證值通路政策上的差別:
- 字段限制:直接反射通路字段的值 -> Field#get(不會執行get方法體)
- 屬性限制:調用屬性get方法 -> getXXX(會執行get方法體)
小貼士:如果你希望執行了驗證就輸出一句日志,又或者你的POJO被位元組碼增強了,那麼屬性限制更适合你。否則,推薦使用字段限制
✍總結
嗯,這篇文章還不錯吧,總體浏覽下來行文簡單,但内容還是挺幹的哈,畢竟1024節嘛,不來點的幹的心裡有愧。
作為此part姊妹篇的上篇,它是每個同學都有必要掌握的使用方式。而下篇我覺得應該更為興奮些,畢竟那裡才能加分。1024,撸起袖子繼續幹。