@Validated、@Valid等注解用法詳解


1.情景展示

SpringBoot,SpringMvc 常用參數校驗用法詳解

在實際開發過程中,針對前端請求參數的校驗是一個不小的工作量。

什么時候需要對請求參數進行校驗?

情形1:前后端分離

前后端分離,雖然會提高項目的開發進度,但同樣也存在前后端開發人員交流不及時等問題。

比方說:性別參數,后端要求只能傳1或2,前端非要給你傳男或女,當前后端對於數據的要求標准不一致時,就會出現問題。

后台對入參進行校驗,一方面,可以提高數據的規范性;另一方面,也可以增加數據的安全性(比如:數據在傳輸過程中被篡改)。

情形2:對外提供接口

本質上,前后端分離,后台提供請求,也是屬於接口,這里特指的是后台對后台。

也就是說,別的項目或者公司需要調用咱們寫的接口,這個時候,參數的校驗就顯得格外重要,不想前后端那種,后台加不加校驗都沒有太大的影響。

2.准備工作

關於校驗標准,可供java使用的一共有兩套:

  一種是:Java API規范 (JSR303) 定義了Bean校驗的標准validation-api,但沒有提供實現。

  另一種是:hibernate validation是對這個規范的實現,並增加了校驗注解如@Email、@Length等。

  Spring Validation是對hibernate validation的二次封裝,用於支持spring mvc參數自動校驗。

  接下來,我們以spring-boot項目為例,介紹Spring Validation的使用。

關於jar包的引用

  如果spring-boot版本小於2.3.x,spring-boot-starter-web會自動傳入hibernate-validator依賴;

  如果spring-boot版本大於2.3.x,則需要手動引入依賴:

<!--spring對參數進行校驗:hibernate validator-->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.18.Final</version>
</dependency>

現在網絡上都是舊的內容,用的是:

完全沒有必要,使用第一個jar包就可以了。

關於jar包的區別,文末會進行詳細解說。

@Valid和@Validated的區別

3.具體實現

用法1:requestParam參數校驗

描述:通常用於get請求或者請求參數比較少的情形。

校驗生效的前提:

必須在Controller類上標注@Validated注解,在方法或者參數前添加無效!

如果校驗失敗,會拋出ConstraintViolationException異常。

用法:

將請求入參一一在請求方法()內,進行羅列,並將校驗注解添加在對應入參的前面。

2022年2月19日17:17:31

@RequestParam注解的required屬性的默認值為true,也就是,要求該參數是必傳項;

如果可以為空的話,需要將其值改成false

2023年9月28日11:27:32

否則的話,會報非空錯誤!

另外,如果請求參數與自己定義的接收的變量名稱不一樣的話,可以進行映射;

即:只要@RequestParam里面的名稱和請求入參名稱保持一致即可。

我們可以看到,只要對照好參數映射關系,就能接收到數據。

2023年9月28日11:42:52

說明:

請求參數,我們的后台不用@RequestParam修飾參數名,並不影響前端發送get請求(form表單格式數據:param1=value1&param2=value2&...)

另外,沒有@RequestParam注解也是可以進行校驗的。

所以說,不用@RequestParam注解也是可以的,用不用的區別就在於:方便knife4j識別接口類型是不是application/x-www-form-urlencoded。

如果接收請求入參的變量被@RequestParam注解修飾,knife4j就將接口類型展示為:form表單格式;

當沒有@RequestParam注解修飾時,展示為JSON格式。

用法2:pathVariable參數校驗

描述:通過{}來動態配置請求路徑,並將請求路徑當成方法的入參之一。

校驗生效的前提:

必須在Controller類上標注@Validated注解,在方法或者參數前添加無效!

如果校驗失敗,會拋出ConstraintViolationException異常。

用法:校驗注解可以放在@PathVariable前面也可以放在它的后面。

用法3:responseBody參數校驗(application/json)

當請求方法入參有@RequestBody注解的時候,spring會將它識別成JSON格式的請求,要求調用方必須發送application/json格式的數據;

第1步:在實體類的字段上加上約束注解;

第2步:在方法參數上聲明校驗注解。

格式:@RequestBody+@Validated+實體類

或者:@RequestBody+@Valid+實體類

這種情況下,使用@Valid和@Validated都可以(只能用@Valid或@Validated的地方,下面會講)

用法4:responseBody參數校驗(application/x-www-form-urlencoded)

當請求方法入參只有實體類接收的時候,spring會將它識別成FORM表單請求,要求調用方必須發送application/x-www-form-urlencoded格式的數據;

第1步:在實體類的字段上加上約束注解;

第2步:在方法參數上聲明校驗注解。

同樣地,使用@Valid和@Validated都可以

4.參數校驗配置(校驗失敗,立即拋出異常)

Hibernate Validator有以下兩種驗證模式:

普通模式:會校驗完所有的屬性,然后返回所有的驗證失敗信息。

快速失敗返回模式:只要有一個驗證失敗,則返回。

默認是普通模式,可以通過一些簡單的配置,開啟Fali Fast模式,一旦校驗失敗就立即返回。

查看代碼
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;


/**
 * 請求參數校驗配置
 * @description:
 * @author: Marydon
 * @date: 2020-08-10 14:57
 * @version: 1.0
 * @email: marydon20170307@163.com
 */
@Configuration
public class WebParamValidateConfig {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                //failFast的意思只要出現校驗失敗的情況,就立即結束校驗,不再進行后續的校驗。
                //.failFast(true)
                .addProperty("hibernate.validator.fail_fast", "true")
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator());
        return methodValidationPostProcessor;
    }
}

5.統一異常處理

如果校驗失敗,會拋出異常,我們需要對異常進行管理,以便返回一個更友好的提示。

下面是我總結的異常攔截配置類:

查看代碼
import org.jetbrains.annotations.NotNull;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;


/**
 * 全局異常處理器
 * @description: @ControllerAdvice默認會對所有的請求進行處理;
 * 對請求入參的校驗異常,根本進不到Controller的方法體內,所以,只能在這攔截異常后返回友好錯誤提示
 * 加basePackages,可以只對具體包名下面的請求進行處理
 * @ControllerAdvice是一個增強的 Controller。使用這個 Controller,可以實現三個方面的功能:
 * 全局異常處理
 * 全局數據綁定
 * 全局數據預處理
 * 只攔截Controller,不會攔截Interceptor的異常
 * @author: Marydon
 * @date: 2020年08月10日 0010 16:39
 */
// 異常攔截位置
@RestControllerAdvice(basePackages = {"com.xx"
                                        ,"com.yy"})
public class CzWebExceptionHandler {

    //處理Get請求中 使用@Valid 驗證路徑中請求實體校驗失敗后拋出的異常,詳情繼續往下看代碼
    @ExceptionHandler(BindException.class)
    public CzResponseDto<JSONObject> BindExceptionHandler(@NotNull BindException e) {
        String errorMsg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return CzResult.error("接口調取失敗:" + errorMsg, null);
    }

    //處理請求參數格式錯誤 @RequestParam上validate失敗后拋出的異常是javax.validation.ConstraintViolationException
    @ExceptionHandler(ConstraintViolationException.class)
    public CzResponseDto<JSONObject> ConstraintViolationExceptionHandler(@NotNull ConstraintViolationException e) {
        String errorMsg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
        return CzResult.error("接口調取失敗:" + errorMsg, null);
    }

    //處理請求參數格式錯誤 @RequestBody上validate失敗后拋出的異常是MethodArgumentNotValidException異常。
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public CzResponseDto<JSONObject> MethodArgumentNotValidExceptionHandler(@NotNull MethodArgumentNotValidException e) {
        String errorMsg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return CzResult.error("接口調取失敗:" + errorMsg, null);
    }

    // 要求Content-type為application/json,但是內容類型卻是text/plain或者text時會被該異常捕獲
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public CzResponseDto<JSONObject> HttpMediaTypeNotSupportedExceptionHandler(@NotNull HttpMediaTypeNotSupportedException e) {
        String errorMsg = "本接口不接收application/json以外格式的數據,請檢查入參是否是標准的json數據!\n" + e.getMessage();
        return CzResult.error("接口調取失敗:" + errorMsg, null);
    }

    @ExceptionHandler(Exception.class)
    public CzResponseDto<JSONObject> handleException(@NotNull Exception e) {
        String errorMsg = "系統異常,請聯系開發人員Marydon進行排錯!\n" + e.getMessage();
        return CzResult.error("接口調取失敗:" + errorMsg, null);
    }

    // 運行異常
    @ExceptionHandler(RuntimeException.class)
    public CzResponseDto<JSONObject> handleRuntimeException(@NotNull Exception e) {
        return CzResult.error("接口調取失敗:" + e.getMessage(), null);
    }

    // 服務異常
    @ExceptionHandler(ServiceException.class)
    public CzResponseDto<JSONObject> handleServiceException(@NotNull Exception e) {
        return CzResult.error("接口調取失敗:" + e.getMessage(), null);
    }

    // 請求方式異常(僅支持post請求)
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public CzResponseDto<JSONObject> handleHttpRequestMethodNotSupportedException(@NotNull Exception e) {
        return CzResult.error("接口調取失敗:" + e.getMessage(), null);
    }

    // 不存在的請求方法InterfaceMethod,在CzRequestParams可以查看支持的接口
    // 注意:當請求數據格式為非text或text/plain時,也會拋出該異常(application/javascript,application/xml,text/xml,text/html)
    // 無請求入參時也會拋出該異常
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public CzResponseDto<JSONObject> handleHttpMessageNotReadableException(@NotNull Exception e) {
        // return CzResult.error("接口調取失敗:未知的接口方法,請仔細核對入參InterfaceMethod的值!", null);
        return CzResult.error("接口調取失敗:" + e.getMessage(), null);
    }
}

僅供參考。

6.常用注解

@NotBlank

@NotBlank:只用在String上,表示:傳進來的值不能為null,而且調用trim()后,長度必須大於0,即:必須有實際字符;

@NotNull

@NotNull:不能為null,但值可以為empty(分配了內存空間),只能校驗String類型和對象

不能校驗:八大基本數據類型(因為基本數據類型有默認值:byte,short,int,long,double,flot,char,boolean);

即該參數是必傳項,但其值可以為空。

如果非得用基礎數據類型接收的話

那就只能和基本數據類型的默認值比較,進行判斷啦。

或者:把基本數據類型改成String,然后,在需要的時候,再將其進行數據類型轉換,轉成自己所需的數據類型。

@NotEmpty

@NotEmpty:不能為null,而且長度必須大於0,只能校驗字符串

@Max

@Max:最大值,限制該參數的最大值。

@Min

@Min:最小值,限制該參數的最小值。

說明:

@Max和@Min只能校驗數值類型,也就是說,限制該參數的數據類型只能為數字

這兩個注解,通常一塊使用,可以單獨使用;

用於接收的數據類型既可以是數值類型,也可以是字符串類型

@Length

@Length:校驗字符串長度。

@Size:校驗數組、集合大小(java,經測試無效);

@Pattern

@Pattern:正則表達式校驗(只能用於校驗字符串,即String類型,不能定義成Integer或Long類型,否則報錯

使用標准的正則表達式用法,帶反斜杠\的會自動轉義。

常用正則表達式

固定字符:regexp = "^(門診|住院|資往)$";

可以為空或8個數字:regexp = "^(\s{0}|\d{8})$";

長度必須為6的字符串:regexp = "^([0-9a-z]{6})$";

可以為空或者正整數[1,99]:regexp = "^(\s{0}|[1-9]\d{0,1})$";

校驗手機號:regexp = "^1(3[0-9]|4[5,7]|5[0,1,2,3,5,6,7,8,9]|6[2,5,6,7]|7[0,1,7,8]|8[0-9]|9[1,8,9])\d{8}$"

身份證號校驗:regexp = "^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$"

密碼校驗:

  密碼設定規則:8-16位,其中必須包含數字、小寫字母、大寫字母;字符僅支持“!@#¥%”,不支持空格  

   1、(?!.*[!·(){}【】“”:;,》¥、。‘’——\\s-……%\\n])  表示的是不含中文的特殊字符,以及空格與回車,里面的符合可以自動補充或刪除;
   2、(?=.*[a-zA-Z])   表示含小寫或大寫的英文字母
   3、(?=.*\\d)  表示必須匹配到數字  ;小寫字母(?=.*[a-z]);大寫字母(?=.*[A-Z])
   4、(?=.*[~!@#$%^&*()_+`\\-={}:\";'<>?,.\\/])   表示含英文的特殊字符,里面的符合可以自動補充或刪除
   5、[^\\u4e00-\\u9fa5]  表示不允許有中文  ;表示允許有中文的,即:[\\u4e00-\\u9fa5]
   6、{8,16}  表示長度要求,8~16位

正整數的正則表達式(不包括0): ^[1-9]\d*$

可為空值或其它:^$|^這里寫其它表達式$

7.擴展延伸

延伸1:分組校驗

在實際項目中,可能多個方法需要使用同一個實體類來接收參數,而不同方法的校驗規則很可能是不一樣的;

這個時候,簡單地在實體的字段上加約束注解無法解決這個問題。

因此,spring-validation支持了分組校驗的功能,專門用來解決這類問題。

舉個栗子:

A方法要求參數1的值必須為1,B方法則要求其值必須為2,如何實現?

第1步:在實體類當中添加接口類;

把接口類用作分組的依據;

注意:在實體類當中添加的接口,沒有實際意義,僅僅將其作為分組依據;

由於spring校驗的groups只能這樣用,沒有辦法。

第1步:在約束注解里,添加該注解生效的的分組信息groups。

多個組之間使用逗號隔開;

並且,組必須以".class"進行結尾。

針對不同的組,可以添加不同的校驗規則。

第2步:@Validated注解上指定校驗分組。

注意:分組校驗只能使用注解@Validated,不能使用@Valid!

另外,方法上使用了分組校驗,實體類需要多組共用的字段規則校驗,也必須添加組,即使是:校驗規則一致的。

否則的話,該校驗規則將會失效。

延伸2:嵌套校驗

當入參實體類的某字段也是對象時,這時,需要對該對象里的字段進行校驗時,這就牽扯到了:嵌套校驗;

此時,入參實體類的對應的字段對象,必須標記@Valid注解。

嵌套的實體類,示例:

延伸3:集合校驗(list)

如果請求體直接傳遞了json數組給后台,並希望對數組中的每一項都進行參數校驗。

此時,如果我們直接使用java.util.Collection下的list或者set來接收數據,參數校驗並不會生效!

我們可以使用自定義list集合來接收參數:

第1步:包裝List類型,並聲明@Valid注解;

查看代碼
public class ValidationList<E> implements List<E> {
    @Delegate
    @Valid
    public List<E> list = new ArrayList<>();

    @Override
    public String toString() {
        return list.toString();
    }
}

@Delegate注解受lombok版本限制,1.18.6以上版本可支持;

如果校驗不通過,會拋出NotReadablePropertyException,同樣可以使用統一異常進行處理。

第2步:校驗調用

格式:@Validated + ValidationList<實體類>。

比如,我們需要一次性保存多個User對象,Controller層的方法可以這么寫:

查看代碼
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {

    return Result.ok();
}

說明:后端如果想要使用list接收數據的話,必須加上注解@RequestBody;

前端必須發送json請求:application/json,也就說前端的參數格式為:JsonArray。

延伸4:自定義校驗

延伸5:編程式校驗

2022年1月20日10:52:04

8.hibernate-validator.jar詳細介紹

當spring-boot-starter-web.jar的版本為1.X時,該jar包依賴的有:hibernate-validator.jar;

hibernate-validator.jar又依賴了:validation-api.jar。

當spring-boot-starter-web.jar的版本為2.X時,該jar包沒有依賴hibernate-validator.jar,也沒有依賴:validation-api.jar;

此時只能使用注解@Validated

但是,只有它的話,並不能完成參數的校驗。

我們由開篇了解到:參數校驗,要么用hibernate校驗,要么使用java校驗。

hibernate-validator.jar包的引入有兩種方式:

方式一:org.hibernate.validator

<!--spring對參數進行校驗:hibernate validator-->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.18.Final</version>
</dependency>

方式二:org.hibernate

<!--spring對參數進行校驗:hibernate validator-->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.8.Final</version>
</dependency>

我們可以看到:這兩種方式引入的hibernate-validator.jar都對jakarta.validation-api.jar有依賴;

因此,無需額外引入依賴:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

小結:

當spring-boot-starter-web.jar的版本為1.X時,該jar包依賴的有:hibernate-validator.jar,無需額外引入;

當spring-boot-starter-web.jar的版本為2.X時,該jar包需要引入依賴:hibernate-validator.jar(以上兩種引入方式均可)。


免責聲明!

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



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