Spring?Validation參數(shù)效驗(yàn)的各種使用姿勢(shì)總結(jié)
前言
在日常的項(xiàng)目開(kāi)發(fā)中,為了防止非法參數(shù)對(duì)業(yè)務(wù)造成的影響,需要對(duì)接口的參數(shù)做合法性校驗(yàn),例如在創(chuàng)建用戶時(shí),需要效驗(yàn)用戶的賬號(hào)名稱不能輸入中文與特殊字符,手機(jī)號(hào)、郵箱格式是否準(zhǔn)確。按照原始的處理邏輯需要對(duì)每個(gè)接口中的參數(shù)進(jìn)行 if/else 處理,如果這樣開(kāi)發(fā),后期代碼難以維護(hù),可讀性極差。
為了解決上述問(wèn)題,validation框架誕生了,代碼量大大減少,參數(shù)的效驗(yàn)不再穿插業(yè)務(wù)邏輯代碼中,代碼美觀又易于維護(hù)。
基本概念
@Valid 是 JSR303 聲明的,JSR是Java Specification Requests的縮寫(xiě),其中 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ī)范,但沒(méi)有實(shí)現(xiàn),而 Hibernate validation 對(duì)其進(jìn)行實(shí)現(xiàn)。
Spring Validation 驗(yàn)證框架對(duì)參數(shù)的驗(yàn)證機(jī)制提供了@Validated(Spring JSR-303規(guī)范,是標(biāo)準(zhǔn)JSR-303的一個(gè)變種)。
@Valid和@Validated區(qū)別
區(qū)別 | @Valid | @Validated |
---|---|---|
來(lái)源 | 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開(kāi)始,spring-boot-starter-web不再引入 spring-boot-starter-validation,所以需要額外手動(dòng)引入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í)際開(kāi)發(fā)的過(guò)程中,請(qǐng)求參數(shù)的格式一般有如下幾種情況:
對(duì)象參數(shù)使用
使用對(duì)象參數(shù)接收分為兩種,一種是使用 @RequestBody注解的application/json提交,還有一種不使用 @RequestBody注解的 form-data提交。
- 使用對(duì)象接收參數(shù),在需要校驗(yàn)對(duì)象的參數(shù)加上 @NotBlank注解,message是校驗(yàn)不通過(guò)的提示信息。
@Data public class UserReq { @NotBlank(message = "name為必傳參數(shù)") private String name; @NotBlank(message = "email為必傳參數(shù)") private String email; }
使用 @RequestBody
- Api,在需要校驗(yàn)的對(duì)象前面加 @RequestBody注解以及@Validated或者@Valid注解,如果校驗(yàn)失敗,會(huì)拋出MethodArgumentNotValidException異常。
@RestController public class GetHeaderController { @PostMapping("save") public void save(@RequestBody @Validated UserReq req){} }
不使用 @RequestBody
只需要校驗(yàn)的對(duì)象前面加@Validated注解或者@Valid注解,如果校驗(yàn)失敗,會(huì)拋出BindException異常。
@PostMapping("save2") public void save2(@Validated UserReq req){ }
基本類(lèi)型使用
- 其實(shí)也就是路徑傳參,在參數(shù)前面加上相對(duì)應(yīng)的校驗(yàn)注解,還必須在Controller類(lèi)上加 @Validated注解。如果校驗(yàn)失敗,會(huì)拋出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 { } }
測(cè)試
save方法測(cè)試
save2
get方法測(cè)試
全局異常處理
通過(guò)前面的測(cè)試,我們知道如果參數(shù)校驗(yàn)失敗,三種使用場(chǎng)景會(huì)拋出三種異?;蛘呔?,分別是MethodArgumentNotValidException、ConstraintViolationException、BindException異常,每種異常的響應(yīng)格式又不一致。所以在項(xiàng)目開(kāi)發(fā)中,通常會(huì)使用統(tǒng)一異常處理來(lái)返回一個(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方法測(cè)試
save2方法測(cè)試
get方法測(cè)試
可以發(fā)現(xiàn)它是將類(lèi)中所有的屬性進(jìn)行效驗(yàn)完成之后,才拋出異常的,但其實(shí)這有點(diǎn)消耗性能,那能不能只要檢測(cè)到一個(gè)效驗(yàn)不通過(guò)的,就拋出異常呢?只需要在容器提供如下代碼:
@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提供的來(lái)實(shí)現(xiàn)基于方法Method的JSR校驗(yàn)的核心處理器,最終會(huì)由 MethodValidationInterceptor進(jìn)行校驗(yàn)攔截。
- 測(cè)試如下:
其余類(lèi)型
上面舉例使用了NotBlank注解,但肯定不只一個(gè)!我們進(jìn)入到 @NotBlank注解所在的包路徑。
哦豁,這么多呀!小杰一個(gè)一個(gè)來(lái)介紹一下作用。
注解 | 備注 | 適用類(lèi)型 | 示例 |
---|---|---|---|
@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以及包裝類(lèi)型 | @DecimalMax(value = "100",message = "該參數(shù)不能大于 100") |
@DecimalMin | 被注釋的元素必須是一個(gè)數(shù)字,其值必須大于或等于指定的最小值,null 值是有效的。 | BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包裝類(lèi)型 | @DecimalMax(value = "0",message = "該參數(shù)不能小于 0") |
@Digits | 被注釋的元素必須是可接受范圍內(nèi)的數(shù)字,null 值是有效的。 | BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包裝類(lèi)型 | @Digits(integer = 3,fraction = 2,message = "該參數(shù)整數(shù)位數(shù)不能超出3位,小數(shù)位數(shù)不能超過(guò)2位") |
@Max | 被注釋的元素必須是一個(gè)數(shù)字,其值必須小于或等于指定的最大值,null 值是有效 | BigDecimal、BigInteger、byte、short、int、long以及包裝類(lèi)型 | @Max(value = 200,message = "最大金額不能超過(guò) 200") |
@Min | 被注釋的元素必須是一個(gè)數(shù)字,其值必須大于或等于指定的最小值,null 值是有效的。 | BigDecimal、BigInteger、byte、short、int、long以及包裝類(lèi)型 | @Min(value = 0,message = "最小金額不能小于 0") |
@Negative | 被注釋的元素必須是負(fù)數(shù),null 值是有效 | BigDecimal、BigInteger、byte、short、int、long、float、double 以及包裝類(lèi)型 | @Negative(message = "必須是負(fù)數(shù)") |
@NegativeOrZero | 被注釋的元素必須是負(fù)數(shù)或 0,null 值是有效的。 | BigDecimal、BigInteger、byte、short、int、long、float、double 以及包裝類(lèi)型 | @NegativeOrZero(message = "必須是負(fù)數(shù)或者為0") |
@Positive | 被注釋的元素必須是正數(shù),null 值是有效的。 | BigDecimal、BigInteger、byte、short、int、long、float、double 以及包裝類(lèi)型 | @Positive(message = "必須是正數(shù)") |
@PositiveOrZero | 被注釋的元素必須是正數(shù)或0,null 值是有效的。 | BigDecimal、BigInteger、byte、short、int、long、float、double 以及包裝類(lèi)型 | @PositiveOrZero(message = "必須是正數(shù)或者為0") |
@Future | 被注釋的元素必須是未來(lái)的日期(年月日),null 值是有效的。 | 基本所有的時(shí)間類(lèi)型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant | @Future(message = "預(yù)約日期要大于當(dāng)前日期") |
@FutureOrPresent | 被注釋的元素必須是現(xiàn)在或者未來(lái)的日期(年月日),null 值是有效的。 | 基本所有的時(shí)間類(lèi)型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant | @FutureOrPresent(message = "預(yù)約日要大于當(dāng)前日期") |
@Past | 被注釋的元素必須是過(guò)去的日期,null 值是有效的。 | 基本所有的時(shí)間類(lèi)型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant | @Past(message = "出生日期要小于當(dāng)前日期") |
@PastOrPresent | 被注釋的元素必須是過(guò)去或者現(xiàn)在的日期,null 值是有效的。 | 基本所有的時(shí)間類(lèi)型都支持。常用的: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 | 任意類(lèi)型 | @NotNull(message = "不能為null") |
@Null | 被注釋的元素必須為null | 任意類(lèi)型 | @Null(message = "必須為null") |
被注釋的元素必須是格式正確的電子郵件地址,null 值是有效的。 | CharSequence | @Email(message = "email格式錯(cuò)誤,請(qǐng)重新填寫(xiě)") | |
@Pattern | 被注釋的元素必須匹配指定的正則表達(dá)式,null 值是有效的。 | CharSequence | @Pattern(regexp = "^1[3456789]\d{9}$",message = "手機(jī)號(hào)格式不正確") |
@Size | 被注釋的元素大小必須在指定范圍內(nèi),null 值是有效的。 | CharSequence、Collection、Map、Array | @Size(min = 5,max = 20,message = "字符長(zhǎng)度在 5 -20 之間") |
以上注解有幾個(gè)需要注意一下,因?yàn)榻?jīng)常用到,也經(jīng)常使用錯(cuò)誤
@NotNull:適用于任何類(lèi)型,不能為null,但可以是 (""," ")
@NotBlank:只能用于 String,不能為null,而且調(diào)用 trim() 后,長(zhǎng)度必須大于0,必須要有實(shí)際字符。
@NotEmpty:用于 String、Collection、Map、Array,不能為null,長(zhǎng)度必須大于0。
使用分組效驗(yàn)
有些小伙伴說(shuō)使用 @Validated校驗(yàn)的對(duì)象不能復(fù)用,這我只能說(shuō)學(xué)的還不夠深入。
小杰使用 PayReq來(lái)舉例,該對(duì)象是一個(gè)公用的請(qǐng)求體,對(duì)接了微信、支付寶兩個(gè)渠道方,對(duì)接微信 payName參數(shù)是非必傳的,對(duì)接支付寶是必傳參數(shù)。payAmount是兩個(gè)渠道必傳參數(shù)。其實(shí)就跟平常寫(xiě)新增方法、修改方法一樣的,用的是同一個(gè) ReqDTO,但是其中 id 字段新增是不用傳遞的,而修改時(shí)是必傳的。
定義 ZfbPayGroup
定義ZfbPayGroup的分組接口,繼承 Default接口。
public interface ZfbPayGroup extends Default { }
添加group
在需要區(qū)分組的字段上加 groups 參數(shù)。在本例中在 payName加了groups 參數(shù),值為 ZfbPayGroup.class,代表對(duì)組為 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)會(huì)對(duì)ZfbPayGroup這個(gè)組失效,payAmount默認(rèn)的組為 Default。
使用 group
創(chuàng)建兩個(gè)接口,在 zfbPaySave接口中聲明@Validated校驗(yàn)組,wxbPaySave接口正常編寫(xiě)。
@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)); }
測(cè)試
- zfbPaySave:提示支付名稱不能為空
- wxbPaySave:沒(méi)被校驗(yàn)攔截。
嵌套校驗(yàn)
什么是嵌套使用呢?就是一個(gè)對(duì)象中包含另外一個(gè)對(duì)象,另外一個(gè)對(duì)象的字段也是需要進(jìn)行校驗(yàn)。示例如下:
- UserReq
@Data public class UserReq { @NotBlank(message = "name為必傳參數(shù)") private String name; private String email; @NotNull(message = "proReq對(duì)象不能為空") @Valid private ProReq proReq; }
嵌套校驗(yàn)需要在效驗(yàn)的對(duì)象加上 @Valid 注解。
- ProReq
@Data public class ProReq { @NotBlank(message = "proName為必傳參數(shù)") private String proName; }
- 測(cè)試
集合校驗(yàn)
在某些場(chǎng)景下,我們需要使用集合接收前端傳遞的參數(shù),并對(duì)集合中的每個(gè)對(duì)象都進(jìn)行參數(shù)校驗(yàn)。但是這時(shí)我們的參數(shù)校驗(yàn)并不會(huì)生效!如下寫(xiě)法:
@PostMapping("save3") public String save3(@RequestBody @Validated List<UserReq> req){ return "成功"; }
下面介紹兩種方式對(duì)集合進(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 "成功"; } }
測(cè)試
方式二
- 自定義一個(gè)List
@Data public class ValidList<E> implements List<E> { // 使用該注解就不需要手動(dòng)重新 List 中的方法了 @Delegate @Valid public List<E> list = new ArrayList<>(); }
- @Delegate,為 lombok 的注解,表示該屬性的所有對(duì)象的實(shí)例方法都將被該類(lèi)代理。
- 編碼如下:
@PostMapping("save4") public String save4(@RequestBody @Validated @NotEmpty(message = "該集合不能為空") ValidList<UserReq> req){ return "成功"; }
測(cè)試
自定義校驗(yàn)規(guī)則
自定義校驗(yàn)規(guī)則小杰在工作當(dāng)中用的比較少。大部分業(yè)務(wù)需求使用自帶的注解已經(jīng)夠平常開(kāi)發(fā)了。當(dāng)然自定義validation規(guī)則也非常簡(jiǎn)單。這里使用校驗(yàn)電話號(hào)碼是否合法來(lái)舉例!別抬杠,說(shuō)我為什么不用 @Pattern(regexp = "^1[3456789]\\d{9}$",message = "手機(jī)號(hào)格式不正確")
直接實(shí)現(xiàn)。對(duì)不起,我不想每次寫(xiě)正則。
- 自定義注解 Phone,跟著內(nèi)置的注解照葫蘆畫(huà)瓢。不過(guò) 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ī)號(hào)碼格式異常"; 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)通過(guò) */ @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 不為null才進(jìn)行校驗(yàn) if (value != null) { return value.matches(REGEX); } return true; } }
- 接下來(lái)就可以使用 @Phone注解了。
@Data public class UserReq { @NotBlank(message = "name為必傳參數(shù)") private String name; @Email(message = "email格式錯(cuò)誤,請(qǐng)重新填寫(xiě)") @NotBlank(message = "email為必傳參數(shù)") private String email; @NotNull(message = "proReq對(duì)象不能為空") @Valid private ProReq proReq; @Phone @NotBlank(message = "手機(jī)號(hào)碼為必傳參數(shù)") private String tel; }
- 測(cè)試
參考博文
總結(jié)
到此這篇關(guān)于Spring Validation參數(shù)效驗(yàn)的各種使用姿勢(shì)總結(jié)的文章就介紹到這了,更多相關(guān)Spring Validation參數(shù)效驗(yàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot整合redis修改分區(qū)的操作流程
這篇文章主要介紹了springboot整合redis修改分區(qū)的操作流程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07Java LocalCache 本地緩存的實(shí)現(xiàn)實(shí)例
本篇文章主要介紹了Java LocalCache 本地緩存的實(shí)現(xiàn)實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-05-05Java Validation Api使用方法實(shí)例解析
這篇文章主要介紹了Java Validation Api使用方法實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09SpringBoot中的@ControllerAdvice使用方法詳細(xì)解析
這篇文章主要介紹了SpringBoot中的@ControllerAdvice使用方法詳細(xì)解析, 加了@ControllerAdvice的類(lèi)為那些聲明了@ExceptionHandler、@InitBinder或@ModelAttribute注解修飾的 方法的類(lèi)而提供的專(zhuān)業(yè)化的@Component,以供多個(gè) Controller類(lèi)所共享,需要的朋友可以參考下2024-01-01Java SpringBoot實(shí)現(xiàn)AOP
AOP包括連接點(diǎn)(JoinPoint)、切入點(diǎn)(Pointcut)、增強(qiáng)(Advisor)、切面(Aspect)、AOP代理(AOP Proxy),具體的方法和類(lèi)型下面文章會(huì)舉例說(shuō)明,感興趣的小伙伴和小編一起閱讀全文吧2021-09-09基于Maven骨架創(chuàng)建JavaWeb項(xiàng)目過(guò)程解析
這篇文章主要介紹了基于Maven骨架創(chuàng)建JavaWeb項(xiàng)目過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08maven項(xiàng)目下solr和spring的整合配置詳解
這篇文章主要介紹了maven項(xiàng)目下solr和spring的整合配置詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11Spring Security角色繼承實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了Spring Security角色繼承實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08