如何使用SpringBoot進行優(yōu)雅的數(shù)據(jù)驗證
JSR-303 規(guī)范
在程序進行數(shù)據(jù)處理之前,對數(shù)據(jù)進行準確性校驗是我們必須要考慮的事情。盡早發(fā)現(xiàn)數(shù)據(jù)錯誤,不僅可以防止錯誤向核心業(yè)務邏輯蔓延,而且這種錯誤非常明顯,容易發(fā)現(xiàn)解決。
JSR303 規(guī)范(Bean Validation 規(guī)范)為 JavaBean 驗證定義了相應的元數(shù)據(jù)模型和 API。在應用程序中,通過使用 Bean Validation 或是你自己定義的 constraint,例如 @NotNull, @Max, @ZipCode , 就可以確保數(shù)據(jù)模型(JavaBean)的正確性。constraint 可以附加到字段,getter 方法,類或者接口上面。對于一些特定的需求,用戶可以很容易的開發(fā)定制化的 constraint。Bean Validation 是一個運行時的數(shù)據(jù)驗證框架,在驗證之后驗證的錯誤信息會被馬上返回。
關于 JSR 303 – Bean Validation 規(guī)范,可以參考官網(wǎng)
對于 JSR 303 規(guī)范,Hibernate Validator 對其進行了參考實現(xiàn) . Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint 的實現(xiàn),除此之外還有一些附加的 constraint。如果想了解更多有關 Hibernate Validator 的信息,請查看官網(wǎng)。
Constraint | 詳細信息 |
---|---|
@AssertFalse | 被注釋的元素必須為 false |
@AssertTrue | 同@AssertFalse |
@DecimalMax | 被注釋的元素必須是一個數(shù)字,其值必須小于等于指定的最大值 |
@DecimalMin | 同DecimalMax |
@Digits | 帶批注的元素必須是一個在可接受范圍內(nèi)的數(shù)字 |
顧名思義 | |
@Future | 將來的日期 |
@FutureOrPresent | 現(xiàn)在或?qū)?/td> |
@Max | 被注釋的元素必須是一個數(shù)字,其值必須小于等于指定的最大值 |
@Min | 被注釋的元素必須是一個數(shù)字,其值必須大于等于指定的最小值 |
@Negative | 帶注釋的元素必須是一個嚴格的負數(shù)(0為無效值) |
@NegativeOrZero | 帶注釋的元素必須是一個嚴格的負數(shù)(包含0) |
@NotBlank | 同StringUtils.isNotBlank |
@NotEmpty | 同StringUtils.isNotEmpty |
@NotNull | 不能是Null |
@Null | 元素是Null |
@Past | 被注釋的元素必須是一個過去的日期 |
@PastOrPresent | 過去和現(xiàn)在 |
@Pattern | 被注釋的元素必須符合指定的正則表達式 |
@Positive | 被注釋的元素必須嚴格的正數(shù)(0為無效值) |
@PositiveOrZero | 被注釋的元素必須嚴格的正數(shù)(包含0) |
@Szie | 帶注釋的元素大小必須介于指定邊界(包括)之間 |
Hibernate Validator 附加的 constraint
Constraint | 詳細信息 |
---|---|
被注釋的元素必須是電子郵箱地址 | |
@Length | 被注釋的字符串的大小必須在指定的范圍內(nèi) |
@NotEmpty | 被注釋的字符串的必須非空 |
@Range | 被注釋的元素必須在合適的范圍內(nèi) |
CreditCardNumber | 被注釋的元素必須符合信用卡格式 |
Hibernate Validator 不同版本附加的 Constraint 可能不太一樣,具體還需要你自己查看你使用版本。Hibernate 提供的 Constraint在org.hibernate.validator.constraints
這個包下面。
一個 constraint 通常由 annotation 和相應的 constraint validator 組成,它們是一對多的關系。也就是說可以有多個 constraint validator 對應一個 annotation。在運行時,Bean Validation 框架本身會根據(jù)被注釋元素的類型來選擇合適的 constraint validator 對數(shù)據(jù)進行驗證。
有些時候,在用戶的應用中需要一些更復雜的 constraint。Bean Validation 提供擴展 constraint 的機制??梢酝ㄟ^兩種方法去實現(xiàn),一種是組合現(xiàn)有的 constraint 來生成一個更復雜的 constraint,另外一種是開發(fā)一個全新的 constraint。
使用Spring Boot進行數(shù)據(jù)校驗
Spring Validation 對 hibernate validation 進行了二次封裝,可以讓我們更加方便地使用數(shù)據(jù)校驗功能。這邊我們通過 Spring Boot 來引用校驗功能。
如果你用的 Spring Boot 版本小于 2.3.x,spring-boot-starter-web 會自動引入 hibernate-validator 的依賴。如果 Spring Boot 版本大于 2.3.x,則需要手動引入依賴:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency>
直接參數(shù)校驗
有時候接口的參數(shù)比較少,只有一個活著兩個參數(shù),這時候就沒必要定義一個DTO來接收參數(shù),可以直接接收參數(shù)。
@Validated @RestController @RequestMapping("/user") public class UserController { private static Logger logger = LoggerFactory.getLogger(UserController.class); @GetMapping("/getUser") @ResponseBody // 注意:如果想在參數(shù)中使用 @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<ConstraintViolation<?>> 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()); } }
調(diào)用結果
# 這里沒有傳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方法 }
接收參數(shù)時使用@Validated進行校驗
@PostMapping("/saveUser") @ResponseBody //注意:如果方法中的參數(shù)是對象類型,則必須要在參數(shù)對象前面添加 @Validated public Response<UserDTO> 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()); } }
調(diào)用結果
### 創(chuàng)建用戶
POST http://127.0.0.1:9999/user/saveUser
Content-Type: application/json{
"name1": "程序員自由之路",
"age": "18"
}# 下面是返回結果
{
"rtnCode": "1000",
"rtnMsg": "姓名不能為空"
}
對Service層方法參數(shù)校驗
個人不太喜歡這種校驗方式,一半情況下調(diào)用service層方法的參數(shù)都需要在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; } }
分組校驗
有時候?qū)τ诓煌慕涌冢枰獙TO進行不同的校驗規(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 //注意:如果方法中的參數(shù)是對象類型,則必須要在參數(shù)對象前面添加 @Validated //進行分組校驗,年齡滿足大于17 public Response<UserDTO> 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注解可以實現(xiàn)這個功能。
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類里面的字段都是基本數(shù)據(jù)類型和String等類型。
但是實際場景中,有可能某個字段也是一個對象,如果我們需要對這個對象里面的數(shù)據(jù)也進行校驗,可以使用嵌套校驗。
假如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)內(nè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<UserDTO1> 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數(shù)組給后臺,并希望對數(shù)組中的每一項都進行參數(shù)校驗。此時,如果我們直接使用java.util.Collection下的list或者set來接收數(shù)據(jù),參數(shù)校驗并不會生效!我們可以使用自定義list集合來接收參數(shù):
包裝List類型,并聲明@Valid注解
public class ValidationList<T> implements List<T> { // @Delegate是lombok注解 // 本來實現(xiàn)List接口需要實現(xiàn)一系列方法,使用這個注解可以委托給ArrayList實現(xiàn) // @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接口方法,其實都是調(diào)用了ArrayList的方法 }
調(diào)用方法
@PostMapping("/batchSaveUser") @ResponseBody public Response batchSaveUser(@Validated(value = UserDTO.Default.class) @RequestBody ValidationList<UserDTO> userDTOs){ return Response.success(); }
調(diào)用結果
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 {}; }
實現(xiàn)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; } }
編程式校驗
上面的示例都是基于注解來實現(xiàn)自動校驗的,在某些情況下,我們可能希望以編程方式調(diào)用驗證。這個時候可以注入
javax.validation.Validator對象,然后再調(diào)用其api。
@Autowired private javax.validation.Validator globalValidator; // 編程式校驗 @PostMapping("/saveWithCodingValidate") public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) { Set<constraintviolation> 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ū)別聯(lián)系
首先,@Validated和@Valid都能實現(xiàn)基本的驗證功能,也就是如果你是想驗證一個參數(shù)是否為空,長度是否滿足要求這些簡單功能,使用哪個注解都可以。
但是這兩個注解在分組、注解作用的地方、嵌套驗證等功能上兩個有所不同。下面列下這兩個注解主要的不同點。
- @Valid注解是JSR303規(guī)范的注解,@Validated注解是Spring框架自帶的注解;
- @Valid不具有分組校驗功能,@Validate具有分組校驗功能;
- @Valid可以用在方法、構造函數(shù)、方法參數(shù)和成員屬性(字段)上,@Validated可以用在類型、方法和方法參數(shù)上。但是不能用在成員屬性(字段)上,兩者是否能用于成員屬性(字段)上直接影響能否提供嵌套驗證的功能;
- @Valid加在成員屬性上可以對成員屬性進行嵌套驗證,而@Validate不能加在成員屬性上,所以不具備這個功能。
這邊說明下,什么叫嵌套驗證。
我們現(xiàn)在有個實體叫做Item:
public class Item { @NotNull(message = "id不能為空") @Min(value = 1, message = "id必須為正整數(shù)") private Long id; @NotNull(message = "props不能為空") @Size(min = 1, message = "至少要有一個屬性") private List<Prop> props; }
Item帶有很多屬性,屬性里面有:pid、vid、pidName和vidName,如下所示:
public class Prop { @NotNull(message = "pid不能為空") @Min(value = 1, message = "pid必須為正整數(shù)") private Long pid; @NotNull(message = "vid不能為空") @Min(value = 1, message = "vid必須為正整數(shù)") private Long vid; @NotBlank(message = "pidName不能為空") private String pidName; @NotBlank(message = "vidName不能為空") private String vidName; }
屬性這個實體也有自己的驗證機制,比如pid和vid不能為空,pidName和vidName不能為空等。
現(xiàn)在我們有個ItemController接受一個Item的入?yún)?,想要對Item進行驗證,如下所示:
@RestController public class ItemController { @RequestMapping("/item/add") public void addItem(@Validated Item item, BindingResult bindingResult) { doSomething(); } }
在上圖中,如果Item實體的props屬性不額外加注釋,只有@NotNull和@Size,無論入?yún)⒉捎聾Validated還是@Valid驗證,Spring Validation框架只會對Item的id和props做非空和數(shù)量驗證,不會對props字段里的Prop實體進行字段驗證,也就是@Validated和@Valid加在方法參數(shù)前,都不會自動對參數(shù)進行嵌套驗證。也就是說如果傳的List中有Prop的pid為空或者是負數(shù),入?yún)Ⅱ炞C不會檢測出來。
為了能夠進行嵌套驗證,必須手動在Item實體的props字段上明確指出這個字段里面的實體也要進行驗證。由于@Validated不能用在成員屬性(字段)上,但是@Valid能加在成員屬性(字段)上,而且@Valid類注解上也說明了它支持嵌套驗證功能,那么我們能夠推斷出:@Valid加在方法參數(shù)時并不能夠自動進行嵌套驗證,而是用在需要嵌套驗證類的相應字段上,來配合方法參數(shù)上@Validated或@Valid來進行嵌套驗證。
我們修改Item類如下所示:
public class Item { @NotNull(message = "id不能為空") @Min(value = 1, message = "id必須為正整數(shù)") private Long id; @Valid // 嵌套驗證必須用@Valid @NotNull(message = "props不能為空") @Size(min = 1, message = "props至少要有一個自定義屬性") private List<Prop> props; }
然后我們在ItemController的addItem函數(shù)上再使用@Validated或者@Valid,就能對Item的入?yún)⑦M行嵌套驗證。此時Item里面的props如果含有Prop的相應字段為空的情況,Spring Validation框架就會檢測出來,bindingResult就會記錄相應的錯誤。
Spring Validation原理簡析
現(xiàn)在我們來簡單分析下Spring校驗功能的原理。
方法級別的參數(shù)校驗實現(xiàn)原理
所謂的方法級別的校驗就是指將@NotNull和@NotEmpty這些約束直接加在方法的參數(shù)上的。
比如
@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等。
其底層實現(xiàn)原理就是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,本質(zhì)就是一個方法攔截器 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<constraintviolation> result; try { //方法入?yún)⑿r?,最終還是委托給Hibernate Validator來校驗 result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { ... } //有異常直接拋出 if (!result.isEmpty()) { throw new ConstraintViolationException(result); } //真正的方法調(diào)用 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 //注意:如果方法中的參數(shù)是對象類型,則必須要在參數(shù)對象前面添加 @Validated public R saveUser(@Validated @RequestBody UserDTO userDTO){ userDTO.setUserId(100); return R.SUCCESS.setData(userDTO); }
這種屬于DTO級別的校驗。在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody標注的參數(shù)以及處理@ResponseBody標注方法的返回值的。顯然,執(zhí)行參數(shù)校驗的邏輯肯定就在解析參數(shù)的方法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(); //將請求數(shù)據(jù)封裝到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í)行數(shù)據(jù)校驗 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()調(diào)用了validateIfApplicable()進行參數(shù)校驗。
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { // 獲取參數(shù)注解,比如@RequestBody、@Valid、@Validated Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { // 先嘗試獲取@Validated注解 Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); //如果直接標注了@Validated,那么直接開啟校驗。 //如果沒有,那么判斷參數(shù)前是否有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()實現(xiàn)。
最終發(fā)現(xiàn)底層最終還是調(diào)用了Hibernate Validator進行真正的校驗處理。
404等錯誤的統(tǒng)一處理
參考博客
參考
Spring Validation實現(xiàn)原理及如何運用
pring Validation最佳實踐及其實現(xiàn)原理,參數(shù)校驗沒那么簡單!
到此這篇關于如何使用SpringBoot進行優(yōu)雅的數(shù)據(jù)驗證的文章就介紹到這了,更多相關SpringBoot數(shù)據(jù)驗證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java+Springboot搭建一個在線網(wǎng)盤文件分享系統(tǒng)
本主要介紹了通過springboot+freemark+jpa+MySQL實現(xiàn)的在線網(wǎng)盤文件分享系統(tǒng),其功能跟百度網(wǎng)盤非常類似,可以實現(xiàn)文件的上傳、移動、復制、下載等,需要的可以參考一下2021-11-11詳解SpringBoot之訪問靜態(tài)資源(webapp...)
這篇文章主要介紹了詳解SpringBoot之訪問靜態(tài)資源(webapp...),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-09-09SpringBoot中自定義注解實現(xiàn)控制器訪問次數(shù)限制實例
本篇文章主要介紹了SpringBoot中自定義注解實現(xiàn)控制器訪問次數(shù)限制實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-04-04SpringBoot項目中Druid自動登錄功能實現(xiàn)
Druid是Java語言中最好的數(shù)據(jù)庫連接池,Druid能夠提供強大的監(jiān)控和擴展功能,這篇文章主要介紹了SpringBoot項目中Druid自動登錄功能實現(xiàn),需要的朋友可以參考下2024-08-08