springboot---检验请求参数
文章目錄
- 0. 背景
- 1. 定制代碼檢驗
- 2. 通用標準校驗
- 2.1. 發展歷程
- 2.1.1. JSR303
- 2.1.2. JSR349
- 2.1.3. JSR380
- 2.1.4. 發展總述
- 2.1.5. 引入依賴
- 2.1.6. 常用注解
- 2.2. @Valid 詳解
- 2.3. @Validated 詳解
- 2.4. 嵌套驗證
- 2.5. 自定義校驗
- 2.6. 類級別驗證(多字段聯合驗證)
- 2.7. Dubbo RPC參數校驗
- 2.7.1. ValidationFilter & JValidator
- 2.7.2. @MethodValidated注解
- 2.7.3. 簡單示例
- 2.8. springboot校驗
- 2.8.1. 代碼示例
- 2.8.2. 注意事項
- 2.8.3. 注解詳解
0. 背景
服務端在向外提供接口服務時,不管是對前端提供HTTP接口,還是面向內部其他服務端提供的RPC接口,常常會面對這樣一個問題,就是如何優雅的解決各種接口參數校驗問題?
早期大家在做面向前端提供的HTTP接口時,對參數的校驗可能都會經歷這幾個階段:
- 每個接口每個參數都寫定制校驗代碼
- 提煉公共校驗邏輯
- 自定義切面進行校驗
- 通用標準的校驗邏輯。
其中最常見的就是定制檢驗代碼和通用標準的校驗邏輯,前者是利用大量的if/else語句,后者指的就是基于JSR303的Java Bean Validation,其中官方指定的具體實現就是 Hibernate Validator,在Web項目中結合Spring可以做到很優雅的去進行參數校驗。
1. 定制代碼檢驗
大量的 if / else 使代碼非常臃腫
/*** 員工對象* * @author sunnyzyq* @since 2019/12/13*/@Data public class Employee {/**姓名 */private String name;/** 年齡 */private Integer age;/**郵箱地址*/private String email;/**手機號*/private String phone; } @Controller public class TestController {@RequestMapping("/add")@ResponseBodypublic String add(Employee employee) {String name = employee.getName0;if (name == null || name.trim().length == 0){return"員工名稱不能為空"}if (name.trim().length0 > 10){return"員工名稱不能超過10個字符"}return "新增員工成功";} }以上代碼肯定是可以正常的校驗員工名稱收為空以及長度是否符合的,但是隨著檢驗條件的增多,我們會需要越來越多的代碼,比如我們規定年齡也是必填項,且范圍在1到100歲,那么此時,我們需要增加對應判定代碼如下:
@Controller public class TestController {@RequestMapping("/add")@ResponseBodypublic String add(Employee employee) {String name = employee.getName0;if (name == null || name.trim().length == 0){return"員工名稱不能為空"}if (name.trim().length0 > 10){return"員工名稱不能超過10個字符"}// 新增校驗條件Integer age = employee.getAge();if(age == null){return "年齡不能為空";}if(age < 1 || age > 10){return "年齡不能大于10歲或者小于1歲";}return "新增員工成功";} }定制檢驗代碼現在就會出現一種情況,每校驗一個字段就需要增加6行的代碼,此時只校驗了兩個字段,要是有20個字段,豈不是要寫 100 多行代碼?通常來說,當一個方法中的無效業務代碼量過多時,往往代碼設計有問題,當然這不是我們所想看到都結果。
2. 通用標準校驗
其實我真的覺得現在作為一個程序員是幸運的,因為有很多的輪子已經造好了,同時,我覺得現在作為程序員是不幸運的,因為很多輪子已經造好了…
沒錯,java早就幫我們準備好了更方便的參數校驗方式。-- Bean Validation
2.1. 發展歷程
Bean Validation技術隸屬于Java EE規范,期間有多個JSR(Java Specification Requests)支持,目前共有三次相關JSR標準發布:
- JSR303 最早(2009)
- JSR349
- JSR380
2.1.1. JSR303
JSR303提出很早(2009年),它為 基于注解的 JavaBean驗證定義元數據模型和API。JSR-303主要是對JavaBean進行驗證,如方法級別(方法參數/返回值)、依賴注入等的驗證是沒有指定的。
作為開山之作,它規定了Java數據校驗的模型和API,這就是Java Bean Validation 1.0版本。
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>1.0.0.GA</version> </dependency>該版本提供了13個現在常見的校驗注解:
| @AssertFalse | bool | 元素必須是false | 否 |
| @AssertTrue | bool | 元素必須是true | 否 |
| @DecimalMax | Number的子類型(浮點數除外)以及String | 元素必須是一個數字,且值必須<=最大值 | 否 |
| @DecimalMin | 同上 | 元素必須是一個數字,且值必須>=最小值 | 否 |
| @Max | 同上 | 同上 | 否 |
| @Min | 同上 | 同上 | 否 |
| @Digits | 同上 | 元素構成是否合法(整數部分和小數部分) | 否 |
| @Future | 時間類型(包括JSR310) | 元素必須為一個將來(不包含相等)的日期(比較精確到毫秒) | 否 |
| @Past | 同上 | 元素必須為一個過去(不包含相等)的日期(比較精確到毫秒) | 否 |
| @NotNull | any | 元素不能為null | 是 |
| @Null | any | 元素必須為null | 是 |
| @Pattern | String | 元素需符合指定的正則表達式 | 否 |
| @Size | String/Collection/Map/Array | 元素大小需在指定范圍中 | 否 |
它的官方參考實現如下:
2.1.2. JSR349
該規范2013年完成伴隨java EE 7一起發布,就是我們比較熟悉的Bean Validation1.1。
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>1.1.0.Final</version> </dependency>相較于1.0版本,它主要的改進/優化有如下幾點:
- 標準化了Java平臺的約束定義、描述、和驗證
- 支持方法級驗證(入參或返回值的驗證)
- Bean驗證組件的依賴注入
- 與上下文和DI依賴注入集成
- 使用EL表達式的錯誤消息插值,讓錯誤消息動態化起來(強依賴于ElManager)
- 跨參數驗證。比如密碼和驗證密碼必須相同
- 注解個數上,相較于1.0版本并沒新增~
它的官方參考實現如下:
注:當你導入了hibernate-validator后,無需再顯示導入javax.validation,反之亦同
2.1.3. JSR380
當下主流版本,也就是Java Bean Validation 2.0,它完成于2017年8月,在2019年8月發布,屬于Java EE 8的一部分。它的官方參考實現只有唯一的Hibernate validator了:
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version> </dependency>
此版本具有很重要的現實意義,主要有以下變化:
- 支持通過注解泛型類型來驗證容器內的元素,如:List<@Positive Integer> positiveNumbers,即容器內元素須為正數
- 更靈活的集合類型級聯驗證;例如,現在可以驗證映射的鍵和值,如:Map<@Valid CustomerType, @Valid Customer> customersByType
- 支持java.util.Optional類型,并且支持通過插入額外的值提取器來支持自定義容器類型
- 讓@Past/@Future注解支持注解在JSR310時間上
- 新增內建的注解類型(共9個):@Email, @NotEmpty, @NotBlank, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent和@FutureOrPresent
- 所有內置的約束現在都支持重復標記
- JDK最低版本要求:JDK 8
新增注解
| String | 元素必須是電子郵箱地址 | 否 | |
| @NotEmpty | 容器類型 | 集合的Size必須大于0 | 是 |
| @NotBlank | String | 字符串必須包含至少一個非空白的字符 | 是 |
| @Positive | Positive | 元素必須必須為正數(不包括0) | 否 |
| @PositiveOrZero | 同上 | 同上(包括0) | 否 |
| @Negative | 同上 | 元素必須必須為負數(不包括0) | 否 |
| @NegativeOrZero | 同上 | 同上(包括0) | 否 |
| @PastOrPresent | 時間類型 | 在@Past基礎上包括相等 | 否 |
| @FutureOrPresent | 時間類型 | 在@Futrue基礎上包括相等 | 否 |
2.1.4. 發展總述
以上就是java中參數校驗輪子的發展歷程。
Validation 從1.1版本起就需要El管理器支持用于錯誤消息動態插值,因此需要自己額外導入EL的實現。EL也屬于Java EE標準技術,可認為是一種表達式語言工具,它并不僅僅是只能用于Web,可以用于任意地方(類比Spring的SpEL)
<dependency><groupId>javax.el</groupId><artifactId>javax.el-api</artifactId><version>3.0.0</version> </dependency>以上是EL技術規范的API,Expression Language 3.0表達式語言規范于2013-4-29發布,Tomcat 8、Jetty 9、GlasshFish 4都已經支持實現了EL 3.0,如果你是web環境,就不用自己手動導入了。
簡單來說以上JSR提供了一套Bean校驗規范的API,維護在包javax.validation.constraints下。該規范使用屬性或者方法參數或者類上的一套簡潔易用的注解來做參數校驗。開發者在開發過程中,僅需在需要校驗的地方加上形如@NotNull, @NotEmpty , @Email的注解,就可以將參數校驗的重任委托給一些第三方校驗框架來處理。
2.1.5. 引入依賴
目前在最常用的springboot 項目中, Spring Boot 2.3.0 之前的 spring-boot-starter-web 依賴中已經自帶了,可以直接使用。但是如果是 2.3.0以后的Spring Boot項目則需要手動引入依賴包
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version> </dependency> <dependency><groupId>jakarta.validation</groupId><artifactId>jakarta.validation-api</artifactId><version>2.0.2</version> </dependency>上面兩個jar隨便引入哪個都可以,就算是都引入了也沒有關系,因為他們的api完全一致。
Hibernate Validator 官網說明:Hibernate Validator
2.1.6. 常用注解
在Spring MVC中,只需要使用@Valid注解標注在方法參數商,Spring MVC即可對參數對象進行校驗,校驗結果會放在BindingResult對象中。除了@Valid 還有 @Validated注解。@validated是對@Valid 進行了二次封裝,在使用上并沒有區別,但在分組、注解位置、嵌套驗證等功能上有所不同:
| 來源 | 是Hibernate validation 的 校驗注解 | 是 Spring Validator 的校驗注解,是 Hibernate validation 基礎上的增加版 |
| 注解位置 | 構造函數、方法、方法參數、成員屬性 | 類、方法、方法參數 |
| 嵌套驗證 | 用在級聯對象的成員屬性上 | 不支持 |
| 分組 | 不支持 | 提供分組功能,可以在入參驗證時,根據不同的分組采用不同的驗證機制 |
| 校驗結果 | 校驗時需要用 BindingResult 來做一個校驗結果接收。當校驗不通過的時候,如果手動不return ,則并不會阻止程序的執行 | 校驗時無需接收校驗結果,當校驗不通過時,程序會拋出400異常,阻止方法中的代碼執行,這時需要再寫一個全局校驗異常捕獲處理類,然后返回校驗提示。(配合@RestControllerAdvice非常好用) |
總體來說,在你不需要嵌套驗證的情況下,@Validated 使用起來要比 @Valid 方便一些,它可以幫我們節省一定的代碼,并且使得方法看上去更加的簡潔,同時還有更友好的分組功能。
2.2. @Valid 詳解
成員屬性上增加注解
package com.zyq.beans;import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull;import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Range;/*** 員工對象* * @author sunnyzyq* @since 2019/12/13*/ public class Employee {/** 姓名 */@NotBlank(message = "請輸入名稱")@Length(message = "名稱不能超過個 {max} 字符", max = 10)public String name;/** 年齡 */@NotNull(message = "請輸入年齡")@Range(message = "年齡范圍為 {min} 到 {max} 之間", min = 1, max = 100)public Integer age; }然后再 Controller 對應方法上,對這個員工標上 @Valid 注解,表示我們對這個對象屬性需要進行驗證,同時使用@Valid 注解時就必須手動處理校驗結果。做法也很簡單,在參數直接添加一個BindingResult,具體如下:
@Controller public class TestController {@RequestMapping("/add")@ResponseBodypublic String add(@Valid Employee employee, BindingResult bindingResult){// 所有字段是否驗證通過,true-數據有誤,false-數據無誤if (bindingResult.hasErrors()) [// 驗證有誤情況,返回第一條錯誤信息到前端return bindingResult.getAllErrors().get(0).getDefaultMessage():}// TODO 保存到數據庫return"新增員工成功"} }可以看到,相比于手動校驗,效果相同,代碼卻簡潔了很多。
2.3. @Validated 詳解
在使用 @Valid 進行驗證的時候,需要用一個對象去接收校驗結果,最后根據校驗結果判斷,此時如果去掉手動接收參數
@Controller public class TestController {@RequestMapping("/add")@ResponseBodypublic String add(@Valid Employee employee, BindingResult bindingResult){// 所有字段是否驗證通過,true-數據有誤,false-數據無誤/*if (bindingResult.hasErrors()) [// 驗證有誤情況,返回第一條錯誤信息到前端return bindingResult.getAllErrors().get(0).getDefaultMessage():}*/// TODO 保存到數據庫return"新增員工成功"} }
可以看到我們的程序繼續往后面去執行完成了。
也就說@Valid并不會阻擋程序的執行,只是將校驗結果進行了一個存儲,使用者需要進入校驗結果集合中進行手動處理。
相比之下,@Validated更加人性,會自動阻塞程序運行,且不需要手動獲取校驗結果
@Controller public class TestController {@RequestMapping("/add")@ResponseBodypublic String add(@Validated Employee employee){// TODO 保存到數據庫return"新增員工成功"} }
在實際開發的過程中,我們肯定不能講異常直接展示給用戶,而是給能看懂的提示。于是,我們不妨可以通過捕獲異常的方式,將該異常進行捕獲。
首先我們創建一個校驗異常捕獲類 ValidExceptionHandler ,然后打上 @RestControllerAdvice 注解,該注解表示他會去抓所有 @Controller 標記類的異常,并在異常處理后返回以 JSON 或字符串的格式響應前端。
package com.zyq.config;import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice public class ValidExceptionHandler {@ExceptionHandler(BindException.class)public String validExceptionHandler(BindException exception) {return exception.getAllErrors().get(0).getDefaultMessage();} }那么,我們現在重啟程序,然后重新請求,就可以發現界面已經不報400錯誤了,而是直接提示了我們的錯誤信息。
2.4. 嵌套驗證
比如我們現在有個實體叫做Item,Item帶有很多屬性,屬性里面有:pid、vid、pidName和vidName
public class Item {@NotNull(message = "id不能為空")@Min(value = 1, message = "id必須為正整數")private Long id;@NotNull(message = "props不能為空")@Size(min = 1, message = "至少要有一個屬性")private List<Prop> props; } public class Prop {@NotNull(message = "pid不能為空")@Min(value = 1, message = "pid必須為正整數")private Long pid;@NotNull(message = "vid不能為空")@Min(value = 1, message = "vid必須為正整數")private Long vid;@NotBlank(message = "pidName不能為空")private String pidName;@NotBlank(message = "vidName不能為空")private String vidName; }正常情況,Spring Validation框架只會對Item的id和props做非空和數量驗證,不會對props字段里的Prop實體進行字段驗證。
如何進行嵌套校驗?
為了能夠進行嵌套驗證,必須手動在Item實體的props字段上明確指出這個字段里面的實體也要進行驗證。由于@Validated不能用在成員屬性(字段)上,但是@Valid能加在成員屬性(字段)上,而且@Valid類注解上也說明了它支持嵌套驗證功能,那么我們能夠推斷出:@Valid加在方法參數時并不能夠自動進行嵌套驗證,而是用在需要嵌套驗證類的相應字段上,來配合方法參數上@Validated或@Valid來進行嵌套驗證。
修改Item類如下所示:
public class Item {@NotNull(message = "id不能為空")@Min(value = 1, message = "id必須為正整數")private Long id;@Valid // 嵌套驗證必須用@Valid@NotNull(message = "props不能為空")@Size(min = 1, message = "props至少要有一個自定義屬性")private List<Prop> props; }除了上面常見的@NotNull、@Min、@NotBlank和@Size等校驗注解我們還可以自定義校驗注解~
2.5. 自定義校驗
舉例說明自定義注解的實現:需要一個自定義注解來校驗入參name不能和已存在name重名
自定義注解
@Target({ElementType.FIELD,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = UniqueConstraintValidator.class) public @interface UniqueConstraint {//下面三個屬性是必須有的屬性String message();Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {}; }新建一個UniqueConstraintValidator類來驗證注解
//自定義校驗注解 的 校驗邏輯 //不需要加注解@Component,因為實現了ConstraintValidator接口自動會注冊為spring bean public class UniqueConstraintValidator implements ConstraintValidator<UniqueConstraint,Object> {@Autowiredprivate UserService userService;@Overridepublic void initialize(UniqueConstraint uniqueConstraint) {System.out.println("my validator init");}//Object為校驗的字段類型//返回true則校驗成功//o為校驗字段的值,constraintValidatorContext為校驗注解里的屬性值@Overridepublic boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {String username = (String) o;TbUser user = userService.findByUsername(username);return user==null?true:false;} }- UniqueConstraintValidator類必須實現ConstraintValidator接口initialize方法以及驗證方法isValid
- 具體的校驗邏輯在isValid方法中做校驗
- 使用的時候在需要的字段上標記該注解即可:
2.6. 類級別驗證(多字段聯合驗證)
約束也可以放在類級別上(也就說注解標注在類上)。在這種情況下,驗證的主體不是單個屬性,而是整個對象。如果驗證依賴于對象的幾個屬性之間的相關性,那么類級別約束就能搞定。
這個需求場景在平時開發中也非常常見,比如此處我舉個簡單場景案例:修改用戶名密碼,需要輸入兩遍新密碼:newPass,newPassAgain,要求newPass.equals(newPassAgain)。如果用事務腳本來實現這個驗證規則,那么你的代碼里肯定穿插著類似這樣的代碼:
if (!this.newPass.equals(this.newPassAgain)){throw new RuntimeException("..."); }雖然這么做也能達到校驗的效果,但很明顯這不夠優雅。
但是基于Hibernate-Validator內置的@ScriptAssert,可以很容易的處理這種case:
@ScriptAssert是內置就提供的,因此使用起來非常的方便和通用。但缺點也是因為過于通用,因此語義上不夠明顯,需要閱讀腳本才知。推薦少量(非重復使用)、邏輯較為簡單時使用,更為輕巧
2.7. Dubbo RPC參數校驗
Dubbo作為國產優秀的開源RPC框架,同樣支持注解方式校驗參數!同時也是基于JSR303去實現的,我們來看下具體是怎么實現的。
2.7.1. ValidationFilter & JValidator
ValidationFilter通過在實際方法調用之前,根據調用者url配置的validation屬性值找到正確的{Validator}實例來調用驗證。
關于ValidationFilter是如何被調用的是dubbo spi的內容這里就不提了,但是要想其生效需要在consumer或者provider端配置一下:
consumer:
@DubboReference(validation = "true")private DemoService demoService;或provider:
@DubboService(validation = "true") public class DemoServiceImpl implements DemoService {注:如果在消費端開啟參數校驗,不通過就不會向服務端發起rpc調用,但是要自己處理校驗異常ConstraintViolationException
javax.validation.ValidationException: Failed to validate service: com.xxx.demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='用戶名不能為空', propertyPath=name, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶名不能為空'}, ConstraintViolationImpl{interpolatedMessage='用戶手機號不能為空', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶手機號不能為空'}, ConstraintViolationImpl{interpolatedMessage='用戶標識不能為空', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶標識不能為空'}] javax.validation.ValidationException: Failed to validate service: com.xxx.demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='用戶名不能為空', propertyPath=name, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶名不能為空'}, ConstraintViolationImpl{interpolatedMessage='用戶手機號不能為空', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶手機號不能為空'}, ConstraintViolationImpl{interpolatedMessage='用戶標識不能為空', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用戶標識不能為空'}]at org.apache.dubbo.validation.filter.ValidationFilter.invoke(ValidationFilter.java:96)at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:83)....at org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:175)at org.apache.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:51)at org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:57)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)從異常堆棧內容我們可以看出這個異常信息返回是由ValidationFilter拋出的,從名字我們可以猜到這個是采用Dubbo的Filter擴展機制的一個內置實現,當我們對Dubbo服務接口啟用參數校驗時(即前文Dubbo服務配置中的validation=“true”),該Filter就會真正起作用,我們來看下其中的關鍵實現邏輯:
@Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {if (validation != null && !invocation.getMethodName().startsWith("$")&& ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), VALIDATION_KEY))) {try {Validator validator = validation.getValidator(invoker.getUrl());if (validator != null) {// 注1validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());}} catch (RpcException e) {throw e;} catch (ValidationException e) {// 注2return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(e.getMessage()), invocation);} catch (Throwable t) {return AsyncRpcResult.newDefaultAsyncResult(t, invocation);}}return invoker.invoke(invocation); }從前文的異常堆棧信息我們可以知道異常信息是由上述代碼「注2」處所產生,這邊是因為捕獲了ValidationException,通過走讀代碼或者調試可以得知,該異常是由「注1」處valiator.validate方法所產生。
而Validator接口在Dubbo框架中實現只有JValidator,這個通過idea工具顯示Validator所有實現的UML類圖可以看出(如下圖所示),當然調試代碼也可以很輕松定位到。
既然定位到JValidator了,我們就繼續看下它里面validate方法的具體實現,關鍵代碼如下所示:
@Override public void validate(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception {List<Class<?>> groups = new ArrayList<>();Class<?> methodClass = methodClass(methodName);if (methodClass != null) {groups.add(methodClass);}Set<ConstraintViolation<?>> violations = new HashSet<>();Method method = clazz.getMethod(methodName, parameterTypes);Class<?>[] methodClasses;if (method.isAnnotationPresent(MethodValidated.class)){methodClasses = method.getAnnotation(MethodValidated.class).value();groups.addAll(Arrays.asList(methodClasses));}groups.add(0, Default.class);groups.add(1, clazz);Class<?>[] classgroups = groups.toArray(new Class[groups.size()]);Object parameterBean = getMethodParameterBean(clazz, method, arguments);if (parameterBean != null) {// 注1violations.addAll(validator.validate(parameterBean, classgroups ));}for (Object arg : arguments) {// 注2validate(violations, arg, classgroups);}if (!violations.isEmpty()) {// 注3logger.error("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations);throw new ConstraintViolationException("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations, violations);} }從上述代碼中可以看出當「注1」和注「2」兩處代碼進行參數校驗時所得到的「違反約束」的信息都被加入到violations集合中,而在「注3」處檢查到「違反約束」不為空時,就會拋出包含「違反約束」信息的ConstraintViolationException,該異常繼承自ValidationException,這樣也就會被ValidationFilter中方法所捕獲,進而向調用方返回相關異常信息。
2.7.2. @MethodValidated注解
在JValidator的validate方法中可以看到有一個@MethodValidated注解
點開查看它的注釋
大意上能明白這注解是標記在方法上支持分組校驗的!
2.7.3. 簡單示例
服務方代碼:
dubbo client interface:
public interface DemoService {String sayHello(String name);@MethodValidated({TestGroup.class})String sayGoodBye(Content content);default CompletableFuture<String> sayHelloAsync(String name) {return CompletableFuture.completedFuture(sayHello(name));}}方法入參Content:
public class Content implements Serializable {@NotNull(message = "name不能為空",groups = {TestGroup.class})private String name;public String getName() {return name;}public void setName(String name) {this.name = name;} }消費方代碼:
@Component("demoServiceComponent") public class DemoServiceComponent implements DemoService {@DubboReference(validation = "true")private DemoService demoService;@Overridepublic String sayHello(String name) {return demoService.sayHello(name);}@Overridepublic String sayGoodBye(Content content) {return demoService.sayGoodBye(content);}@Overridepublic CompletableFuture<String> sayHelloAsync(String name) {return null;} }注:沒有設置groups的校驗注解也會進行校驗,作為默認分組(像kafka一樣分配一個默認組)。最后捕獲下拋出的ConstraintViolationException以結構化的json格式返回給調用方"校驗錯誤信息"
2.8. springboot校驗
2.8.1. 代碼示例
參數校驗
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @AllArgsConstructor @NoArgsConstructor public class WangDianTongQO implements Serializable {private static final long serialVersionUID = 6427241139887596421L;/*** 簽名*/@NotBlank(message = "簽名不能為空", groups = {WangDianTongQO.PosterMsg.class,WangDianTongQO.ShopBindMsg.class,WangDianTongQO.SyncShopMsg.class})private String signature;/*** 網店通系統的門店編號*/@NotNull(message = "門店編號不能為空",groups = {WangDianTongQO.ShopBindMsg.class})private Long shopNum;/*** 海報碼信息*/@NotBlank(message = "海報碼不能為空",groups = {WangDianTongQO.PosterMsg.class})private String posterCode;/*** 門店更新信息*/@Valid@NotNull(message = "data不能為空", groups = {WangDianTongQO.SyncShopMsg.class})@Size(min = 1, message = "data至少有1條信息", groups = {WangDianTongQO.SyncShopMsg.class})@Size(max = 500, message = "data至多有500條信息", groups = {WangDianTongQO.SyncShopMsg.class})private List<WdtShopBindMsgSyncRO> data;/*** 店主綁定信息查詢分組*/public interface ShopBindMsg extends Default {}/*** 海報關聯信息查詢分組*/public interface PosterMsg extends Default {}/*** 門店綁定信息同步*/public interface SyncShopMsg extends Default {} } @Data @NoArgsConstructor @AllArgsConstructor public class WdtShopBindMsgSyncRO implements Serializable {private static final long serialVersionUID = -264965558542189902L;@NotBlank(message = "門店編號不能為空")private String shopId;@NotBlank(message = "門店名稱不能為空")private String shopName;private String shopKeeperName;private String shopKeeperPhone;private String shopCountry;private String shopProvince;private String shopCity;private String shopDistrict;private String shopStreet; }異常捕獲:
@RestControllerAdvice public class ValidExceptionConfig {@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})public ResponseBean validExceptionHandler(Exception e) {String msg = null;if (e instanceof MethodArgumentNotValidException){MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();}if (e instanceof ConstraintViolationException){ConstraintViolationException ex = (ConstraintViolationException) e;msg = ex.getConstraintViolations().iterator().next().getMessage();}if (e instanceof BindException){BindException ex = (BindException) e;msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();}if (msg == null){return new ResponseBean<>("500", JmlConstant.Common.ERROR_MSG, null);}return new ResponseBean<>("401", msg, null);} }參數使用:
@RefreshScope @RestController @RequestMapping(value = "/docking") @Api(value = "docking", tags = "第三方對接接口") public class DockingController extends ABaseController {@PostMapping(value = "/queryBindMsg", produces = "application/json;charset=utf-8")@ApiOperation("店主綁定信息查詢|利店通")@SentinelResource(value = "queryBindMsg")public ResponseBean queryBindMsg(@RequestBody @Validated({WangDianTongQO.ShopBindMsg.class}) WangDianTongQO req) {try {logger.info("店主綁定信息查詢:{}",req.toString());if (req.getSignature() == null || req.getShopNum() == null){return new ResponseBean<>("400", "參數有誤",null);}if (!RSAUtil.verify(Sha1Util.getSha1(String.valueOf(req.getShopNum())).getBytes(), JmlConstant.RSAKey.LIDIANTONG_PUBLIC_KEY, req.getSignature())) {return new ResponseBean<>("401", "驗簽錯誤", null);}return new ResponseBean<>(ResponseCodeEnum.SUCCESS_CODE, iJmlShopService.queryBindMsg(req.getShopNum()));} catch (Exception e) {logger.info("店主綁定信息查詢失敗",e);return new ResponseBean<>(ResponseCodeEnum.OPERATION_FAILURE, "操作失敗");}}@PostMapping(value = "/queryPosterMsg",produces = "application/json;charset=utf-8")@ApiOperation("海報信息查詢接口")@SentinelResource(value = "queryPosterMsg")public ResponseBean queryPosterMsg(@RequestBody @Validated({WangDianTongQO.PosterMsg.class}) WangDianTongQO req){logger.info("海報信息查詢:{}",req.toString());if (!RSAUtil.verify(Sha1Util.getSha1(req.getPosterCode()).getBytes(), JmlConstant.RSAKey.LIDIANTONG_PUBLIC_KEY, req.getSignature())) {return new ResponseBean<>("401", "驗簽錯誤", null);}return new ResponseBean<>(ResponseCodeEnum.SUCCESS_CODE, iJmlPcPosteractivityService.WdtQuery(req.getPosterCode()));}@PostMapping(value = "/updateBindMsg",produces = "application/json;charset=utf-8")@ApiOperation("門店信息更新接口")@SentinelResource(value = "updateBindMsg")public ResponseBean updateBindMsg(@RequestBody @Validated({WangDianTongQO.SyncShopMsg.class}) WangDianTongQO req){if (!RSAUtil.verify(Sha1Util.getSha1(req.getData().toString()).getBytes(), JmlConstant.RSAKey.LIDIANTONG_PUBLIC_KEY, req.getSignature())) {return new ResponseBean<>("401", "驗簽錯誤", null);}return new ResponseBean<>(ResponseCodeEnum.SUCCESS_CODE, iJmlShopService.SyncShopMsg(req));} }2.8.2. 注意事項
@valid注解 可以用在成員屬性上,決定了它可以嵌套校驗的功能
@valid只能?在controller。@Validated可以?在其他被spring管理的類上
@Validated不指定分組時 只會匹配未分組的注解;分組后則會匹配組內注解+未分組的注解。它的機制為未分組也是一個組,也就是說Validated 始終有組的概念,即使你沒有顯示指定
校驗bean時:
- @Valid 和 @Validated 都是直接修飾方法參數就可以生效,拋出org.springframework.web.bind.MethodArgumentNotValidException異常,都會阻斷方法執行,@RestControllerAdvice只是優化方法返回值而已。
- @Valid 和 @Validated 放到Controller類和參數類都不起作用, Controller類的方法上也不起作用。
校驗字段時:
- @Validated 加在類上,配合參數校驗注解可以生效,其余情況 @Validated在 方法上、 參數上均不生效。
- @Valid注解無法校驗非bean類型參數。
參數校驗未通過Spring會拋出三種類型的異常:
- 當對@RequestBody需要的參數進行校驗時會出現org.springframework.web.bind.MethodArgumentNotValidException
public String test1(@Validated @RequestBody ValidEntity validEntity){} - 當直接校驗具體參數時會出現javax.validation.ConstraintViolationException,也屬于ValidationException異常
public String test2(@Email Stringemail){} - 當直接校驗對象時會出現org.springframework.validation.BindException
public String test3(@Validated ValidEntity validEntity){}
最后,一定要注意注解的修飾類型,類型不符時會直接報錯的,并且提示很不友好。第6條的報錯只有是進入校驗邏輯之后才會出現,但是如果用錯了注解,根本不會進入校驗邏輯,而且也沒有異常輸出,十分不友好!
2.8.3. 注解詳解
Validation基本注解:
| AssertFalse AssertTrue | 被標記的元素值必須為false/true | boolean、Boolean | @AssertTrue(message = “xxx必須為true”) | |
| DecimalMax DecimalMin | 被標記的元素必須小/大于或等于指定的值 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | @DecimalMax(value = “30000”) | |
| Digits | 被標記的元素整數位數和小數位數必須小于或等于指定的值 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | 1)識別不了字段值為null的場景2)使用在不支持的Java類型,程序會拋出javax.validation.UnexpectedTypeException異常 | @Digits(integer = 6, fraction = 2) |
| 被標記的元素必須是郵箱地址 | String | |||
| Future | 被標記的元素必須為當前時間之后 | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate等只要是時間類即可 | ||
| FutureOrPresent | 被標記的元素必須為當前時間或之后 | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate等只要是時間類即可 | ||
| Past | 被標記的元素必須為當前時間之前 | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate等只要是時間類即可 | ||
| PastOrPresent | 被標記的元素必須為當前時間或之前 | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate等只要是時間類即可 | ||
| Max | 被標記的元素必須小于或等于指定的值 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | 不支持double、float | @Max(value = 10000) |
| Min | 被標記的元素必須大于或等于指定的值 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | 不支持double、float | @Min(value = 10000) |
| Negative | 被標記的元素必須是負數 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、double、Double | ||
| NegativeOrZero | 被標記的元素必須是負數或0 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、double、Double | ||
| Positive | 被標記的元素必須是正數 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、double、Double | ||
| PositiveOrZero | 被標記的元素必須是正數或0 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、double、Double | ||
| Null | 被標記的元素必須為null | Object | ||
| NotNull | 被標記的元素必須不為null | Object | ||
| NotEmpty | 被標記的元素不為null,且不為空(字符串的話,就是length要大于0,集合的話,就是size要大于0) | String、Collection、Map、Array | 整型不支持!!! | |
| NotBlank | 被標記的元素不為null,且必須有一個非空格字符 | 只支持String | 和@NotEmpty的區別,作用于字符串的話,@NotEmpty能校驗出null、”“這2種場景,而@NotBlank能校驗出null、”“、” “這3種場景,作用于集合的話,@NotEmpty支持,但@NotBlank不支持 | |
| Size | 被標記的元素長度/大小必須在指定的范圍內(字符串的話,就是length要在指定的范圍內,集合的話,就是size要在指定的范圍內) | String、Collection、Map、Array | @Size(min = 2, max = 5) | |
| Pattern | 被標記的元素必須匹配指定的正則表達式 | 只支持String | @Pattern(regexp = “1\d{5}$”) |
Hibernate Validator除了支持上面提到的22個原生注解外,還擴展了一些注解
常用注解的總結:
| Length | 被標記的元素必須在指定的長度范圍內 | 只支持String | 此注解多余了,可以直接用size | @Length(min = 2, max = 5) |
| Range | @Range注解相當于同時融合了@Min注解和@Max注解的功能 | 它支持的Java類型也和@Min注解和@Max注解一致:BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | 相當于整合了Max和Min | @Range(min = 1000L, max = 10000L) |
| URL | 被標記的元素必須是一個有效的url地址 | 它的內部其實是使用了@Pattern注解,因此它支持的Java類型和@Pattern注解一致:String |
1-9 ??
總結
以上是生活随笔為你收集整理的springboot---检验请求参数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 目标明确很重要
- 下一篇: 学计算机科学与技术专业需要考什么证书?