問題痛點
用 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
註解來幫助我們簡化驗證邏輯。
Tips技術點
1. @Valid註解
- 註解 @Valid 的主要作用是用於數據效驗,可以在定義的實體中的屬性上,添加不同的註解來完成不同的校驗規則,而在接口類中的接收數據參數中添加 @valid 註解,這時你的實體將會開啓一個校驗的功能。
2. @Valid 的相關注解
下面是 @Valid 相關的註解,在實體類中不同的屬性上添加不同的註解,就能實現不同數據的效驗功能
註解名稱 作用描述
@Null 限制只能爲null
@NotNull 限制必須不爲null
@AssertFalse 限制必須爲false
@AssertTrue 限制必須爲true
@DecimalMax(value) 限制必須爲一個不大於指定值的數字
@DecimalMin(value) 限制必須爲一個不小於指定值的數字
@Digits(integer,fraction) 限制必須爲一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction
@Future 限制必須是一個將來的日期
@Max(value) 限制必須爲一個不大於指定值的數字
@Min(value) 限制必須爲一個不小於指定值的數字
@Past 限制必須是一個過去的日期
@Pattern(value) 限制必須符合指定的正則表達式
@Size(max,min) 限制字符長度必須在min到max之間
@Past 驗證註解的元素值(日期類型)比當前時間早
@NotEmpty 驗證註解的元素值不爲null且不爲空(字符串長度不爲0、集合大小不爲0)
@NotBlank 驗證註解的元素值不爲空(不爲null、去除首位空格後長度爲0),不同於@NotEmpty,@NotBlank只應用於字符串且在比較時會去除字符串的空格
@Email 驗證註解的元素值是Email,也可以通過正則表達式和flag指定自定義的email格式
3. 使用 @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
異常,這裏我們需要對該異常進行處理即可。
package com.md.demo.exception;
import java.util.List;
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 com.md.demo.util.JsonResult;
import com.md.demo.util.ResultCode;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestControllerAdvice("com.md") //指定異常處理的包名
public class GlobalExceptionHandler {
/**
* 參數效驗異常處理器
*
* @param e 參數驗證異常
* @return ResponseInfo
*/
@ResponseStatus(HttpStatus.BAD_REQUEST) //設置狀態碼爲 400
@ExceptionHandler(MethodArgumentNotValidException.class)
public JsonResult 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 JsonResult(ResultCode.PARAM_ERROR, fieldError.getDefaultMessage());
}
}
return new JsonResult(ResultCode.PARAM_ERROR);
}
}
代碼演示
1. 項目目錄結構
2. pom.xml依賴組件(使用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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.md</groupId>
<artifactId>spring-boot2-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>spring-boot2-valid</artifactId>
<packaging>jar</packaging>
<name>spring-boot2-valid</name>
<description>Spring Boot, MVC, Rest API for App</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 構建成可運行的Web項目 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib-ext-spring</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3. 自定義個異常類
自定義個異常類,方便我們處理 GET 請求(GET 請求參數中一般是沒有實體對象的,所以不能使用 @Valid),當請求驗證失敗時,手動拋出自定義異常,交由全局異常處理。
package com.md.demo.exception;
public class ParamaErrorException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ParamaErrorException() {
}
public ParamaErrorException(String message) {
super(message);
}
}
4. 自定義DTO類中添加 @Valid 相關注解
GetUserByIdDTO:
package com.md.demo.dto;
import javax.validation.constraints.NotEmpty;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("測試-查詢條件")
public class GetUserByIdDTO {
@ApiModelProperty(value = "id標識值", required = true)
@NotEmpty(message = "[userId值]不能爲空")
private String userId;
@ApiModelProperty(value = "用戶名")
private String userName;
}
5. Controller 中添加 @Valid 註解(這裏我定義了一個BaseDTO基本請求數據模型)
GetController:
package com.md.demo.controller;
import javax.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.md.demo.controller.base.BaseDTO;
import com.md.demo.dto.GetUserByIdDTO;
import com.md.demo.exception.ParamaErrorException;
import com.md.demo.util.JsonResult;
import com.md.demo.util.ResultCode;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
/**
* @author Minbo
*/
@RestController
@RequestMapping("/api/")
@Api(tags = { "查詢接口" })
@Slf4j
public class GetController {
/**
* 測試Post請求
*/
@ApiOperation(value = "TestPost接口", httpMethod = "POST")
@PostMapping("/test/post")
public JsonResult testPost(@Valid @RequestBody BaseDTO<GetUserByIdDTO> dto) {
log.debug("enter test post api...");
return new JsonResult(ResultCode.SUCCESS);
}
/**
* 測試Get請求
*/
@Validated
@ApiOperation(value = "TestGet接口", httpMethod = "GET")
@GetMapping("/test/get/{userName}")
public JsonResult testGet(@PathVariable String userName) {
log.debug("enter test get api...");
if (userName == null || "".equals(userName)) {
throw new ParamaErrorException("userName 不能爲空");
}
return new JsonResult(ResultCode.SUCCESS);
}
}
6. 定義全局異常處理類
這裏創建一個全局異常處理類,方便統一處理異常錯誤信息。裏面添加了不同異常處理的方法,專門用於處理接口中拋出的異常信息
GlobalExceptionHandler:
package com.md.demo.exception;
import java.util.List;
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 com.md.demo.util.JsonResult;
import com.md.demo.util.ResultCode;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestControllerAdvice("com.md")
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public JsonResult handleException(Exception e) {
log.error("系統異常【全局異常處理】:" + e.getMessage(), e);
return new JsonResult(ResultCode.SYS_EXCEPTION, "系統異常:" + e.getMessage());
}
/**
* 忽略參數異常處理器
*
* @param e 忽略參數異常
* @return ResponseResult
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public JsonResult parameterMissingExceptionHandler(MissingServletRequestParameterException e) {
log.error("忽略參數異常", e);
return new JsonResult(ResultCode.PARAM_ERROR, "請求參數 " + e.getParameterName() + " 不能爲空");
}
/**
* 缺少請求體異常處理器
*
* @param e 缺少請求體異常
* @return ResponseResult
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public JsonResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {
log.error("缺少請求體異常", e);
return new JsonResult(ResultCode.PARAM_ERROR, "參數體不能爲空");
}
/**
* 參數效驗異常處理器
*
* @param e 參數驗證異常
* @return ResponseInfo
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public JsonResult 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 JsonResult(ResultCode.PARAM_ERROR, fieldError.getDefaultMessage());
}
}
return new JsonResult(ResultCode.PARAM_ERROR);
}
/**
* 自定義參數錯誤異常處理器
*
* @param e 自定義參數
* @return ResponseInfo
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({ ParamaErrorException.class })
public JsonResult paramExceptionHandler(ParamaErrorException e) {
log.error("自定義參數參數", e);
// 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認消息
if (!StringUtils.isEmpty(e.getMessage())) {
return new JsonResult(ResultCode.PARAM_ERROR, e.getMessage());
}
return new JsonResult(ResultCode.PARAM_ERROR);
}
}
7. 啓動類
Application:
package com.md.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
/**
* 程序主入口
*
* @author Minbo
*
*/
@SpringBootApplication
@EnableSwaggerBootstrapUI
@ComponentScan(basePackages = "com.md")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
/**
* 開啓過濾器功能
*
* @return
*/
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
/**
* 跨域過濾器
*
* @return
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
接口測試
1. 啓動後,訪問地址:http://localhost:9090/doc.html (已集成了swagger2框架,Swagger集成用法教程)
2. 測試post接口
可以看到在執行 POST 請求,也能正常按我們全局異常處理器中的設置處理異常信息,且提示信息爲我們設置在實體類中的 Message
3. 測試get接口
完整源碼下載
下一章教程
SpringBoot從入門到精通教程(二十八)- 動態修改日誌輸出級別用法
該系列教程
我的專欄
至此,全部介紹就結束了
-------------------------------
-------------------------------
關於我(個人域名)
期望和大家一起學習,一起成長,共勉,O(∩_∩)O謝謝
歡迎交流問題,可加個人QQ 469580884,
或者,加我的羣號 751925591,一起探討交流問題
不講虛的,只做實幹家
Talk is cheap,show me the code