04-Spring(Boot) 整合 hibernate validator


零、使用場景

日常開發中,難免需要對參數進行一些參數正確性的校驗,比如字段非空,字段長度限制,郵箱格式驗證等等,可能普通操作就是寫一些字段校驗的代碼去做處理判斷,不同的地方可能會重復編寫(不停搬磚~~),而這些校驗出現在業務代碼中,讓我們的業務代碼顯得臃腫,並且不方便維護。

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等。

image-20220316195738508

可以看到,兩個依賴包都有一些共同的注解,在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、包引入

  1. 可以直接引入org.hibernate.hibernate-validator包,需要指定具體版本

    <dependency>
    	<groupId>org.hibernate</groupId>
    	<artifactId>hibernate-validator</artifactId>
    	<version>6.0.0.Final</version>
    </dependency>
    
  2. 如果是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.MethodArgumentNotValidExceptionMethodArgumentNotValidException繼承於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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM