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>

        使用 Spring Validation 優(yōu)雅地進行參數(shù)校驗

        共 13507字,需瀏覽 28分鐘

         ·

        2021-09-05 18:30

        寫得好的沒我寫得全,寫得全的沒我寫得好

        引言

        不知道大家平時的業(yè)務開發(fā)過程中 controller 層的參數(shù)校驗都是怎么寫的?是否也存在下面這樣的直接判斷?

        public String add(UserVO userVO) {
            if(userVO.getAge() == null){
                return "年齡不能為空";
            }
            if(userVO.getAge() > 120){
                return "年齡不能超過120";
            }
            if(userVO.getName().isEmpty()){
                return "用戶名不能為空";
            }
            // 省略一堆參數(shù)校驗...
            return "OK";
        }

        業(yè)務代碼還沒開始寫呢,光參數(shù)校驗就寫了一堆判斷。這樣寫雖然沒什么錯,但是給人的感覺就是:不優(yōu)雅,不專業(yè)。

        其實Spring框架已經(jīng)給我們封裝了一套校驗組件:validation。其特點是簡單易用,自由度高。接下來課代表使用springboot-2.3.1.RELEASE搭建一個簡單的 Web 工程,給大家一步一步講解在開發(fā)過程中如何優(yōu)雅地做參數(shù)校驗。

        1. 環(huán)境搭建

        springboot-2.3開始,校驗包被獨立成了一個starter組件,所以需要引入如下依賴:

        <!--校驗組件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!--web組件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        springboot-2.3之前的版本只需要引入 web 依賴就可以了。

        2.小試牛刀

        參數(shù)校驗非常簡單,首先在待校驗字段上增加校驗規(guī)則注解

        public class UserVO {
            @NotNull(message = "age 不能為空")
            private Integer age;
        }

        然后在controller方法中添加@Validated和用于接收錯誤信息的BindingResult就可以了,于是有了第一版:

        public String add1(@Validated UserVO userVO, BindingResult result) {
            List<FieldError> fieldErrors = result.getFieldErrors();
            if(!fieldErrors.isEmpty()){
                return fieldErrors.get(0).getDefaultMessage();
            }
            return "OK";
        }

        通過工具(postman)去請求接口,如果參數(shù)不符合規(guī)則,會將相應的 message信息返回:

        age 不能為空

        內(nèi)置的校驗注解有很多,羅列如下:

        注解校驗功能
        @AssertFalse必須是false
        @AssertTrue必須是true
        @DecimalMax小于等于給定的值
        @DecimalMin大于等于給定的值
        @Digits可設定最大整數(shù)位數(shù)和最大小數(shù)位數(shù)
        @Email校驗是否符合Email格式
        @Future必須是將來的時間
        @FutureOrPresent當前或?qū)頃r間
        @Max最大值
        @Min最小值
        @Negative負數(shù)(不包括0)
        @NegativeOrZero負數(shù)或0
        @NotBlank不為null并且包含至少一個非空白字符
        @NotEmpty不為null并且不為空
        @NotNull不為null
        @Null為null
        @Past必須是過去的時間
        @PastOrPresent必須是過去的時間,包含現(xiàn)在
        @PositiveOrZero正數(shù)或0
        @Size校驗容器的元素個數(shù)

        3. 規(guī)范返回值

        待校驗參數(shù)多了之后我們希望一次返回所有校驗失敗信息,方便接口調(diào)用方進行調(diào)整,這就需要統(tǒng)一返回格式,常見的就是封裝一個結(jié)果類。

        public class ResultInfo<T>{
            private Integer status;
            private String message;
            private T response;
            // 省略其他代碼...
        }

        改造一下controller 方法,第二版:

        public ResultInfo add2(@Validated UserVO userVO, BindingResult result) {
            List<FieldError> fieldErrors = result.getFieldErrors();
            List<String> collect = fieldErrors.stream()
                    .map(o -> o.getDefaultMessage())
                    .collect(Collectors.toList());
            return new ResultInfo<>().success(400,"請求參數(shù)錯誤",collect);
        }

        請求該方法時,所有的錯誤參數(shù)就都返回了:

        {
            "status"400,
            "message""請求參數(shù)錯誤",
            "response": [
                "年齡必須在[1,120]之間",
                "bg 字段的整數(shù)位最多為3位,小數(shù)位最多為1位",
                "name 不能為空",
                "email 格式錯誤"
            ]
        }

        4. 全局異常處理

        每個Controller方法中如果都寫一遍BindingResult信息的處理,使用起來還是很繁瑣??梢酝ㄟ^全局異常處理的方式統(tǒng)一處理校驗異常。

        當我們寫了@validated注解,不寫BindingResult的時候,Spring 就會拋出異常。由此,可以寫一個全局異常處理類來統(tǒng)一處理這種校驗異常,從而免去重復組織異常信息的代碼。

        全局異常處理類只需要在類上標注@RestControllerAdvice,并在處理相應異常的方法上使用@ExceptionHandler注解,寫明處理哪個異常即可。

        @RestControllerAdvice
        public class GlobalControllerAdvice {
            private static final String BAD_REQUEST_MSG = "客戶端請求參數(shù)錯誤";
            // <1> 處理 form data方式調(diào)用接口校驗失敗拋出的異常 
            @ExceptionHandler(BindException.class)
            public ResultInfo bindExceptionHandler(BindException e) {
                List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
                List<String> collect = fieldErrors.stream()
                        .map(o -> o.getDefaultMessage())
                        .collect(Collectors.toList());
                return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
            }
            // <2> 處理 json 請求體調(diào)用接口校驗失敗拋出的異常 
            @ExceptionHandler(MethodArgumentNotValidException.class)
            public ResultInfo methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
                List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
                List<String> collect = fieldErrors.stream()
                        .map(o -> o.getDefaultMessage())
                        .collect(Collectors.toList());
                return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
            }
            // <3> 處理單個參數(shù)校驗失敗拋出的異常
            @ExceptionHandler(ConstraintViolationException.class)
            public ResultInfo constraintViolationExceptionHandler(ConstraintViolationException e) {
                Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
                List<String> collect = constraintViolations.stream()
                        .map(o -> o.getMessage())
                        .collect(Collectors.toList());
                return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
            }
            

        }

        事實上,在全局異常處理類中,我們可以寫多個異常處理方法,課代表總結(jié)了三種參數(shù)校驗時可能引發(fā)的異常:

        1. 使用form data方式調(diào)用接口,校驗異常拋出 BindException
        2. 使用 json 請求體調(diào)用接口,校驗異常拋出 MethodArgumentNotValidException
        3. 單個參數(shù)校驗異常拋出ConstraintViolationException

        注:單個參數(shù)校驗需要在參數(shù)上增加校驗注解,并在類上標注@Validated。

        全局異常處理類可以添加各種需要處理的異常,比如添加一個對Exception.class的異常處理,當所有ExceptionHandler都無法處理時,由其記錄異常信息,并返回友好提示。

        5.分組校驗

        如果同一個參數(shù),需要在不同場景下應用不同的校驗規(guī)則,就需要用到分組校驗了。比如:新注冊用戶還沒起名字,我們允許name字段為空,但是不允許將名字更新為空字符。

        分組校驗有三個步驟:

        1. 定義一個分組類(或接口)
        2. 在校驗注解上添加groups屬性指定分組
        3. Controller方法的@Validated注解添加分組類
        public interface Update extends Default{
        }
        public class UserVO {
            @NotBlank(message = "name 不能為空",groups = Update.class)
            private String name;
            // 省略其他代碼...
        }
        @PostMapping("update")
        public ResultInfo update(@Validated({Update.class}) UserVO userVO) {
            return new ResultInfo().success(userVO);
        }

        細心的同學可能已經(jīng)注意到,自定義的Update分組接口繼承了Default接口。校驗注解(如:@NotBlank)和@validated默認都屬于Default.class分組,這一點在javax.validation.groups.Default注釋中有說明

        /**
         * Default Jakarta Bean Validation group.
         * <p>
         * Unless a list of groups is explicitly defined:
         * <ul>
         *     <li>constraints belong to the {@code Default} group</li>
         *     <li>validation applies to the {@code Default} group</li>
         * </ul>
         * Most structural constraints should belong to the default group.
         *
         * @author Emmanuel Bernard
         */

        public interface Default {
        }

        在編寫Update分組接口時,如果繼承了Default,下面兩個寫法就是等效的:

        @Validated({Update.class})

        @Validated({Update.class,Default.class})

        請求一下/update接口可以看到,不僅校驗了name字段,也校驗了其他默認屬于Default.class分組的字段

        {
            "status"400,
            "message""客戶端請求參數(shù)錯誤",
            "response": [
                "name 不能為空",
                "age 不能為空",
                "email 不能為空"
            ]
        }

        如果Update不繼承Default,@Validated({Update.class})就只會校驗屬于Update.class分組的參數(shù)字段,修改后再次請求該接口得到如下結(jié)果,可以看到, 其他字段沒有參與校驗:

        {
            "status"400,
            "message""客戶端請求參數(shù)錯誤",
            "response": [
                "name 不能為空"
            ]
        }

        6.遞歸校驗

        如果 UserVO 類中增加一個 OrderVO 類的屬性,而 OrderVO 中的屬性也需要校驗,就用到遞歸校驗了,只要在相應屬性上增加@Valid注解即可實現(xiàn)(對于集合同樣適用)

        OrderVO類如下

        public class OrderVO {
            @NotNull
            private Long id;
            @NotBlank(message = "itemName 不能為空")
            private String itemName;
            // 省略其他代碼...
        }

        在 UserVO 類中增加一個 OrderVO 類型的屬性

        public class UserVO {
            @NotBlank(message = "name 不能為空",groups = Update.class)
            private String name;
            //需要遞歸校驗的OrderVO
            @Valid
            private OrderVO orderVO;
            // 省略其他代碼...
        }   

        調(diào)用請求驗證如下:

        7. 自定義校驗

        Spring 的 validation 為我們提供了這么多特性,幾乎可以滿足日常開發(fā)中絕大多數(shù)參數(shù)校驗場景了。但是,一個好的框架一定是方便擴展的。有了擴展能力,就能應對更多復雜的業(yè)務場景,畢竟在開發(fā)過程中,唯一不變的就是變化本身。

        Spring Validation允許用戶自定義校驗,實現(xiàn)很簡單,分兩步:

        1. 自定義校驗注解
        2. 編寫校驗者類

        代碼也很簡單,結(jié)合注釋你一看就能懂

        @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
        @Retention(RUNTIME)
        @Documented
        @Constraint(validatedBy = {HaveNoBlankValidator.class})// 標明由哪個類執(zhí)行校驗邏輯
        public @interface HaveNoBlank {
         
            // 校驗出錯時默認返回的消息
            String message() default "字符串中不能含有空格";

            Class<?>[] groups() default { };

            Class<? extends Payload>[] payload() default { };

            /**
             * 同一個元素上指定多個該注解時使用
             */

            @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
            @Retention(RUNTIME)
            @Documented
            public @interface List {
                NotBlank[] value();
            }
        }
        public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlankString{
            @Override
            public boolean isValid(String value, ConstraintValidatorContext context) {
                // null 不做檢驗
                if (value == null) {
                    return true;
                }
                if (value.contains(" ")) {
                    // 校驗失敗
                    return false;
                }
                // 校驗成功
                return true;
            }
        }

        自定義校驗注解使用起來和內(nèi)置注解無異,在需要的字段上添加相應注解即可,同學們可以自行驗證

        回顧

        以上就是如何使用 Spring Validation 優(yōu)雅地校驗參數(shù)的全部內(nèi)容,下面重點總結(jié)一下文中提到的校驗特性

        1. 內(nèi)置多種常用校驗注解
        2. 支持單個參數(shù)校驗
        3. 結(jié)合全局異常處理自動組裝校驗異常
        4. 分組校驗
        5. 支持遞歸校驗
        6. 自定義校驗

        往期精彩文章:


        老外為了在MacBook上玩原神,讓M1支持了所有iOS應用


        老是忘記?Linux常用命令精編匯總


        API網(wǎng)關才是大勢所趨?SpringCloud Gateway保姆級入門教程


        從吳某凡事件中理解什么是"中間人攻擊"


        畢業(yè)三年,從P5到P7,他的晉升心得分享

        瀏覽 11
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        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浪潮 | 老熟女 露脸 嗷嗷叫91 | 久久露脸精品nv511com | 影音先锋AV啪啪资源 | 宝贝屁股翘起来浪一点 | 五月天色婷婷综合 | 五月丁香无码 | 香蕉毛片| 欧美一级AAA大片免费观看 | 国产精品乱伦视频 |