5. Bean Validation宣告式驗證四大級別:欄位、屬性、容器元素、類

YourBatman發表於2020-10-26

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共支援四個級別的約束:

  1. 欄位約束(Field)
  2. 屬性約束(Property)
  3. 容器元素約束(Container Element)
  4. 類約束(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()得到欄位的值

使用細節

  1. 欄位約束可以應用於任何訪問修飾符的欄位
  2. 不支援對靜態欄位的約束(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來獲取待校驗的值。

使用細節

  1. 約束放在get方法上優於放在set方法上,這樣只讀屬性(沒有get方法)依然可以執行約束邏輯
  2. 不要在屬性和欄位上都標註註解,否則會重複執行約束邏輯(有多少個註解就執行多少次)
  3. 不要既在屬性的get方法上又在set方法上標註約束註解

3、容器元素級別約束(Container Element)

還有一種非常非常常見的驗證場景:驗證容器內(每個)元素,也就驗證引數化型別parameterized type。形如List<Room>希望裡面裝的每個Room都是合法的,傳統的做法是在for迴圈裡對每個room進行驗證:

List<Room> beans = new ArrayList<>();
for (Room bean : beans) {
    validate(bean);
    ...
}

很明顯這麼做至少存在下面兩個不足:

  1. 驗證邏輯具有侵入性
  2. 驗證邏輯是黑匣子(不看內部原始碼無法知道你有哪些約束),非宣告式

在本專欄第一篇知道了從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這個註解就不是必須的了

使用細節

  1. 若約束註解想標註在容器元素上,那麼註解定義的@Target裡必須包含TYPE_USE(Java8新增)這個型別
    1. BV和HV(除了Class級別)的所有註解均能標註在容器元素上
  2. 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;
}

欄位和屬性的區別

  1. 欄位具有儲存功能:欄位是類的一個成員,值在記憶體中真實存在;而屬性它不具有儲存功能,屬於Java Bean規範抽象出來的一個叫法
  2. 欄位一般用於類內部(一般是private),而屬性可供外部訪問(get/set一般是public)
    1. 這指的是一般情況下的規律
  3. 欄位的本質是Field,屬性的本質是Method
  4. 屬性並不依賴於欄位而存在,只是他們一般都成雙成對出現
    1. getClass()你可認為它有名為class的屬性,但是它並沒有名為class的欄位

知曉了欄位和屬性的區別,再去理解欄位約束屬性約束的差異就簡單了,它倆的差異僅僅體現在待驗證值訪問策略上的區別:

  • 欄位約束:直接反射訪問欄位的值 -> Field#get(不會執行get方法體)
  • 屬性約束:呼叫屬性get方法 -> getXXX(會執行get方法體)

小貼士:如果你希望執行了驗證就輸出一句日誌,又或者你的POJO被位元組碼增強了,那麼屬性約束更適合你。否則,推薦使用欄位約束

✍總結

嗯,這篇文章還不錯吧,總體瀏覽下來行文簡單,但內容還是挺乾的哈,畢竟1024節嘛,不來點的乾的心裡有愧。

作為此part姊妹篇的上篇,它是每個同學都有必要掌握的使用方式。而下篇我覺得應該更為興奮些,畢竟那裡才能加分。1024,擼起袖子繼續幹。

✔推薦閱讀:

♥關注A哥♥

AuthorA哥(YourBatman)
個人站點www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活躍平臺
公眾號BAT的烏托邦(ID:BAT-utopia)
知識星球BAT的烏托邦
每日文章推薦每日文章推薦

BAT的烏托邦

相關文章