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¶m2=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(以上兩種引入方式均可)。
寫在最后
哪位大佬如若發現文章存在紕漏之處或需要補充更多內容,歡迎留言!!!