在日常 web 開發(fā)中發(fā)生了異常,往往需要通過一個統(tǒng)一的 異常處理,來保證客戶端能夠收到友好的提示。本文將會介紹 Spring Boot 中的全局統(tǒng)一異常處理。
要點:
- 介紹Spring Boot默認的異常處理機制
- 如何自定義錯誤頁面
- 通過@ControllerAdvice注解來處理異常
從 spring 3.2 開始,新增了 @ControllerAdvice 注解,可以用于定義@ExceptionHandler,并應(yīng)用到配置了@RequestMapping 的控制器中。
默認情況下,Spring Boot為兩種情況提供了不同的響應(yīng)方式
- 一種是瀏覽器客戶端請求一個不存在的頁面或服務(wù)端處理發(fā)生異常時,一般情況下瀏覽器默認發(fā)送的請求頭中Accept: text/html,所以Spring Boot默認會響應(yīng)一個html文檔內(nèi)容,稱作“Whitelabel Error Page”。
如果這接口是給第三方調(diào)用那是不行的,至此大致能了解到為啥需要對異常進行全局捕獲了。
- 另一種是使用Postman等調(diào)試工具發(fā)送請求一個不存在的url或服務(wù)端處理發(fā)生異常時,Spring Boot會返回類似如下的Json格式字符串信息
{
"timestamp": "2018-05-12T06:11:45.209+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/index.html"
}
原理也很簡單,Spring Boot 默認提供了程序出錯的結(jié)果映射路徑/error。這個/error請求會在BasicErrorController中處理,其內(nèi)部是通過判斷請求頭中的Accept的內(nèi)容是否為text/html來區(qū)分請求是來自客戶端瀏覽器(瀏覽器通常默認自動發(fā)送請求頭內(nèi)容Accept:text/html)還是客戶端接口的調(diào)用,以此來決定返回頁面視圖還是 JSON 消息內(nèi)容。
自定義錯誤頁面
瀏覽器端訪問的話,任何錯誤Spring Boot返回的都是一個Whitelabel Error Page的錯誤頁面,這個很不友好,所以我們可以自定義下錯誤頁面。
- 先從最簡單的開始,直接在/resources/templates下面創(chuàng)建error.html就可以覆蓋默認的Whitelabel Error Page的錯誤頁面,我項目用的是thymeleaf模板,對應(yīng)的error.html代碼如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
動態(tài)error錯誤頁面
<p th:text="${error}"></p>
<p th:text="${status}"></p>
<p th:text="${message}"></p>
</body>
</html>
這樣運行的時候,請求一個不存在的頁面或服務(wù)端處理發(fā)生異常時,展示的自定義錯誤界面。
通過@ControllerAdvice注解來處理異常
Spring Boot提供的ErrorController是一種全局性的容錯機制。此外,你還可以用@ControllerAdvice注解和@ExceptionHandler注解實現(xiàn)對指定異常的特殊處理。
這里介紹兩種情況:
- 局部異常處理 @Controller + @ExceptionHandler
- 全局異常處理 @ControllerAdvice + @ExceptionHandler
局部異常處理 @Controller + @ExceptionHandler
局部異常主要用到的是@ExceptionHandler注解,此注解注解到類的方法上,當(dāng)此注解里定義的異常拋出時,此方法會被執(zhí)行。如果@ExceptionHandler所在的類是@Controller,則此方法只作用在此類。如果@ExceptionHandler所在的類帶有@ControllerAdvice注解,則此方法會作用在全局。
全局異常處理 @ControllerAdvice + @ExceptionHandler
在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定義@ExceptionHandler、@InitBinder、@ModelAttribute,并應(yīng)用到所有@RequestMapping中。
簡單的說,進入Controller層的錯誤才會由@ControllerAdvice處理,攔截器拋出的錯誤以及訪問錯誤地址的情況@ControllerAdvice處理不了,由SpringBoot默認的異常處理機制處理。
我們實際開發(fā)中,如果是要實現(xiàn)RESTful API,那么默認的JSON錯誤信息就不是我們想要的,這時候就需要統(tǒng)一一下JSON格式,所以需要封裝一下。
package com.pay.common.message;
import lombok.Data;
import java.io.Serializable;
/**
* 統(tǒng)一返回對象
*/
@Data
public class JsonResult<T> implements Serializable {
/**
*
*/
private static final long serialVersionUID = 17721020985L;
/**
* 通信數(shù)據(jù)
*/
private T data;
/**
* 通信狀態(tài)
*/
private boolean flag = true;
/**
* 狀態(tài)碼,0000代表無錯誤,錯誤代碼應(yīng)該用枚舉定義。
*/
private String code;
/**
* 通信描述
*/
private String msg = "";
/**
* 通過靜態(tài)方法獲取實例
*/
public static <T> JsonResult<T> of(T data) {
return new JsonResult<>(data);
}
public static <T> JsonResult<T> of(T data, boolean flag) {
return new JsonResult<>(data, flag);
}
public static <T> JsonResult<T> of(T data, boolean flag, String msg) {
return new JsonResult<>(data, flag, msg);
}
public static <T> JsonResult<T> of(T data, boolean flag, String code, String msg) {
return new JsonResult<>(data, flag, code, msg);
}
@Deprecated
public JsonResult() {
}
private JsonResult(T data) {
this.data = data;
}
private JsonResult(T data, boolean flag) {
this.data = data;
this.flag = flag;
}
private JsonResult(T data, boolean flag, String msg) {
this.data = data;
this.flag = flag;
this.msg = msg;
}
private JsonResult(T data, boolean flag, String code, String msg) {
this.data = data;
this.flag = flag;
this.code = code;
this.msg = msg;
}
}
創(chuàng)建全局異常處理類
package com.pay.common.exception;
import com.pay.common.message.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.persistence.EntityNotFoundException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
/**
* @ClassName: GlobalExceptionHandler
* @Description: 全局異常處理器
* @author: 郭秀志 jbcode@126.com
* @date: 2020/4/25 15:35
* @Copyright:
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 用來處理bean validation異常
*
* @param ex
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public JsonResult resolveConstraintViolationException(ConstraintViolationException ex) {
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
if (!CollectionUtils.isEmpty(constraintViolations)) {
StringBuilder msgBuilder = new StringBuilder();
for (ConstraintViolation constraintViolation : constraintViolations) {
msgBuilder.append(constraintViolation.getMessage()).append(",");
}
String errorMessage = msgBuilder.toString();
if (errorMessage.length() > 1) {
errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
}
return JsonResult.of(errorMessage);
}
return JsonResult.of(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public JsonResult resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
List<ObjectError> objectErrors = ex.getBindingResult().getAllErrors();
if (!CollectionUtils.isEmpty(objectErrors)) {
StringBuilder msgBuilder = new StringBuilder();
for (ObjectError objectError : objectErrors) {
msgBuilder.append(objectError.getDefaultMessage()).append(",");
}
String errorMessage = msgBuilder.toString();
if (errorMessage.length() > 1) {
errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
}
return JsonResult.of(errorMessage, false, "MethodArgumentNotValid");
}
return JsonResult.of(ex.getMessage(), false, "MethodArgumentNotValid");
}
@ExceptionHandler(value = EntityNotFoundException.class)
@ResponseBody
public JsonResult resolveEntityNotFoundException(EntityNotFoundException ex) {
return JsonResult.of(ex.getMessage(), false, "未查到數(shù)據(jù)");
}
}
此類在common的項目,要暴露出去給依賴的項目使用,在文件src\main\resources\META-INF\spring.factories中添加最后一行
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pay.common.autoconfig.schedlock.ShedlockConfig,\
com.pay.common.service.MailService,\
com.pay.common.exception.GlobalExceptionHandler
controller進行拋出異常
可以被全局異常捕捉并處理成json
@ApiVersion(5)
@RequestMapping(value = "/findByOne") // 加入接口的版本控制http://localhost:8080/v5/packageIndex/findByOne
public JsonResult<BzPackageIndex> findByOne() {
BzPackageIndex bzPackageIndex = new BzPackageIndex();
bzPackageIndex.setPackageId("BZ-20200107000001");
bzPackageIndex.setSort(5);
Example<BzPackageIndex> example = Example.of(bzPackageIndex);
Optional<BzPackageIndex> one = packageIndexService.findOne(example);
return JsonResult.of(one.orElseThrow(() -> new EntityNotFoundException("package id為:BZ-20200107000005 的indexpackage無記錄")), true, "0000", "獲取數(shù)據(jù)成功");
// return JsonResult.of("v5接口", true, "成功調(diào)用");
}
訪問接口,如果無數(shù)據(jù),則輸出異常信息
{"data":"package id為:BZ-20200107000005 的indexpackage無記錄","flag":false,"code":null,"msg":"未查到數(shù)據(jù)"}
全局異常類可以用@RestControllerAdvice
,替代@ControllerAdvice
,因為這里返回的主要是json格式,這樣可以少寫一個@ResponseBody
。