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

喬丹是我聽過的籃球之神,科比是我親眼見過的籃球之神。本文已被 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啥嘛。不過這麼使用是有缺陷的,主要體現在這兩個方面:

  1. 不夠抽象。new的方式嘛,和抽象談不上關係
  2. 強耦合了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它做了很好的補充,咱們下文見。

✔推薦閱讀:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章