Validation框架的應用

Validation框架的應用

一,前言

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

若是以後須要的話,會再開博客描寫,這樣會顯得主題突出一些。java

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

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

二,簡介

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

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

而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框架的核心依賴。json

該依賴是包含在SpringBoot的spring-boot-web-starter中的。因此若是使用了前面Spring-boot-web-starter依賴,則不須要再次引入Validation框架的依賴。後端

至於EL等依賴,經常使用於自定義註解,具體能夠根據須要進行依賴引入。api

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框架是優秀的數據校驗規範的落實方案,配合全局異常處理等,更棒。

最後,願與諸君共進步。

七,附錄

參考

相關文章
相關標籤/搜索