SpringBoot從入門到精通教程(二十七)- @Valid註解用法詳解+全局處理器Exception優雅處理參數驗證用法

問題痛點

用 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接口

完整源碼下載

我的Github源碼地址:

https://github.com/hemin1003/spring-boot-study/tree/master/spring-boot2-study/spring-boot2-parent/spring-boot2-valid

下一章教程

SpringBoot從入門到精通教程(二十八)- 動態修改日誌輸出級別用法

該系列教程

SpringBoot從入門到精通教程

 

我的專欄

 

 

至此,全部介紹就結束了

 

 

-------------------------------

-------------------------------

 

我的CSDN主頁

關於我(個人域名)

我的開源項目集Github

 

期望和大家一起學習,一起成長,共勉,O(∩_∩)O謝謝

歡迎交流問題,可加個人QQ 469580884,

或者,加我的羣號 751925591,一起探討交流問題

不講虛的,只做實幹家

Talk is cheap,show me the code

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章