springMVC請(qǐng)求參數(shù)校驗(yàn)

下下下周,爭(zhēng)取做只水煮魚(yú)~~~
算了吧,買(mǎi)現(xiàn)成的調(diào)料吧~~~


fish.png

1 場(chǎng)景

JavaWeb后臺(tái)應(yīng)用程序,具體的執(zhí)行方法,收到請(qǐng)求,需要對(duì)請(qǐng)求的數(shù)據(jù)進(jìn)行基礎(chǔ)校驗(yàn),如字符串長(zhǎng)度限制、正則校驗(yàn)、數(shù)字區(qū)間校驗(yàn)等。

推薦在springMVC中對(duì)前臺(tái)的請(qǐng)求參數(shù)進(jìn)行統(tǒng)一校驗(yàn),校驗(yàn)方式建議采用JSR30標(biāo)準(zhǔn)進(jìn)行校驗(yàn)。

1.1 普通校驗(yàn)方式

最簡(jiǎn)單的校驗(yàn)方式是,對(duì)請(qǐng)求的參數(shù)手動(dòng)一個(gè)個(gè)進(jìn)行校驗(yàn),如下代碼:

@GetMapping("saveWithOld")
public JSONObject saveWithOld(User user) {
    JSONObject result = new JSONObject();
    if (user.getUserCode() == null || user.getUserCode() == "") {
        result.put("success", true);
        result.put("message", "用戶代碼不可為空");
        return result;
    }
    if (user.getUserName() == null || user.getUserName() == "") {
        result.put("success", true);
        result.put("message", "用戶名稱不可為空");
        return result;
    }
    // do something ......

    result.put("success", true);
    return result;
}

這種方式,代碼量非常大,代碼非常不友好。

1.2 springMVC校驗(yàn)方式

springMVC,在執(zhí)行后臺(tái)方法之前,可以對(duì)請(qǐng)求的數(shù)據(jù)通過(guò)注解進(jìn)行校驗(yàn)。此校驗(yàn)方式基于JSR303規(guī)范。

如下代碼所示:

@Data
public class User {
    @NotNull(message = "用戶代碼不可為空")
    private String userCode;
}
@GetMapping("saveWithNormal")
    public JSONObject saveWithNormal(@Valid User user) {
        JSONObject result = new JSONObject();
        result.put("success", true);
        result.put("message", user.toString());
        return result;
    }
@PostMapping("saveWithRequestParam")
public JSONObject saveWithRequestParam(@NotNull(message = "用戶代碼不可為空") String userCode) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", userCode);
    return result;
}

此種方式,可以使用注解,已更簡(jiǎn)單的方式對(duì)請(qǐng)求參數(shù)進(jìn)行校驗(yàn)。

3 版本說(shuō)明

本文中代碼涉及到的相關(guān)版本如下:

3.1 JDK

JDK1.8

3.2 maven依賴

spring-boot-starter-web中已包含了我們需要的依賴。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.7 </version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.18</version>
    <scope>provided</scope>
</dependency>

2 名詞關(guān)系說(shuō)明

這里講下springMVC中使用JSR303進(jìn)行參數(shù)校驗(yàn),相關(guān)的名詞含義及名詞之間的關(guān)系說(shuō)明。

2.1 基本說(shuō)明

springMVC基于JSR303規(guī)范進(jìn)行校驗(yàn)。

官網(wǎng)說(shuō)明:https://jcp.org/en/jsr/detail?id=303

規(guī)范的相關(guān)說(shuō)明如下:

JSR是Java Specification Requests的縮寫(xiě),意思是Java 規(guī)范提案 。
JSR-303 是JAVA EE 6 中的一項(xiàng)子規(guī)范,叫做Bean Validation。
Hibernate Validator是 Bean Validation的參考實(shí)現(xiàn)
Hibernate Validator提供了JSR 303 規(guī)范中所有內(nèi)置 constrain(約束)的實(shí)現(xiàn),除此之外還有一些附加的constraint(約束)。

2.2 詳細(xì)說(shuō)明

關(guān)于springMVC請(qǐng)求參數(shù)校驗(yàn),涉及幾個(gè)對(duì)應(yīng)的名詞,如下是:

名詞 說(shuō)明
constraint(約束) 對(duì)參數(shù)的校驗(yàn)約束注解,如@NotNull表示參數(shù)不可以為Null
校驗(yàn)注解 為元素加上約束后,有時(shí)候需要在參數(shù)前加上校驗(yàn)注解來(lái)開(kāi)啟驗(yàn)證。
相關(guān)注解有@Valid@Validated,如沒(méi)有用到注解獨(dú)有的特性(分組、嵌套)等,用哪個(gè)注解都一樣。

需注意不是所有的校驗(yàn)都需要開(kāi)啟校驗(yàn),如下不需要加上校驗(yàn)注解:
saveWithRequestParam(@NotNull(message = "用戶代碼不可為空") String userCode)
但是需要在Controller類上加上注解@Valid或@Validated
JSR303規(guī)范 行業(yè)規(guī)范標(biāo)準(zhǔn),包括校驗(yàn)的constraint(約束,如@NotNull)開(kāi)啟校驗(yàn)注解@Valid
體現(xiàn):代碼中體現(xiàn)為注解、接口無(wú)具體實(shí)現(xiàn)代碼
jar包:jakarta.validation-api-2.0.2.jar
約束注解:javax.validation.constraints包下注解+hibernate增強(qiáng)注解org.hibernate.validator.constraints
校驗(yàn)注解:javax.validation.Valid
Hibernate Validator Hibernate對(duì)JSR303規(guī)范中的約束constraint具體代碼實(shí)現(xiàn)
jar包:hibernate-validator-6.0.20.Final.jar
增強(qiáng):在原有JSR303的constraint(約束)增加了約束(如@Range)
spring JSR303 spring對(duì)JSR303的包裝,對(duì)原有的校驗(yàn)進(jìn)行了增強(qiáng)
增強(qiáng):分組校驗(yàn)順序校驗(yàn)
缺點(diǎn):不支持嵌套校驗(yàn)
約束注解:javax.validation.constraints包下注解+hibernate增強(qiáng)注解org.hibernate.validator.constraints
校驗(yàn)注解:org.springframework.validation.annotation.Validated
增強(qiáng)說(shuō)明:所謂的包裝和增強(qiáng),只是將@Valid注解擴(kuò)展為@Validated注解。約束注解和JSR303一樣。

2.3 關(guān)系圖

一圖勝千言。參數(shù)校驗(yàn)的相關(guān)說(shuō)明,關(guān)系圖如下:

spring參數(shù)校驗(yàn).jpg

3 校驗(yàn)流程

3.1 對(duì)象參數(shù)

3.1.1 說(shuō)明

對(duì)象參數(shù)中進(jìn)行約束校驗(yàn)。需滿足以下條件:

(1)在mapping方法中通過(guò)注解@Valid或@Validated指定要校驗(yàn)的參數(shù)對(duì)象

如下:

@GetMapping("saveWithNormal")
public JSONObject saveWithNormal(@Valid User user) {......}

(2)在對(duì)象參數(shù)對(duì)應(yīng)的類中,對(duì)需要校驗(yàn)的參數(shù)加上約束注解

如下:

@Data
public class User {
    /**
     * 用戶代碼
     */
    @NotNull(message = "用戶代碼不可為空")
    private String userCode;
}
3.1.2 校驗(yàn)流程

校驗(yàn)失敗后,需要對(duì)失敗的異常信息進(jìn)行處理,處理方式有兩種:

1、在mapping方法上加上參數(shù)BindingResult bindingResult

此種方式,校驗(yàn)失敗后,會(huì)將異常信息封裝到參數(shù)對(duì)象bindingResult中,可以自行對(duì)其中的異常信息進(jìn)行處理,封裝錯(cuò)位信息,返回請(qǐng)求結(jié)果。

這種情況,需要每個(gè)請(qǐng)求,都對(duì)參數(shù)BindingResult進(jìn)行處理,較為繁瑣,不建議此種方式。

如下:

@GetMapping("saveWithBind")
public JSONObject saveWithBind(@Valid User user, BindingResult bindingResult) {
    // --------------------[手動(dòng)檢測(cè)驗(yàn)證是否通過(guò)]--------------------
    if (bindingResult.hasErrors()) {
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            JSONObject result = new JSONObject();
            result.put("success", false);
            result.put("message", fieldError.getDefaultMessage());
            return result;
        }
    }
    // --------------------[驗(yàn)證檢測(cè)通過(guò)后執(zhí)行其他操作]--------------------
    // ......
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}

2、定義spring全局異常處理,捕捉對(duì)應(yīng)的異常信息,進(jìn)行統(tǒng)一處理

mapping方法上不加參數(shù)BindingResult bindingResult,校驗(yàn)失敗后,會(huì)拋出異常,異常信息,通過(guò)spring全局異常管理,統(tǒng)一對(duì)拋出的異常信息進(jìn)行處理,處理后統(tǒng)一封裝錯(cuò)位信息。這種方式,代碼量較少,且處理錯(cuò)誤信息集中,推薦此種方式。

如下代碼:

@Data
public class User {
    /**
     * 用戶代碼
     */
    @NotNull(message = "用戶代碼不可為空")
    private String userCode;
}
// 參數(shù)校驗(yàn)失敗,拋出異常:org.springframework.validation.BindException
@GetMapping("saveWithNormal")
public JSONObject saveWithNormal(@Valid User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}
/**
   * 捕捉全局異常:org.springframework.validation.BindException
   * <div>普通請(qǐng)求的參數(shù),校驗(yàn)失敗,拋出此異常</div>
   * <div>如:(@Valid User user)</div>
   *
   * @param exception
   * @return
   */
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {
    log.info("全局異常[BindException]:" + exception.getMessage());
    JSONObject result = new JSONObject();
    result.put("success", false);
    if (exception != null) {
        String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
        result.put("message", message);
    }
    return result;
}

需注意:參數(shù)上@Valid和@Validated使用方式的不同,校驗(yàn)失敗后,會(huì)拋出不同的異常

如下:

/**
  * 捕捉全局異常:org.springframework.validation.BindException
  * <div>普通請(qǐng)求的參數(shù),校驗(yàn)失敗,拋出此異常</div>
  * <div>如:(@Valid User user)</div>
  * @param exception
  * @return
  */
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {......}
/**
  * 捕捉全局異常:org.springframework.web.bind.MethodArgumentNotValidException
  * <div>@RequestBody修飾的參數(shù),校驗(yàn)失敗,拋出此異常</div>
  * <div>如:(@RequestBody @Valid User user)</div>
  * @param exception
  * @return
  */
@ExceptionHandler({MethodArgumentNotValidException.class})
public JSONObject handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {......}

總結(jié)校驗(yàn)流程圖如下:

對(duì)象參數(shù)校驗(yàn)流程.jpg

3.2 普通類型參數(shù)

3.2.1 說(shuō)明

普通類型參數(shù)中進(jìn)行約束校驗(yàn)。需滿足以下條件:

(1)在Controller類上通過(guò)注解@Validated開(kāi)啟校驗(yàn)

注意:需為@Validated注解,而不是@Valid注解

如下:

@Validated
@RestController
@RequestMapping("user")
public class UserController {......}

(2)在mapping方法普通類型參數(shù)前面加上約束注解

如下:

// 參數(shù)校驗(yàn)失敗,拋出異常:javax.validation.ConstraintViolationException
@PostMapping("saveWithRequestParam")
public JSONObject saveWithRequestParam(@NotNull(message="用戶代碼不可為空") String userCode){......}
// 參數(shù)校驗(yàn)失敗,拋出異常:org.springframework.web.bind.ConstraintViolationException
@GetMapping("saveWithRestful/{userCode}")
public JSONObject saveWithRest(@PathVariable("userCode") @Length(max = 10,message="用戶代碼不可超過(guò)10位") String userCode) {......}
3.2.1 校驗(yàn)流程

校驗(yàn)結(jié)果的處理流程同《3.1對(duì)象參數(shù)》

需注意:如使用全局異常捕捉,校驗(yàn)失敗后拋出的異常如下:

 /**
   * 捕捉全局異常:javax.validation.ConstraintViolationException
   * <div>直接在參數(shù)上加的校驗(yàn),校驗(yàn)失敗,拋出此異常</div>
   * <div>如:(@NotNull(message = "用戶代碼不可為空") String userCode)</div>
   *
   * @param exception
   * @return
   */
@ExceptionHandler(ConstraintViolationException.class)
public JSONObject handlerConstraintViolationException(ConstraintViolationException exception) {......}

總結(jié)校驗(yàn)流程圖如下:

普通參數(shù)校驗(yàn)流程.jpg

4 嵌套校驗(yàn)

嵌套校驗(yàn),準(zhǔn)確來(lái)說(shuō),是對(duì)象內(nèi)約束的嵌套校驗(yàn)。指的是校驗(yàn)A對(duì)象,A對(duì)象內(nèi)有個(gè)屬性B是對(duì)象,B對(duì)象內(nèi)部屬性仍然有約束。需要對(duì)A對(duì)象的約束+A對(duì)象內(nèi)B對(duì)象的約束進(jìn)行校驗(yàn),這種就是嵌套約束。

4.1 代碼示例

這里既校驗(yàn)參數(shù)user中的userCode約束又需要校驗(yàn)user中的屬性對(duì)象department中的departmentCode的約束

  • 實(shí)體定義
@Data
public class Department {
    @NotNull(message = "部門(mén)代碼不可為空")
    private String departmentCode;
}
@Data
public class User {
    @NotNull(message = "用戶代碼不可為空")
    private String userCode;
    
    @Valid
    @NotNull(message = "部門(mén)不可為空")
    private Department department;
    
    private String userName;
}
  • mapping方法
@GetMapping("saveWithLevel")
public JSONObject saveWithLevel(@Valid User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}
  • 異常處理
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {
    log.info("全局異常[BindException]:" + exception.getMessage());
    JSONObject result = new JSONObject();
    result.put("success", false);
    if (exception != null) {
        String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
        result.put("message", message);
    }
    return result;
}

4.2 代碼測(cè)試

  • 請(qǐng)求信息

http://localhost:8080/user/saveWithLevel?department.departmentCode=001

  • 返回結(jié)果
{"success":false,"message":"部門(mén)代碼長(zhǎng)度需在5~10之間,用戶代碼不可為空"}

可見(jiàn)嵌套校驗(yàn)起作用了,對(duì)象user的內(nèi)部普通屬性u(píng)serCode和內(nèi)部對(duì)象department的自己的約束都起作用了。

4.3 總結(jié)

  • 實(shí)現(xiàn)嵌套校驗(yàn),在被校驗(yàn)對(duì)象的內(nèi)部屬性對(duì)象上,必須加上@Valid注解

  • mapping方法參數(shù)前,用@Valid注解和@Validated沒(méi)有區(qū)別

5 分組校驗(yàn)

同一個(gè)javaBean,我們加上約束注解后,這個(gè)javaBean作為請(qǐng)求參數(shù)的對(duì)象類型,其中的約束注解,會(huì)對(duì)參數(shù)對(duì)象的內(nèi)容進(jìn)行校驗(yàn)。

有時(shí)候,不同的請(qǐng)求我們會(huì)使用相同的javaBean作為對(duì)象的參數(shù)類型,如新增用戶更新用戶我們都會(huì)使用用戶這個(gè)JavaBean作為請(qǐng)求參數(shù)的封裝對(duì)象。

5.1 代碼示例

比如,我們新增用戶,需要設(shè)置密碼;更新用戶,不需要設(shè)置密碼。

代碼如下:

  • 分組類型

分組接口不需要有實(shí)現(xiàn),僅僅作為一個(gè)分組類型

public interface Add {
}
public interface Edit {
}
  • 實(shí)體定義

通過(guò)約束中的group參數(shù),來(lái)指定對(duì)應(yīng)的分組類型,可以指定多個(gè)

@Data
public class User {
    @NotNull(message = "用戶代碼不可為空", groups = {Add.class, Edit.class})
    private String userCode;
    
    @NotNull(message = "密碼不可為空", groups = {Add.class})
    private String password;
}
  • mapping方法

校驗(yàn)方式,只能指定@Validated,其中的value為這個(gè)參數(shù)的分組類型,和類中約束注解的groups屬性相對(duì)性可以指定多個(gè)。

@GetMapping("groupAdd")
public JSONObject groupAdd(@Validated(Add.class) User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}

@GetMapping("groupEdit")
public JSONObject groupEdit(@Validated(Edit.class) User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}
  • 異常處理

同4.1

5.2 代碼測(cè)試

5.3 總結(jié)

(1)分組校驗(yàn)中,定義的分組類型接口,不需要有實(shí)現(xiàn)內(nèi)容,僅僅是作為分組的一個(gè)類型存在,不同的業(yè)務(wù),可以共用相同的類型。

(2)約束中的分組類型,可以定義多個(gè)。

(3)@Validated中的分組類型,也可以指定多個(gè)。

(4)校驗(yàn)的時(shí)候,根據(jù)@Validated中指定分組類型,,去找校驗(yàn)對(duì)象中的對(duì)應(yīng)有此分組類型的約束,進(jìn)行校驗(yàn)。

(5)指定分組后,不滿足分組的約束(不加分組的約束為默認(rèn)分組,也是一種分組),不會(huì)進(jìn)行校驗(yàn)

6 順序校驗(yàn)

如不進(jìn)行順序校驗(yàn)配置,校驗(yàn)對(duì)象內(nèi)的屬性,校驗(yàn)順序是隨機(jī)的。

有時(shí)候想先校驗(yàn)比較簡(jiǎn)單的約束,再校驗(yàn)復(fù)雜的,因此需要指定約束的校驗(yàn)順序。可以結(jié)合《7 驗(yàn)證將檢測(cè)到第一個(gè)約束違例時(shí)停止》一起使用。

6.1 代碼示例

  • 分組類型
// 分組類型:第一個(gè)執(zhí)行
public interface FirstCheck {
}
// 分組類型:第二個(gè)執(zhí)行
public interface SecondCheck {
}
// 待順序的分組類型組
@GroupSequence({FirstCheck.class, SecondCheck.class})
public interface UserGroupCheck {
}
  • 實(shí)體定義
@Data
public class User {
    @NotNull(message = "用戶代碼不可為空", groups = {FirstCheck.class})
    private String userCode;
    
    @NotNull(message = "密碼不可為空", groups = {SecondCheck.class})
    private String password;
    
    @NotNull(message = "用戶名不可為空")
    private String userName;
}
  • mapping方法
@GetMapping("orderCheck")
public JSONObject orderCheck(@Validated(UserGroupCheck.class) User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}
  • 異常處理

同4.1

6.2 代碼測(cè)試

  • 請(qǐng)求1

    • 請(qǐng)求

    http://localhost:8080/user/orderCheck

    • 結(jié)果
    {"success":false,"message":"用戶代碼不可為空"}
    
  • 請(qǐng)求2

    • 請(qǐng)求

    http://localhost:8080/user/orderCheck?userCode=001

    • 結(jié)果
    {"success":false,"message":"密碼不可為空"}
    

    請(qǐng)求3

    • 請(qǐng)求

    http://localhost:8080/user/orderCheck?userCode=001&password=123456

    • 結(jié)果
    {"success":true,"message":"User(userCode=001, password=123456, userName=null)"}
    
  • 特殊請(qǐng)求

    • 變更

      將實(shí)體類進(jìn)行變更,userName上的約束也加上分組為FirstCheck。此時(shí)userCode和userName的約束分組均為FirstCheck。

      如下:

      @Data
      public class User {
          @NotNull(message = "用戶代碼不可為空", groups = {FirstCheck.class})
          private String userCode;
          
          @NotNull(message = "密碼不可為空", groups = {SecondCheck.class})
          private String password;
          
          @NotNull(message = "用戶名不可為空", groups = {FirstCheck.class})
          private String userName;
      }
      
    • 請(qǐng)求

      http://localhost:8080/user/orderCheck

    • 結(jié)果

{"success":false,"message":"用戶代碼不可為空,用戶名不可為空"}


或

```json
{"success":false,"message":"用戶名不可為空,用戶代碼不可為空"}

可以看出,當(dāng)一個(gè)分組內(nèi)有多個(gè)約束,約束的校驗(yàn)順序仍然是隨機(jī)的

6.3 總結(jié)

(1)根據(jù)參數(shù)中的分組對(duì)應(yīng)的接口中@GroupSequence指定的分組類型的順序進(jìn)行加校驗(yàn)

(2)只有當(dāng)一個(gè)分組內(nèi)的所有約束都校驗(yàn)通過(guò)后,才會(huì)進(jìn)入下一個(gè)分組進(jìn)行校驗(yàn)。

(3)順序校驗(yàn),指的是@GroupSequence內(nèi)配置的分組的順序,當(dāng)一個(gè)分組內(nèi)有多個(gè)約束,這個(gè)分組內(nèi)約束校驗(yàn)順序仍然隨機(jī)

7 驗(yàn)證將檢測(cè)到第一個(gè)約束違例時(shí)停止

默認(rèn),有多個(gè)約束的情況下,將會(huì)對(duì)所有參數(shù)進(jìn)行校驗(yàn),如果存在校驗(yàn)失敗的約束,返回的校驗(yàn)結(jié)果(BindingResult或?qū)?yīng)Exception)中會(huì)有所有的參數(shù)校驗(yàn)錯(cuò)誤信息。即如果多個(gè)不滿足約束,則返回結(jié)果中會(huì)有多個(gè)失敗信息。

有時(shí)候,我們只需要返回第一個(gè)一個(gè)校驗(yàn)失敗的約束信息就好,校驗(yàn)到一個(gè)約束失敗后,沒(méi)有必要再花費(fèi)代價(jià)進(jìn)行其他約束校驗(yàn)。

springBoot中,參數(shù)校驗(yàn)的實(shí)現(xiàn),基于MethodValidationPostProcessor

@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
                                                                          @Lazy Validator validator) {
    MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
    boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
    processor.setProxyTargetClass(proxyTargetClass);
    processor.setValidator(validator);
    return processor;
}

這個(gè)postProcessor的校驗(yàn)配置基于spring中的beanValidator,我們創(chuàng)建自己的Validator的bean,配置failFast,即可實(shí)現(xiàn)驗(yàn)證將檢測(cè)到第一個(gè)約束違例時(shí)停止這個(gè)要求。

實(shí)現(xiàn)代碼如下:

@Bean
public Validator validator() {
    HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class).configure();
    //驗(yàn)證將檢測(cè)到第一個(gè)約束違例時(shí)停止
    configuration.failFast(true);
    ValidatorFactory validatorFactory = configuration.buildValidatorFactory();
    return validatorFactory.getValidator();
}

或使用更簡(jiǎn)潔的寫(xiě)法:

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
        .configure()
        //驗(yàn)證將檢測(cè)到第一個(gè)約束違例時(shí)停止
        .failFast(true)
        .buildValidatorFactory();
    return validatorFactory.getValidator();
}

failFastHibernateValidatorConfiguration中的一個(gè)屬性配置,配置中還有其他配置屬性,可以定制我們的校驗(yàn)器。

8 自定義校驗(yàn)器

自定義校驗(yàn)器,注意點(diǎn)比較多,不是本文的重點(diǎn),暫時(shí)不進(jìn)行記錄,后續(xù)有時(shí)間會(huì)有專門(mén)的文章進(jìn)行分析。

9 生產(chǎn)環(huán)境配置

前面說(shuō)的都是原理和使用細(xì)節(jié),這里記錄下生產(chǎn)環(huán)境,需要進(jìn)行哪些全局配置。

9.1 全局異常處理

建議使用全局異常處理,對(duì)請(qǐng)求的異常信息進(jìn)行統(tǒng)一處理。

代碼如下:

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;

/**
 * 統(tǒng)一異常處理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 捕捉全局異常:org.springframework.web.bind.MethodArgumentNotValidException
     * <div>@RequestBody修飾的參數(shù),校驗(yàn)失敗,拋出此異常</div>
     * <div>如:xxxAction(@RequestBody @Valid User user)</div>
     * @param exception
     * @return
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public JSONObject handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
        log.info("全局異常[MethodArgumentNotValidException]:" + exception.getMessage());
        JSONObject result = new JSONObject();
        result.put("success", false);
        if (exception != null) {
            String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
            result.put("message", message);
        }
        return result;
    }
    
    /**
     * 捕捉全局異常:org.springframework.validation.BindException
     * <div>普通請(qǐng)求的參數(shù),校驗(yàn)失敗,拋出此異常</div>
     * <div>如:xxxAction(@Valid User user)</div>
     * @param exception
     * @return
     */
    @ExceptionHandler(BindException.class)
    public JSONObject handlerBindException(BindException exception) {
        log.info("全局異常[BindException]:" + exception.getMessage());
        JSONObject result = new JSONObject();
        result.put("success", false);
        if (exception != null) {
            String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
            result.put("message", message);
        }
        return result;
    }
    
    /**
     * 捕捉全局異常:javax.validation.ConstraintViolationException
     * <div>直接在參數(shù)上加的校驗(yàn),校驗(yàn)失敗,拋出此異常</div>
     * <div>如:xxxAction(@NotNull(message = "用戶代碼不可為空") String userCode)</div>
     * <div>如:xxxAction(@PathVariable("userCode") @Length(max = 10,message="用戶代碼不可超過(guò)10位") String userCode)</div>
     * @param exception
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public JSONObject handlerConstraintViolationException(ConstraintViolationException exception) {
        log.info("全局異常[ConstraintViolationException]:" + exception.getMessage());
        JSONObject result = new JSONObject();
        result.put("success", false);
        if (exception != null) {
            result.put("message", exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")));
        }
        return result;
    }
    
}

參數(shù)校驗(yàn)失敗,返回的錯(cuò)誤json信息如下,可以根據(jù)項(xiàng)目的實(shí)際情況進(jìn)行定制:

{"success":false,"message":"用戶代碼不可超過(guò)10位"}

9.2 驗(yàn)證將檢測(cè)到第一個(gè)約束違例時(shí)停止

import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

@Configuration
public class ValidConfig {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                //驗(yàn)證將檢測(cè)到第一個(gè)約束違例時(shí)停止
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

10 補(bǔ)充

10.1 校驗(yàn)限制

@Valid支持嵌套驗(yàn)證、@Validated支持分組驗(yàn)證、排序驗(yàn)證(準(zhǔn)確來(lái)說(shuō),排序驗(yàn)證也是分組驗(yàn)證的一種)。

無(wú)法實(shí)現(xiàn):“嵌套驗(yàn)證+分組驗(yàn)證”和“嵌套驗(yàn)證+排序驗(yàn)證”這種組合形式的驗(yàn)證。

10.2 建議

雖然JSR303支持自定義校驗(yàn)器,筆者不建議將太復(fù)雜的校驗(yàn)交給JSR303的標(biāo)準(zhǔn)進(jìn)行校驗(yàn)。

如果是參數(shù)基本的屬性校驗(yàn)(是否為空、長(zhǎng)度、大小、枚舉、正則格式),可以以這種形式進(jìn)行校驗(yàn)。

但是如果是太復(fù)雜的校驗(yàn),如需要連接數(shù)據(jù)庫(kù)進(jìn)行業(yè)務(wù)判斷的校驗(yàn),筆者仍然建議在具體的業(yè)務(wù)代碼中進(jìn)行校驗(yàn)。

10.2 校驗(yàn)順序的隨機(jī)性

如不使用@Validated指定約束的校驗(yàn)順序,所有約束的校驗(yàn)順序是隨機(jī)的,即相同的情況,返回的校驗(yàn)結(jié)果的順序可能不一樣。

10.3 一個(gè)字段多個(gè)約束

同一個(gè)字段可以加多個(gè)約束注解,并不是只能有一個(gè)約束注解。如下:

@NotNull(message = "用戶代碼不可為空")
@Length(min = 5, max = 10, message = "用戶代碼長(zhǎng)度需在5~10之間")
private String userCode;

如userCode為空,則拋出異常:用戶代碼不可為空

如userCode不為空,則校驗(yàn)約束:用戶代碼長(zhǎng)度需在5~10之間

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容