Spring Boot - 數(shù)據(jù)校驗(yàn)

[TOC]

簡(jiǎn)介

后端編程中觉增,通常對(duì)于前端傳遞過(guò)來(lái)的數(shù)據(jù),我們都需要進(jìn)行校驗(yàn)翻斟,確保數(shù)據(jù)正確且安全逾礁。

最直接的方法當(dāng)然是在 Controller 相應(yīng)方法內(nèi)對(duì)數(shù)據(jù)進(jìn)行手動(dòng)校驗(yàn),但是访惜,由于很多校驗(yàn)都具備相似性嘹履,因此這種做法稍顯冗余。

因此债热,相關(guān)的校驗(yàn)規(guī)范就應(yīng)運(yùn)而生砾嫉。比如:

  • JSR-303:它是一項(xiàng) Bean Validation 校驗(yàn)標(biāo)準(zhǔn),規(guī)定了一些校驗(yàn)規(guī)范窒篱,比如@Null焕刮,@NotNull@Pattern墙杯,相關(guān)注解都位于javax.validation.constraints包下配并。需要注意的是,JSR-303 只提供校驗(yàn)規(guī)范高镐,不提供實(shí)現(xiàn)溉旋。

JSR-303 是 Bean Validation 1.0 版本,隨著越來(lái)越多的新規(guī)范并入嫉髓,它的版本也一直在更新观腊,比如,JSR-349 就是 Bean Validation 1.1 版本算行,而當(dāng)前最新的版本為 JSR-380梧油,也即 Bean Validation 2.0 版本...

由于 JSR-303 只提供規(guī)范,因此其實(shí)現(xiàn)需要其他庫(kù)進(jìn)行提供州邢。當(dāng)前使用最廣泛的 Bean Validation 實(shí)現(xiàn)庫(kù)為:hibernate-validator儡陨。

hibernate-validator 是對(duì) JSR-303 的實(shí)現(xiàn),同時(shí)它也增添了其他一些校驗(yàn)注解偷霉,比如迄委,@URL@Length类少,@Ranger等叙身。

而在 Spring 中,其也提供了相應(yīng)的 Bean Validation 實(shí)現(xiàn):Java Bean Validation硫狞。
Spring Validation 主要是對(duì) hibernate-validator 進(jìn)行了二次封裝信轿,并在 SpringMVC 中添加了自動(dòng)校驗(yàn)晃痴,以及將校驗(yàn)信息封裝進(jìn)特定類(lèi)中等功能。

本文主要介紹下在 Spring Boot 中進(jìn)行數(shù)據(jù)校驗(yàn)(Bean Validation)财忽。

依賴(lài)添加

Spring Boot 中進(jìn)行數(shù)據(jù)校驗(yàn)需要添加起步依賴(lài):spring-boot-starter-validation倘核,如下所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

:在spring-boot-starter-web舊版本中,其內(nèi)置了spring-boot-starter-validation即彪,但是 Spring Boot 官方似乎認(rèn)為并不是很多應(yīng)用會(huì)使用數(shù)據(jù)校驗(yàn)功能岳链,因此對(duì)其進(jìn)行了移除迅办。具體請(qǐng)參考:issue#19550

基本使用

數(shù)據(jù)校驗(yàn)最基本的操作就是使用相關(guān)注解對(duì)一個(gè) Java Bean 內(nèi)的相關(guān)字段進(jìn)行約束,然后前端傳遞上來(lái)的數(shù)據(jù)會(huì)首先組裝為相應(yīng)的 Java Bean 對(duì)象奴饮,該對(duì)象會(huì)被移交到一個(gè)Validator黍翎,讓其檢查對(duì)象字段(即數(shù)據(jù))是否滿(mǎn)足約束盏浙,如果不滿(mǎn)足的話(huà)佩研,則會(huì)通過(guò)如拋出異常等方式通知系統(tǒng)。

具體的使用步驟如下所示:

  1. 首先定義一個(gè)需要校驗(yàn)的 Java Bean 類(lèi):

    @Data
    public class User {
        private int id;
    
        @NotBlank(message = "用戶(hù)名不能為空")
        private String name;
    
        @NotNull(message = "請(qǐng)輸入密碼")
        @Length(min = 6, max = 10, message = "密碼為 6 到 10 位")
        private String password;
    
        @Email
        private String email;
    }
    

    上述代碼中舞终,我們使用@NotBlank轻庆、@NotNull@Length@Email等注解對(duì)User類(lèi)中的相應(yīng)字段進(jìn)行了約束敛劝。
    各注解對(duì)應(yīng)的約束內(nèi)容請(qǐng)參考后文余爆。

  2. 在 Controller 相應(yīng)接口方法中,使用@Valid/@Validated等注解開(kāi)啟數(shù)據(jù)校驗(yàn)功能:

    @RestController
    @RequestMapping("validate")
    public class ValidationController {
    
        @PostMapping("/user")
        public String addUser(@Validated @RequestBody User user){
            return "add user successfully! " + user;
        }
    }
    
  3. 如果數(shù)據(jù)校驗(yàn)不通過(guò)攘蔽,就會(huì)拋出一個(gè)MethodArgumentNotValidException異常龙屉。默認(rèn)情況下呐粘,Spring 會(huì)將該異常及其信息以錯(cuò)誤碼 400 進(jìn)行下發(fā)满俗。我們可以通過(guò)自定義一個(gè)全局異常捕獲器攔截該異常,提取出數(shù)據(jù)校驗(yàn)出錯(cuò)信息作岖,進(jìn)行展示:

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public String handlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
            return e.getBindingResult().getFieldErrors()
                    .stream()
                    .map(fieldError -> {
                        return String.format("[%s: %s]\n", fieldError.getField(), fieldError.getDefaultMessage());
                    }).collect(Collectors.joining());
        }
    }
    

以上唆垃,就完成了一個(gè)基礎(chǔ)的數(shù)據(jù)校驗(yàn)功能。

此時(shí)我們進(jìn)行如下訪(fǎng)問(wèn):

$ curl http://localhost:8080/validate/user --header "Content-Type: application/json;charset=UTF-8" -X POST --data "{\"password\": \"123456\"}"
[name: 用戶(hù)名不能為空]

$ curl http://localhost:8080/validate/user --header "Content-Type: application/json;charset=UTF-8" -X POST --data "{\"name\": \"Whyn\",\"password\": \"12345\"}"
[password: 密碼為 6 到 10 位]

$ curl http://localhost:8080/validate/user --header "Content-Type: application/json;charset=UTF-8" -X POST --data "{\"name\": \"Whyn\",\"password\": \"123456\"}"
add user successfully! User(id=0, name=Whyn, password=123456, email=null)

可以看到痘儡,結(jié)果符合預(yù)期辕万。

:上述代碼如果數(shù)據(jù)校驗(yàn)不通過(guò),就會(huì)拋出MethodArgumentNotValidException沉删,其實(shí)是因?yàn)槲覀冊(cè)跒閰?shù)注解了@RequestBody渐尿,此時(shí)HttpMessageConverter會(huì)負(fù)責(zé)轉(zhuǎn)換過(guò)程,當(dāng)遇到數(shù)據(jù)校驗(yàn)失敗時(shí)矾瑰,就會(huì)拋出MethodArgumentNotValidException砖茸。
而如果去除@RequestBody注解,默認(rèn)就會(huì)由@ModelAttribute負(fù)責(zé)數(shù)據(jù)綁定和校驗(yàn)殴穴,如果此時(shí)校驗(yàn)失敗凉夯,則會(huì)拋出BindException(更多詳情货葬,可參考:issue#14790),因此劲够,為了程序更加健壯震桶,最好為我們的全局異常處理器增加BindException異常捕獲。如下所示:

@RestControllerAdvice
public class GlobalExceptionHandler {

    ...
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleBindException(BindException e){
        return e.getBindingResult().getFieldErrors()
                .stream()
                .map(fieldError -> {
                    return String.format("[%s: %s]\n", fieldError.getField(), fieldError.getDefaultMessage());
                }).collect(Collectors.joining());
    }
}

此時(shí)征绎,請(qǐng)求上述代碼蹲姐,結(jié)果如下:

$ curl http://localhost:8080/validate/user -X POST
[name: 用戶(hù)名不能為空]
[password: 請(qǐng)輸入密碼]

$ curl http://localhost:8080/validate/user -X POST --data "name=Whyn"
[password: 請(qǐng)輸入密碼]

$ curl http://localhost:8080/validate/user -X POST --data "name=Whyn" --data "password=123456"
add user successfully! User(id=0, name=Whyn, password=123456, email=null, phoneNo=null)

上面是對(duì)復(fù)雜數(shù)據(jù)(Java Bean)的校驗(yàn)使用方式,而如果前端傳遞的是簡(jiǎn)單基本類(lèi)型(比如String)或者是對(duì)路徑變量(Path Variable)進(jìn)行校驗(yàn)人柿,可使用如下方式:

@RestController
@RequestMapping("validate")
@Validated
public class ValidationController {

    @GetMapping("/user/{id}")
    public String getUser(@PathVariable("id") @Min(10) int id) {
        return "User id is " + id;
    }

    @PutMapping("/user")
    public String updateUser(@RequestParam("name") @NotBlank String name,
                             @RequestParam("email") @Email String email) {
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        return "update user done: " + user;
    }
}

可以看到淤堵,對(duì)于簡(jiǎn)單數(shù)據(jù)類(lèi)型,我們將約束注解直接注解到相應(yīng)參數(shù)上顷扩,然后在Controller類(lèi)上使用@Validated注解拐邪,啟動(dòng)數(shù)據(jù)校驗(yàn)。

對(duì)于這種數(shù)據(jù)校驗(yàn)方式隘截,當(dāng)校驗(yàn)失敗時(shí)扎阶,會(huì)拋出ConstraintViolationException,而不是我們上面對(duì) Java Bean 校驗(yàn)失敗拋出的MethodArgumentNotValidException異常婶芭,因此东臀,可以為我們的全局異常處理器捕獲該異常,進(jìn)行處理犀农。如下所示:

@RestControllerAdvice
public class GlobalExceptionHandler {
    ...
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleConstraintViolationException(ConstraintViolationException e) {
        return e.getConstraintViolations()
                .stream()
                .map(constraintViolation -> {
                    return String.format("[%s: %s]\n",
                            constraintViolation.getPropertyPath().toString(),
                            constraintViolation.getMessage());
                }).collect(Collectors.joining());
    }
}

請(qǐng)求上述代碼惰赋,如下所示:

$ curl -X GET http://localhost:8080/validate/user/1
[getUser.id: must be greater than or equal to 10]

$ curl -X GET http://localhost:8080/validate/user/10
User id is 10

$ curl http://localhost:8080/validate/user -X PUT --data "name=" --data "email=10"
[updateUser.name: must not be blank]
[updateUser.email: must be a well-formed email address]

$ curl http://localhost:8080/validate/user -X PUT --data "name=Whyn" --data "email=10@qq.com"
update user done: User(id=0, name=Whyn, password=null, email=10@qq.com, extraInfo=null)

Bean Validation 相關(guān)注解

  • 下面主要介紹下 JSR 中一些常用的相關(guān)約束注解,如下所示:

    注解 釋義 可被注解元素類(lèi)型
    @NotNull 被注解的元素不能為null 所有類(lèi)型
    @NotBlank 被注解的元素不能為null呵哨,且至少包含一個(gè)非空白字符 支持CharSequence
    @NotEmpty 被注解的元素不能為null赁濒,且不能為空(即不能為空集合) 支持CharSequenceCollection孟害、Map拒炎、Array
    @Min(value) 被注解的元素值必須大于或等于@Min指定的值 支持BigDecimalBigInteger挨务,以及byte击你、short等基本數(shù)值類(lèi)型及其他們相應(yīng)的包裝類(lèi)型
    @Max(value) 被注解的元素值必須小于或等于@Max指定的值 支持BigDecimalBigInteger谎柄,以及byte丁侄、short等基本數(shù)值類(lèi)型及其他們相應(yīng)的包裝類(lèi)型
    @Size(max=, min=) 被注解的元素大小必須在指定的范圍內(nèi) CharSequenceCollection朝巫、Map鸿摇、Array以及null
    null元素會(huì)被認(rèn)為是有效值
    @Pattern 被注解的元素必須符合指定的正則匹配 CharSequence
    null類(lèi)型元素會(huì)被認(rèn)為是有效值
    @AssertTrue 被注解的元素值必須為true 支持boolean捍歪、Boolean類(lèi)型
    @AssertFalse 被注解的元素值必須為false 支持boolean户辱、Boolean類(lèi)型

    更多 JSR 相關(guān)注解內(nèi)容鸵钝,請(qǐng)參考:javax.validation.constraints

  • 下面介紹下 hibernate-validator 的一些常用特有注解:

    注解 釋義 可被注解元素類(lèi)型
    @Length(min=,max=) 被注解的字符串長(zhǎng)度必須在指定范圍內(nèi) 字符串
    @Range(min=,max=) 被注解的元素必須在指定范圍內(nèi) 數(shù)值類(lèi)型或者數(shù)值字符串類(lèi)型
    @URL 被注解的字符串匹配 URL 字符串

    更多 hibernate-validator 相關(guān)注解內(nèi)容,請(qǐng)參考:org.hibernate.validator.constraints

  • 下面介紹下 Spring Bean Validation 的一些常用特有注解:

    注解 釋義 可被注解元素類(lèi)型
    @Validated 開(kāi)啟數(shù)據(jù)校驗(yàn)功能庐镐,支持分組校驗(yàn) 任何非原子類(lèi)型

    更多 Spring Bean Validation 相關(guān)注解內(nèi)容恩商,請(qǐng)參考:org.springframework.validation.annotation

    @Validated注解是@Valid注解的一個(gè)變種實(shí)現(xiàn),它們都主要用于啟動(dòng)數(shù)據(jù)校驗(yàn)功能必逆,而不同之處大致有以下幾方面:

    • @Valid是屬于 JSR 規(guī)范怠堪,其位于包javax內(nèi);而@Validated是屬于 Spring Bean Validation名眉,其位于包org.springframework.validation內(nèi)粟矿。

    • @Valid支持嵌套校驗(yàn)(就是一個(gè) Bean 內(nèi)嵌套另一個(gè) Bean),而@Validated不支持损拢。如下所示:

      @Data
      public class User {
          ...
          @Valid // 嵌套校驗(yàn)
          private ExtraInfo extraInfo;
      
      
          @Data
          public static class ExtraInfo {
              @Pattern(regexp = "\\b(male|female)\\b", message = "male or female")
              @NotBlank(message = "性別不能為空")
              private String sex;
      
              @Min(0)
              @Max(130)
              private int age;
      
          }
      }
      

      :嵌套校驗(yàn)只需要求嵌套 Bean 內(nèi)使用@Valid注解陌粹,而啟動(dòng)數(shù)據(jù)校驗(yàn)(即 Controller 層)使用@Valid或者@Validated都可以。

      請(qǐng)求上述代碼福压,如下所示:

      $ curl http://localhost:8080/validate/user --header "Content-Type: application/json;charset=UTF-8" -X POST --data "{\"name\": \"Whyn\",\"password\": \"123456\",\"extraInfo\": {\"sex\": \"男\(zhòng)"}}"
      [extraInfo.sex: male or female]
      
      $ curl http://localhost:8080/validate/user --header "Content-Type: application/json;charset=UTF-8" -X POST --data "{\"name\": \"Whyn\",\"password\": \"123456\",\"extraInfo\": {\"sex\": \"male\"}}"
      add user successfully! User(id=0, name=Whyn, password=123456, email=null, extraInfo=User.ExtraInfo(sex=male, age=0))
      
    • @Validated支持分組校驗(yàn)功能掏秩,而@Valid不支持。啟動(dòng)分組校驗(yàn)步驟如下所示:

      1. 首先創(chuàng)建兩個(gè)分組接口:
      public interface ValidationGroup1 {}
      public interface ValidationGroup2 {}
      
      1. 在實(shí)體類(lèi)中添加分組信息:
      @Data
      public class User {
          private int id;
      
          // 隸屬分組 1
          @NotBlank(message = "用戶(hù)名不能為空", groups = ValidationGroup1.class)
          private String name;
      
          // 隸屬分組 1 和 2
          @NotNull(message = "請(qǐng)輸入密碼", groups = {ValidationGroup1.class, ValidationGroup2.class})
          // 不進(jìn)行分組
          @Length(min = 6, max = 10, message = "密碼為 6 到 10 位")
          private String password;
      
          // 不進(jìn)行分組
          @Email
          private String email;
      }
      
      1. 使用@Validated指定分組:
      @RestController
      @RequestMapping("validate")
      public class ValidationController {
      
          @PostMapping("/user")
          public String addUser(@Validated(ValidationGroup2.class) @RequestBody User user){
              return "add user successfully! " + user;
          }
      }
      

      上述代碼我們指定使用分組ValidationGroup2進(jìn)行數(shù)據(jù)校驗(yàn)荆姆,ValidationGroup2只對(duì)password進(jìn)行NotNull約束蒙幻,因此,只要我們發(fā)送的數(shù)據(jù)滿(mǎn)足password不為null胆筒,就可以通過(guò)校驗(yàn)邮破,如下所示:

      $ curl http://localhost:8080/validate/user --header "Content-Type: application/json;charset=UTF-8" -X POST --data "{\"name\": \"Whyn\"}"
      [password: 請(qǐng)輸入密碼]
      
      $ curl http://localhost:8080/validate/user --header "Content-Type: application/json;charset=UTF-8" -X POST --data "{\"name\": \"Whyn\",\"password\": \"\"}"
      add user successfully! User(id=0, name=Whyn, password=, email=null)
      

      :分組校驗(yàn)的一個(gè)問(wèn)題就是,對(duì)于未指定分組的其他校驗(yàn)仆救,直接忽略抒和,通常這并不是我們想要的結(jié)果。對(duì)于未指定分組的校驗(yàn)派桩,我們通常期望的是构诚,無(wú)論使用哪種分組校驗(yàn),這些未指定的分組校驗(yàn)均生效铆惑。
      實(shí)際上,未指定分組的校驗(yàn)都?xì)w類(lèi)為 默認(rèn)分組(Default送膳,且分組支持繼承员魏,子類(lèi)分組可完全繼承父類(lèi)分組的約束校驗(yàn),因此叠聋,只需讓我們的自定義分組繼承默認(rèn)分組撕阎,即可完成分組校驗(yàn)以及默認(rèn)分組生效,代碼如下:

      public interface ValidationGroup1 extends Default {}
      public interface ValidationGroup2 extends Default {}
      

    綜上碌补,一個(gè)比較推薦的使用方式就是:?jiǎn)?dòng)校驗(yàn)(即 Controller 層)時(shí)使用@Validated注解虏束,嵌套校驗(yàn)時(shí)使用@Valid注解棉饶,這樣,就能同時(shí)使用分組校驗(yàn)和嵌套校驗(yàn)功能镇匀。

自定義Validator

前文講過(guò)照藻,數(shù)據(jù)校驗(yàn)功能是由Validator負(fù)責(zé)開(kāi)啟并校驗(yàn)的,在 SpringMVC 中汗侵,如果檢測(cè)到 Bean Validation(比如幸缕,Hibernate Validator)存在于classpath路徑上時(shí),就會(huì)默認(rèn)全局注冊(cè)了一個(gè)ValidatorLocalValidatorFactoryBean晰韵,它會(huì)驅(qū)動(dòng)@Valid@Validated開(kāi)啟數(shù)據(jù)校驗(yàn)发乔。

LocalValidatorFactoryBean同時(shí)實(shí)現(xiàn)了javax.validation.ValidatorFactoryjavax.validation.Validatororg.springframework.validation.Validator三個(gè)接口雪猪,所以如果需要手動(dòng)調(diào)用數(shù)據(jù)校驗(yàn)邏輯栏尚,可以通過(guò) IOC 容器獲取到這些接口的實(shí)例。如下所示:

  • 獲取javax.validation.Validator接口實(shí)例:
    import javax.validation.Validator;
    
    @Service
    public class MyService {
    
        @Autowired
        private Validator validator;
    }
    
  • 獲取org.springframework.validation.Validator接口實(shí)例:
    import org.springframework.validation.Validator;
    
    @Service
    public class MyService {
    
        @Autowired
        private Validator validator;
    }
    

上述獲取的是系統(tǒng)默認(rèn)的Validator只恨,而如果我們想注入一個(gè)自定義Validator抵栈,有如下幾種方法:

  • 注入自定義Validator到 Spring IOC 容器:

    import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
    
    @Configuration
    public class AppConfig {
    
        @Bean
        public LocalValidatorFactoryBean validator() {
            return new LocalValidatorFactoryBean();
        }
    }
    
  • 為 SpringMVC 配置一個(gè)全局Validator

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public Validator getValidator() {
            // ...
        }
    }
    

    也可以為單獨(dú)一個(gè) Controller 設(shè)置一個(gè)局部Validator,如下所示:

    @Controller
    public class MyController {
    
        @InitBinder
        protected void initBinder(WebDataBinder binder) {
            binder.addValidators(new FooValidator());
        }
    }
    

自定義約束注解

如果現(xiàn)存的約束注解無(wú)法滿(mǎn)足我們的需求坤次,那么我們可以通過(guò)自定義約束注解古劲,來(lái)定制我們的數(shù)據(jù)校驗(yàn)邏輯。

在 Spring 中缰猴,自定義約束注解主要就是定義一個(gè)約束注解及其對(duì)應(yīng)的Validator产艾,兩者通過(guò)@Constraint關(guān)聯(lián)到一起。
默認(rèn)情況下滑绒,全局校驗(yàn)器LocalValidatorFactoryBean會(huì)配置一個(gè)SpringConstraintValidatorFactory實(shí)例闷堡,SpringConstraintValidatorFactory實(shí)現(xiàn)了接口ConstraintValidatorFactory,因此它會(huì)在遇到自定義約束注解的時(shí)候疑故,就會(huì)自動(dòng)實(shí)例化@Constraint指定的關(guān)聯(lián)Validator杠览,從而完成數(shù)據(jù)校驗(yàn)過(guò)程。

詳細(xì)過(guò)程可參考如下示例:

例子:假設(shè)我們想自定義一個(gè)約束注解纵势,用于對(duì)手機(jī)號(hào)進(jìn)行校驗(yàn)踱阿,要求滿(mǎn)足手機(jī)號(hào)碼的格式為:+86 13699328716,即以+86開(kāi)頭钦铁,然后中間一個(gè)或多個(gè)空格软舌,后面是有效的手機(jī)號(hào)碼。

自定義約束注解的步驟如下所示:

  1. 自定義一個(gè)約束注解:

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = PhoneNoConstraintValidator.class)
    public @interface PhoneNoConstraint {
        String message() default "手機(jī)號(hào)碼格式錯(cuò)誤";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    

    這里通過(guò)注解@Constraint將自定義注解PhoneNoConstraintPhoneNoConstraintValidator(即一個(gè)自定義Validator)關(guān)聯(lián)到一起牛曹。

  2. 自定義一個(gè)Validator

    public class PhoneNoConstraintValidator implements ConstraintValidator<PhoneNoConstraint, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            String regex = "\\+86\\s+\\d{11}";
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(value);
            return matcher.matches();
        }
    }
    
  3. 使用自定義約束注解:

    @RestController
    @RequestMapping("validate")
    @Validated
    public class ValidationController {
    
        @PostMapping("/user/{id}")
        public String addPhoneNo(@PathVariable("id") int id,
                                 @RequestParam("phoneNo")
                                 @NotBlank(message = "手機(jī)號(hào)不能為空")
                                 @PhoneNoConstraint(message = "手機(jī)號(hào)必須以 +86 開(kāi)頭")
                                         String phoneNo) {
            return id + " => add phoneNo done: " + phoneNo;
    
        }
    }
    

    當(dāng)程序運(yùn)行時(shí)佛点,遇到自定義約束注解@PhoneNoConstraint時(shí),SpringConstraintValidatorFactory就會(huì)通過(guò)@PhoneNoConstraint上的@Constraint注解,獲取得到其對(duì)應(yīng)的Valiator超营,然后通過(guò) Spring 創(chuàng)建該Validator實(shí)例鸳玩,進(jìn)行數(shù)據(jù)校驗(yàn)。利用這種機(jī)制演闭,可以使得我們的自定義Validator享受到其他 Java Bean 一樣的依賴(lài)注入功能不跟。

    請(qǐng)求上述代碼,結(jié)果如下:

    $ curl localhost:8080/validate/user/1 -X POST --data-urlencode "phoneNo=13699328716"
    [addPhoneNo.phoneNo: 手機(jī)號(hào)必須以 +86 開(kāi)頭]
    
    $ curl localhost:8080/validate/user/1 -X POST --data-urlencode "phoneNo=+86 13699328716"
    1 => add phoneNo done: +86 13699328716
    

    :如果 URL 包含+船响、=躬拢、&等特殊符號(hào)時(shí),會(huì)被進(jìn)行轉(zhuǎn)義见间,比如聊闯,+會(huì)被轉(zhuǎn)義為空格,這樣后端接收的數(shù)據(jù)格式就永遠(yuǎn)是錯(cuò)誤的米诉,因此菱蔬,發(fā)送數(shù)據(jù)前,應(yīng)先對(duì)數(shù)據(jù)進(jìn)行編碼史侣,所以上述curl命令使用--data-urlencode對(duì)數(shù)據(jù)進(jìn)行編碼拴泌,以確保特殊字符能成功發(fā)送。

其他

  • 除了對(duì) Controller 層添加數(shù)據(jù)校驗(yàn)外惊橱,還可以為 Spring 其他組件添加數(shù)據(jù)校驗(yàn)功能蚪腐,只需結(jié)合@Validated@Valid這兩個(gè)注解。
    比如税朴,對(duì) Serivce 層添加數(shù)據(jù)校驗(yàn)功能回季,如下所示:
    @Service
    @Validated
    class ValidatingService{
    
        void validateInput(@Valid Input input){
          // do something
        }
    }
    

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末正林,一起剝皮案震驚了整個(gè)濱河市泡一,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌觅廓,老刑警劉巖鼻忠,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異杈绸,居然都是意外死亡帖蔓,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)蝇棉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)讨阻,“玉大人,你說(shuō)我怎么就攤上這事篡殷。” “怎么了埋涧?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵板辽,是天一觀的道長(zhǎng)奇瘦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)劲弦,這世上最難降的妖魔是什么耳标? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮邑跪,結(jié)果婚禮上次坡,老公的妹妹穿的比我還像新娘。我一直安慰自己画畅,他們只是感情好砸琅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著轴踱,像睡著了一般症脂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上淫僻,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天诱篷,我揣著相機(jī)與錄音,去河邊找鬼雳灵。 笑死棕所,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悯辙。 我是一名探鬼主播琳省,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼笑撞!你這毒婦竟也來(lái)了岛啸?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤茴肥,失蹤者是張志新(化名)和其女友劉穎坚踩,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瓤狐,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瞬铸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了础锐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嗓节。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖皆警,靈堂內(nèi)的尸體忽然破棺而出拦宣,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布鸵隧,位于F島的核電站绸罗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏豆瘫。R本人自食惡果不足惜珊蟀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望外驱。 院中可真熱鬧育灸,春花似錦、人聲如沸昵宇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)趟薄。三九已至绽诚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間杭煎,已是汗流浹背恩够。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留羡铲,地道東北人蜂桶。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像也切,于是被迫代替她去往敵國(guó)和親扑媚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345