SpringBoot 中使用 @Valid 註解 + Exception 全域性處理器優雅處理引數驗證

語言: CN / TW / HK

目錄

一、為什麼使用 @Valid 來驗證引數

二、@Valid 註解的作用

三、@Valid 的相關注解

四、使用 @Valid 進行引數效驗步驟

  • 實體類中新增 @Valid 相關注解
  • 介面類中新增 @Valid 註解
  • 全域性異常處理類中處理 @Valid 丟擲的異常

五、SpringBoot 中使用 @Valid 示例

  • Maven 引入相關依賴
  • 自定義個異常類
  • 自定義響應列舉類
  • 自定義響應物件類
  • 自定義實體類中新增 @Valid 相關注解
  • Controller 中新增 @Valid 註解
  • 全域性異常處理
  • 啟動類
  • 示例測試

相關地址:

Spring Servlet 文件:https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/web/servlet示例專案 Github:https://github.com/my-dlq/blog-example/tree/master/springboot/springboot-filter-example系統環境:

Jdk 版本:jdk 8

SpringBoot 版本:2.2.1.RELEASE

一、為什麼使用 @Valid 來驗證引數

在平常通過 Spring 框架寫程式碼時候,會經常寫介面類,相信大家對該類的寫法非常熟悉。在寫介面時經常要寫效驗請求引數邏輯,這時候我們會常用做法是寫大量的 if 與 if else 類似這樣的程式碼來做判斷,如下:

``` @RestController public class TestController {

@PostMapping("/user")     public String addUserInfo(@RequestBody User user) {         if (user.getName() == null || "".equals(user.getName()) {             ......         } else if(user.getSex() == null || "".equals(user.getSex())) {             ......         } else if(user.getUsername() == null || "".equals(user.getUsername())) {             ......         } else {             ......         }         ......     }

} ```

這樣的程式碼如果按正常程式碼邏輯來說,是沒有什麼問題的,不過按優雅來說,簡直糟糕透了。不僅不優雅,而且如果存在大量的驗證邏輯,這會使程式碼看起來亂糟糟,大大降低程式碼可讀性,那麼有沒有更好的方法能夠簡化這個過程呢?

答案當然是有,推薦的是使用 @Valid 註解來幫助我們簡化驗證邏輯。

二、@Valid 註解的作用

註解 @Valid 的主要作用是用於資料效驗,可以在定義的實體中的屬性上,新增不同的註解來完成不同的校驗規則,而在介面類中的接收資料引數中新增 @valid 註解,這時你的實體將會開啟一個校驗的功能。

三、@Valid 的相關注解

下面是 @Valid 相關的註解,在實體類中不同的屬性上新增不同的註解,就能實現不同資料的效驗功能。

圖片

四、使用 @Valid 進行引數效驗步驟

整個過程如下圖所示,使用者訪問介面,然後進行引數效驗,因為 @Valid 不支援平面的引數效驗(直接寫在引數中欄位的效驗)所以基於 GET 請求的引數還是按照原先方式進行效驗,而 POST 則可以以實體物件為引數,可以使用 @Valid 方式進行效驗。如果效驗通過,則進入業務邏輯,否則丟擲異常,交由全域性異常處理器進行處理。

圖片

1、實體類中新增 @Valid 相關注解

使用 @Valid 相關注解非常簡單,只需要在引數的實體類中屬性上面新增如 @NotBlank、@Max、@Min 等註解來對該欄位進限制,如下:

User:

public class User {     @NotBlank(message = "姓名不為空")     private String username;     @NotBlank(message = "密碼不為空")     private String password; }

如果是巢狀的實體物件,則需要在最外層屬性上新增 @Valid 註解:

User:

public class User {     @NotBlank(message = "姓名不為空")     private String username;     @NotBlank(message = "密碼不為空")     private String password;     //巢狀必須加 @Valid,否則巢狀中的驗證不生效     @Valid     @NotNull(message = "使用者資訊不能為空")     private UserInfo userInfo; }

UserInfo:

public class User {     @NotBlank(message = "年齡不為空")     @Max(value = 18, message = "不能超過18歲")     private String age;     @NotBlank(message = "性別不能為空")     private String gender; }

2、介面類中新增 @Valid 註解

在 Controller 類中新增介面,POST 方法中接收設定了 @Valid 相關注解的實體物件,然後在引數中新增 @Valid 註解來開啟效驗功能,需要注意的是, @Valid 對 Get 請求中接收的平面引數請求無效,稍微略顯遺憾。

``` @RestController public class TestController {

@PostMapping("/user")     public String addUserInfo(@Valid @RequestBody User user) {         return "呼叫成功!";     }

} ```

3、全域性異常處理類中處理 @Valid 丟擲的異常

最後,我們寫一個全域性異常處理類,然後對介面中丟擲的異常進行處理,而 @Valid 配合 Spring 會丟擲 MethodArgumentNotValidException 異常,這裡我們需要對該異常進行處理即可。

``` @RestControllerAdvice("club.mydlq.valid")   //指定異常處理的包名 public class GlobalExceptionHandler {

@ResponseStatus(HttpStatus.BAD_REQUEST) //設定狀態碼為 400     @ExceptionHandler({MethodArgumentNotValidException.class})     public String paramExceptionHandler(MethodArgumentNotValidException e) {         BindingResult exceptions = e.getBindingResult();         // 判斷異常中是否有錯誤資訊,如果存在就使用異常中的訊息,否則使用預設訊息         if (exceptions.hasErrors()) {             List errors = exceptions.getAllErrors();             if (!errors.isEmpty()) {                 // 這裡列出了全部錯誤引數,按正常邏輯,只需要第一條錯誤即可                 FieldError fieldError = (FieldError) errors.get(0);                 return fieldError.getDefaultMessage();             }         }         return "請求引數錯誤";     }

} ```

五、SpringBoot 中使用 @Valid 示例

1、Maven 引入相關依賴

Maven 引入 SpringBoot 相關依賴,這裡引入了 Lombok 包來簡化開發過程。

```

    4.0.0              org.springframework.boot         spring-boot-starter-parent         2.2.1.RELEASE     

com.aspire     springboot-valid-demo     0.0.1-SNAPSHOT     springboot-valid-demo     @valid demo

        1.8     

                     org.springframework.boot             spring-boot-starter-web                               org.projectlombok             lombok             true              

                                      org.springframework.boot                 spring-boot-maven-plugin                           

```

2、自定義個異常類

自定義個異常類,方便我們處理 GET 請求(GET 請求引數中一般是沒有實體物件的,所以不能使用 @Valid),當請求驗證失敗時,手動丟擲自定義異常,交由全域性異常處理。

``` public class ParamaErrorException extends RuntimeException {

public ParamaErrorException() {     }

public ParamaErrorException(String message) {         super(message);     }

} ```

3、自定義響應列舉類

定義一個返回資訊的列舉類,方便我們快速響應資訊,不必每次都寫返回訊息和響應碼。

``` public enum ResultEnum {

SUCCESS(1000, "請求成功"),     PARAMETER_ERROR(1001, "請求引數有誤!"),     UNKNOWN_ERROR(9999, "未知的錯誤!");

private Integer code;     private String message;

ResultEnum(Integer code, String message) {         this.code = code;         this.message = message;     }

public Integer getCode() {         return code;     }

public String getMessage() {         return message;     } } ```

4、自定義響應物件類

建立用於返回呼叫方的響應資訊的實體類。

``` import com.aspire.parameter.enums.ResultEnum; import lombok.Data;

@Data public class ResponseResult {     private Integer code;     private String msg;

public ResponseResult(){     }

public ResponseResult(ResultEnum resultEnum){         this.code = resultEnum.getCode();         this.msg = resultEnum.getMessage();     }

public ResponseResult(Integer code, String msg) {         this.code = code;         this.msg = msg;     } } ```

5、自定義實體類中新增 @Valid 相關注解

下面將建立用於 POST 方法接收引數的實體物件,裡面新增 @Valid 相關驗證註解,並在註解中添加出錯時的響應訊息。

User

``` import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull;

/*  * user實體類  / @Data public class User {     @NotBlank(message = "姓名不為空")     private String username;     @NotBlank(message = "密碼不為空")     private String password;     // 巢狀必須加 @Valid,否則巢狀中的驗證不生效     @Valid     @NotNull(message = "userinfo不能為空")     private UserInfo userInfo; } ```

UserInfo

``` import lombok.Data; import javax.validation.constraints.Max; import javax.validation.constraints.NotBlank;

@Data public class UserInfo {     @NotBlank(message = "年齡不為空")     @Max(value = 18, message = "不能超過18歲")     private String age;     @NotBlank(message = "性別不能為空")     private String gender; } ```

6、Controller 中新增 @Valid 註解

介面類中新增 GET 和 POST 方法的兩個介面用於測試,其中 POST 方法以上面建立的 Uer 實體物件接收引數,並使用 @Valid,而 GET 請求一般接收引數較少,所以使用正常判斷邏輯進行引數效驗。

``` import club.mydlq.valid.entity.ResponseResult; import club.mydlq.valid.entity.User; import club.mydlq.valid.enums.ResultEnum; import club.mydlq.valid.exception.ParamaErrorException; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid;

@RestController public class TestController {

/*      * 獲取使用者資訊      *      * @param username 姓名      * @return ResponseResult      /     @Validated     @GetMapping("/user/{username}")     public ResponseResult findUserInfo(@PathVariable String username) {         if (username == null || "".equals(username)) {             throw new ParamaErrorException("username 不能為空");         }         return new ResponseResult(ResultEnum.SUCCESS);     }

/*      * 新增使用者      *      * @param user 使用者資訊      * @return ResponseResult      /     @PostMapping("/user")     public ResponseResult addUserInfo(@Valid @RequestBody User user) {         return new ResponseResult(ResultEnum.SUCCESS);     }

} ```

7、全域性異常處理

這裡建立一個全域性異常處理類,方便統一處理異常錯誤資訊。裡面添加了不同異常處理的方法,專門用於處理介面中丟擲的異常信。

``` import club.mydlq.valid.entity.ResponseResult; import club.mydlq.valid.enums.ResultEnum; import club.mydlq.valid.exception.ParamaErrorException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.List;

@Slf4j @RestControllerAdvice("club.mydlq.valid") public class GlobalExceptionHandler {

/*      * 忽略引數異常處理器      *      * @param e 忽略引數異常      * @return ResponseResult      /     @ResponseStatus(HttpStatus.BAD_REQUEST)     @ExceptionHandler(MissingServletRequestParameterException.class)     public ResponseResult parameterMissingExceptionHandler(MissingServletRequestParameterException e) {         log.error("", e);         return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "請求引數 " + e.getParameterName() + " 不能為空");     }

/*      * 缺少請求體異常處理器      *      * @param e 缺少請求體異常      * @return ResponseResult      /     @ResponseStatus(HttpStatus.BAD_REQUEST)     @ExceptionHandler(HttpMessageNotReadableException.class)     public ResponseResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {         log.error("", e);         return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "引數體不能為空");     }

/*      * 引數效驗異常處理器      *      * @param e 引數驗證異常      * @return ResponseInfo      /     @ResponseStatus(HttpStatus.BAD_REQUEST)     @ExceptionHandler(MethodArgumentNotValidException.class)     public ResponseResult parameterExceptionHandler(MethodArgumentNotValidException e) {         log.error("", e);         // 獲取異常資訊         BindingResult exceptions = e.getBindingResult();         // 判斷異常中是否有錯誤資訊,如果存在就使用異常中的訊息,否則使用預設訊息         if (exceptions.hasErrors()) {             List errors = exceptions.getAllErrors();             if (!errors.isEmpty()) {                 // 這裡列出了全部錯誤引數,按正常邏輯,只需要第一條錯誤即可                 FieldError fieldError = (FieldError) errors.get(0);                 return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), fieldError.getDefaultMessage());             }         }         return new ResponseResult(ResultEnum.PARAMETER_ERROR);     }

/*      * 自定義引數錯誤異常處理器      *      * @param e 自定義引數      * @return ResponseInfo      /     @ResponseStatus(HttpStatus.BAD_REQUEST)     @ExceptionHandler({ParamaErrorException.class})     public ResponseResult paramExceptionHandler(ParamaErrorException e) {         log.error("", e);         // 判斷異常中是否有錯誤資訊,如果存在就使用異常中的訊息,否則使用預設訊息         if (!StringUtils.isEmpty(e.getMessage())) {             return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), e.getMessage());         }         return new ResponseResult(ResultEnum.PARAMETER_ERROR);     }

} ```

8、啟動類

``` import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication public class Application {

public static void main(String[] args) {         SpringApplication.run(Application.class, args);     }

} ```

9、示例測試

下面將針對上面示例中設定的兩種介面進行測試,分別來驗證引數效驗功能。

|| - 測試介面 /user/{username}

使用 GET 方法請求地址 http://localhost:8080/user?username=test 時,返回資訊:

{     "code": 1000,     "msg": "請求成功" }

當不輸入引數,輸入地址 http://localhost:8080/user 時,返回資訊:

{     "code": 1001,     "msg": "請求引數 username 不能為空" }

可以看到在執行 GET 請求,能夠正常按我們全域性異常處理器中的設定處理異常資訊。

|| - 測試介面 /user

(1)、使用 POST 方法發起請求,首先進行不加 JSON 請求體來對 http://localhost:8080/user 地址進行請求,返回資訊:

{     "code": 1001,     "msg": "引數體不能為空" }

(2)、輸入部分引數進行測試。

請求內容:

{  "username":"test",  "password":"123" }

返回資訊:

{     "code": 1001,     "msg": "userinfo不能為空" }

(3)、輸入完整引數,且設定 age > 18 時,進行測試。

{  "username":"111",  "password":"sa",   "userInfo":{     "age":19,     "gender":"男"   } }

返回資訊:

{     "code": 1001,     "msg": "不能超過18歲" }

可以看到在執行 POST 請求,也能正常按我們全域性異常處理器中的設定處理異常資訊,且提示資訊為我們設定在實體類中的 Message。