1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SpringBoot 如何進(jìn)行優(yōu)雅的數(shù)據(jù)校驗

        共 3147字,需瀏覽 7分鐘

         ·

        2020-11-25 20:28

        點擊上方「藍(lán)字」關(guān)注我們

        JSR-303 規(guī)范

        在程序進(jìn)行數(shù)據(jù)處理之前,對數(shù)據(jù)進(jìn)行準(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 是一個運(yùn)行時的數(shù)據(jù)驗證框架,在驗證之后驗證的錯誤信息會被馬上返回。

        關(guān)于 JSR 303 – Bean Validation 規(guī)范,可以參考官網(wǎng)

        對于 JSR 303 規(guī)范,Hibernate Validator 對其進(jìn)行了參考實現(xiàn) . Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint 的實現(xiàn),除此之外還有一些附加的 constraint。如果想了解更多有關(guān) Hibernate Validator 的信息,請查看官網(wǎng)。

        validation-api 內(nèi)置的 constraint 清單

        Constraint詳細(xì)信息
        @AssertFalse被注釋的元素必須為 false
        @AssertTrue同@AssertFalse
        @DecimalMax被注釋的元素必須是一個數(shù)字,其值必須小于等于指定的最大值
        @DecimalMin同DecimalMax
        @Digits帶批注的元素必須是一個在可接受范圍內(nèi)的數(shù)字
        @Email顧名思義
        @Future將來的日期
        @FutureOrPresent現(xiàn)在或?qū)?/span>
        @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被注釋的元素必須符合指定的正則表達(dá)式
        @Positive被注釋的元素必須嚴(yán)格的正數(shù)(0為無效值)
        @PositiveOrZero被注釋的元素必須嚴(yán)格的正數(shù)(包含0)
        @Szie帶注釋的元素大小必須介于指定邊界(包括)之間

        Hibernate Validator 附加的 constraint

        Constraint詳細(xì)信息
        @Email被注釋的元素必須是電子郵箱地址
        @Length被注釋的字符串的大小必須在指定的范圍內(nèi)
        @NotEmpty被注釋的字符串的必須非空
        @Range被注釋的元素必須在合適的范圍內(nèi)
        CreditCardNumber被注釋的元素必須符合信用卡格式

        Hibernate Validator 不同版本附加的 Constraint 可能不太一樣,具體還需要你自己查看你使用版本。Hibernate 提供的?Constraintorg.hibernate.validator.constraints這個包下面。

        一個 constraint 通常由 annotation 和相應(yīng)的 constraint validator 組成,它們是一對多的關(guān)系。也就是說可以有多個 constraint validator 對應(yīng)一個 annotation。在運(yùn)行時,Bean Validation 框架本身會根據(jù)被注釋元素的類型來選擇合適的 constraint validator 對數(shù)據(jù)進(jìn)行驗證。

        有些時候,在用戶的應(yīng)用中需要一些更復(fù)雜的 constraint。Bean Validation 提供擴(kuò)展 constraint 的機(jī)制??梢酝ㄟ^兩種方法去實現(xiàn),一種是組合現(xiàn)有的 constraint 來生成一個更復(fù)雜的 constraint,另外一種是開發(fā)一個全新的 constraint。

        使用Spring Boot進(jìn)行數(shù)據(jù)校驗

        Spring Validation 對 hibernate validation 進(jìn)行了二次封裝,可以讓我們更加方便地使用數(shù)據(jù)校驗功能。這邊我們通過 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>

        直接參數(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>?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進(jìn)行校驗

        @PostMapping("/saveUser")
        @ResponseBody
        //注意:如果方法中的參數(shù)是對象類型,則必須要在參數(shù)對象前面添加?@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());
        ????}
        }

        調(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進(jìn)行不同的校驗規(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
        //進(jìn)行分組校驗,年齡滿足大于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分組進(jìn)行校驗,因為DTO中,Group1分組對name屬性沒有校驗,所以這個校驗將不會生效。

        分組校驗的好處是可以對同一個DTO設(shè)置不同的校驗規(guī)則,缺點就是對于每一個新的校驗分組,都需要重新設(shè)置下這個分組下面每個屬性的校驗規(guī)則。

        分組校驗還有一個按順序校驗功能。

        考慮一種場景:一個bean有1個屬性(假如說是attrA),這個屬性上添加了3個約束(假如說是@NotNull、@NotEmpty、@NotBlank)。默認(rèn)情況下,validation-api對這3個約束的校驗順序是隨機(jī)的。也就是說,可能先校驗@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ù)也進(jìn)行校驗,可以使用嵌套校驗。

        假如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?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é)合分組校驗一起使用。還有就是嵌套集合校驗會對集合里面的每一項都進(jìn)行校驗,例如List字段會對這個list里面的每一個Job對象都進(jìn)行校驗。這個點

        在下面的@Valid和@Validated的區(qū)別章節(jié)有詳細(xì)講到。

        集合校驗

        如果請求體直接傳遞了json數(shù)組給后臺,并希望對數(shù)組中的每一項都進(jìn)行參數(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?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才進(jìn)行校驗
        ????????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?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加在成員屬性上可以對成員屬性進(jìn)行嵌套驗證,而@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?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;
        }

        屬性這個實體也有自己的驗證機(jī)制,比如pid和vid不能為空,pidName和vidName不能為空等。

        現(xiàn)在我們有個ItemController接受一個Item的入?yún)ⅲ胍獙tem進(jìn)行驗證,如下所示:

        @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實體進(jìn)行字段驗證,也就是@Validated和@Valid加在方法參數(shù)前,都不會自動對參數(shù)進(jìn)行嵌套驗證。也就是說如果傳的List中有Prop的pid為空或者是負(fù)數(shù),入?yún)Ⅱ炞C不會檢測出來。

        為了能夠進(jìn)行嵌套驗證,必須手動在Item實體的props字段上明確指出這個字段里面的實體也要進(jìn)行驗證。由于@Validated不能用在成員屬性(字段)上,但是@Valid能加在成員屬性(字段)上,而且@Valid類注解上也說明了它支持嵌套驗證功能,那么我們能夠推斷出:@Valid加在方法參數(shù)時并不能夠自動進(jìn)行嵌套驗證,而是用在需要嵌套驗證類的相應(yīng)字段上,來配合方法參數(shù)上@Validated或@Valid來進(jìn)行嵌套驗證。

        我們修改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?props;
        }

        然后我們在ItemController的addItem函數(shù)上再使用@Validated或者@Valid,就能對Item的入?yún)⑦M(jìn)行嵌套驗證。此時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對切點方法織入增強(qiáng)。

        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進(jìn)行增強(qiáng)
        ????????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?{
        ????????//無需增強(qiáng)的方法,直接跳過
        ????????if?(isFactoryBeanMetadataMethod(invocation.getMethod()))?{
        ????????????return?invocation.proceed();
        ????????}
        ????????//獲取分組信息
        ????????Class[]?groups?=?determineValidationGroups(invocation);
        ????????ExecutableValidator?execVal?=?this.validator.forExecutables();
        ????????Method?methodToValidate?=?invocation.getMethod();
        ????????Set?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()進(jìn)行參數(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進(jìn)行真正的校驗處理。

        404等錯誤的統(tǒng)一處理

        參考博客

        參考

        Spring?Validation實現(xiàn)原理及如何運(yùn)用
        SpringBoot參數(shù)校驗和國際化使用
        @Valid@Validated區(qū)別
        Spring Validation最佳實踐及其實現(xiàn)原理,參數(shù)校驗沒那么簡單!

        來源:https://www.cnblogs.com/54chensongxia/p/14016179.html

        掃碼二維碼

        獲取更多精彩

        Java樂園

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

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            秋霞午夜成人无码精品 | 日本一道本精品网站 | 国产精品美女久久久久久久网站 | 狠狠干狠狠草 | 亚洲AV人片五月天婷婷在线播放 | 少妇高潮视频在线观看 | 97爱爱爱 | 插穴视频在线观看 | 亚洲h片 | 日本xxxxxxxxx8泡妞 |