天天看點

5. Bean Validation聲明式驗證四大級别:字段、屬性、容器元素、類✍前言✍正文✍總結

1024,代碼改變世界。本文已被 https://www.yourbatman.cn 收錄,裡面一并有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以免費學習。關注公衆号【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗辄止。
5. Bean Validation聲明式驗證四大級别:字段、屬性、容器元素、類✍前言✍正文✍總結

✍前言

你好,我是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,撸起袖子繼續幹。

✔推薦閱讀: