Fluent-Validator 業務校驗器
背景
在互聯網行業中,基于Java開發的業務類系統,不管是服務端還是客戶端,業務邏輯代碼的更新往往是非常頻繁的,這源于功能的快速迭代特性,在一般公司內部,特別是使用Java web技術構建的平臺中,不管是基于模塊化還是服務化的,業務邏輯都會相對復雜,
這些系統之間、系統內部往往存在大量的API介面,這些介面一般都需要對入參(輸入引數的簡稱)做校驗,以保證:
1) 核心業務邏輯能夠順利按照預期執行,
2) 資料能夠正常存取,
3) 資料安全性,包括符合約束以及限制,有訪問權限控制以及不出現SQL注入等問題,
開發人員在維護核心業務邏輯的同時,還需要為輸入做嚴格的校驗,當輸入不合法時,能夠給caller一個明確的反饋,最常見的反饋就是回傳封裝了result的物件或者拋出exception,
一些常見的驗證代碼片段如下所示:
public Response execute(Request request) {
if (request == null) {
throw BizException();
}
List cars = request.getCars();
if (CollectionUtils.isEmpty(cars)) {
throw BizException();
}
for (Car car : cars) {
if (car.getSeatCount() < 2) {
throw BizException();
}
}
// do core business logic
}
我們可以發現,它不夠優雅而且違反一些范式:
1)違反單一職責原則(Single responsibility),核心業務邏輯(core business logic)和驗證邏輯(validation logic)耦合在一個類中,
2)開閉原則(Open/closed),我們應該對擴展開放,對修改封閉,驗證邏輯不好擴展,而且一旦需要修改需要動整體這個類,
3)DRY原則(Don’t repeat yourself),代碼冗余,相同邏輯可能散落多處,長此以往不好收殮,
1.簡介
FluentValidato是一個適用于以Java語言開發的程式,讓開發人員回歸focus到業務邏輯上,使用流式(Fluent Interface)呼叫風格讓驗證跑起來很優雅,同時驗證器(Validator)可以做到開閉原則,實作最大程度的復用的工具庫,
2.特點
- 驗證邏輯與業務邏輯不再耦合
摒棄原來不規范的驗證邏輯散落的現象, - 校驗器各司其職,好維護,可復用,可擴展
一個校驗器(Validator)只負責某個屬性或者物件的校驗,可以做到職責單一,易于維護,并且可復用, - 流式風格(Fluent Interface)呼叫
- 使用注解方式驗證
可以裝飾在屬性上,減少硬編碼量, - 支持JSR 303 – Bean Validation標準
或許你已經使用了Hibernate Validator,不用拋棄它,FluentValidator可以站在巨人的肩膀上, - Spring良好集成
校驗器可以由Spring IoC容器托管,校驗入參可以直接使用注解,配置好攔截器,核心業務邏輯完全沒有驗證邏輯的影子,干凈利落, - 回呼給予你充分的自由度
驗證程序中發生的錯誤、例外,驗證結果的回傳,開發人員都可以定制,
3.上手
3.1引入maven依賴:
<dependency>
<groupId>com.baidu.unbiz</groupId>
<artifactId>fluent-validator</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
<version>1.0.5</version>
</dependency>
3.2 業務領域模型
從廣義角度來說DTO(Data Transfer Object)、VO(Value Object)、BO(Business Object)、POJO等都可以看做是業務表達模型,
創建一個學生類,包含 name(姓名)、age(年齡)、schoolName(學校名稱)、(area)地區
package com.example.fluentvalidator;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @author :jianyul
* @date : 2022/5/16 18:00
*/
@Data
@AllArgsConstructor
public class StudentDto {
private String name;
private Integer age;
private String schoolName;
private String area;
}
3.3 Validator樣例
針對schoolName(學校名稱)創建一個Validator,代碼如下:
public class SchoolNameValidator extends ValidatorHandler<String> implements Validator<String> {
@Override
public boolean validate(ValidatorContext context, String schoolName) {
if (!"無錫中學".equals(schoolName)) {
context.addErrorMsg("學校名稱不正確");
return false;
}
return true;
}
}
很簡單,實作Validator介面,泛型T規范這個校驗器待驗證的物件的型別,繼承ValidatorHandler可以避免實作一些默認的方法,validate()方法第一個引數是整個校驗程序的背景關系,第二個引數是待驗證物件,也就是學校名稱,
驗證邏輯:假設學校名稱必須是無錫中學,否則通過context放入錯誤訊息并且回傳false,成功回傳true,
3.4 驗證
StudentDto studentDto = new StudentDto("張三", 18, "蘇州中學", "無錫");
Result result =
FluentValidator.checkAll()
.on(studentDto.getSchoolName(), new SchoolNameValidator())
.doValidate()
.result(toSimple());
System.out.println(result);
//列印結果:Result{isSuccess=false, errors=[學校名稱不正確]}
首先我們通過FluentValidator.checkAll()獲取了一個FluentValidator實體,緊接著呼叫了failFast()表示有錯了立即回傳,它的反義詞是failOver,然后,、on()操作表示在指定屬性上使用對應校驗器進行校驗,截止到此,真正的校驗還并沒有做,這就是所謂的“惰性求值(Lazy valuation)”,有點像Java8 Stream API中的filter()、map()方法,直到doValidate()驗證才真正執行了,最后我們需要收殮出來一個結果供caller獲取列印,直接使用默認提供的靜態方法toSimple()來做一個回呼函式傳入result()方法,最侄訓傳Result類,
4.深入了解
4.1 Validator詳解
Validator介面代碼如下:
public interface Validator<T> {
/**
* 判斷在該物件上是否接受或者需要驗證
* <p/>
* 如果回傳true,那么則呼叫{@link #validate(ValidatorContext, Object)},否則跳過該驗證器
*
* @param context 驗證背景關系
* @param t 待驗證物件
*
* @return 是否接受驗證
*/
boolean accept(ValidatorContext context, T t);
/**
* 執行驗證
* <p/>
* 如果發生錯誤內部需要呼叫{@link ValidatorContext#addErrorMsg(String)}方法,也即<code>context.addErrorMsg(String)
* </code>來添加錯誤,該錯誤會被添加到結果存根{@link Result}的錯誤訊息串列中,
*
* @param context 驗證背景關系
* @param t 待驗證物件
*
* @return 是否驗證通過
*/
boolean validate(ValidatorContext context, T t);
/**
* 例外回呼
* <p/>
* 當執行{@link #accept(ValidatorContext, Object)}或者{@link #validate(ValidatorContext, Object)}發生例外時的如何處理
*
* @param e 例外
* @param context 驗證背景關系
* @param t 待驗證物件
*/
void onException(Exception e, ValidatorContext context, T t);
}
ValidatorHandler是實作Validator介面的一個模板類,如果你自己實作的Validator不想覆寫上面3個方法,可以繼承這個ValidatorHandler,
public class ValidatorHandler<T> implements Validator<T> {
@Override
public boolean accept(ValidatorContext context, T t) {
return true;
}
@Override
public boolean validate(ValidatorContext context, T t) {
return true;
}
@Override
public void onException(Exception e, ValidatorContext context, T t) {
}
}
內部校驗邏輯發生錯誤時候,有兩個處理辦法,
第一,簡單處理,如上述3.3中代碼所示:
context.addErrorMsg("學校名稱不正確");
第二,需要詳細的資訊,包括錯誤訊息,錯誤屬性/欄位,錯誤值,錯誤碼,都可以自己定義,放入錯誤的方法如下,create()方法傳入訊息(必填),setErrorCode()方法設定錯誤碼(選填),setField()設定錯誤欄位(選填),setInvalidValue()設定錯誤值(選填),當然這些資訊需要result(toComplex())才可以獲取到,
public class AreaValidator extends ValidatorHandler<String> implements Validator<String> {
// 實作Validator介面,泛型T規范這個校驗器待驗證的物件的型別
@Override
public boolean validate(ValidatorContext context, String area) {
if (!"無錫".equals(area)) {
context.addError(
ValidationError.create("地址不正確")
.setErrorCode(5000)
.setField("area")
.setInvalidValue(area));
// context.addErrorMsg("地址不正確");
return false;
}
return true;
}
}
如果需要可以使用復雜ComplexResult,內含錯誤訊息,錯誤屬性/欄位,錯誤值,錯誤碼,如下所示:
ComplexResult ret =
FluentValidator.checkAll()
.failOver()
.on(studentDto.getArea(), new AreaValidator())
.doValidate()
.result(toComplex());
System.out.println(ret);
//列印結果:Result{isSuccess=false, errors=[ValidationError{errorCode=5000, errorMsg='地址不正確', field='area', invalidValue=https://www.cnblogs.com/askajohnny/p/蘇州}], timeElapsedInMillis=1}
上述都是針對單個屬性值撰寫對應的Validator代碼,實際開發中,我們需要對整個物件的多個屬性進行業務校驗,這時我們可以針對整個物件撰寫對應的Validator,最后用ComplexResult來接收校驗結果,代碼如下:
public class StudentValidator extends ValidatorHandler<StudentDto>
implements Validator<StudentDto> {
@Override
public boolean validate(ValidatorContext context, StudentDto studentDto) {
if (!"無錫".equals(studentDto.getArea())) {
context.addError(
ValidationError.create("地址不正確")
.setErrorCode(5000)
.setField("area")
.setInvalidValue(studentDto.getArea()));
}
if (!"無錫中學".equals(studentDto.getSchoolName())) {
context.addError(
ValidationError.create("學校名稱不正確")
.setErrorCode(5000)
.setField("schoolName")
.setInvalidValue(studentDto.getSchoolName()));
}
//校驗有沒有Error資訊
if (CollectionUtils.isNotEmpty(context.result.getErrors())) {
return false;
}
return true;
}
}
on()的一連串呼叫實際就是構建呼叫鏈,因此理所當然可以傳入一個呼叫鏈,
ValidatorChain chain = new ValidatorChain();
List<Validator> validators = new ArrayList<Validator>();
validators.add(new StudentValidator());
chain.setValidators(validators);
ComplexResult rets =
FluentValidator.checkAll().on(studentDto, chain).doValidate().result(toComplex());
System.out.println(rets);
//列印結果:Result{isSuccess=false, errors=[ValidationError{errorCode=5000, errorMsg='地址不正確', field='area', invalidValue=https://www.cnblogs.com/askajohnny/p/蘇州}, ValidationError{errorCode=5000, errorMsg='學校名稱不正確', field='schoolName', invalidValue=https://www.cnblogs.com/askajohnny/p/蘇州中學}], timeElapsedInMillis=7}
拓展
可根據專案自定義回傳結果型別,實作ResultCollector即可
public interface ResultCollector<T> {
/**
* 轉換為對外結果
*
* @param result 框架內部驗證結果
*
* @return 對外驗證結果物件
*/
T toResult(ValidationResult result);
}
4.2 onEach
如果要驗證的是一個集合(Collection)或者陣列,那么可以使用onEach,FluentValidator會自動為你遍歷:
ComplexResult result =
FluentValidator.checkAll()
.onEach(list, new StudentValidator())
.doValidate()
.result(toComplex());
4.3 fail fast or fail over
當出現校驗失敗時,也就是Validator的validate()方法回傳了false,那么是繼續還是直接退出呢?默認為使用failFast()方法,直接退出,如果你想繼續完成所有校驗,使用failOver()來skip掉,
ComplexResult result1 =
FluentValidator.checkAll()
.failFast()
.on(liS.getArea(), new AreaValidator())
.on(liS.getSchoolName(), new SchoolNameValidator())
.doValidate()
.result(toComplex());
ComplexResult result2 =
FluentValidator.checkAll()
.failOver()
.on(liS.getArea(), new AreaValidator())
.on(liS.getSchoolName(), new SchoolNameValidator())
.doValidate()
.result(toComplex());
4.4 when
on()后面可以緊跟一個when(),當when滿足expression運算式on才啟用驗證,否則skip呼叫,
ComplexResult result =
FluentValidator.checkAll()
.failOver()
.on(liS.getArea(), new AreaValidator())
.when("20".equals(liS.getAge()))
.on(liS.getSchoolName(), new SchoolNameValidator())
.doValidate()
.result(toComplex());
System.out.println(result);
4.5 驗證回呼callBack
doValidate()方法接受一個ValidateCallback介面
public interface ValidateCallback {
/**
* 所有驗證完成并且成功后
*
* @param validatorElementList 驗證器list
*/
void onSuccess(ValidatorElementList validatorElementList);
/**
* 所有驗證步驟結束,發現驗證存在失敗后
*
* @param validatorElementList 驗證器list
* @param errors 驗證程序中發生的錯誤
*/
void onFail(ValidatorElementList validatorElementList, List<ValidationError> errors);
/**
* 執行驗證程序中發生了例外后
*
* @param validator 驗證器
* @param e 例外
* @param target 正在驗證的物件
*
* @throws Exception
*/
void onUncaughtException(Validator validator, Exception e, Object target) throws Exception;
}
我們可以根據業務需求,在校驗回呼介面中做其他邏輯處理,如下所示:
FluentValidator.checkAll()
.on(liS.getSchoolName(), new SchoolNameValidator())
.doValidate(
new DefaultValidateCallback() {
@Override
public void onSuccess(
ValidatorElementList validatorElementList) {
System.out.println("校驗成功");
}
@Override
public void onFail(
ValidatorElementList validatorElementList,
List<ValidationError> errors) {
System.out.println("校驗失敗");
}
})
.result(toComplex());
4.6 RuntimeValidateException
如果驗證中發生了一些不可控例外,例如資料庫呼叫失敗,RPC連接失效等,會拋出一些例外,如果Validator沒有try-catch處理,FluentValidator會將這些例外封裝在RuntimeValidateException,然后再re-throw出去,
4.7 背景關系傳遞
通過putAttribute2Context()方法,可以往FluentValidator注入一些鍵值對,在所有Validator中共享,有時候這相當有用,
Result result =
FluentValidator.checkAll()
.putAttribute2Context("school", "常州中學")
.on(liS.getSchoolName(), new SchoolNameValidator())
.doValidate()
.result(toSimple());
可在Validator中通過context.getAttribute拿到這個值
String name = context.getAttribute("school", String.class);
if (!name.equals(schoolName)) {
context.addErrorMsg("學校名稱不正確");
return false;
}
return true;
4.8 閉包
通過putClosure2Context()方法,可以往FluentValidator注入一個閉包,這個閉包的作用是在Validator內部可以呼叫,并且快取結果到Closure中,這樣caller在上層可以獲取這個結果,
典型的應用場景是,當需要頻繁呼叫一個RPC的時候,往往該執行執行緒內部一次呼叫就夠了,多次呼叫會影響性能,我們就可以快取住這個結果,在所有Validator間和caller中共享,
下面展示了在caller處存在一個getAreas()方法,它假如需要RPC才能獲取所有地區資訊,顯然是很耗時的,可以在validator中呼叫,然后validator內部共享的同時,caller可以利用閉包拿到結果,用于后續的業務邏輯,
StudentDto liS = new StudentDto("李四", 18, "常州中學", "常州");
Closure<List<String>> closure =
new ClosureHandler<List<String>>() {
private List<String> allManufacturers;
@Override
public List<String> getResult() {
return allManufacturers;
}
@SneakyThrows
@Override
public void doExecute(Object... input) {
// getAreas()模擬RPC遠程介面呼叫
allManufacturers = getAreas();
}
};
FluentValidator.checkAll()
.putClosure2Context("area", closure)
.on(liS.getArea(), new AreaValidator())
.doValidate()
.result(toSimple());
Validator中獲取介面查詢的資料:
Closure<List<String>> closure = context.getClosure("area");
List<String> areas = closure.executeAndGetResult();
if (!areas.contains(area)) {
context.addError(
ValidationError.create("地址不正確")
.setErrorCode(5000)
.setField("area")
.setInvalidValue(area));
return false;
}
return true;
}
5.高級玩法
與Hibernate Validator集成
Hibernate Validator是JSR 303 – Bean Validation規范的一個最佳的實作類別庫,他僅僅是jboss家族的一員,和大名鼎鼎的Hibernate ORM是系出同門,屬于遠房親戚關系,很多框架都會天然集成這個優秀類別庫,例如Spring MVC的@Valid注解可以為Controller方法上的引數做校驗,
FluentValidator當然不會重復早輪子,這么好的類別庫,一定要使用站在巨人肩膀上的策略,將它集成進來,
想要了解更多Hibernate Validator用法,參考這個鏈接,
fluent-validator 集成 hibernate-validator 需要添加依賴
<dependency>
<groupId>com.baidu.unbiz</groupId>
<artifactId>fluent-validator-jsr303</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
<version>1.0.5</version>
</dependency>
5.1 注解驗證
上述都是通過顯示的API呼叫來進行驗證,FluentValidator同樣提供簡潔的基于注解配置的方式來達到同樣的效果,
@FluentValidate可以裝飾在屬性上,內部接收一個Class[]陣列引數,這些個classes必須是Validator的子類,這叫表明在某個屬性上依次用這些Validator做驗證,如下,我們改造下StudentDto這個類:
@Data
@AllArgsConstructor
public class StudentDto {
@NotNull private String name;
private Integer age;
@Length(max = 5)
@FluentValidate({SchoolNameValidator.class})
private String schoolName;
@FluentValidate({AreaValidator.class})
private String area;
}
然后還是利用on()或者onEach()方法來校驗,這里只不過不用傳入Validator或者ValidatorChain了,
ComplexResult ret =
FluentValidator.checkAll()
.failOver()
.configure(new SimpleRegistry())
.on(liS)
.doValidate()
.result(toComplex());
默認的,FluentValidator使用SimpleRegistry,它會嘗試從當前的class loader中呼叫Class.newInstance()方法來新建一個Validator,
5.2 分組驗證
當使用注解驗證時候,會遇到這樣的情況,某些時候例如添加操作,我們會驗證A/B/C三個屬性,而修改操作,我們需要驗證B/C/D/E 4個屬性
@FluentValidate注解另外一個接受的引數是groups,里面也是Class[]陣列,只不過這個Class可以是開發人員隨意寫的一個簡單的類,不含有任何屬性方法都可以,例如:
@Data
@AllArgsConstructor
public class StudentDto {
@NotNull private String name;
private Integer age;
@Length(max = 5)
@FluentValidate(
value = https://www.cnblogs.com/askajohnny/p/{SchoolNameValidator.class},
groups = {Add.class})
private String schoolName;
@FluentValidate(
value = {AreaValidator.class},
groups = Update.class)
private String area;
}
那么驗證的時候,只需要在checkAll()方法中傳入想要驗證的group,就只會做選擇性的分組驗證,例如下面例子,只有area(地區)會被驗證,
ComplexResult result =
FluentValidator.checkAll(new Class<?>[] {Update.class})
.on(liS)
.doValidate()
.result(toComplex());
5.3 級聯驗證
級聯驗證(cascade validation),也叫做物件圖(object graphs),指一個類嵌套另外一個類的時候做的驗證,
如下例所示,我們在車庫(Garage)類中含有一個汽車串列(carList),可以在這個汽車串列屬性上使用@FluentValid注解,表示需要級聯到內部Car做onEach驗證,
public class Garage {
@FluentValidate({CarNotExceedLimitValidator.class})
@FluentValid
private List<Car> carList;
}
注意,@FluentValid和@FluentValidate兩個注解不互相沖突,如下所示,呼叫鏈會先驗證carList上的CarNotExceedLimitValidator,然后再遍歷carList,對每個car做內部的生產商、座椅數、牌照驗證,
6.SpringBoot實戰
6.1 添加依賴
fluent-validator 集成 spring 需要添加依賴
<dependency>
<groupId>com.baidu.unbiz</groupId>
<artifactId>fluent-validator-spring</artifactId>
<version>1.0.9</version>
</dependency>
6.2 注冊 Fluent-validator
fluent-validate 與 spring 結合使用 annotation 方式進行引數校驗,需要借助于 spring 的 AOP,fluent-validate 提供了處理類 FluentValidateInterceptor,但是 fluent-validate 提供的默認驗證回呼類 DefaultValidateCallback 對校驗失敗的情況并沒有處理,所以需要自行實作一個
@Slf4j
public class MyValidateCallBack extends DefaultValidateCallback implements ValidateCallback {
@Override
public void onSuccess(ValidatorElementList validatorElementList) {
log.info("校驗成功");
super.onSuccess(validatorElementList);
}
@Override
public void onFail(ValidatorElementList validatorElementList, List<ValidationError> errors) {
log.info("校驗失敗");
throw new RuntimeException(errors.get(0).getErrorMsg());
}
@Override
public void onUncaughtException(Validator validator, Exception e, Object target)
throws Exception {
log.info("校驗例外");
throw new RuntimeException(e);
}
6.3 注冊IOC
注冊 FluentValidateInterceptor攔截器及MyValidateCallBack回呼方法,最后配置一個 AOP 規則
@Configuration
public class ValidateCallbackConfig {
@Bean
public FluentValidateInterceptor fluentValidateInterceptor() {
FluentValidateInterceptor fluentValidateInterceptor = new FluentValidateInterceptor();
fluentValidateInterceptor.setCallback(validateCallback());
return fluentValidateInterceptor;
}
public MyValidateCallBack validateCallback() {
return new MyValidateCallBack();
}
@Bean
public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
// 使用BeanNameAutoProxyCreator來創建代理
BeanNameAutoProxyCreator proxyCreator = new BeanNameAutoProxyCreator();
// 設定要創建代理的那些Bean的名字
proxyCreator.setBeanNames("*ServiceImpl");
proxyCreator.setInterceptorNames("fluentValidateInterceptor");
return proxyCreator;
}
}
6.4 使用校驗
為了方便,在StudentServiceImpl實作類上增加引數校驗
@Service
public class StudentServiceImpl implements StudentService {
@Override
public Integer getAge(@FluentValid StudentDto studentDto) {
return studentDto.getAge();
}
}
總結
fluent-validate 可以全方位兼容 hibernate-validate,基于 spring 的 AOP 可以提供基于注解的方法入參校驗,同時也可以提供流式編程的工具類業務校驗,替代 hibernate-validate 的同時提供了更多擴展性
參考檔案:http://neoremind.com/2016/02/java%E7%9A%84%E4%B8%9A%E5%8A%A1%E9%80%BB%E8%BE%91%E9%AA%8C%E8%AF%81%E6%A1%86%E6%9E%B6fluent-validator/
歡迎大家訪問 個人博客 Johnny小屋
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/477514.html
標籤:Java
上一篇:萬惡的Jackson
