<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 3147字,需瀏覽 7分鐘

           ·

          2020-11-25 20:28

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

          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帶批注的元素必須是一個在可接受范圍內的數字
          @Email顧名思義
          @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 提供的?Constraintorg.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樂園

          有用!分享+在看?
          瀏覽 47
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩国产免费一区二区 | 欧美成人性生活视频 | 国产永久免费视频 | 999黄色一级视频 | 曰韩中文三级片 |