如何使用SpringBoot進行優(yōu)雅的數(shù)據(jù)驗證
JSR-303 規(guī)范
在程序進行數(shù)據(jù)處理之前,對數(shù)據(jù)進行準(zhǔn)確性校驗是我們必須要考慮的事情。盡早發(fā)現(xiàn)數(shù)據(jù)錯誤,不僅可以防止錯誤向核心業(yè)務(wù)邏輯蔓延,而且這種錯誤非常明顯,容易發(fā)現(xiàn)解決。
JSR303 規(guī)范(Bean Validation 規(guī)范)為 JavaBean 驗證定義了相應(yīng)的元數(shù)據(jù)模型和 API。在應(yīng)用程序中,通過使用 Bean Validation 或是你自己定義的 constraint,例如 @NotNull, @Max, @ZipCode , 就可以確保數(shù)據(jù)模型(JavaBean)的正確性。constraint 可以附加到字段,getter 方法,類或者接口上面。對于一些特定的需求,用戶可以很容易的開發(fā)定制化的 constraint。Bean Validation 是一個運行時的數(shù)據(jù)驗證框架,在驗證之后驗證的錯誤信息會被馬上返回。
關(guān)于 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。如果想了解更多有關(guān) Hibernate Validator 的信息,請查看官網(wǎng)。
| Constraint | 詳細(xì)信息 |
|---|---|
| @AssertFalse | 被注釋的元素必須為 false |
| @AssertTrue | 同@AssertFalse |
| @DecimalMax | 被注釋的元素必須是一個數(shù)字,其值必須小于等于指定的最大值 |
| @DecimalMin | 同DecimalMax |
| @Digits | 帶批注的元素必須是一個在可接受范圍內(nèi)的數(shù)字 |
| 顧名思義 | |
| @Future | 將來的日期 |
| @FutureOrPresent | 現(xiàn)在或?qū)?/td> |
| @Max | 被注釋的元素必須是一個數(shù)字,其值必須小于等于指定的最大值 |
| @Min | 被注釋的元素必須是一個數(shù)字,其值必須大于等于指定的最小值 |
| @Negative | 帶注釋的元素必須是一個嚴(yán)格的負(fù)數(shù)(0為無效值) |
| @NegativeOrZero | 帶注釋的元素必須是一個嚴(yán)格的負(fù)數(shù)(包含0) |
| @NotBlank | 同StringUtils.isNotBlank |
| @NotEmpty | 同StringUtils.isNotEmpty |
| @NotNull | 不能是Null |
| @Null | 元素是Null |
| @Past | 被注釋的元素必須是一個過去的日期 |
| @PastOrPresent | 過去和現(xiàn)在 |
| @Pattern | 被注釋的元素必須符合指定的正則表達式 |
| @Positive | 被注釋的元素必須嚴(yán)格的正數(shù)(0為無效值) |
| @PositiveOrZero | 被注釋的元素必須嚴(yán)格的正數(shù)(包含0) |
| @Szie | 帶注釋的元素大小必須介于指定邊界(包括)之間 |
Hibernate Validator 附加的 constraint
| Constraint | 詳細(xì)信息 |
|---|---|
| 被注釋的元素必須是電子郵箱地址 | |
| @Length | 被注釋的字符串的大小必須在指定的范圍內(nèi) |
| @NotEmpty | 被注釋的字符串的必須非空 |
| @Range | 被注釋的元素必須在合適的范圍內(nèi) |
| CreditCardNumber | 被注釋的元素必須符合信用卡格式 |
Hibernate Validator 不同版本附加的 Constraint 可能不太一樣,具體還需要你自己查看你使用版本。Hibernate 提供的 Constraint在org.hibernate.validator.constraints這個包下面。
一個 constraint 通常由 annotation 和相應(yīng)的 constraint validator 組成,它們是一對多的關(guān)系。也就是說可以有多個 constraint validator 對應(yīng)一個 annotation。在運行時,Bean Validation 框架本身會根據(jù)被注釋元素的類型來選擇合適的 constraint validator 對數(shù)據(jù)進行驗證。
有些時候,在用戶的應(yīng)用中需要一些更復(fù)雜的 constraint。Bean Validation 提供擴展 constraint 的機制。可以通過兩種方法去實現(xiàn),一種是組合現(xiàn)有的 constraint 來生成一個更復(fù)雜的 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類必須有g(shù)et和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)用結(jié)果
# 這里沒有傳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)用結(jié)果
### 創(chuàng)建用戶
POST http://127.0.0.1:9999/user/saveUser
Content-Type: application/json{
"name1": "程序員自由之路",
"age": "18"
}# 下面是返回結(jié)果
{
"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ū)τ诓煌慕涌?,需要對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
//注意:如果方法中的參數(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設(shè)置不同的校驗規(guī)則,缺點就是對于每一個新的校驗分組,都需要重新設(shè)置下這個分組下面每個屬性的校驗規(guī)則。
分組校驗還有一個按順序校驗功能。
考慮一種場景:一個bean有1個屬性(假如說是attrA),這個屬性上添加了3個約束(假如說是@NotNull、@NotEmpty、@NotBlank)。默認(rèn)情況下,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對象,比如下面的結(jié)構(gòu)。需要注意的是,在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;
}
/**
* 這邊必須設(shè)置成靜態(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;
}
測試結(jié)果
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"
}
嵌套校驗可以結(jié)合分組校驗一起使用。還有就是嵌套集合校驗會對集合里面的每一項都進行校驗,例如List字段會對這個list里面的每一個Job對象都進行校驗。這個點
在下面的@Valid和@Validated的區(qū)別章節(jié)有詳細(xì)講到。
集合校驗
如果請求體直接傳遞了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)用結(jié)果
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 {
// 默認(rèn)錯誤消息
String message() default "加密id格式錯誤";
// 分組
Class[] groups() default {};
// 負(fù)載
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è)務(wù)邏輯處理
} else {
for (ConstraintViolation userDTOConstraintViolation : validate) {
// 校驗失敗,做其它邏輯
System.out.println(userDTOConstraintViolation);
}
}
return Result.ok();
}
快速失敗(Fail Fast)配置
Spring Validation默認(rèn)會校驗完所有字段,然后才拋出異常??梢酝ㄟ^一些簡單的配置,開啟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可以用在方法、構(gòu)造函數(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)ⅲ胍獙tem進行驗證,如下所示:
@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為空或者是負(fù)數(shù),入?yún)Ⅱ炞C不會檢測出來。
為了能夠進行嵌套驗證,必須手動在Item實體的props字段上明確指出這個字段里面的實體也要進行驗證。由于@Validated不能用在成員屬性(字段)上,但是@Valid能加在成員屬性(字段)上,而且@Valid類注解上也說明了它支持嵌套驗證功能,那么我們能夠推斷出:@Valid加在方法參數(shù)時并不能夠自動進行嵌套驗證,而是用在需要嵌套驗證類的相應(yīng)字段上,來配合方法參數(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的相應(yīng)字段為空的情況,Spring Validation框架就會檢測出來,bindingResult就會記錄相應(yīng)的錯誤。
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`標(biāo)注的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標(biāo)注的參數(shù)以及處理@ResponseBody標(biāo)注方法的返回值的。顯然,執(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);
//如果直接標(biāo)注了@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;
}
}
}
看到這里,大家應(yīng)該能明白為什么這種場景下@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ù)校驗沒那么簡單!
到此這篇關(guān)于如何使用SpringBoot進行優(yōu)雅的數(shù)據(jù)驗證的文章就介紹到這了,更多相關(guān)SpringBoot數(shù)據(jù)驗證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java+Springboot搭建一個在線網(wǎng)盤文件分享系統(tǒng)
本主要介紹了通過springboot+freemark+jpa+MySQL實現(xiàn)的在線網(wǎng)盤文件分享系統(tǒng),其功能跟百度網(wǎng)盤非常類似,可以實現(xiàn)文件的上傳、移動、復(fù)制、下載等,需要的可以參考一下2021-11-11
詳解SpringBoot之訪問靜態(tài)資源(webapp...)
這篇文章主要介紹了詳解SpringBoot之訪問靜態(tài)資源(webapp...),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
SpringBoot中自定義注解實現(xiàn)控制器訪問次數(shù)限制實例
本篇文章主要介紹了SpringBoot中自定義注解實現(xiàn)控制器訪問次數(shù)限制實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-04-04
SpringBoot項目中Druid自動登錄功能實現(xiàn)
Druid是Java語言中最好的數(shù)據(jù)庫連接池,Druid能夠提供強大的監(jiān)控和擴展功能,這篇文章主要介紹了SpringBoot項目中Druid自動登錄功能實現(xiàn),需要的朋友可以參考下2024-08-08

