Spring?Validation參數(shù)效驗(yàn)的各種使用姿勢總結(jié)
前言
在日常的項(xiàng)目開發(fā)中,為了防止非法參數(shù)對業(yè)務(wù)造成的影響,需要對接口的參數(shù)做合法性校驗(yàn),例如在創(chuàng)建用戶時(shí),需要效驗(yàn)用戶的賬號名稱不能輸入中文與特殊字符,手機(jī)號、郵箱格式是否準(zhǔn)確。按照原始的處理邏輯需要對每個(gè)接口中的參數(shù)進(jìn)行 if/else 處理,如果這樣開發(fā),后期代碼難以維護(hù),可讀性極差。
為了解決上述問題,validation框架誕生了,代碼量大大減少,參數(shù)的效驗(yàn)不再穿插業(yè)務(wù)邏輯代碼中,代碼美觀又易于維護(hù)。
基本概念
@Valid 是 JSR303 聲明的,JSR是Java Specification Requests的縮寫,其中 JSR303 是JAVA EE 6 中的一項(xiàng)子規(guī)范,叫做 Bean Validation,為 JavaBean 驗(yàn)證定義了相應(yīng)的元數(shù)據(jù)模型和 API,需要注意的是,JSR 只是一項(xiàng)標(biāo)準(zhǔn),它規(guī)定了一些校驗(yàn)注解的規(guī)范,但沒有實(shí)現(xiàn),而 Hibernate validation 對其進(jìn)行實(shí)現(xiàn)。
Spring Validation 驗(yàn)證框架對參數(shù)的驗(yàn)證機(jī)制提供了@Validated(Spring JSR-303規(guī)范,是標(biāo)準(zhǔn)JSR-303的一個(gè)變種)。
@Valid和@Validated區(qū)別
| 區(qū)別 | @Valid | @Validated |
|---|---|---|
| 來源 | JSR-303規(guī)范 | Spring |
| 是否支持分組 | 不支持 | 支持 |
| 標(biāo)注位置 | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE,METHOD,PARAMETER |
| 嵌套校驗(yàn) | 支持 | 不支持 |
基本使用
加入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>注:從 boot-2.3.x開始,spring-boot-starter-web不再引入 spring-boot-starter-validation,所以需要額外手動引入validation依賴,而 2.3之前的版本只需要引入 web 依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<!-- <dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
<scope>compile</scope>
</dependency>-->以上兩個(gè)依賴都是可以實(shí)現(xiàn)功能的。hibernate-validator、spring-boot-starter-validation底層都引入了 jakarta.validation-api依賴。
在實(shí)際開發(fā)的過程中,請求參數(shù)的格式一般有如下幾種情況:
對象參數(shù)使用
使用對象參數(shù)接收分為兩種,一種是使用 @RequestBody注解的application/json提交,還有一種不使用 @RequestBody注解的 form-data提交。
- 使用對象接收參數(shù),在需要校驗(yàn)對象的參數(shù)加上 @NotBlank注解,message是校驗(yàn)不通過的提示信息。
@Data
public class UserReq {
@NotBlank(message = "name為必傳參數(shù)")
private String name;
@NotBlank(message = "email為必傳參數(shù)")
private String email;
}使用 @RequestBody
- Api,在需要校驗(yàn)的對象前面加 @RequestBody注解以及@Validated或者@Valid注解,如果校驗(yàn)失敗,會拋出MethodArgumentNotValidException異常。
@RestController
public class GetHeaderController {
@PostMapping("save")
public void save(@RequestBody @Validated UserReq req){}
}不使用 @RequestBody
只需要校驗(yàn)的對象前面加@Validated注解或者@Valid注解,如果校驗(yàn)失敗,會拋出BindException異常。
@PostMapping("save2")
public void save2(@Validated UserReq req){
}基本類型使用
- 其實(shí)也就是路徑傳參,在參數(shù)前面加上相對應(yīng)的校驗(yàn)注解,還必須在Controller類上加 @Validated注解。如果校驗(yàn)失敗,會拋出ConstraintViolationException異常。
@RestController
@Validated
public class GetHeaderController {
@PostMapping("get")
public void get(@NotBlank(message = "名稱 is required") String name,@NotBlank(message = "郵箱 is required") String email) throws JsonProcessingException {
}
}測試
save方法測試

save2

get方法測試

全局異常處理
通過前面的測試,我們知道如果參數(shù)校驗(yàn)失敗,三種使用場景會拋出三種異?;蛘呔妫謩e是MethodArgumentNotValidException、ConstraintViolationException、BindException異常,每種異常的響應(yīng)格式又不一致。所以在項(xiàng)目開發(fā)中,通常會使用統(tǒng)一異常處理來返回一個(gè)統(tǒng)一格式并友好的提示。
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* @RequestBody 上校驗(yàn)失敗后拋出的異常是 MethodArgumentNotValidException 異常。
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public String handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String messages = bindingResult.getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";"));
return messages;
}
/**
* 不加 @RequestBody注解,校驗(yàn)失敗拋出的則是 BindException
*/
@ExceptionHandler(value = BindException.class)
public String exceptionHandler(BindException e){
String messages = e.getBindingResult().getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";"));
return messages;
}
/**
* @RequestParam 上校驗(yàn)失敗后拋出的異常是 ConstraintViolationException
*/
@ExceptionHandler({ConstraintViolationException.class})
public String methodArgumentNotValid(ConstraintViolationException exception) {
String message = exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
return message;
}
}save方法測試

save2方法測試

get方法測試

可以發(fā)現(xiàn)它是將類中所有的屬性進(jìn)行效驗(yàn)完成之后,才拋出異常的,但其實(shí)這有點(diǎn)消耗性能,那能不能只要檢測到一個(gè)效驗(yàn)不通過的,就拋出異常呢?只需要在容器提供如下代碼:
@Configuration
public class ParamValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
//failFast:只要出現(xiàn)校驗(yàn)失敗的情況,就立即結(jié)束校驗(yàn),不再進(jìn)行后續(xù)的校驗(yàn)。
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
methodValidationPostProcessor.setValidator(validator());
return methodValidationPostProcessor;
}
}MethodValidationPostProcessor是Spring提供的來實(shí)現(xiàn)基于方法Method的JSR校驗(yàn)的核心處理器,最終會由 MethodValidationInterceptor進(jìn)行校驗(yàn)攔截。
- 測試如下:

其余類型
上面舉例使用了NotBlank注解,但肯定不只一個(gè)!我們進(jìn)入到 @NotBlank注解所在的包路徑。

哦豁,這么多呀!小杰一個(gè)一個(gè)來介紹一下作用。
| 注解 | 備注 | 適用類型 | 示例 |
|---|---|---|---|
| @AssertFalse | 被注釋的元素必須為 false,null 值是有效的。 | boolean 和 Boolean | @AssertFalse(message = "該參數(shù)必須為 false") |
| @AssertTrue | 被注釋的元素必須為 true,null 值是有效的。 | boolean 和 Boolean | @AssertTrue(message = "該參數(shù)必須為 true") |
| @DecimalMax | 被注釋的元素必須是一個(gè)數(shù)字,其值必須小于或等于指定的最大值,null 值是有效的。 | BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包裝類型 | @DecimalMax(value = "100",message = "該參數(shù)不能大于 100") |
| @DecimalMin | 被注釋的元素必須是一個(gè)數(shù)字,其值必須大于或等于指定的最小值,null 值是有效的。 | BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包裝類型 | @DecimalMax(value = "0",message = "該參數(shù)不能小于 0") |
| @Digits | 被注釋的元素必須是可接受范圍內(nèi)的數(shù)字,null 值是有效的。 | BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包裝類型 | @Digits(integer = 3,fraction = 2,message = "該參數(shù)整數(shù)位數(shù)不能超出3位,小數(shù)位數(shù)不能超過2位") |
| @Max | 被注釋的元素必須是一個(gè)數(shù)字,其值必須小于或等于指定的最大值,null 值是有效 | BigDecimal、BigInteger、byte、short、int、long以及包裝類型 | @Max(value = 200,message = "最大金額不能超過 200") |
| @Min | 被注釋的元素必須是一個(gè)數(shù)字,其值必須大于或等于指定的最小值,null 值是有效的。 | BigDecimal、BigInteger、byte、short、int、long以及包裝類型 | @Min(value = 0,message = "最小金額不能小于 0") |
| @Negative | 被注釋的元素必須是負(fù)數(shù),null 值是有效 | BigDecimal、BigInteger、byte、short、int、long、float、double 以及包裝類型 | @Negative(message = "必須是負(fù)數(shù)") |
| @NegativeOrZero | 被注釋的元素必須是負(fù)數(shù)或 0,null 值是有效的。 | BigDecimal、BigInteger、byte、short、int、long、float、double 以及包裝類型 | @NegativeOrZero(message = "必須是負(fù)數(shù)或者為0") |
| @Positive | 被注釋的元素必須是正數(shù),null 值是有效的。 | BigDecimal、BigInteger、byte、short、int、long、float、double 以及包裝類型 | @Positive(message = "必須是正數(shù)") |
| @PositiveOrZero | 被注釋的元素必須是正數(shù)或0,null 值是有效的。 | BigDecimal、BigInteger、byte、short、int、long、float、double 以及包裝類型 | @PositiveOrZero(message = "必須是正數(shù)或者為0") |
| @Future | 被注釋的元素必須是未來的日期(年月日),null 值是有效的。 | 基本所有的時(shí)間類型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant | @Future(message = "預(yù)約日期要大于當(dāng)前日期") |
| @FutureOrPresent | 被注釋的元素必須是現(xiàn)在或者未來的日期(年月日),null 值是有效的。 | 基本所有的時(shí)間類型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant | @FutureOrPresent(message = "預(yù)約日要大于當(dāng)前日期") |
| @Past | 被注釋的元素必須是過去的日期,null 值是有效的。 | 基本所有的時(shí)間類型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant | @Past(message = "出生日期要小于當(dāng)前日期") |
| @PastOrPresent | 被注釋的元素必須是過去或者現(xiàn)在的日期,null 值是有效的。 | 基本所有的時(shí)間類型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant | @PastOrPresent(message = "出生時(shí)間要小于當(dāng)前時(shí)間") |
| @NotBlank | 被注釋的元素不能為空,并且必須至少包含一個(gè)非空白字符 | CharSequence | @NotBlank(message = "name為必傳參數(shù)") |
| @NotEmpty | 被注釋的元素不能為 null 也不能為空 | CharSequence、Collection、Map、Array | @NotEmpty(message = "不能為null或者為空") |
| @NotNull | 被注釋的元素不能為null | 任意類型 | @NotNull(message = "不能為null") |
| @Null | 被注釋的元素必須為null | 任意類型 | @Null(message = "必須為null") |
| 被注釋的元素必須是格式正確的電子郵件地址,null 值是有效的。 | CharSequence | @Email(message = "email格式錯誤,請重新填寫") | |
| @Pattern | 被注釋的元素必須匹配指定的正則表達(dá)式,null 值是有效的。 | CharSequence | @Pattern(regexp = "^1[3456789]\d{9}$",message = "手機(jī)號格式不正確") |
| @Size | 被注釋的元素大小必須在指定范圍內(nèi),null 值是有效的。 | CharSequence、Collection、Map、Array | @Size(min = 5,max = 20,message = "字符長度在 5 -20 之間") |
以上注解有幾個(gè)需要注意一下,因?yàn)榻?jīng)常用到,也經(jīng)常使用錯誤
@NotNull:適用于任何類型,不能為null,但可以是 (""," ")
@NotBlank:只能用于 String,不能為null,而且調(diào)用 trim() 后,長度必須大于0,必須要有實(shí)際字符。
@NotEmpty:用于 String、Collection、Map、Array,不能為null,長度必須大于0。
使用分組效驗(yàn)
有些小伙伴說使用 @Validated校驗(yàn)的對象不能復(fù)用,這我只能說學(xué)的還不夠深入。
小杰使用 PayReq來舉例,該對象是一個(gè)公用的請求體,對接了微信、支付寶兩個(gè)渠道方,對接微信 payName參數(shù)是非必傳的,對接支付寶是必傳參數(shù)。payAmount是兩個(gè)渠道必傳參數(shù)。其實(shí)就跟平常寫新增方法、修改方法一樣的,用的是同一個(gè) ReqDTO,但是其中 id 字段新增是不用傳遞的,而修改時(shí)是必傳的。
定義 ZfbPayGroup
定義ZfbPayGroup的分組接口,繼承 Default接口。
public interface ZfbPayGroup extends Default {
}添加group
在需要區(qū)分組的字段上加 groups 參數(shù)。在本例中在 payName加了groups 參數(shù),值為 ZfbPayGroup.class,代表對組為 ZfbPayGroup的進(jìn)行payName參數(shù)校驗(yàn)。
@Data
public class PayReq {
@NotBlank(message = "支付名稱不能為空",groups = {ZfbPayGroup.class})
private String payName;
@NotNull(message = "支付金額不能為空")
private BigDecimal payAmount;
}注意:ZfbPayGroup 要繼承 Default接口,不然 payAmount字段的效驗(yàn)會對ZfbPayGroup這個(gè)組失效,payAmount默認(rèn)的組為 Default。
使用 group
創(chuàng)建兩個(gè)接口,在 zfbPaySave接口中聲明@Validated校驗(yàn)組,wxbPaySave接口正常編寫。
@PostMapping("zfbPaySave")
public void zfbPaySave(@RequestBody @Validated(value = {ZfbPayGroup.class}) PayReq req) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
System.out.println( mapper.writeValueAsString(req));
}
@PostMapping("wxbPaySave")
public void wxbPaySave(@RequestBody @Validated PayReq req) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
System.out.println( mapper.writeValueAsString(req));
}測試
- zfbPaySave:提示支付名稱不能為空

- wxbPaySave:沒被校驗(yàn)攔截。

嵌套校驗(yàn)
什么是嵌套使用呢?就是一個(gè)對象中包含另外一個(gè)對象,另外一個(gè)對象的字段也是需要進(jìn)行校驗(yàn)。示例如下:
- UserReq
@Data
public class UserReq {
@NotBlank(message = "name為必傳參數(shù)")
private String name;
private String email;
@NotNull(message = "proReq對象不能為空")
@Valid
private ProReq proReq;
}嵌套校驗(yàn)需要在效驗(yàn)的對象加上 @Valid 注解。
- ProReq
@Data
public class ProReq {
@NotBlank(message = "proName為必傳參數(shù)")
private String proName;
}- 測試

集合校驗(yàn)
在某些場景下,我們需要使用集合接收前端傳遞的參數(shù),并對集合中的每個(gè)對象都進(jìn)行參數(shù)校驗(yàn)。但是這時(shí)我們的參數(shù)校驗(yàn)并不會生效!如下寫法:
@PostMapping("save3")
public String save3(@RequestBody @Validated List<UserReq> req){
return "成功";
}
下面介紹兩種方式對集合進(jìn)行效驗(yàn)!
方式一
@Validated + @Valid兩個(gè)注解同時(shí)使用!缺點(diǎn):不能使用分組效驗(yàn)!如果該實(shí)體不需要用到分組功能,可以使用該方式!
@RestController
@Validated
public class GetHeaderController {
@PostMapping("save3")
public String save3(@RequestBody @Valid @NotEmpty(message = "該集合不能為空") List<UserReq> req){
return "成功";
}
}測試

方式二
- 自定義一個(gè)List
@Data
public class ValidList<E> implements List<E> {
// 使用該注解就不需要手動重新 List 中的方法了
@Delegate
@Valid
public List<E> list = new ArrayList<>();
}- @Delegate,為 lombok 的注解,表示該屬性的所有對象的實(shí)例方法都將被該類代理。
- 編碼如下:
@PostMapping("save4")
public String save4(@RequestBody @Validated @NotEmpty(message = "該集合不能為空") ValidList<UserReq> req){
return "成功";
}測試

自定義校驗(yàn)規(guī)則
自定義校驗(yàn)規(guī)則小杰在工作當(dāng)中用的比較少。大部分業(yè)務(wù)需求使用自帶的注解已經(jīng)夠平常開發(fā)了。當(dāng)然自定義validation規(guī)則也非常簡單。這里使用校驗(yàn)電話號碼是否合法來舉例!別抬杠,說我為什么不用 @Pattern(regexp = "^1[3456789]\\d{9}$",message = "手機(jī)號格式不正確")直接實(shí)現(xiàn)。對不起,我不想每次寫正則。
- 自定義注解 Phone,跟著內(nèi)置的注解照葫蘆畫瓢。不過 validatedBy的值要指定我們自定義的約束驗(yàn)證器!
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { PhoneValidator.class })
public @interface Phone {
String message() default "手機(jī)號碼格式異常";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}- 實(shí)現(xiàn)ConstraintValidator約束驗(yàn)證器接口
public class PhoneValidator implements ConstraintValidator<Phone,String> {
private static final String REGEX = "^1[3456789]\\\\d{9}$";
/**
*
* @param value
* @param context
* @return:返回 true 表示效驗(yàn)通過
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 不為null才進(jìn)行校驗(yàn)
if (value != null) {
return value.matches(REGEX);
}
return true;
}
}- 接下來就可以使用 @Phone注解了。
@Data
public class UserReq {
@NotBlank(message = "name為必傳參數(shù)")
private String name;
@Email(message = "email格式錯誤,請重新填寫")
@NotBlank(message = "email為必傳參數(shù)")
private String email;
@NotNull(message = "proReq對象不能為空")
@Valid
private ProReq proReq;
@Phone
@NotBlank(message = "手機(jī)號碼為必傳參數(shù)")
private String tel;
}- 測試

參考博文
總結(jié)
到此這篇關(guān)于Spring Validation參數(shù)效驗(yàn)的各種使用姿勢總結(jié)的文章就介紹到這了,更多相關(guān)Spring Validation參數(shù)效驗(yàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot整合redis修改分區(qū)的操作流程
這篇文章主要介紹了springboot整合redis修改分區(qū)的操作流程,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
Java LocalCache 本地緩存的實(shí)現(xiàn)實(shí)例
本篇文章主要介紹了Java LocalCache 本地緩存的實(shí)現(xiàn)實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-05-05
Java Validation Api使用方法實(shí)例解析
這篇文章主要介紹了Java Validation Api使用方法實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
SpringBoot中的@ControllerAdvice使用方法詳細(xì)解析
這篇文章主要介紹了SpringBoot中的@ControllerAdvice使用方法詳細(xì)解析, 加了@ControllerAdvice的類為那些聲明了@ExceptionHandler、@InitBinder或@ModelAttribute注解修飾的 方法的類而提供的專業(yè)化的@Component,以供多個(gè) Controller類所共享,需要的朋友可以參考下2024-01-01
Java SpringBoot實(shí)現(xiàn)AOP
AOP包括連接點(diǎn)(JoinPoint)、切入點(diǎn)(Pointcut)、增強(qiáng)(Advisor)、切面(Aspect)、AOP代理(AOP Proxy),具體的方法和類型下面文章會舉例說明,感興趣的小伙伴和小編一起閱讀全文吧2021-09-09
基于Maven骨架創(chuàng)建JavaWeb項(xiàng)目過程解析
這篇文章主要介紹了基于Maven骨架創(chuàng)建JavaWeb項(xiàng)目過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
maven項(xiàng)目下solr和spring的整合配置詳解
這篇文章主要介紹了maven項(xiàng)目下solr和spring的整合配置詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11
Spring Security角色繼承實(shí)現(xiàn)過程解析
這篇文章主要介紹了Spring Security角色繼承實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08

