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)雅地進(jìn)行參數(shù)校驗(yàn)

        共 3882字,需瀏覽 8分鐘

         ·

        2020-10-11 09:09

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

        引言

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

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

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

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

        1. 環(huán)境搭建

        springboot-2.3開始,校驗(yàn)包被獨(dú)立成了一個(gè)starter組件,所以需要引入如下依賴:



        ????org.springframework.boot
        ????spring-boot-starter-validation



        ????org.springframework.boot
        ????spring-boot-starter-web

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

        2.小試牛刀

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

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

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

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

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

        age?不能為空

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

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

        3. 規(guī)范返回值

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

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

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

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

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

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

        4. 全局異常處理

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

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

        全局異常處理類只需要在類上標(biāo)注@RestControllerAdvice,并在處理相應(yīng)異常的方法上使用@ExceptionHandler注解,寫明處理哪個(gè)異常即可。

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

        }

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

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

        注:?jiǎn)蝹€(gè)參數(shù)校驗(yàn)需要在參數(shù)上增加校驗(yàn)注解,并在類上標(biāo)注@Validated

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

        5.分組校驗(yàn)

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

        分組校驗(yàn)有三個(gè)步驟:

        1. 定義一個(gè)分組類(或接口)
        2. 在校驗(yàn)注解上添加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);
        }

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

        /**
        ?*?Default?Jakarta?Bean?Validation?group.
        ?*?


        ?*?Unless?a?list?of?groups?is?explicitly?defined:
        ?*?


          ?*?????
        • constraints?belong?to?the?{@code?Default}?group

        • ?*?????
        • validation?applies?to?the?{@code?Default}?group

        • ?*?

        ?*?Most?structural?constraints?should?belong?to?the?default?group.
        ?*
        ?*?@author?Emmanuel?Bernard
        ?*/

        public?interface?Default?{
        }

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

        @Validated({Update.class})

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

        請(qǐng)求一下/update接口可以看到,不僅校驗(yàn)了name字段,也校驗(yàn)了其他默認(rèn)屬于Default.class分組的字段

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

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

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

        6.遞歸校驗(yàn)

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

        OrderVO類如下

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

        在 UserVO 類中增加一個(gè) OrderVO 類型的屬性

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

        調(diào)用請(qǐng)求驗(yàn)證如下:

        7. 自定義校驗(yàn)

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

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

        1. 自定義校驗(yàn)注解
        2. 編寫校驗(yàn)者類

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

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

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

        ????Class[]?payload()?default?{?};

        ????/**
        ?????*?同一個(gè)元素上指定多個(gè)該注解時(shí)使用
        ?????*/

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

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

        回顧

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

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

        推薦閱讀:


        喜歡我可以給我設(shè)為星標(biāo)哦

        好文章,我“在看”
        瀏覽 24
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        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>
            a√农村老熟女又粗又大 | 在线观看三级视频 | 国产视频亚洲视频 | 社区操逼视频 | 国产一级婬A片AAA人与鲁 | 国产精品美女久久久久av爽李椋 | 黄色毛片在线免费观看 | 黄色免费视频网站 | 亚洲无码在线观看免费 | 亚洲va韩国va欧美va精品 |