Dubbo注解方式數(shù)據校驗支持詳解
數(shù)據校驗
簡介:
作為一個Java開發(fā)者我們或多或少在Spring MVC使用場景中接觸過數(shù)據校驗(Bean Validation)。Bean Validation技術隸屬于Java EE規(guī)范,期間有多個JSR(Java Specification Requests)支持,目前共有三次相關JSR標準發(fā)布:
- JSR303 最早(2009)
- JSR349
- JSR380

JSR303
JSR303提出很早(2009年),它為 基于注解的 JavaBean驗證定義元數(shù)據模型和API。JSR-303主要是對JavaBean進行驗證,如方法級別(方法參數(shù)/返回值)、依賴注入等的驗證是沒有指定的。
作為開山之作,它規(guī)定了Java數(shù)據校驗的模型和API,這就是Java Bean Validation 1.0版本。
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.0.0.GA</version>
</dependency>
該版本提供了13個現(xiàn)在常見的校驗注解:
| 注解 | 支持類型 | 含義 | null值是否校驗 |
|---|---|---|---|
| @AssertFalse | bool | 元素必須是false | 否 |
| @AssertTrue | bool | 元素必須是true | 否 |
| @DecimalMax | Number的子類型(浮點數(shù)除外)以及String | 元素必須是一個數(shù)字,且值必須<=最大值 | 否 |
| @DecimalMin | 同上 | 元素必須是一個數(shù)字,且值必須>=最小值 | 否 |
| @Max | 同上 | 同上 | 否 |
| @Min | 同上 | 同上 | 否 |
| @Digits | 同上 | 元素構成是否合法(整數(shù)部分和小數(shù)部分) | 否 |
| @Future | 時間類型(包括JSR310) | 元素必須為一個將來(不包含相等)的日期(比較精確到毫秒) | 否 |
| @Past | 同上 | 元素必須為一個過去(不包含相等)的日期(比較精確到毫秒) | 否 |
| @NotNull | any | 元素不能為null | 是 |
| @Null | any | 元素必須為null | 是 |
| @Pattern | String | 元素需符合指定的正則表達式 | 否 |
| @Size | String/Collection/Map/Array | 元素大小需在指定范圍中 | 否 |
它的官方參考實現(xiàn)如下:

JSR349
該規(guī)范2013年完成伴隨java EE 7一起發(fā)布,就是我們比較熟悉的Bean Validation1.1。
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
相較于1.0版本,它主要的改進/優(yōu)化有如下幾點:
- 標準化了Java平臺的約束定義、描述、和驗證
- 支持方法級驗證(入參或返回值的驗證)
- Bean驗證組件的依賴注入
- 與上下文和DI依賴注入集成
- 使用EL表達式的錯誤消息插值,讓錯誤消息動態(tài)化起來(強依賴于ElManager)
- 跨參數(shù)驗證。比如密碼和驗證密碼必須相同
注解個數(shù)上,相較于1.0版本并沒新增~
它的官方參考實現(xiàn)如下:

注:當你導入了hibernate-validator后,無需再顯示導入javax.validation
JSR380
當下主流版本,也就是Java Bean Validation 2.0,它完成于2017年8月,在2019年8月發(fā)布,屬于Java EE 8的一部分。它的官方參考實現(xiàn)只有唯一的Hibernate validator了:

此版本具有很重要的現(xiàn)實意義,主要有以下變化:
支持通過注解泛型類型來驗證容器內的元素,如:List<@Positive Integer> positiveNumbers,即容器內元素須為正數(shù)
- 1. 更靈活的集合類型級聯(lián)驗證;例如,現(xiàn)在可以驗證映射的鍵和值,如:
Map<@Valid CustomerType, @Valid Customer> customersByType - 2. 支持java.util.Optional類型,并且支持通過插入額外的值提取器來支持自定義容器類型
讓@Past/@Future注解支持注解在JSR310時間上
新增內建的注解類型(共9個):@Email, @NotEmpty, @NotBlank, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent和@FutureOrPresent
所有內置的約束現(xiàn)在都支持重復標記
JDK最低版本要求:JDK 8
新增注解
| 注解 | 支持類型 | 含義 | null值是否校驗 |
|---|---|---|---|
| String | 元素必須是電子郵箱地址 | 否 | |
| @NotEmpty | 容器類型 | 集合的Size必須大于0 | 是 |
| @NotBlank | String | 字符串必須包含至少一個非空白的字符 | 是 |
| @Positive | Positive | 元素必須必須為正數(shù)(不包括0) | 否 |
| @PositiveOrZero | 同上 | 同上(包括0) | 否 |
| @Negative | 同上 | 元素必須必須為負數(shù)(不包括0) | 否 |
| @NegativeOrZero | 同上 | 同上(包括0) | 否 |
| @PastOrPresent | 時間類型 | 在@Past基礎上包括相等 | 否 |
| @FutureOrPresent | 時間類型 | 在@Futrue基礎上包括相等 | 否 |
從1.1版本起就需要El管理器支持用于錯誤消息動態(tài)插值,因此需要自己額外導入EL的實現(xiàn)。EL也屬于Java EE標準技術,可認為是一種表達式語言工具,它并不僅僅是只能用于Web,可以用于任意地方(類比Spring的SpEL)
這是EL技術規(guī)范的API:
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
Expression Language 3.0表達式語言規(guī)范于2013-4-29發(fā)布,Tomcat 8、Jetty 9、GlasshFish 4都已經支持實現(xiàn)了EL 3.0,如果你是web環(huán)境,就不用自己手動導入了
簡單來說以上JSR提供了一套Bean校驗規(guī)范的API,維護在包javax.validation.constraints下。該規(guī)范使用屬性或者方法參數(shù)或者類上的一套簡潔易用的注解來做參數(shù)校驗。開發(fā)者在開發(fā)過程中,僅需在需要校驗的地方加上形如@NotNull, @NotEmpty , @Email的注解,就可以將參數(shù)校驗的重任委托給一些第三方校驗框架來處理。
在Spring MVC中,只需要使用@Valid注解標注在方法參數(shù)商,Spring MVC即可對參數(shù)對象進行校驗,校驗結果會放在BindingResult對象中。除了@Valid 還有 @Validated注解支持參數(shù)的分組校驗。它們的區(qū)別:
- @Valid:沒有分組的功能。
- @Valid:可以用在方法、構造函數(shù)、方法參數(shù)和成員屬性(字段)上
- @Validated:提供了一個分組功能,可以在入參驗證時,根據不同的分組采用不同的驗證機制
- @Validated:可以用在類型、方法和方法參數(shù)上。但是不能用在成員屬性(字段)上
兩者是否能用于成員屬性(字段)上直接影響能否提供嵌套驗證的功能
嵌套驗證
看下圖:
比如我們現(xiàn)在有個實體叫做Item:
public class Item {
@NotNull(message = "id不能為空")
@Min(value = 1, message = "id必須為正整數(shù)")
private Long id;
@NotNull(message = "props不能為空")
@Size(min = 1, message = "至少要有一個屬性")
private List<Prop> props;
}
Item帶有很多屬性,屬性里面有:pid、vid、pidName和vidName,如下所示:
public class Prop {
@NotNull(message = "pid不能為空")
@Min(value = 1, message = "pid必須為正整數(shù)")
private Long pid;
@NotNull(message = "vid不能為空")
@Min(value = 1, message = "vid必須為正整數(shù)")
private Long vid;
@NotBlank(message = "pidName不能為空")
private String pidName;
@NotBlank(message = "vidName不能為空")
private String vidName;
}
屬性這個實體也有自己的驗證機制,比如pid和vid不能為空,pidName和vidName不能為空等。
正常情況,Spring Validation框架只會對Item的id和props做非空和數(shù)量驗證,不會對props字段里的Prop實體進行字段驗證。
如何進行嵌套校驗?
為了能夠進行嵌套驗證,必須手動在Item實體的props字段上明確指出這個字段里面的實體也要進行驗證。由于@Validated不能用在成員屬性(字段)上,但是@Valid能加在成員屬性(字段)上,而且@Valid類注解上也說明了它支持嵌套驗證功能,那么我們能夠推斷出:@Valid加在方法參數(shù)時并不能夠自動進行嵌套驗證,而是用在需要嵌套驗證類的相應字段上,來配合方法參數(shù)上@Validated或@Valid來進行嵌套驗證。
修改Item類如下所示:
public class Item {
@NotNull(message = "id不能為空")
@Min(value = 1, message = "id必須為正整數(shù)")
private Long id;
@Valid // 嵌套驗證必須用@Valid
@NotNull(message = "props不能為空")
@Size(min = 1, message = "props至少要有一個自定義屬性")
private List<Prop> props;
}
除了上面常見的@NotNull、@Min、@NotBlank和@Size等校驗注解我們還可以自定義校驗注解~
類級別驗證(多字段聯(lián)合驗證)
約束也可以放在類級別上(也就說注解標注在類上)。在這種情況下,驗證的主體不是單個屬性,而是整個對象。如果驗證依賴于對象的幾個屬性之間的相關性,那么類級別約束就能搞定。
這個需求場景在平時開發(fā)中也非常常見,比如此處我舉個簡單場景案例:修改用戶名密碼,需要輸入兩遍新密碼:newPass,newPassAgain,要求newPass.equals(newPassAgain)。
如果用事務腳本來實現(xiàn)這個驗證規(guī)則,那么你的代碼里肯定穿插著類似這樣的代碼:
if (!this.newPass.equals(this.newPassAgain)){
throw new RuntimeException("...");
}
雖然這么做也能達到校驗的效果,但很明顯這不夠優(yōu)雅。
但是基于Hibernate-Validator內置的@ScriptAssert,可以很容易的處理這種case:
@ScriptAssert(lang = "javascript", alias = "_", script = "_.newPass.equals(_.newPassAgain)",message = "兩個密碼不相等")
public class SecContent implements Serializable {
@NotNull(message = "age 不能為空",groups = {TestGroup.class})
private Integer age;
@NotBlank
private String newPass;
@NotBlank
private String newPassAgain;
...
}
@ScriptAssert支持寫腳本來完成驗證邏輯,這里使用的是javascript(缺省情況下的唯一選擇,也是默認選擇)
@ScriptAssert是內置就提供的,因此使用起來非常的方便和通用。但缺點也是因為過于通用,因此語義上不夠明顯,需要閱讀腳本才知。推薦少量(非重復使用)、邏輯較為簡單時使用,更為輕巧
自定義校驗
舉例說明自定義注解的實現(xiàn):需要一個自定義注解來校驗入參name不能和已存在name重名
1.自定義注解
@Target({ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueConstraintValidator.class)
public @interface UniqueConstraint {
//下面三個屬性是必須有的屬性
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2.新建一個UniqueConstraintValidator類來驗證注解
//自定義校驗注解 的 校驗邏輯
//不需要加注解@Component,因為實現(xiàn)了ConstraintValidator接口自動會注冊為spring bean
public class UniqueConstraintValidator implements ConstraintValidator<UniqueConstraint,Object> {
@Autowired
private UserService userService;
@Override
public void initialize(UniqueConstraint uniqueConstraint) {
System.out.println("my validator init");
}
//Object為校驗的字段類型
//返回true則校驗成功
//o為校驗字段的值,constraintValidatorContext為校驗注解里的屬性值
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
String username = (String) o;
TbUser user = userService.findByUsername(username);
return user==null?true:false;
}
}
UniqueConstraintValidator類必須實現(xiàn)ConstraintValidator接口initialize方法以及驗證方法isValid- 具體的校驗邏輯在
isValid方法中做校驗
使用的時候在需要的字段上標記該注解即可:

Dubbo RPC參數(shù)校驗
那么Dubbo作為國產優(yōu)秀的開源RPC框架,支持注解方式校驗參數(shù)么?答案當然是支持的~
ValidationFilter

ValidationFilter通過在實際方法調用之前,根據調用者url配置的validation屬性值找到正確的{Validator}實例來調用驗證。
關于ValidationFilter是如何被調用的是dubbo spi的內容這里就不提了,但是要想其生效需要在consumer或者provider端配置一下:
consumer:
@DubboReference(validation = "true")
private DemoService demoService;
或provider:
@DubboService(validation = "true")
public class DemoServiceImpl implements DemoService {
注:如果在消費端開啟參數(shù)校驗,不通過就不會向服務端發(fā)起rpc調用,但是要自己處理校驗異常ConstraintViolationException
Validator校驗器
默認ValidationFilter使用的校驗器看下圖

JValidation實現(xiàn)代碼還是比較少的
/**
* Creates a new instance of {@link Validator} using input argument url.
* @see AbstractValidation
* @see Validator
*/
public class JValidation extends AbstractValidation {
/**
* Return new instance of {@link JValidator}
* @param url Valid URL instance
* @return Instance of JValidator
*/
@Override
protected Validator createValidator(URL url) {
return new JValidator(url);
}
}
可以看到實際干活的是org.apache.dubbo.validation.support.jvalidation.JValidator
在其validate方法中可以看到有一個@MethodValidated注解

點開查看它的注釋

大意上能明白這注解是標記在方法上支持分組校驗的!
簡單示例:
消費方代碼:
@Component("demoServiceComponent")
public class DemoServiceComponent implements DemoService {
@DubboReference(validation = "true")
private DemoService demoService;
@Override
public String sayHello(String name) {
return demoService.sayHello(name);
}
@Override
public String sayGoodBye(Content content) {
return demoService.sayGoodBye(content);
}
@Override
public CompletableFuture<String> sayHelloAsync(String name) {
return null;
}
}
dubbo client interface:
public interface DemoService {
String sayHello(String name);
@MethodValidated({TestGroup.class})
String sayGoodBye(Content content);
default CompletableFuture<String> sayHelloAsync(String name) {
return CompletableFuture.completedFuture(sayHello(name));
}
}
方法入參Content:
public class Content implements Serializable {
@NotNull(message = "name不能為空",groups = {TestGroup.class})
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
注:沒有設置groups的校驗注解也會進行校驗,作為默認分組。最后捕獲下拋出的ConstraintViolationException以結構化的json格式返回給調用方"校驗錯誤信息"
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
feign客戶端HTTP狀態(tài)碼為204時?響應體被忽略的問題
這篇文章主要介紹了feign客戶端HTTP狀態(tài)碼為204時?響應體被忽略的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
JAVA 格式化JSON數(shù)據并保存到json文件中的實例
這篇文章主要介紹了JAVA 格式化JSON數(shù)據并保存到json文件中的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
解決spring cloud zuul與nginx的域名轉發(fā)問題
這篇文章主要介紹了spring cloud zuul與nginx的域名轉發(fā)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07

