目錄
一、為什么使用 @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<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。
{
"username":"111",
"password":"sa",
"userInfo":{
"age":19,
"gender":"男"
}
}