SpringBoot中使用@Valid註解+Exception全局處理器處理參數驗證

java的小本家 發佈 2020-02-29T18:09:13+00:00

Jdk 版本:jdk 8SpringBoot 版本:2.2.1.RELEASE一、為什麼使用 @Valid 來驗證參數在平常通過 Spring 框架寫代碼時候,會經常寫接口類,相信大家對該類的寫法非常熟悉。

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<ObjectError> errors = exceptions.getAllErrors();
            if (!errors.isEmpty()) {
                // 這裡列出了全部錯誤參數,按正常邏輯,只需要第一條錯誤即可
                FieldError fieldError = (FieldError) errors.get(0);
                return fieldError.getDefaultMessage();
            }
        }
        return "請求參數錯誤";
    }

}

五、SpringBoot 中使用 @Valid 示例

1、Maven 引入相關依賴

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

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>

    <groupId>com.aspire</groupId>
    <artifactId>springboot-valid-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-valid-demo</name>
    <description>@valid demo</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

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<ObjectError> 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。

關鍵字: