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

YourBatman發表於2020-09-07

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

✔推薦閱讀:

相關文章