這篇博客只說一下Validation框架的應用,不涉及相關JSR,相關理論,以及源碼的解析。html
若是以後須要的話,會再開博客描寫,這樣會顯得主題突出一些。java
後續擴展部分會解釋message,groups,payload三個核心屬性等。git
自定義註解部分,會給出螞蟻金服內部真實採用的自定義校驗註解。web
簡單來講,就是經過Validation框架,進行數據的各種校驗。從Java的基本數據類型到自定義封裝數據類型,從非空判斷到正則表達式判斷,都是Validation框架所支持的。正則表達式
在Validation以前,層次架構中,開發者老是採用分層驗證模型。就是分別在控制層,服務層,數據層等分別對目標對象的目標屬性進行校驗。很明顯,這是很是不優雅的,並且開發效率低,由於存在大量重複校驗邏輯。spring
而Validation則提出一個元數據驗證模型,而在Spring體系中,則表現爲Java Bean驗證模型。站在Spring角度來講,不管是在哪一個層次,都是針對Java Bean進行驗證的。因此,Validation則經過在目標Bean上添加約束註解,以及背後的驗證程序,實現了一個對業務代碼無侵入的校驗功能。編程
<!-- Validation 相關依賴 --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>
這是Validation框架的核心依賴。json
該依賴是包含在SpringBoot的spring-boot-web-starter中的。因此若是使用了前面Spring-boot-web-starter依賴,則不須要再次引入Validation框架的依賴。後端
至於EL等依賴,經常使用於自定義註解,具體能夠根據須要進行依賴引入。api
針對目標Bean,針對不一樣屬性的驗證需求,添加不一樣的約束註解。
如UserVo的userId,添加@NotNull註解,表示這個屬性在驗證框架中不可爲空。
有關約束註解,後面有詳盡描述。
即便對元數據模型添加了約束註解,可是尚未明確開啓驗證流程。站在Validation框架的角度,它並不知道應該在何時進行校驗。由於除了控制層,咱們還可能在服務層驗證。即便是在服務層,一個調用鏈路,可能涉及多個方法,也須要肯定在哪一個方法進行驗證。
那麼,開啓驗證的方法有兩種(也許還有別的方法,歡迎補充):
@Validated註解的效果與@Valid是同樣的,畢竟@Validated是SpringBoot對@Valid註解的封裝(@Valid是Java的自帶的註解)。而@Validated註解是包含在SpringBoot的spring-boot-web-starter中的。
在對應位置添加@Validated註解(當程序執行到這裏,就會執行對應的校驗邏輯):
@PostMapping("save.do") @ResponseBody public ServerResponse saveConfig(@Validated(InclinationConfig.ConfigCommitGroup.class) InclinationConfig inclinationConfig) { // 業務邏輯 }
@Validated public class demo { @PostMapping("get.do") @ResponseBody public ServerResponse getConfig(int configId) { // 業務邏輯 } }
針對Java基本數據類型的@NotNull,則須要將對應類上添加@Validated註解。
初始化,創建驗證器對象(Validator對象):
// 驗證器對象 private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
獲取驗證結果集合(這裏也就是開啓驗證的時間位置):
// 驗證結果集合 private Set<ConstraintViolation<UserInfo>> set = validator.validate(userInfo); // 驗證過程能夠添加分組信息 private Set<ConstraintViolation<UserInfo>> set = validator.validate(userInfo,UserInfo.RegisterGroup.class);
處理驗證結果集合:
set.forEach(item -> { // 輸出驗證錯誤信息 System.out.println(item.getMessage()); });
固然啦。更多狀況下,咱們是直接拋出異常的:
// 判斷驗證結果集是否爲空(驗證結果集放的都是驗證失敗時的message) if(!CollectionUtils.isEmpty(set)) { // 循環時,採用StringBuilder能夠有效提升效率(詳見String,StringBuilder,StringBuffer三者區別) StringBuilder exceptionMessage = new StringBuilder(); set.forEach(validationItem -> { exceptionMessage.append(validationItem.getMessage()); }); // 直接拋出異常(其實這也就是@Valid註解的默認校驗器的作法) throw new Exception(exceptionMessage.toStrring()); }
這裏給出了Validation框架(validation-api-2.0.1.Final)中constraints下所有的註解說明:
空值校驗:
範圍校驗:
其餘校驗:
上面有關NotNull,NotEmpty,NotBlank,能夠參考StringUtils的相似API。
另外,就是上述的@Pattern註解,能夠說是最爲靈活的註解。許多自定義註解,其實均可以經過@Pattern註解實現。
我認爲Validation框架的中級應用有三個:
首先強調一點,正常狀況下,經常使用約束註解配合Validation框架的中級應用,足以應付大多數狀況。尤爲是@Pattern註解採用了靈活的正則表達式,能夠解決大部分複雜問題。
舉個例子,正常的Email地址校驗,能夠經過@Email註解進行校驗,更能夠經過@Pattern實現更爲精準的校驗。至於自定義校驗註解,則能夠實現根據配置,動態驗證Email地址的功能。
自定義校驗註解,其實就相似於配合自定義註解的切面編程,只不過利用了Validation框架的一些基礎方法。
自定義校驗註解分爲如下三步:
爲了更直觀的感覺,這裏給出一個簡單的demo。
另外,這裏的依賴,須要單獨引入,能只依靠springboot自帶的validation依賴。
package tech.jarry.learning.demo.common.anno; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; /** * @author jarry * @description 自定義動態屬性校驗約束註解 */ @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) // 關聯約束註解與約束規則 @Constraint(validatedBy = DynamicPropertyVerificationValidator.class) public @interface DynamicPropertyVerification { // 約束註解校驗失敗時的輸出信息 String message() default "property verification fail"; // 約束註解在驗證時所屬的組別 Class<?>[] groups() default {}; // 約束註解的負載(可用來保存一些數據) Class<? extends Payload>[] payload() default {}; }
package tech.jarry.learning.demo.common.anno; import com.alibaba.fastjson.JSON; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.ArrayList; import java.util.List; /** * @author jarry * @description 動態屬性的自定義約束校驗器 */ public class DynamicPropertyVerificationValidator implements ConstraintValidator<DynamicPropertyVerification, String> { // 爲了便於進行測試,這裏先放入一些本地數據 private static final List<String> REX_LIST = new ArrayList<String>() { { add("auth_1"); add("auth_2"); add("auth_3"); add("auth_4"); } }; @Override public void initialize(DynamicPropertyVerification dynamicPropertyVerification) { // 經過zk等獲取遠程配置,或加載本地配置(這個看狀況了) } @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { // 判斷須要校驗的屬性屬於單個屬性值,仍是集合屬性值 // 這裏只針對"Admin"與["auth_1","auth_3","auth_2"]這樣的格式進行校驗 if (JSON.isValidArray(value)) { // 須要校驗的屬性,是一個集合類型(如權限列表) List<String> requestValueList = JSON.parseArray(value, String.class); boolean result = requestValueList.stream() .allMatch(requestValue -> isValidRequestValue(requestValue)); return result; } else { // 須要校驗的屬性,是一個單一屬性字符串(如gender) boolean result = isValidRequestValue(value); return result; } } private boolean isValidRequestValue(final String value) { return REX_LIST.stream() .anyMatch(legalValue ->legalValue.equals(value)); } }
首先這個註解是真實項目的代碼,是我參與的螞蟻金服某項目的商業平臺代碼。
爲了實現商業化SDK,便須要後端自行負責數據校驗。正好當時這塊的負責人但願規範代碼,因此就交給我,經過統一的Validation框架進行數據校驗。
不過這個代碼很快就增長禁止字段等,並經過接口實現了邏輯上的關注點分離。
之因此沒有引入完整版,一方面完整代碼,代碼量較多,放在這裏會形成主題的偏移。另外一方面,完整代碼涉及內部的一些配置服務,不方便泄露。
其實上述三個核心屬性,最爲神祕的,就是payload屬性。一方面,這個屬性用得最少,絕大部分人都不會使用。另外一方面,國內的百度很難找到這方面資料。
我在百度的前兩頁,都看不到幾個相關的解釋。即便有解釋,也只是一句乾巴巴的有效負載(其實就是翻譯過來,具體功能和這個沒太大關係)。百度中只有兩條博客,提到payload能夠做爲用戶校驗,以及元數據。而一些Validation框架的教學視頻,也大多一筆帶過。最後仍是在谷歌上找到較爲全面的解釋。。。
我以前使用Validation框架,也沒有使用這個註解。直到在螞蟻某項目推動數據校驗規範時,纔去深刻了解它。還有一個比較重要的緣由,當時一方面須要在message中保存自定義的異常信息,另外一方面須要保存錯誤類型的Code(系統有一個專門的異常Enum),從而對接阿里內部的國際化文案平臺-美杜莎(特地查了一些,外網是有資料的。囧)。
那麼須要保存的信息就不止兩處。若是經過Json配合BO的方式,就有些複雜化了,並且顯得比較重(尤爲是有更好的方案)。前期不瞭解payload的狀況下,就經過BindExcpetion的解析,獲取所需的核心信息,放棄非核心的信息。那麼在瞭解payload後,問題就簡單了。直接經過payload配合對應Payload接口的子接口,能夠保存所需的信息。
以後有機會,能夠考慮寫一篇博客,來談談有關payload的實踐應用。
先上圖,能夠看到BindException繼承Exception,實現了BindingResult接口。
Exception,相信你們都熟悉,那麼就直接上BindingResult接口吧。
至於最終效果如何,能夠看下圖。
從上圖的紅框,我都不用展現具體註解應用,你們就懂了。很明顯是一個inclinaionOrigin的對象上,有一個屬性dataId沒有經過@NotNull註解的校驗。而且還能夠從上圖中找到@NotNull註解的message等信息,以及異常堆棧的追蹤信息。
而且因爲返回異常信息的格式固定,因此能夠直接經過對BindException的解析,來獲取所需的絕大部分異常信息。
簡單來講,就五點:
最後,願與諸君共進步。