十五分鐘,學會Validation框架的全面應用

十五分鐘,學會Validation框架的全面應用

一,前言

這篇博客只說一下Validation框架的應用,不涉及相關JSR,相關理論,以及源碼的解析。

如果之後需要的話,會再開博客描寫,這樣會顯得主題突出一些。

後續擴展部分會解釋message,groups,payload三個核心屬性等。

自定義註解部分,會給出螞蟻金服內部真實採用的自定義校驗註解。

二,簡介

簡單來說,就是通過Validation框架,進行數據的各類校驗。從Java的基本數據類型到自定義封裝數據類型,從非空判斷到正則表達式判斷,都是Validation框架所支持的。

在Validation之前,層次架構中,開發者總是採用分層驗證模型。就是分別在控制層,服務層,數據層等分別對目標對象的目標屬性進行校驗。很明顯,這是非常不優雅的,而且開發效率低,因爲存在大量重複校驗邏輯。

而Validation則提出一個元數據驗證模型,而在Spring體系中,則表現爲Java Bean驗證模型。站在Spring角度來說,無論是在哪個層次,都是針對Java Bean進行驗證的。所以,Validation則通過在目標Bean上添加約束註解,以及背後的驗證程序,實現了一個對業務代碼無侵入的校驗功能。

三,使用方法

1.添加依賴


    <!-- Validation 相關依賴 -->
    <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>2.0.1.Final</version>
    </dependency>

這是Validation框架的核心依賴。

該依賴是包含在SpringBoot的spring-boot-web-starter中的。所以如果使用了前面Spring-boot-web-starter依賴,則不需要再次引入Validation框架的依賴。

至於EL等依賴,常用於自定義註解,具體可以根據需要進行依賴引入。

2.添加約束註解

針對目標Bean,針對不同屬性的驗證需求,添加不同的約束註解。

如UserVo的userId,添加@NotNull註解,表示這個屬性在驗證框架中不可爲空。

有關約束註解,後面有詳盡描述。

3.開啓驗證

即使對元數據模型添加了約束註解,但是還沒有明確開啓驗證流程。站在Validation框架的角度,它並不知道應該在什麼時候進行校驗。因爲除了控制層,我們還可能在服務層驗證。即使是在服務層,一個調用鏈路,可能涉及多個方法,也需要確定在哪個方法進行驗證。

那麼,開啓驗證的方法有兩種(也許還有別的方法,歡迎補充):

  • 驗證註解:@Validated或者@Valid
  • 初始化驗證器:Validation.buildDefaultValidatorFactory().getValidator();

驗證註解

@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());
	}

四,約束註解

1.初級應用:常用註解

這裏給出了Validation框架(validation-api-2.0.1.Final)中constraints下全部的註解說明:

  • 空值校驗:

    • @Null:目標值爲null。比如,註冊時的userId當然是null(即使不爲null,系統也不會採用的)。
    • @NotNull:目標值不爲null。比如,登錄時的userId當然不爲null(當然也可能是通過了外部鑑權,然後內部裸奔)。
    • @NotEmpty:目標值不爲empty。相較於上者,增加了對空值的判斷(就是""無法通過@NotEmpty的校驗)
    • @NotBlank:目標值不爲blank。相較於上者,增加了對空格的判斷(就是空格無法通過@NotBlank校驗的)
  • 範圍校驗:

    • @Min:針對數值類型,目標值不能低於該註解設定的值。
    • @Max:針對數值類型,目標值不能高於該註解設定的值。
    • @Size:針對集合類型,目標集合的元素數量不可以高於max參數,不可以低於min參數。
    • @Digits:針對數值類型,目標值的整數位數必須等於integer參數設定的值,小數位數必須等於fraction參數設定的值。
    • @DecimalMax:針對數值類型,目標值必須小於該註解設定的值。
    • @DecimalMin:針對數值類型,目標值必須大於該註解設定的值。
    • @Past:針對於日期類型,目標值必須是一個過去的時間。
    • @PastOrPresent:針對於日期類型,目標值必須是一個過去或現在的時間。
    • @Future:針對於日期類型,目標值必須是未來的時間。
    • @FutureOrPresent:針對於日期類型,目標值必須是未來或未來的時間。
    • @Negative:針對數值類型,目標值必須是負數。
    • NegativeOrZero:針對數值類型,目標值必須是非正數。
    • @Positive:針對數值類型,目標值必須是正數。
    • @PositiveOrZero:針對數值類型,目標值必須是非負數。
  • 其他校驗:

    • @AssertTrue:針對布爾類型,目標值必須爲true。
    • @AssertFalse:針對布爾類型,目標值必須爲false。
    • @Email:針對字符串類型,目標值必須是Email格式。
    • @URL:針對字符串類型,目標值必須是URL格式。
    • @Pattern:針對字符串類型,目標值必須通過註解設定的正則表達式。

上面有關NotNull,NotEmpty,NotBlank,可以參考StringUtils的類似API。
另外,就是上述的@Pattern註解,可以說是最爲靈活的註解。許多自定義註解,其實都可以通過@Pattern註解實現。

2.中級應用:級聯,分組,序列

我認爲Validation框架的中級應用有三個:

  • 級聯驗證:通過@Valid註解實現級聯校驗。舉個例子,我的ScriptionBO中有一個List屬性。我希望Validation框架在校驗ScriptionBO的時候,不僅僅校驗ScriptionBO的屬性,還要驗證其中List涉及的User們。那麼在List上添加@Valid註解,就可以實現了。
  • 分組校驗:通過分組Interface與校驗註解的group參數,就可以實現分組校驗。舉個例子,同樣是User實體類,既需要滿足登錄驗證(有userId這樣的屬性),也需要滿足註冊驗證(不需要userId這樣的屬性)。那麼可以在User實體類中,建立用於登錄場景的interface LoginGroup {}接口,與用於註冊場景的interface RegisterGroup {}。在userId屬性上,增加非空校驗的@NotNull(groups = LoginGroup.class),就可以實現了。
  • 分組序列:通過分組校驗,再加上@GroupSequence({xxxGroup.class,xxxGroup.class}),就可以實現分組序列了。舉個例子,登錄場景下,User連userId的非空校驗都沒有通過,那麼就更不需要校驗手機號碼,郵箱等。

3.高級應用:自定義校驗註解

首先強調一點,正常情況下,常用約束註解配合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框架進行數據校驗。

不過這個代碼很快就增加禁止字段等,並通過接口實現了邏輯上的關注點分離。

之所以沒有引入完整版,一方面完整代碼,代碼量較多,放在這裏會造成主題的偏移。另一方面,完整代碼涉及內部的一些配置服務,不方便泄露。

五,擴展

1.核心屬性解釋

  • message:異常消息。在校驗失敗時,返回的message。通常會將校驗失敗時的異常消息,甚至是異常類型等放在這裏(異常堆棧,是可以通過校驗失敗時拋出的BindException獲取)。
  • groups:分組信息。通過該屬性,進行分組校驗。詳見中級應用:分組信息部分。
  • payload:有效負載。用於保存一些關鍵信息。

其實上述三個核心屬性,最爲神祕的,就是payload屬性。一方面,這個屬性用得最少,絕大部分人都不會使用。另一方面,國內的百度很難找到這方面資料。

我在百度的前兩頁,都看不到幾個相關的解釋。即使有解釋,也只是一句乾巴巴的有效負載(其實就是翻譯過來,具體功能和這個沒太大關係)。百度中只有兩條博客,提到payload可以作爲用戶校驗,以及元數據。而一些Validation框架的教學視頻,也大多一筆帶過。最後還是在谷歌上找到較爲全面的解釋。。。

2.payload的實踐應用

我之前使用Validation框架,也沒有使用這個註解。直到在螞蟻某項目推進數據校驗規範時,纔去深入瞭解它。還有一個比較重要的原因,當時一方面需要在message中保存自定義的異常信息,另一方面需要保存錯誤類型的Code(系統有一個專門的異常Enum),從而對接阿里內部的國際化文案平臺-美杜莎(特意查了一些,外網是有資料的。囧)。

那麼需要保存的信息就不止兩處。如果通過Json配合BO的方式,就有些複雜化了,而且顯得比較重(尤其是有更好的方案)。前期不瞭解payload的情況下,就通過BindExcpetion的解析,獲取所需的核心信息,放棄非核心的信息。那麼在瞭解payload後,問題就簡單了。直接通過payload配合對應Payload接口的子接口,可以保存所需的信息。

之後有機會,可以考慮寫一篇博客,來談談有關payload的實踐應用。

3.BindException的解析

先上圖,可以看到BindException繼承Exception,實現了BindingResult接口。

在這裏插入圖片描述
Exception,相信大家都熟悉,那麼就直接上BindingResult接口吧。

在這裏插入圖片描述
至於最終效果如何,可以看下圖。

在這裏插入圖片描述
從上圖的紅框,我都不用展示具體註解應用,大家就懂了。很明顯是一個inclinaionOrigin的對象上,有一個屬性dataId沒有通過@NotNull註解的校驗。並且還可以從上圖中找到@NotNull註解的message等信息,以及異常堆棧的追蹤信息。

並且由於返回異常信息的格式固定,所以可以直接通過對BindException的解析,來獲取所需的絕大部分異常信息。

六,總結

簡單來說,就五點:

  1. 儘量使用Validation框架自帶的註解。
  2. 使用自定義註解前,想想是否可以通過@Pattern解決問題。
  3. payload其實類似groups,不過對應的接口需要繼承Payload接口。
  4. Validation框架校驗失敗時,拋出的BindException,包含絕大部分所需的異常信息。
  5. Validation框架是優秀的數據校驗規範的落實方案,配合全局異常處理等,更棒。

最後,願與諸君共進步。

七,附錄

參考

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