天天看點

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接口。

Validation架構的應用

Exception,相信大家都熟悉,那麼就直接上BindingResult接口吧。

Validation架構的應用

至于最終效果如何,可以看下圖。

Validation架構的應用

從上圖的紅框,我都不用展示具體注解應用,大家就懂了。很明顯是一個inclinaionOrigin的對象上,有一個屬性dataId沒有通過@NotNull注解的校驗。并且還可以從上圖中找到@NotNull注解的message等資訊,以及異常堆棧的追蹤資訊。

并且由于傳回異常資訊的格式固定,是以可以直接通過對BindException的解析,來擷取所需的絕大部分異常資訊。

六,總結

簡單來說,就五點:

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

最後,願與諸君共進步。

七,附錄

參考

  • 告别996 實作高效程式設計 減少開發壓力
  • Bean Validation specification
  • @Valid與@Validated注解
  • @Validated和@Valid差別...
  • JavaBean Validation - javax.validation.Payload Examples
  • JavaBean Validation - Constraint payloads
  • Chapter 3. Creating custom constraints