使用 Yavi 進行驗證
一、簡介
Yavi是一個 Java 驗證庫,它允許我們輕鬆、乾淨地確保我們的物件處於有效狀態。
Yavi 是 Java 應用程式中物件驗證的絕佳輕量級選擇。它不依賴反射或向正在驗證的物件添加額外的註釋,因此它可以與我們希望驗證的類別完全分開使用。它還強調類型安全的 API,確保我們不會意外定義不可能的驗證規則。此外,它完全支援我們可以在應用程式中定義的任何類型,並且具有大量可供依賴的預定義約束,同時仍然允許我們在必要時輕鬆定義自己的約束。
在本教程中,我們將了解 Yavi。我們將了解它是什麼、我們可以用它做什麼以及如何使用它。
2. 依賴關係
在使用 Yavi 之前,我們需要在建置中包含最新版本,在撰寫本文時為 0.14.1。
如果我們使用 Maven,我們可以在pom.xml
檔案中包含此依賴項:
<dependency>
<groupId>am.ik.yavi</groupId>
<artifactId>yavi</artifactId>
<version>0.14.1</version>
</dependency>
此時,我們已準備好開始在我們的應用程式中使用它。
3. 簡單驗證
一旦我們的專案中使用了 Yavi,我們就準備好開始使用它來驗證我們的物件。
我們可以建立的最簡單的驗證器適用於簡單值類型,例如String
或Integer
。每個受支援的類型都是使用am.ik.yavi.builder
中的建構器類別建構的:
StringValidator<String> validator = StringValidatorBuilder.of("name", c -> c.notBlank())
.build();
這建構了一個驗證器,我們可以用它來驗證String
實例以確保它們不為空。建構器的第一個參數是我們正在驗證的值的名稱,第二個參數是定義要套用的驗證規則的 lambda。
但更常見的是,我們希望驗證整個 bean,而不僅僅是單一值。這些驗證器是使用ValidatorBuilder
的,我們可以為不同的欄位添加多個規則:
public record Person(String name, int age) {}
Validator<Person> validator = ValidatorBuilder.of(Person.class)
.constraint(Person::name, "name", c -> c.notBlank())
.constraint(Person::age, "age", c -> c.positiveOrZero().lessThan(150))
.build();
每個constraint()
呼叫都會為不同欄位的驗證器新增一個約束。第一個參數是字段的 getter,它必須是我們正在驗證的類型的方法。第三個參數是用來定義限制條件的 lambda,與之前相同。 Yavi 確保這適合我們 getter 方法的回傳類型。例如,我們可以在String
欄位上使用notBlank()
,但不能在整數欄位上使用 notBlank()。
一旦我們有了驗證器,我們就可以用它來驗證適當的物件:
ConstraintViolations result = validator.validate(new Person("", 42));
assertFalse(result.isValid());
傳回的ConstraintViolations
物件告訴我們提供的物件是否有效,如果無效,我們可以看到實際的違規情況:
assertEquals(1, result.size());
assertEquals("name", result.get(0).name());
assertEquals("charSequence.notBlank", result.get(0).messageKey());
在這裡我們可以看到name
欄位無效,並且違規是因為它不應該為空。
3.1.驗證嵌套對象
通常,我們想要驗證的 Bean 中也包含其他 Bean,我們希望確保這些 Bean 也是有效的。我們可以使用建構器的nest()
方法而不是constraint()
呼叫來實現這一點:
public record Name(String firstName, String surname) {}
public record Person(Name name, int age) {}
Validator<Name> nameValidator = ValidatorBuilder.of(Name.class)
.constraint(Name::firstName, "firstName", c -> c.notBlank())
.constraint(Name::surname, "surname", c -> c.notBlank())
.build();
Validator<Person> personValidator = ValidatorBuilder.of(Person.class)
.nest(Person::name, "name", nameValidator)
.constraint(Person::age, "age", c -> c.positiveOrZero().lessThan(150))
.build();
定義後,我們可以像以前一樣使用它。但現在,Yavi 會自動使用點分符號來組成任何違規的名稱,以便我們可以準確地看到發生了什麼:
assertEquals(2, result.size());
assertEquals("name.firstName", result.get(0).name());
assertEquals("name.surname", result.get(1).name());
在這裡,我們得到了兩個預期的違規行為——一個是在name.firstName
上,另一個是在name.surname
上。這些告訴我們相關欄位嵌套在外部物件的name
欄位中。
3.2.跨領域驗證
在某些情況下,我們無法單獨驗證單一欄位。驗證規則可能取決於同一物件中其他欄位的值。我們可以使用constraintOnTarget()
方法來實現這一點,該方法驗證提供的物件而不是它的單一欄位:
record Range(int start, int end) {}
Validator<Range> validator = ValidatorBuilder.of(Range.class)
.constraintOnTarget(range -> range.end > range.start, "end", "range.endGreaterThanStart",
"\"end\" must be greater than \"start\"")
.build();
在這種情況下,我們確保範圍的end
值大於start
。執行此操作時,我們需要提供一些額外的值,因為我們實際上正在建立自訂約束。
不出所料,使用這個驗證器和以前一樣。但是,因為我們自己定義了約束,所以我們將在違規中獲得自訂值:
assertEquals(1, result.size());
assertEquals("end", result.get(0).name());
assertEquals("range.endGreaterThanStart", result.get(0).messageKey());
4. 自訂約束
大多數時候,Yavi 為我們提供了驗證物件所需的所有限制。但是,在某些情況下,我們可能需要標準集未涵蓋的內容。
我們之前看到了一個透過提供 lambda 內聯編寫自訂限制的範例。我們可以在約束建構器中執行類似的操作,為任何欄位定義自訂約束:
Validator<Data> validator = ValidatorBuilder.of(Data.class)
.constraint(Data::palindrome, "palindrome",
c -> c.predicate(s -> validatePalindrome(s), "palindrome.valid", "\"{0}\" must be a palindrome"))
.build();
在這裡,我們使用predicate()
方法來提供 lambda,並為其提供訊息鍵和預設訊息。這個 lambda 可以做我們想做的任何事情,只要它符合[java.util.function.Predicate<T>](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/function/Predicate.html) .
在本例中,我們使用函數來檢查字串是否為回文。
有時,儘管我們可能希望以更可重複使用的方式編寫自訂約束**,但**我們可以透過建立實作CustomConstraint
介面的類別來做到這一點:
class PalindromeConstraint implements CustomConstraint<String> {
@Override
public boolean test(String input) {
String reversed = new StringBuilder()
.append(input)
.reverse()
.toString();
return input.equals(reversed);
}
@Override
public String messageKey() {
return "palindrome.valid";
}
@Override
public String defaultMessageFormat() {
return "\"{0}\" must be a palindrome";
}
}
從功能上來說,這與我們的 lambda 相同,只是作為一個類,我們可以更輕鬆地在驗證器之間重複使用它。在這種情況下,我們只需將其實例傳遞給我們的predicate()
調用,其他所有內容都已為我們配置:
Validator<Data> validator = ValidatorBuilder.of(Data.class)
.constraint(Data::palindrome, "palindrome", c -> c.predicate(new PalindromeConstraint()))
.build();
無論我們使用哪種方法,我們都可以完全按照預期使用產生的驗證器:
ConstraintViolations result = validator.validate(new Data("other"));
assertFalse(result.isValid());
assertEquals(1, result.size());
assertEquals("palindrome", result.get(0).name());
assertEquals("palindrome.valid", result.get(0).messageKey());
在這裡我們可以看到我們的欄位無效,結果包括我們定義的訊息鍵以準確指示它出了什麼問題。
5. 條件約束
並非所有約束都適用於所有情況。 Yavi 為我們提供了一些工具來配置一些約束,使其僅在某些情況下運作。
我們的一個選擇是為驗證器提供上下文。我們可以將其定義為我們想要的任何類型,只要它實現ConstraintGroup
接口,儘管枚舉是一個非常方便的選項:
enum Action implements ConstraintGroup {
CREATE,
UPDATE,
DELETE
}
然後,我們可以使用constraintOnCondition()
包裝器來定義約束,以定義僅適用於特定上下文的約束:
Validator<Person> validator = ValidatorBuilder.of(Person.class)
.constraint(Person::name, "name", c -> c.notBlank())
.constraintOnCondition(Action.UPDATE.toCondition(),
b -> b.constraint(Person::id, "id", c -> c.notBlank()))
.build();
這將始終驗證name
欄位不為空,但僅在我們提供UPDATE
上下文時驗證id
欄位不為空。
使用此功能時,我們需要透過提供上下文以及我們正在驗證的值來進行稍微不同的驗證:
ConstraintViolations result = validator.validate(new Person(null, "Baeldung"), Action.UPDATE);
assertFalse(result.isValid());
如果我們想要擁有更多控制權, constraintOnCondition()
方法可以採用一個 lambda,它接受正在驗證的值和上下文,並指示是否應套用限制。這允許我們定義我們想要的任何條件:
Validator<Person> validator = ValidatorBuilder.of(Person.class)
.constraintOnCondition((person, ctx) -> person.id() != null,
b -> b.constraint(Person::name, "name", c -> c.notBlank()))
.build();
在這種情況下,只有當id
欄位有值時, name
欄位才會被驗證:
ConstraintViolations result = validator.validate(new Person(null, null));
assertTrue(result.isValid());
6. 參數驗證
Yavi 的獨特之處之一是它能夠在驗證中包裝方法調用,確保在調用方法之前參數有效。
參數驗證器都是使用ArgumentsValidatorBuilder
建構器類別建構的。為了確保型別安全,這會建構 16 種可能的類型之一,支援該方法的 1 到 16 個參數。
這對於包裝對建構函數的呼叫特別有用。這使我們能夠在呼叫建構函數之前保證參數有效,而不是建構一個可能無效的物件並在之後驗證它:
Arguments2Validator<String, Integer, Person> validator = ArgumentsValidatorBuilder.of(Person::new)
.builder(b -> b
._string(Arguments1::arg1, "name", c -> c.notBlank())
._integer(Arguments2::arg2, "age", c -> c.positiveOrZero())
)
.build();
_string()
和_integer()
的語法有點不尋常,因此編譯器知道每個參數使用的類型。
一旦我們建立了驗證器,我們就可以呼叫它並傳入所有適當的參數:
Validated<Person> result = validator.validate("", -1);
這個結果告訴我們參數是否有效,如果無效則回傳驗證錯誤:
assertFalse(result.isValid());
assertEquals(2, result.errors().size());
assertEquals("name", result.errors().get(0).name());
assertEquals("charSequence.notBlank", result.errors().get(0).messageKey());
assertEquals("age", result.errors().get(1).name());
assertEquals("numeric.positiveOrZero", result.errors().get(1).messageKey());
如果參數全部有效,那麼我們可以取回方法的結果 - 在本例中是建構的物件:
assertTrue(result.isValid());
Person person = result.value();
我們也可以使用相同的技術來包裝物件上的方法:
record Person(String name, int age) {
boolean isOlderThan(int check) {
return this.age > check;
}
}
Arguments2Validator<Person, Integer, Boolean> validator = ArgumentsValidatorBuilder.of(Person::isOlderThan)
.builder(b -> b
._integer(Arguments2::arg2, "age", c -> c.positiveOrZero())
)
.build();
這將驗證方法呼叫上的參數,並且僅在它們全部有效時才呼叫該方法。在這種情況下,我們將呼叫方法的實例作為第一個參數傳遞,然後傳遞所有其他參數:
Person person = new Person("Baeldung", 42);
Validated<Boolean> result = validator.validate(person, -1);
和以前一樣,如果參數通過驗證,那麼 Yavi 就會呼叫該方法,我們就可以存取回傳值。如果參數驗證失敗,它永遠不會呼叫包裝的方法,而是傳回驗證錯誤。
7.註釋處理
到目前為止,Yavi 在很多地方都相當重複。例如,我們需要指定欄位名稱和方法參考來取得值,它們通常具有相同的名稱。 Yavi 附帶了一個 Java 註解處理器,可以在這方面提供協助。
7.1.註解字段
我們可以使用@ConstraintTarget
註解來註解物件上的字段,以自動產生一些元類別:
record Person(@ConstraintTarget String name, @ConstraintTarget int age) {}
這些註解可以用於建構函數參數、getter 或字段,並且其工作方式相同。
然後,我們可以在建立驗證器時使用這些生成的類別:
Validator<Person> validator = ValidatorBuilder.of(Person.class)
.constraint(_PersonMeta.NAME, c -> c.notBlank())
.constraint(_PersonMeta.AGE, c -> c.positiveOrZero().lessThan(150))
.build();
我們不再需要同時指定欄位名稱和對應的 getter。此外,如果我們嘗試使用不存在的字段,那麼這將不再編譯。
建置完成後,該驗證器與以前相同,並且可以像以前一樣使用。
7.2.註解參數
當使用包裝方法呼叫的支援時,我們也可以對方法參數使用相同的技術。在這種情況下,我們使用@ConstraintArguments
註解來代替,註解我們計劃驗證的方法或建構子:
record Person(String name, int age) {
@ConstraintArguments
Person {
}
@ConstraintArguments
boolean isOlderThan(int check) {
return this.age() > check;
}
}
Yavi 為每個方法產生一個元類,我們可以像以前一樣使用它來產生驗證器。
Arguments2Validator<String, Integer, Person> validator = ArgumentsValidatorBuilder.of(Person::new)
.builder(b -> b
.constraint(_PersonArgumentsMeta.NAME, c -> c.notBlank())
.constraint(_PersonArgumentsMeta.AGE, c -> c.positiveOrZero())
)
.build();
和以前一樣,我們不再需要手動指定參數名稱或位置。我們也不再需要指定正確的約束類型——我們的元類別已經為我們定義了所有這些。
八、總結
這是對 Yavi 的快速介紹。這個庫可以做很多事情,並提供與 Spring 等流行框架的良好整合。下次您需要驗證庫時,為什麼不嘗試呢?
與往常一樣,本文中的所有範例都可以在 GitHub 上找到。