1. 程式人生 > >3. 站在使用層面,Bean Validation這些標準介面你需要爛熟於胸

3. 站在使用層面,Bean Validation這些標準介面你需要爛熟於胸

> 喬丹是我聽過的籃球之神,科比是我親眼見過的籃球之神。本文已被 [**https://www.yourbatman.cn**](https://www.yourbatman.cn) 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的**專欄**供以免費學習。關注公眾號【**BAT的烏托邦**】逐個擊破,深入掌握,拒絕淺嘗輒止。 [TOC] ![](https://img-blog.csdnimg.cn/20200902115418572.jpg#pic_center) # ✍前言 你好,我是YourBatman。 通過前兩篇文章的敘述,相信能勾起你對**Bean Validation**的興趣。那麼本文就站在一個使用者的角度來看,要使用Bean Validation完成校驗的話我們應該掌握、熟悉哪些介面、介面方法呢? ## 版本約定 - Bean Validation版本:`2.0.2` - Hibernate Validator版本:`6.1.5.Final` # ✍正文 Bean Validation屬於Java EE標準技術,擁有對應的JSR抽象,因此我們實際使用過程中僅需要面向標準使用即可,並不需要關心具體實現(是hibernate實現,還是apache的實現並不重要),也就是我們常說的**面向介面程式設計**。 Tips:為了方便下面做示例講解,對一些簡單、公用的方法抽取如下: ```java 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 void printViolations(Set> violations) { violations.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); } } ``` ## Validator **校驗器介面:校驗的入口**,可實現對Java Bean、某個屬性、方法、構造器等完成校驗。 ```java public interface Validator { ... } ``` 它是使用者接觸得**最多**的一個API,當然也是最重要的嘍。因此下面對其每個方法做出解釋+使用示例。 ### validate:校驗Java Bean ```java Set> validate(T object, Class... groups); ``` 驗證Java Bean物件上的**所有**約束。示例如下: Java Bean: ```java @ScriptAssert(script = "_this.name==_this.fullName", lang = "javascript") @Data public class User { @NotNull private String name; @Length(min = 20) @NotNull private String fullName; } @Test public void test5() { User user = new User(); user.setName("YourBatman"); Set> result = ValidatorUtil.obtainValidator().validate(user); ValidatorUtil.printViolations(result); } ``` > 說明:`@ScriptAssert`是Hibernate Validator提供的一個指令碼約束註解,可以實現垮欄位邏輯校驗,功能非常之強大,後面詳解 執行程式,控制檯輸出: ```java 執行指令碼表示式"_this.name==_this.fullName"沒有返回期望結果: User(name=YourBatman, fullName=null) fullName 不能為null: null ``` 符合預期。值得注意的是:針對fullName中的@Length約束來說,null是合法的喲,所以不會有相應日誌輸出的 校驗Java Bean所有約束中的所有包括: 1、屬性上的約束 2、**類上的約束** ### validateProperty:校驗指定屬性 ```java Set> validateProperty(T object, String propertyName, Class... groups); ``` 校驗某個Java Bean中的**某個屬性**上的所有約束。示例如下: ```java @Test public void test6() { User user = new User(); user.setFullName("YourBatman"); Set> result = ValidatorUtil.obtainValidator().validateProperty(user, "fullName"); ValidatorUtil.printViolations(result); } ``` 執行程式,控制檯輸出: ```java fullName 長度需要在20和2147483647之間: YourBatman ``` 符合預期。它會校驗屬性上的**所有**約束,注意只是屬性上的哦,其它地方的不管。 ### validateValue:校驗value值 校驗某個value值,是否符合指定屬性上的**所有約束**。可理解為:若我把這個value值賦值給這個屬性,是否合法? ```java Set> validateValue(Class beanType, String propertyName, Object value, Class... groups); ``` 這個校驗方法比較特殊:**不用先存在物件例項**,直接校驗某個值是否滿足某個屬性的所有約束,所以它可以做事錢校驗判斷,還是挺好用的。示例如下: ```java @Test public void test7() { Set> result = ValidatorUtil.obtainValidator().validateValue(User.class, "fullName", "A哥"); ValidatorUtil.printViolations(result); } ``` 執行程式,輸出: ```java fullName 長度需要在20和2147483647之間: A哥 ``` 若程式改為:`.validateValue(User.class, "fullName", "YourBatman-YourBatman");`,再次執行程式,控制檯將不再輸出(字串長度超過20,合法了嘛)。 ### 獲取Class型別描述資訊 ```java BeanDescriptor getConstraintsForClass(Class clazz); ``` 這個clazz可以是類or介面型別。`BeanDescriptor`:描述受約束的Java Bean和與其關聯的約束。示例如下: ```java @Test public void test8() { BeanDescriptor beanDescriptor = obtainValidator().getConstraintsForClass(User.class); System.out.println("此類是否需要校驗:" + beanDescriptor.isBeanConstrained()); // 獲取屬性、方法、構造器的約束 Set constrainedProperties = beanDescriptor.getConstrainedProperties(); Set constrainedMethods = beanDescriptor.getConstrainedMethods(MethodType.GETTER); Set constrainedConstructors = beanDescriptor.getConstrainedConstructors(); System.out.println("需要校驗的屬性:" + constrainedProperties); System.out.println("需要校驗的方法:" + constrainedMethods); System.out.println("需要校驗的構造器:" + constrainedConstructors); PropertyDescriptor fullNameDesc = beanDescriptor.getConstraintsForProperty("fullName"); System.out.println(fullNameDesc); System.out.println("fullName屬性的約束註解個數:"fullNameDesc.getConstraintDescriptors().size()); } ``` 執行程式,輸出: ```java 此類是否需要校驗:true 需要校驗的屬性:[PropertyDescriptorImpl{propertyName=name, cascaded=false}, PropertyDescriptorImpl{propertyName=fullName, cascaded=false}] 需要校驗的方法:[] 需要校驗的構造器:[] PropertyDescriptorImpl{propertyName=fullName, cascaded=false} fullName屬性的約束註解個數:2 ``` ### 獲得Executable校驗器 ```java @since 1.1 ExecutableValidator forExecutables(); ``` Validator這個API是1.0就提出的,它只能校驗Java Bean,對於方法、構造器的引數、返回值等校驗還無能為力。 這不1.1版本就提供了`ExecutableValidator`這個API解決這類需求,它的例項可通過呼叫Validator的該方法獲得,非常方便。關於`ExecutableValidator `的具體使用請移步[上篇文章](https://mp.weixin.qq.com/s/-KeOCq2rsXCvrqD8HYHSpQ)。 ## ConstraintViolation **約束違反詳情**。此物件儲存了**違反約束的上下文**以及描述訊息。 ```java // :root bean public interface ConstraintViolation { } ``` 簡單的說,它儲存著執行完所有約束後(不管是Java Bean約束、方法約束等等)的結果,提供了訪問結果的API,比較簡單: > 小貼士:只有違反的約束才會生成此物件哦。違反一個約束對應一個例項 ```java // 已經插值(interpolated)的訊息 String getMessage(); // 未插值的訊息模版(裡面變數還未替換,若存在的話) String getMessageTemplate(); // 從rootBean開始的屬性路徑。如:parent.fullName Path getPropertyPath(); // 告訴是哪個約束沒有通過(的詳情) ConstraintDescriptor getConstraintDescriptor(); ``` 示例:略。 ## ValidatorContext 校驗器上下文,根據此上下文建立Validator例項。不同的上下文可以創建出不同例項(這裡的不同指的是內部元件不同),滿足各種個性化的定製需求。 ValidatorContext介面提供設定方法可以定製校驗器的核心元件,它們就是Validator校驗器的五大核心元件: ```java public interface ValidatorContext { ValidatorContext messageInterpolator(MessageInterpolator messageInterpolator); ValidatorContext traversableResolver(TraversableResolver traversableResolver); ValidatorContext constraintValidatorFactory(ConstraintValidatorFactory factory); ValidatorContext parameterNameProvider(ParameterNameProvider parameterNameProvider); ValidatorContext clockProvider(ClockProvider clockProvider); // @since 2.0 值提取器。 // 注意:它是add方法,屬於新增哦 ValidatorContext addValueExtractor(ValueExtractor extractor); Validator getValidator(); } ``` 可以通過這些方法設定不同的元件實現,設定好後再來個`getValidator()`就得到一個定製化的校驗器,不再千篇一律嘍。所以呢,首先就是要得到ValidatorContext例項,下面介紹兩種方法。 ### 方式一:自己new ```java @Test public void test2() { ValidatorFactoryImpl validatorFactory = (ValidatorFactoryImpl) ValidatorUtil.obtainValidatorFactory(); // 使用預設的Context上下文,並且初始化一個Validator例項 // 必須傳入一個校驗器工廠例項哦 ValidatorContext validatorContext = new ValidatorContextImpl(validatorFactory) .parameterNameProvider(new DefaultParameterNameProvider()) .clockProvider(DefaultClockProvider.INSTANCE); // 通過該上下文,生成校驗器例項(注意:呼叫多次,生成例項是多個喲) System.out.println(validatorContext.getValidator()); } ``` 執行程式,控制檯輸出: ```java org.hibernate.validator.internal.engine.ValidatorImpl@1757cd72 ``` 這種是**最直接**的方式,想要啥就new啥嘛。不過這麼使用是有缺陷的,主要體現在這兩個方面: 1. 不夠抽象。new的方式嘛,和抽象談不上關係 2. 強耦合了Hibernate Validator的API,如:`org.hibernate.validator.internal.engine.ValidatorContextImpl#ValidatorContextImpl` ### 方式二:工廠生成 上面即使通過自己new的方式得到`ValidatorContext`例項也需要傳入校驗器工廠,那還不如直接使用工廠生成呢。恰好`ValidatorFactory`也提供了對應的方法: ```java ValidatorContext usingContext(); ``` 該方法用於得到一個ValidatorContext例項,它具有高度抽象、與底層API無關的特點,**是推薦的獲取方式**,並且使用起來有流式程式設計的效果,如下所示: ```java @Test public void test3() { Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext() .parameterNameProvider(new DefaultParameterNameProvider()) .clockProvider(DefaultClockProvider.INSTANCE) .getValidator(); } ``` 很明顯,這種方式是被**推薦**的。 ## 獲得Validator例項的兩種姿勢 在文章最後,再回頭看看Validator例項獲取的兩種姿勢。`Validator`校驗器介面是完成資料校驗(Java Bean校驗、方法校驗等)最主要API,經過了上面的講述,下面可以來個獲取方式的小總結了。 ### 方式一:工廠直接獲取 ```java @Test public void test3() { Validator validator = ValidatorUtil.obtainValidatorFactory().getValidator(); } ``` 這種方式十分簡單、簡約,對初學者十分的友好,入門簡單,優點明顯。各元件全部使用預設方式,省心。如果要挑缺點那肯定也是有的:無法滿足個性化、定製化需求,說白了:無法自定義五大元件 + 值提取器的實現。 作為這麼優秀的Java EE標準技術,怎麼少得了對擴充套件的開放呢?繼續方式二吧~ ### 方式二:從上下文獲取 校驗器上下文也就是ValidatorContext嘍,它的步驟是先得到上下文例項,然後做定製,再通過上下文例項創建出Validator校驗器例項了。 示例程式碼: ```java @Test public void test3() { Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext() .parameterNameProvider(new DefaultParameterNameProvider()) .clockProvider(DefaultClockProvider.INSTANCE) .getValidator(); } ``` 這種方式給與了極大的定製性,你可以任意指定核心元件實現,來達到自己的要求。 這兩種方式結合起來,不就是典型的**預設 + 定製擴充套件**的搭配麼?另外,Validator是執行緒安全的,一般來說一個應用只需要初始化**一個** Validator例項即可,所以推薦使用方式二進行初始化,對個性擴充套件更友好。 # ✍總結 本文站在一個使用者的角度去看如何使用Bean Validation,以及哪些**標準的**介面API是必須掌握了,有了這些知識點在平時絕大部分case都能應對自如了。 > 規範介面/標準介面一般能解決絕大多數問題,這就是規範的邊界,有些可為,有些不為 當然嘍,這些是基本功。要想深入理解Bean Validation的功能,必須深入瞭解Hibernate Validator實現,因為有些比較常用的case它做了很好的補充,咱們下文見。 ##### ✔推薦閱讀: - [1. 不吹不擂,第一篇就能提升你對Bean Validation資料校驗的認知](https://mp.weixin.qq.com/s/g04HMhrjbvbPn1Mb9JYa5g) - [2. Bean Validation宣告式校驗方法的引數、返回值](https://mp.weixin.qq.com/s/-KeOCq2rsXCvrqD8HYHSpQ)