SpringBoot 如何進行優(yōu)雅的數據校驗

點擊上方「藍字」關注我們

JSR-303 規(guī)范
在程序進行數據處理之前,對數據進行準確性校驗是我們必須要考慮的事情。盡早發(fā)現數據錯誤,不僅可以防止錯誤向核心業(yè)務邏輯蔓延,而且這種錯誤非常明顯,容易發(fā)現解決。
JSR303 規(guī)范(Bean Validation 規(guī)范)為 JavaBean 驗證定義了相應的元數據模型和 API。在應用程序中,通過使用 Bean Validation 或是你自己定義的 constraint,例如 @NotNull, @Max, @ZipCode , 就可以確保數據模型(JavaBean)的正確性。constraint 可以附加到字段,getter 方法,類或者接口上面。對于一些特定的需求,用戶可以很容易的開發(fā)定制化的 constraint。Bean Validation 是一個運行時的數據驗證框架,在驗證之后驗證的錯誤信息會被馬上返回。
關于 JSR 303 – Bean Validation 規(guī)范,可以參考官網
對于 JSR 303 規(guī)范,Hibernate Validator 對其進行了參考實現 . Hibernate Validator 提供了 JSR 303 規(guī)范中所有內置 constraint 的實現,除此之外還有一些附加的 constraint。如果想了解更多有關 Hibernate Validator 的信息,請查看官網。
validation-api 內置的 constraint 清單
| Constraint | 詳細信息 |
|---|---|
| @AssertFalse | 被注釋的元素必須為 false |
| @AssertTrue | 同@AssertFalse |
| @DecimalMax | 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值 |
| @DecimalMin | 同DecimalMax |
| @Digits | 帶批注的元素必須是一個在可接受范圍內的數字 |
| 顧名思義 | |
| @Future | 將來的日期 |
| @FutureOrPresent | 現在或將來 |
| @Max | 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值 |
| @Min | 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值 |
| @Negative | 帶注釋的元素必須是一個嚴格的負數(0為無效值) |
| @NegativeOrZero | 帶注釋的元素必須是一個嚴格的負數(包含0) |
| @NotBlank | 同StringUtils.isNotBlank |
| @NotEmpty | 同StringUtils.isNotEmpty |
| @NotNull | 不能是Null |
| @Null | 元素是Null |
| @Past | 被注釋的元素必須是一個過去的日期 |
| @PastOrPresent | 過去和現在 |
| @Pattern | 被注釋的元素必須符合指定的正則表達式 |
| @Positive | 被注釋的元素必須嚴格的正數(0為無效值) |
| @PositiveOrZero | 被注釋的元素必須嚴格的正數(包含0) |
| @Szie | 帶注釋的元素大小必須介于指定邊界(包括)之間 |
Hibernate Validator 附加的 constraint
| Constraint | 詳細信息 |
|---|---|
@Email | 被注釋的元素必須是電子郵箱地址 |
@Length | 被注釋的字符串的大小必須在指定的范圍內 |
@NotEmpty | 被注釋的字符串的必須非空 |
@Range | 被注釋的元素必須在合適的范圍內 |
CreditCardNumber | 被注釋的元素必須符合信用卡格式 |
Hibernate Validator 不同版本附加的 Constraint 可能不太一樣,具體還需要你自己查看你使用版本。Hibernate 提供的?Constraint在org.hibernate.validator.constraints這個包下面。
一個 constraint 通常由 annotation 和相應的 constraint validator 組成,它們是一對多的關系。也就是說可以有多個 constraint validator 對應一個 annotation。在運行時,Bean Validation 框架本身會根據被注釋元素的類型來選擇合適的 constraint validator 對數據進行驗證。
有些時候,在用戶的應用中需要一些更復雜的 constraint。Bean Validation 提供擴展 constraint 的機制。可以通過兩種方法去實現,一種是組合現有的 constraint 來生成一個更復雜的 constraint,另外一種是開發(fā)一個全新的 constraint。
使用Spring Boot進行數據校驗
Spring Validation 對 hibernate validation 進行了二次封裝,可以讓我們更加方便地使用數據校驗功能。這邊我們通過 Spring Boot 來引用校驗功能。
如果你用的 Spring Boot 版本小于 2.3.x,spring-boot-starter-web 會自動引入 hibernate-validator 的依賴。如果 Spring Boot 版本大于 2.3.x,則需要手動引入依賴:
<dependency>
????<groupId>org.hibernategroupId>
????<artifactId>hibernate-validatorartifactId>
????<version>6.0.1.Finalversion>
dependency>
直接參數校驗
有時候接口的參數比較少,只有一個活著兩個參數,這時候就沒必要定義一個DTO來接收參數,可以直接接收參數。
@Validated
@RestController
@RequestMapping("/user")
public?class?UserController?{
????private?static?Logger?logger?=?LoggerFactory.getLogger(UserController.class);
????@GetMapping("/getUser")
????@ResponseBody
????//?注意:如果想在參數中使用?@NotNull 這種注解校驗,就必須在類上添加?@Validated;
????public?UserDTO?getUser(@NotNull(message?=?"userId不能為空")?Integer?userId){
????????logger.info("userId:[{}]",userId);
????????UserDTO?res?=?new?UserDTO();
????????res.setUserId(userId);
????????res.setName("程序員自由之路");
????????res.setAge(8);
????????return?res;
????}
}
下面是統(tǒng)一異常處理類
@RestControllerAdvice
public?class?GlobalExceptionHandler?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(GlobalExceptionHandler.class);
????@ExceptionHandler(value?=?ConstraintViolationException.class)
????public?Response?handle1(ConstraintViolationException?ex){
????????????StringBuilder?msg?=?new?StringBuilder();
????????Set>?constraintViolations?=?ex.getConstraintViolations();
????????for?(ConstraintViolation>?constraintViolation?:?constraintViolations)?{
????????????PathImpl?pathImpl?=?(PathImpl)?constraintViolation.getPropertyPath();
????????????String?paramName?=?pathImpl.getLeafNode().getName();
????????????String?message?=?constraintViolation.getMessage();
????????????msg.append("[").append(message).append("]");
????????}
????????logger.error(msg.toString(),ex);
????????//?注意:Response類必須有get和set方法,不然會報錯
????????return?new?Response(RCode.PARAM_INVALID.getCode(),msg.toString());
????}
????@ExceptionHandler(value?=?Exception.class)
????public?Response?handle1(Exception?ex){
????????logger.error(ex.getMessage(),ex);
????????return?new?Response(RCode.ERROR.getCode(),RCode.ERROR.getMsg());
????}
}
調用結果
#?這里沒有傳userId
GET?http://127.0.0.1:9999/user/getUser
HTTP/1.1?200?
Content-Type:?application/json
Transfer-Encoding:?chunked
Date:?Sat,?14?Nov?2020?07:35:44?GMT
Keep-Alive:?timeout=60
Connection:?keep-alive
{
??"rtnCode":?"1000",
??"rtnMsg":?"[userId不能為空]"
}
實體類DTO校驗
定義一個DTO
import?org.hibernate.validator.constraints.Range;
import?javax.validation.constraints.NotEmpty;
public?class?UserDTO?{
????private?Integer?userId;
????@NotEmpty(message?=?"姓名不能為空")
????private?String?name;
????@Range(min?=?18,max?=?50,message?=?"年齡必須在18和50之間")
????private?Integer?age;
????//省略get和set方法
}
接收參數時使用@Validated進行校驗
@PostMapping("/saveUser")
@ResponseBody
//注意:如果方法中的參數是對象類型,則必須要在參數對象前面添加?@Validated
public?Response?getUser(@Validated?@RequestBody?UserDTO?userDTO){
????userDTO.setUserId(100);
????Response?response?=?Response.success();
????response.setData(userDTO);
????return?response;
}
統(tǒng)一異常處理
@ExceptionHandler(value?=?MethodArgumentNotValidException.class)
public?Response?handle2(MethodArgumentNotValidException?ex){
????BindingResult?bindingResult?=?ex.getBindingResult();
????if(bindingResult!=null){
????????if(bindingResult.hasErrors()){
????????????FieldError?fieldError?=?bindingResult.getFieldError();
????????????String?field?=?fieldError.getField();
????????????String?defaultMessage?=?fieldError.getDefaultMessage();
????????????logger.error(ex.getMessage(),ex);
????????????return?new?Response(RCode.PARAM_INVALID.getCode(),field+":"+defaultMessage);
????????}else?{
????????????logger.error(ex.getMessage(),ex);
????????????return?new?Response(RCode.ERROR.getCode(),RCode.ERROR.getMsg());
????????}
????}else?{
????????logger.error(ex.getMessage(),ex);
????????return?new?Response(RCode.ERROR.getCode(),RCode.ERROR.getMsg());
????}
}
調用結果
###?創(chuàng)建用戶
POST?http://127.0.0.1:9999/user/saveUser
Content-Type:?application/json
{
??"name1":?"程序員自由之路",
??"age":?"18"
}
#?下面是返回結果
{
??"rtnCode":?"1000",
??"rtnMsg":?"姓名不能為空"
}
對Service層方法參數校驗
個人不太喜歡這種校驗方式,一半情況下調用service層方法的參數都需要在controller層校驗好,不需要再校驗一次。這邊列舉這個功能,只是想說 Spring 也支持這個。
@Validated
@Service
public?class?ValidatorService?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(ValidatorService.class);
????public?String?show(@NotNull(message?=?"不能為空")?@Min(value?=?18,?message?=?"最小18")?String?age)?{
????????logger.info("age?=?{}",?age);
????????return?age;
????}
}
分組校驗
有時候對于不同的接口,需要對DTO進行不同的校驗規(guī)則。還是以上面的UserDTO為列,另外一個接口可能不需要將age限制在18~50之間,只需要大于18就可以了。
這樣上面的校驗規(guī)則就不適用了。分組校驗就是來解決這個問題的,同一個DTO,不同的分組采用不同的校驗策略。
public?class?UserDTO?{
????public?interface?Default?{
????}
????public?interface?Group1?{
????}
????private?Integer?userId;
????//注意:@Validated 注解中加上groups屬性后,DTO中沒有加group屬性的校驗規(guī)則將失效
????@NotEmpty(message?=?"姓名不能為空",groups?=?Default.class)
????private?String?name;
????//注意:加了groups屬性之后,必須在@Validated 注解中也加上groups屬性后,校驗規(guī)則才能生效,不然下面的校驗限制就失效了
????@Range(min?=?18,?max?=?50,?message?=?"年齡必須在18和50之間",groups?=?Default.class)
????@Range(min?=?17,?message?=?"年齡必須大于17",?groups?=?Group1.class)
????private?Integer?age;
}
使用方式
@PostMapping("/saveUserGroup")
@ResponseBody
//注意:如果方法中的參數是對象類型,則必須要在參數對象前面添加?@Validated
//進行分組校驗,年齡滿足大于17
public?Response?saveUserGroup(@Validated(value?=?{UserDTO.Group1.class})?@RequestBody?UserDTO?userDTO){
????userDTO.setUserId(100);
????Response?response?=?Response.success();
????response.setData(userDTO);
????return?response;
}
使用Group1分組進行校驗,因為DTO中,Group1分組對name屬性沒有校驗,所以這個校驗將不會生效。
分組校驗的好處是可以對同一個DTO設置不同的校驗規(guī)則,缺點就是對于每一個新的校驗分組,都需要重新設置下這個分組下面每個屬性的校驗規(guī)則。
分組校驗還有一個按順序校驗功能。
考慮一種場景:一個bean有1個屬性(假如說是attrA),這個屬性上添加了3個約束(假如說是@NotNull、@NotEmpty、@NotBlank)。默認情況下,validation-api對這3個約束的校驗順序是隨機的。也就是說,可能先校驗@NotNull,再校驗@NotEmpty,最后校驗@NotBlank,也有可能先校驗@NotBlank,再校驗@NotEmpty,最后校驗@NotNull。
那么,如果我們的需求是先校驗@NotNull,再校驗@NotBlank,最后校驗@NotEmpty。@GroupSequence注解可以實現這個功能。
public?class?GroupSequenceDemoForm?{
????@NotBlank(message?=?"至少包含一個非空字符",?groups?=?{First.class})
????@Size(min?=?11,?max?=?11,?message?=?"長度必須是11",?groups?=?{Second.class})
????private?String?demoAttr;
????public?interface?First?{
????}
????public?interface?Second?{
????}
????@GroupSequence(value?=?{First.class,?Second.class})
????public?interface?GroupOrderedOne?{
????????//?先計算屬于?First?組的約束,再計算屬于?Second?組的約束
????}
????@GroupSequence(value?=?{Second.class,?First.class})
????public?interface?GroupOrderedTwo?{
????????//?先計算屬于?Second?組的約束,再計算屬于?First?組的約束
????}
}
使用方式
//?先計算屬于?First?組的約束,再計算屬于?Second?組的約束
@Validated(value?=?{GroupOrderedOne.class})?@RequestBody?GroupSequenceDemoForm?form
嵌套校驗
前面的示例中,DTO類里面的字段都是基本數據類型和String等類型。
但是實際場景中,有可能某個字段也是一個對象,如果我們需要對這個對象里面的數據也進行校驗,可以使用嵌套校驗。
假如UserDTO中還用一個Job對象,比如下面的結構。需要注意的是,在job類的校驗上面一定要加上@Valid注解。
public?class?UserDTO1?{
????private?Integer?userId;
????@NotEmpty
????private?String?name;
????@NotNull
????private?Integer?age;
????@Valid
????@NotNull
????private?Job?job;
????public?Integer?getUserId()?{
????????return?userId;
????}
????public?void?setUserId(Integer?userId)?{
????????this.userId?=?userId;
????}
????public?String?getName()?{
????????return?name;
????}
????public?void?setName(String?name)?{
????????this.name?=?name;
????}
????public?Integer?getAge()?{
????????return?age;
????}
????public?void?setAge(Integer?age)?{
????????this.age?=?age;
????}
????public?Job?getJob()?{
????????return?job;
????}
????public?void?setJob(Job?job)?{
????????this.job?=?job;
????}
????/**
?????*?這邊必須設置成靜態(tài)內部類
?????*/
????static?class?Job?{
????????@NotEmpty
????????private?String?jobType;
????????@DecimalMax(value?=?"1000.99")
????????private?Double?salary;
????????public?String?getJobType()?{
????????????return?jobType;
????????}
????????public?void?setJobType(String?jobType)?{
????????????this.jobType?=?jobType;
????????}
????????public?Double?getSalary()?{
????????????return?salary;
????????}
????????public?void?setSalary(Double?salary)?{
????????????this.salary?=?salary;
????????}
????}
}
使用方式
@PostMapping("/saveUserWithJob")
@ResponseBody
public?Response?saveUserWithJob(@Validated?@RequestBody?UserDTO1?userDTO){
????userDTO.setUserId(100);
????Response?response?=?Response.success();
????response.setData(userDTO);
????return?response;
}
測試結果
POST?http://127.0.0.1:9999/user/saveUserWithJob
Content-Type:?application/json
{
??"name":?"程序員自由之路",
??"age":?"16",
??"job":?{
????"jobType":?"1",
????"salary":?"9999.99"
??}
}
{
??"rtnCode":?"1000",
??"rtnMsg":?"job.salary:必須小于或等于1000.99"
}
嵌套校驗可以結合分組校驗一起使用。還有就是嵌套集合校驗會對集合里面的每一項都進行校驗,例如List字段會對這個list里面的每一個Job對象都進行校驗。這個點
在下面的@Valid和@Validated的區(qū)別章節(jié)有詳細講到。
集合校驗
如果請求體直接傳遞了json數組給后臺,并希望對數組中的每一項都進行參數校驗。此時,如果我們直接使用java.util.Collection下的list或者set來接收數據,參數校驗并不會生效!我們可以使用自定義list集合來接收參數:
包裝List類型,并聲明@Valid注解
public?class?ValidationList<T>?implements?List<T>?{
????//?@Delegate是lombok注解
????//?本來實現List接口需要實現一系列方法,使用這個注解可以委托給ArrayList實現
????//?@Delegate
????@Valid
????public?List?list?=?new?ArrayList<>();
????@Override
????public?int?size()?{
????????return?list.size();
????}
????@Override
????public?boolean?isEmpty()?{
????????return?list.isEmpty();
????}
????@Override
????public?boolean?contains(Object?o)?{
????????return?list.contains(o);
????}
????//....?下面省略一系列List接口方法,其實都是調用了ArrayList的方法
}
調用方法
@PostMapping("/batchSaveUser")
@ResponseBody
public?Response?batchSaveUser(@Validated(value?=?UserDTO.Default.class)?@RequestBody?ValidationList?userDTOs){
???return?Response.success();
}
調用結果
Caused?by:?org.springframework.beans.NotReadablePropertyException:?Invalid?property?'list[1]'?of?bean?class?[com.csx.demo.spring.boot.dto.ValidationList]:?Bean?property?'list[1]'?is?not?readable?or?has?an?invalid?getter?method:?Does?the?return?type?of?the?getter?match?the?parameter?type?of?the?setter?
????at?org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:622)?~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
????at?org.springframework.beans.AbstractNestablePropertyAccessor.getNestedPropertyAccessor(AbstractNestablePropertyAccessor.java:839)?~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
????at?org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyAccessorForPropertyPath(AbstractNestablePropertyAccessor.java:816)?~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
????at?org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:610)?~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
會拋出NotReadablePropertyException異常,需要對這個異常做統(tǒng)一處理。這邊代碼就不貼了。
自定義校驗器
在Spring中自定義校驗器非常簡單,分兩步走。
自定義約束注解
@Target({METHOD,?FIELD,?ANNOTATION_TYPE,?CONSTRUCTOR,?PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy?=?{EncryptIdValidator.class})
public?@interface?EncryptId?{
????//?默認錯誤消息
????String?message()?default?"加密id格式錯誤";
????//?分組
????Class[]?groups()?default?{};
????//?負載
????Class[]?payload()?default?{};
}
實現ConstraintValidator接口編寫約束校驗器
public?class?EncryptIdValidator?implements?ConstraintValidator<EncryptId,?String>?{
????private?static?final?Pattern?PATTERN?=?Pattern.compile("^[a-f\\d]{32,256}$");
????@Override
????public?boolean?isValid(String?value,?ConstraintValidatorContext?context)?{
????????//?不為null才進行校驗
????????if?(value?!=?null)?{
????????????Matcher?matcher?=?PATTERN.matcher(value);
????????????return?matcher.find();
????????}
????????return?true;
????}
}
編程式校驗
上面的示例都是基于注解來實現自動校驗的,在某些情況下,我們可能希望以編程方式調用驗證。這個時候可以注入
javax.validation.Validator對象,然后再調用其api。
@Autowired
private?javax.validation.Validator?globalValidator;
//?編程式校驗
@PostMapping("/saveWithCodingValidate")
public?Result?saveWithCodingValidate(@RequestBody?UserDTO?userDTO)?{
????Set?validate?=?globalValidator.validate(userDTO,?UserDTO.Save.class);
????//?如果校驗通過,validate為空;否則,validate包含未校驗通過項
????if?(validate.isEmpty())?{
????????//?校驗通過,才會執(zhí)行業(yè)務邏輯處理
????}?else?{
????????for?(ConstraintViolation?userDTOConstraintViolation?:?validate)?{
????????????//?校驗失敗,做其它邏輯
????????????System.out.println(userDTOConstraintViolation);
????????}
????}
????return?Result.ok();
}
快速失敗(Fail Fast)配置
Spring Validation默認會校驗完所有字段,然后才拋出異常。可以通過一些簡單的配置,開啟Fali Fast模式,一旦校驗失敗就立即返回。
@Bean
public?Validator?validator()?{
????ValidatorFactory?validatorFactory?=?Validation.byProvider(HibernateValidator.class)
????????????.configure()
????????????//?快速失敗模式
????????????.failFast(true)
????????????.buildValidatorFactory();
????return?validatorFactory.getValidator();
}
校驗信息的國際化
Spring 的校驗功能可以返回很友好的校驗信息提示,而且這個信息支持國際化。
這塊功能暫時暫時不常用,具體可以參考這篇文章
@Validated和@Valid的區(qū)別聯系
首先,@Validated和@Valid都能實現基本的驗證功能,也就是如果你是想驗證一個參數是否為空,長度是否滿足要求這些簡單功能,使用哪個注解都可以。
但是這兩個注解在分組、注解作用的地方、嵌套驗證等功能上兩個有所不同。下面列下這兩個注解主要的不同點。
@Valid注解是JSR303規(guī)范的注解,@Validated注解是Spring框架自帶的注解;
@Valid不具有分組校驗功能,@Validate具有分組校驗功能;
@Valid可以用在方法、構造函數、方法參數和成員屬性(字段)上,@Validated可以用在類型、方法和方法參數上。但是不能用在成員屬性(字段)上,兩者是否能用于成員屬性(字段)上直接影響能否提供嵌套驗證的功能;
@Valid加在成員屬性上可以對成員屬性進行嵌套驗證,而@Validate不能加在成員屬性上,所以不具備這個功能。
這邊說明下,什么叫嵌套驗證。
我們現在有個實體叫做Item:
public?class?Item?{
????@NotNull(message?=?"id不能為空")
????@Min(value?=?1,?message?=?"id必須為正整數")
????private?Long?id;
????@NotNull(message?=?"props不能為空")
????@Size(min?=?1,?message?=?"至少要有一個屬性")
????private?List?props;
}
Item帶有很多屬性,屬性里面有:pid、vid、pidName和vidName,如下所示:
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;
}
屬性這個實體也有自己的驗證機制,比如pid和vid不能為空,pidName和vidName不能為空等。
現在我們有個ItemController接受一個Item的入參,想要對Item進行驗證,如下所示:
@RestController
public?class?ItemController?{
????@RequestMapping("/item/add")
????public?void?addItem(@Validated?Item?item,?BindingResult?bindingResult)?{
????????doSomething();
????}
}
在上圖中,如果Item實體的props屬性不額外加注釋,只有@NotNull和@Size,無論入參采用@Validated還是@Valid驗證,Spring Validation框架只會對Item的id和props做非空和數量驗證,不會對props字段里的Prop實體進行字段驗證,也就是@Validated和@Valid加在方法參數前,都不會自動對參數進行嵌套驗證。也就是說如果傳的List中有Prop的pid為空或者是負數,入參驗證不會檢測出來。
為了能夠進行嵌套驗證,必須手動在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?props;
}
然后我們在ItemController的addItem函數上再使用@Validated或者@Valid,就能對Item的入參進行嵌套驗證。此時Item里面的props如果含有Prop的相應字段為空的情況,Spring Validation框架就會檢測出來,bindingResult就會記錄相應的錯誤。
Spring Validation原理簡析
現在我們來簡單分析下Spring校驗功能的原理。
方法級別的參數校驗實現原理
所謂的方法級別的校驗就是指將@NotNull和@NotEmpty這些約束直接加在方法的參數上的。
比如
@GetMapping("/getUser")
@ResponseBody
public?R?getUser(@NotNull(message?=?"userId不能為空")?Integer?userId){
???//
}
或者
@Validated
@Service
public?class?ValidatorService?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(ValidatorService.class);
????public?String?show(@NotNull(message?=?"不能為空")?@Min(value?=?18,?message?=?"最小18")?String?age)?{
????????logger.info("age?=?{}",?age);
????????return?age;
????}
}
都屬于方法級別的校驗。這種方式可用于任何Spring Bean的方法上,比如Controller/Service等。
其底層實現原理就是AOP,具體來說是通過MethodValidationPostProcessor動態(tài)注冊AOP切面,然后使用MethodValidationInterceptor對切點方法織入增強。
public?class?MethodValidationPostProcessor?extends?AbstractBeanFactoryAwareAdvisingPostProcessorimplements?InitializingBean?{
????@Override
????public?void?afterPropertiesSet()?{
????????//為所有`@Validated`標注的Bean創(chuàng)建切面
????????Pointcut?pointcut?=?new?AnnotationMatchingPointcut(this.validatedAnnotationType,?true);
????????//創(chuàng)建Advisor進行增強
????????this.advisor?=?new?DefaultPointcutAdvisor(pointcut,?createMethodValidationAdvice(this.validator));
????}
????//創(chuàng)建Advice,本質就是一個方法攔截器
????protected?Advice?createMethodValidationAdvice(@Nullable?Validator?validator)?{
????????return?(validator?!=?null???new?MethodValidationInterceptor(validator)?:?new?MethodValidationInterceptor());
????}
}
接著看一下MethodValidationInterceptor:
public?class?MethodValidationInterceptor?implements?MethodInterceptor?{
????@Override
????public?Object?invoke(MethodInvocation?invocation)?throws?Throwable?{
????????//無需增強的方法,直接跳過
????????if?(isFactoryBeanMetadataMethod(invocation.getMethod()))?{
????????????return?invocation.proceed();
????????}
????????//獲取分組信息
????????Class[]?groups?=?determineValidationGroups(invocation);
????????ExecutableValidator?execVal?=?this.validator.forExecutables();
????????Method?methodToValidate?=?invocation.getMethod();
????????Set?result;
????????try?{
????????????//方法入參校驗,最終還是委托給Hibernate?Validator來校驗
????????????result?=?execVal.validateParameters(
????????????????invocation.getThis(),?methodToValidate,?invocation.getArguments(),?groups);
????????}
????????catch?(IllegalArgumentException?ex)?{
????????????...
????????}
????????//有異常直接拋出
????????if?(!result.isEmpty())?{
????????????throw?new?ConstraintViolationException(result);
????????}
????????//真正的方法調用
????????Object?returnValue?=?invocation.proceed();
????????//對返回值做校驗,最終還是委托給Hibernate?Validator來校驗
????????result?=?execVal.validateReturnValue(invocation.getThis(),?methodToValidate,?returnValue,?groups);
????????//有異常直接拋出
????????if?(!result.isEmpty())?{
????????????throw?new?ConstraintViolationException(result);
????????}
????????return?returnValue;
????}
}
DTO級別的校驗
@PostMapping("/saveUser")
@ResponseBody
//注意:如果方法中的參數是對象類型,則必須要在參數對象前面添加?@Validated
public?R?saveUser(@Validated?@RequestBody?UserDTO?userDTO){
????userDTO.setUserId(100);
????return?R.SUCCESS.setData(userDTO);
}
這種屬于DTO級別的校驗。在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody標注的參數以及處理@ResponseBody標注方法的返回值的。顯然,執(zhí)行參數校驗的邏輯肯定就在解析參數的方法resolveArgument()中。
public?class?RequestResponseBodyMethodProcessor?extends?AbstractMessageConverterMethodProcessor?{
????@Override
????public?Object?resolveArgument(MethodParameter?parameter,?@Nullable?ModelAndViewContainer?mavContainer,
??????????????????????????????????NativeWebRequest?webRequest,?@Nullable?WebDataBinderFactory?binderFactory)?throws?Exception?{
????????parameter?=?parameter.nestedIfOptional();
????????//將請求數據封裝到DTO對象中
????????Object?arg?=?readWithMessageConverters(webRequest,?parameter,?parameter.getNestedGenericParameterType());
????????String?name?=?Conventions.getVariableNameForParameter(parameter);
????????if?(binderFactory?!=?null)?{
????????????WebDataBinder?binder?=?binderFactory.createBinder(webRequest,?arg,?name);
????????????if?(arg?!=?null)?{
????????????????//?執(zhí)行數據校驗
????????????????validateIfApplicable(binder,?parameter);
????????????????if?(binder.getBindingResult().hasErrors()?&&?isBindExceptionRequired(binder,?parameter))?{
????????????????????throw?new?MethodArgumentNotValidException(parameter,?binder.getBindingResult());
????????????????}
????????????}
????????????if?(mavContainer?!=?null)?{
????????????????mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX?+?name,?binder.getBindingResult());
????????????}
????????}
????????return?adaptArgumentIfNecessary(arg,?parameter);
????}
}
可以看到,resolveArgument()調用了validateIfApplicable()進行參數校驗。
protected?void?validateIfApplicable(WebDataBinder?binder,?MethodParameter?parameter)?{
????//?獲取參數注解,比如@RequestBody、@Valid、@Validated
????Annotation[]?annotations?=?parameter.getParameterAnnotations();
????for?(Annotation?ann?:?annotations)?{
????????//?先嘗試獲取@Validated注解
????????Validated?validatedAnn?=?AnnotationUtils.getAnnotation(ann,?Validated.class);
????????//如果直接標注了@Validated,那么直接開啟校驗。
????????//如果沒有,那么判斷參數前是否有Valid起頭的注解。
????????if?(validatedAnn?!=?null?||?ann.annotationType().getSimpleName().startsWith("Valid"))?{
????????????Object?hints?=?(validatedAnn?!=?null???validatedAnn.value()?:?AnnotationUtils.getValue(ann));
????????????Object[]?validationHints?=?(hints?instanceof?Object[]???(Object[])?hints?:?new?Object[]?{hints});
????????????//執(zhí)行校驗
????????????binder.validate(validationHints);
????????????break;
????????}
????}
}
看到這里,大家應該能明白為什么這種場景下@Validated、@Valid兩個注解可以混用。我們接下來繼續(xù)看WebDataBinder.validate()實現。
最終發(fā)現底層最終還是調用了Hibernate Validator進行真正的校驗處理。
404等錯誤的統(tǒng)一處理
參考博客
參考
Spring?Validation實現原理及如何運用
SpringBoot參數校驗和國際化使用
@Valid和@Validated區(qū)別
Spring Validation最佳實踐及其實現原理,參數校驗沒那么簡單!
來源:https://www.cnblogs.com/54chensongxia/p/14016179.html
掃碼二維碼
獲取更多精彩
Java樂園

