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