喬丹是我聽過的籃球之神,科比是我親眼見過的籃球之神。本文已被 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它做了很好的補充,咱們下文見。