零、使用場景
日常開發中,難免需要對參數進行一些參數正確性的校驗,比如字段非空,字段長度限制,郵箱格式驗證等等,可能普通操作就是寫一些字段校驗的代碼去做處理判斷,不同的地方可能會重復編寫(不停搬磚~~),而這些校驗出現在業務代碼中,讓我們的業務代碼顯得臃腫,並且不方便維護。
Hibernate Validator 框架剛好解決了這些問題,可以很優雅的方式實現參數的校驗,讓業務代碼和校驗邏輯 分開,不再編寫重復的校驗邏輯,從此在參數校驗上不用花費太多時間。
一、Hibernate Validator 簡介
Hibernate Validator是 Bean Validation 的參考實現 。 Hibernate Validator 提供了 JSR 303 規范中所有內置 constraint 的實現,除此之外還有一些附加的 constraint。
Bean Validation 為 JavaBean 驗證定義了相應的元數據模型和API。缺省的元數據是 Java Annotations,通過使用 XML 可以對原有的元數據信息進行覆蓋和擴展。Bean Validation 是一個運行時的數據驗證框架,在驗證之后驗證的錯誤信息會被馬上返回。
二、常用注解
1、常用注解如下
驗證注解 | 驗證的數據類型 | 說明 | 包 |
---|---|---|---|
@AssertFalse | Boolean,boolean | 驗證注解的元素值是false | javax.validation.constraints |
@AssertTrue | Boolean,boolean | 驗證注解的元素值是true | javax.validation.constraints |
@NotNull | 任意類型 | 驗證注解的元素值不是null | javax.validation.constraints |
@Null | 任意類型 | 驗證注解的元素值是null | javax.validation.constraints |
@Min(value=值) | BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存儲的是數字)子類型 | 驗證注解的元素值大於等於@Min指定的value值 | javax.validation.constraints |
@Max(value=值) | 和@Min要求一樣 | 驗證注解的元素值小於等於@Max指定的value值 | javax.validation.constraints |
@DecimalMin(value=值) | 和@Min要求一樣 | 驗證注解的元素值大於等於@ DecimalMin指定的value值 | javax.validation.constraints |
@DecimalMax(value=值) | 和@Min要求一樣 | 驗證注解的元素值小於等於@ DecimalMax指定的value值 | javax.validation.constraints |
@Digits(integer=整數位數, fraction=小數位數) | 和@Min要求一樣 | 驗證注解的元素值的整數位數和小數位數上限 | javax.validation.constraints |
@Size(min=下限, max=上限) | 字符串、Collection、Map、數組等 | 驗證注解的元素值的在min和max(包含)指定區間之內,如字符長度、集合大小 | javax.validation.constraints |
@NotBlank | CharSequence子類型 | 驗證注解的元素值不為空(不為null、去除首位空格后長度為0),不同於@NotEmpty,@NotBlank只應用於字符串且在比較時會去除字符串的首位空格 | javax.validation.constraints,org.hibernate.validator.constraints |
@Length(min=下限, max=上限) | CharSequence子類型 | 驗證注解的元素值長度在min和max區間內 | org.hibernate.validator.constraints |
@NotEmpty | CharSequence子類型、Collection、Map、數組 | 驗證注解的元素值不為null且不為空(字符串長度不為0、集合大小不為0) | javax.validation.constraints,org.hibernate.validator.constraints |
@Range(min=最小值, max=最大值) | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子類型和包裝類型 | 驗證注解的元素值在最小值和最大值之間 | org.hibernate.validator.constraints |
@Email(regexp=正則表達式,flag=標志的模式) | CharSequence子類型(如String) | 驗證注解的元素值是Email,也可以通過regexp和flag指定自定義的email格式 | javax.validation.constraints,org.hibernate.validator.constraints |
@Pattern(regexp=正則表達式,flag=標志的模式) | String,任何CharSequence的子類型 | 驗證注解的元素值與指定的正則表達式匹配 | javax.validation.constraints |
@Valid | 任何非原子類型 | 指定遞歸驗證關聯的對象如用戶對象中有個地址對象屬性,如果想在驗證用戶對象時一起驗證地址對象的話,在地址對象上加@Valid注解即可級聯驗證 | javax.validation.constraints |
1、對於基本數據類型,使用@Max、@Min等注解時,如果被注解元素為null,則該元素會被賦默認值,如int的默認值為0,boolean的默認值為false;
2、對於包裝類型,使用@Max、@Min等注解時,如果被注解元素為null,則被注解元素就為null,因此當被注解元素不能為null時,則必須配合@NotNull等非空注解使用
3、對於字符類型和集合類型:@Length、@Size 僅判斷被注解元素的長度/大小是否符合要求,不判斷被注解元素是否為空,因此要保證被注解元素為空時必須與@NotNull等非空注解配合使用。
2、依賴包及版本關系
以上注解來源於兩個jar包,一個是org.hibernate.hibernate-validator
包,一個是javax.validation.validation-api
,如下圖所示,其中org.hibernate.hibernate-validator
依賴於javax.validation.validation-api
,可以看到,org.hibernate.hibernate-validator
包下擁有自己的實現,如@Length、@Range、以及被標注為過時的@Email等。
可以看到,兩個依賴包都有一些共同的注解,在6.x.x的版本中,@Email、@NotBlank、@NotEmpty注解已經被標記為過時,因此不存在不知道引入那個版本的問題,而在5.x.x的版本中,這三個注解都未過時,因此必須引入org.hibernate.hibernate-validator
下的注解,否則會報錯。
javax.validation.validation-api
3.0.0版本開始,包路徑由javax.validation.constraints
變為jakarta.validation.constraints.NotNull
,如@NotNull注解的引用路徑從javax.validation.constraints.NotNull
變為jakarta.validation.constraints.NotNull
。具體原因可以搜索Jakarta EE
,了解世事滄桑。
三、使用
1、包引入
-
可以直接引入
org.hibernate.hibernate-validator
包,需要指定具體版本<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.0.Final</version> </dependency>
-
如果是spring boot項目,則推薦如下方式:通過引入
spring-boot-starter-validation
引入org.hibernate.hibernate-validator
,無需指定版本,且由spring boot來保證版本間的兼容性。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
2、基礎使用
1、創建參數校驗類
新建實體類:User
import java.util.List;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
// 如果org.hibernate.hibernate-validator的版本為5.x.x,則這里必須引入該包下的NotBlank和NotEmpty,否則會報錯
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
@Data
public class User {
@Max(value = 10, message = "最大不能超過10")
private Integer i1;
@Min(value = 20, message = "最小不能小於20")
private Integer i2;
@NotNull(message = "不能為null")
@Min(value = 30, message = "最小不能小於30")
@Max(value = 30, message = "最大不能超過30")
private Integer i3;
@Range(min = 40, max = 40, message = "值必須等於40")
private int i4;
@NotNull(message = "不能為null")
private String s1;
@NotBlank(message = "不能為空")
private String s2;
@NotEmpty(message = "不能為空")
private String s3;
@Length(min = 2, max = 5, message = "范圍為2~5")
private String s4;
@NotNull(message = "不能null")
private List<String> l1;
@NotEmpty(message = "不能為空")
private List<String> l2;
}
2、異常處理方式一
新建接口:ValidController
@RestController
public class ValidController {
@GetMapping("valid1")
public ResultBack user1(@Valid User user, BindingResult result) {
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
StringJoiner error = new StringJoiner(",");
result.getFieldErrors().forEach(data -> error.add(data.getDefaultMessage()));
return ResultBack.fail(error.toString());
}
System.out.println(user);
return ResultBack.success(user);
}
}
這里在接口參數中通過@Valid
注解來標識該實體類需求被校驗,並通過緊跟在@Valid
注解參數后面的BindingResult
對象接受錯誤信息,然后在方法中進行判斷、封裝處理,返回錯誤信息。
優點:關於參數校驗的邏輯只有錯誤處理的一個if塊代碼,沒有其他多余的校驗邏輯,簡潔明了。
缺點:對於每個需求校驗的接口,都需要添加BindingResult
參數,而且要保證參數位置緊跟在@Valid
參數后面,另外每個接口都有一樣的錯誤處理邏輯,重復編碼,不方便維護。
3、異常處理方式二
通過全局異常的方式進行處理:
上面的代碼中,如果不加BindingResult
,則程序會拋出一個異常:org.springframework.validation.BindException
,其實是org.springframework.validation.MethodArgumentNotValidException
,MethodArgumentNotValidException
繼承於BindException
。因此,這里可以用全局異常處理的方式,進行統一處理,就不用每個校驗接口都加BindingResult
參數和錯誤處理邏輯了。
完整的代碼如下:
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler({BindException.class})
@ResponseStatus(HttpStatus.OK)
public ResultBack handleBindException(BindException ex) {
BasicLogUtil.info("BindException");
StringBuilder errorMsg = new StringBuilder();
// 獲取所有字段驗證出錯的信息
List<FieldError> allErrors = ex.getFieldErrors();
allErrors.forEach(fieldError -> errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append("; "));
return new ResultBack(ResultStatus.FAILURE, errorMsg.toString());
}
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
public ResultBack handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BasicLogUtil.info("MethodArgumentNotValidException");
StringBuilder errorMsg = new StringBuilder();
// 獲取所有字段驗證出錯的信息
List<FieldError> allErrors = ex.getFieldErrors();
allErrors.forEach(fieldError -> errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append("; "));
return new ResultBack(ResultStatus.FAILURE, errorMsg.toString());
}
}
這里對兩個異常分別做了處理,可以看到,處理方式都是一樣的,只是第一行打印輸出的內容不同,從而判斷出這里到底拋出的是那個異常。有興趣的同學可以試一下,只有注釋掉
handleMethodArgumentNotValidException
方法時,控制台才打印BindException
,從此可以推斷這里拋出的異常是MethodArgumentNotValidException
。
完整項目代碼詳見:OMaster/valid (gitee.com)