唯一真實


你是否考慮過這個問題:很多時候,我們只是一些簡單的獨立參數(比如方法入參int age),並不需要大動干戈的弄個Java Bean裝起來,比如我希望像這樣寫達到相應約束效果:

public @NotNull Person getOne(@NotNull @Min(1) Integer id, String name) { ... };

本文就來探討探討如何借助Bean Validation 優雅的、聲明式的實現方法參數、返回值以及構造器參數、返回值的校驗。

聲明式除了有代碼優雅、無侵入的好處之外,還有一個不可忽視的優點是:任何一個人只需要看聲明就知道語義,而並不需要了解你的實現,這樣使用起來也更有安全感

版本約定

  • Bean Validation版本:2.0.2
  • Hibernate Validator版本:6.1.5.Final

✍正文

Bean Validation 1.0版本只支持對Java Bean進行校驗,到1.1版本就已支持到了對方法/構造方法的校驗,使用的校驗器便是1.1版本新增的ExecutableValidator

public interface ExecutableValidator { // 方法校驗:參數+返回值 <T> Set<ConstraintViolation<T>> validateParameters(T object, Method method, Object[] parameterValues, Class<?>... groups); <T> Set<ConstraintViolation<T>> validateReturnValue(T object, Method method, Object returnValue, Class<?>... groups); // 構造器校驗:參數+返回值 <T> Set<ConstraintViolation<T>> validateConstructorParameters(Constructor<? extends T> constructor, Object[] parameterValues, Class<?>... groups); <T> Set<ConstraintViolation<T>> validateConstructorReturnValue(Constructor<? extends T> constructor, T createdObject, Class<?>... groups); }

其實我們對Executable這個字眼並不陌生,向JDK的接口java.lang.reflect.Executable它的唯二兩個實現便是Method和Constructor,剛好和這里相呼應。

在下面的代碼示例之前,先提供兩個方法用於獲取校驗器(使用默認配置),方便后續使用:

// 用於Java Bean校驗的校驗器 private Validator obtainValidator() { // 1、使用【默認配置】得到一個校驗工廠 這個配置可以來自於provider、SPI提供 ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); // 2、得到一個校驗器 return validatorFactory.getValidator(); } // 用於方法校驗的校驗器 private ExecutableValidator obtainExecutableValidator() { return obtainValidator().forExecutables(); }

因為Validator等校驗器是線程安全的,因此一般來說一個應用全局僅需一份即可,因此只需要初始化一次。

校驗Java Bean

先來回顧下對Java Bean的校驗方式。書寫JavaBean和校驗程序(全部使用JSR標准API),聲明上約束注解:

@ToString @Setter @Getter public class Person { @NotNull public String name; @NotNull @Min(0) public Integer age; } @Test public void test1() { Validator validator = obtainValidator(); Person person = new Person(); person.setAge(-1); Set<ConstraintViolation<Person>> result = validator.validate(person); // 輸出校驗結果 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); }

運行程序,控制台輸出:

name 不能為null: null age 需要在1和18之間: -1

這是最經典的應用了。那么問題來了,如果你的方法參數就是個Java Bean,你該如何對它進行校驗呢?

小貼士:有的人認為把約束注解標注在屬性上,和標注在set方法上效果是一樣的,其實不然,你有這種錯覺全是因為Spring幫你處理了寫東西,至於原因將在后面和Spring整合使用時展開

校驗方法

對方法的校驗是本文的重點。比如我有個Service如下:

public class PersonService { public Person getOne(Integer id, String name) { return null; } }

現在對該方法的執行,有如下約束要求:

  1. id是必傳(不為null)且最小值為1,但對name沒有要求
  2. 返回值不能為null

下面分為校驗方法參數和校驗返回值兩部分分別展開。

校驗方法參數

如上,getOne方法有兩個入參,我們需要對id這個參數做校驗。如果不使用Bean Validation的話代碼就需要這么寫校驗邏輯:

public Person getOne(Integer id, String name) { if (id == null) { throw new IllegalArgumentException("id不能為null"); } if (id < 1) { throw new IllegalArgumentException("id必須大於等於1"); } return null; }

這么寫固然是沒毛病的,但是它的弊端也非常明顯:

  1. 這類代碼沒啥營養,如果校驗邏輯稍微多點就會顯得臭長臭長的
  2. 不看你的執行邏輯,調用者無法知道你的語義。比如它並不知道id是傳還是不傳也行,沒有形成契約
  3. 代碼侵入性強

優化方案

既然學習了Bean Validation,關於校驗方面的工作交給更專業的它當然更加優雅:

public Person getOne(@NotNull @Min(1) Integer id, String name) throws NoSuchMethodException { // 校驗邏輯 Method currMethod = this.getClass().getMethod("getOne", Integer.class, String.class); Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateParameters(this, currMethod, new Object[]{id, name}); if (!validResult.isEmpty()) { // ... 輸出錯誤詳情validResult validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); throw new IllegalArgumentException("參數錯誤"); } return null; }

測試程序就很簡單嘍:

@Test public void test2() throws NoSuchMethodException { new PersonService().getOne(0, "A哥"); }

運行程序,控制台輸出:

getOne.arg0 最小不能小於1: 0 java.lang.IllegalArgumentException: 參數錯誤 ...

完美的符合預期。不過,arg0是什么鬼?如果你有興趣可以自行加上編譯參數-parameters再運行試試,有驚喜哦~

通過把約束規則用注解寫上去,成功的解決上面3個問題中的兩個,特別是聲明式約束解決問題3,這對於平時開發效率的提升是很有幫助的,因為契約已形成

此外還剩一個問題:代碼侵入性強。是的,相比起來校驗的邏輯依舊寫在了方法體里面,但一聊到如何解決代碼侵入問題,相信不用我說都能想到AOP。一般來說,我們有兩種AOP方式供以使用:

  1. 基於Java EE的@Inteceptors實現
  2. 基於Spring Framework實現

顯然,前者是Java官方的標准技術,而后者是實際的標准,所以這個小問題先mark下來,等到后面講到Bean Validation和Spring整合使用時再殺回來吧。

校驗方法返回值

相較於方法參數,返回值的校驗可能很多人沒聽過沒用過,或者接觸得非常少。其實從原則上來講,一個方法理應對其輸入輸出負責的:有效的輸入,明確的輸出,這種明確就最好是有約束的。

上面的getOne方法題目要求返回值不能為null。若通過硬編碼方式校驗,無非就是在return之前來個if(result == null)的判斷嘛:

public Person getOne(Integer id, String name) throws NoSuchMethodException { // ... 模擬邏輯執行,得到一個result結果,准備返回 Person result = null; // 在結果返回之前校驗 if (result == null) { throw new IllegalArgumentException("返回結果不能為null"); } return result; }

同樣的,這種代碼依舊有如下三個問題:

  1. 這類代碼沒啥營養,如果校驗邏輯稍微多點就會顯得臭長臭長的
  2. 不看你的執行邏輯,調用者無法知道你的語義。比如調用者不知道返回是是否可能為null,沒有形成契約
  3. 代碼侵入性強

優化方案

話不多說,直接上代碼。

public @NotNull Person getOne(@NotNull @Min(1) Integer id, String name) throws NoSuchMethodException { // ... 模擬邏輯執行,得到一個result Person result = null; // 在結果返回之前校驗 Method currMethod = this.getClass().getMethod("getOne", Integer.class, String.class); Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateReturnValue(this, currMethod, result); if (!validResult.isEmpty()) { // ... 輸出錯誤詳情validResult validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); throw new IllegalArgumentException("參數錯誤"); } return result; }

書寫測試代碼:

@Test public void test2() throws NoSuchMethodException { // 看到沒 IDEA自動幫你前面加了個notNull @NotNull Person result = new PersonService().getOne(1, "A哥"); }

運行程序,控制台輸出:

getOne.<return value> 不能為null: null java.lang.IllegalArgumentException: 參數錯誤 ...

這里面有個小細節:當你調用getOne方法,讓IDEA自動幫你填充返回值時,前面把校驗規則也給你顯示出來了,這就是契約。明明白白的,拿到這樣的result你是不是可以非常放心的使用,不再戰戰兢兢的啥都來個if(xxx !=null)的判斷了呢?這就是契約編程的力量,在團隊內能指數級的提升編程效率,試試吧~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM