本篇要點
- 介紹SpringBoot預設的異常處理機制。
- 如何定義錯誤頁面。
- 如何自定義異常資料。
- 如何自定義檢視解析。
- 介紹@ControllerAdvice註解處理異常。
一、SpringBoot預設的異常處理機制
預設情況下,SpringBoot為以下兩種情況提供了不同的響應方式:
- Browser Clients瀏覽器客戶端:通常情況下請求頭中的Accept會包含text/html,如果未定義/error的請求處理,就會出現如下html頁面:Whitelabel Error Page,關於error頁面的定製,接下來會詳細介紹。
- Machine Clients機器客戶端:Ajax請求,返回ResponseEntity實體json字串資訊。
{
"timestamp": "2020-10-30T15:01:17.353+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "java.lang.ArithmeticException: / by zero...",
"message": "/ by zero",
"path": "/"
}
SpringBoot預設提供了程式出錯的結果對映路徑/error,這個請求的處理邏輯在BasicErrorController中處理,處理邏輯如下:
// 判斷mediaType型別是否為text/html
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 建立ModelAndView物件,返回頁面
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
二、錯誤頁面的定製
相信Whitelabel Error Pag頁面我們經常會遇到,這樣體驗不是很好,在SpringBoot中可以嘗試定製錯誤頁面,定製方式主要分靜態和動態兩種:
- 靜態異常頁面
在classpath:/public/error
或classpath:/static/error
路徑下定義相關頁面:檔名應為確切的狀態程式碼,如404.html,或系列掩碼,如4xx.html。
舉個例子,如果你想匹配404或5開頭的所有狀態程式碼到靜態的html檔案,你的檔案目錄應該是下面這個樣子:
src/
+- main/
+- java/
| + <source code>
+- resources/
+- public/
+- error/
| +- 404.html
| +- 5xx.html
+- <other public assets>
如果500.html和5xx.html同時生效,那麼優先展示500.html頁面。
- 使用模板的動態頁面
放置在classpath:/templates/error
路徑下:這裡使用thymeleaf模板舉例:
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 5xx.html
+- <other templates>
頁面如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>5xx</title>
</head>
<body>
<h1>5xx</h1>
<p th:text="'error -->'+ ${error}"></p>
<p th:text="'status -->' + ${status}"></p>
<p th:text="'timestamp -->' + ${timestamp}"></p>
<p th:text="'message -->' + ${message}"></p>
<p th:text="'path -->' +${path}"></p>
<p th:text="'trace -->' + ${trace}"></p>
</body>
</html>
如果靜態頁面和動態頁面同時存在且都能匹配,SpringBoot對於錯誤頁面的優先展示規則如下:
如果發生了500錯誤:
動態500 -> 靜態500 -> 動態5xx -> 靜態5xx
三、自定義異常資料
預設的資料主要是以下幾個,這些資料定義在org.springframework.boot.web.servlet.error.DefaultErrorAttributes
中,資料的定義在getErrorAttributes
方法中。
DefaultErrorAttributes類在org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
自動配置類中定義:
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class,
search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
如果我們沒有提供ErrorAttributes的例項,SpringBoot預設提供一個DefaultErrorAttributes例項。
因此,我們就該知道如何去自定義異常資料屬性:
- 實現ErrorAttributes介面。
- 繼承DefaultErrorAttributes,本身已經定義對異常資料的處理,繼承更具效率。
定義方式如下:
/**
* 自定義異常資料
* @author Summerday
*/
@Slf4j
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
if(map.get("status").equals(500)){
log.warn("伺服器內部異常");
}
return map;
}
}
四、自定義異常檢視
自定義檢視的載入邏輯存在於BasicErrorController類的errorHtml方法中,用於返回一個ModelAndView物件,這個方法中,首先通過getErrorAttributes獲取到異常資料,然後呼叫resolveErrorView去建立一個ModelAndView物件,只有建立失敗的時候,使用者才會看到預設的錯誤提示頁面。
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(
//ErrorAttributes # getErrorAttributes
getErrorAttributes(request,
getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// E
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
DefaultErrorViewResolver類的resolveErrorView方法:
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 以異常狀態碼作為檢視名去locations路徑中去查詢頁面
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
// 如果找不到,以4xx,5xx series去查詢
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
那麼如何自定義呢?和自定義異常資料相同,如果我們定義了一個ErrorViewResolver的例項,預設的配置就會失效。
/**
* 自定義異常檢視解析
* @author Summerday
*/
@Component
public class MyErrorViewResolver extends DefaultErrorViewResolver {
public MyErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
super(applicationContext, resourceProperties);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
return new ModelAndView("/hyh/resolve",model);
}
}
此時,SpringBoot將會去/hyh目錄下尋找resolve.html頁面。
五、@ControllerAdvice註解處理異常
前後端分離的年代,後端往往需要向前端返回統一格式的json資訊,以下為封裝的AjaxResult物件:
public class AjaxResult extends HashMap<String, Object> {
//狀態碼
public static final String CODE_TAG = "code";
//返回內容
public static final String MSG_TAG = "msg";
//資料物件
public static final String DATA_TAG = "data";
private static final long serialVersionUID = 1L;
public AjaxResult() {
}
public AjaxResult(int code, String msg) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
public AjaxResult(int code, String msg, Object data) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (data != null) {
super.put(DATA_TAG, data);
}
}
public static AjaxResult ok() {
return AjaxResult.ok("操作成功");
}
public static AjaxResult ok(Object data) {
return AjaxResult.ok("操作成功", data);
}
public static AjaxResult ok(String msg) {
return AjaxResult.ok(msg, null);
}
public static AjaxResult ok(String msg, Object data) {
return new AjaxResult(HttpStatus.OK.value(), msg, data);
}
public static AjaxResult error() {
return AjaxResult.error("操作失敗");
}
public static AjaxResult error(String msg) {
return AjaxResult.error(msg, null);
}
public static AjaxResult error(String msg, Object data) {
return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data);
}
public static AjaxResult error(int code, String msg) {
return new AjaxResult(code, msg, null);
}
}
根據業務的需求不同,我們往往也需要自定義異常類,便於維護:
/**
* 自定義異常
*
* @author Summerday
*/
public class CustomException extends RuntimeException {
private static final long serialVersionUID = 1L;
private Integer code;
private final String message;
public CustomException(String message) {
this.message = message;
}
public CustomException(String message, Integer code) {
this.message = message;
this.code = code;
}
public CustomException(String message, Throwable e) {
super(message, e);
this.message = message;
}
@Override
public String getMessage() {
return message;
}
public Integer getCode() {
return code;
}
}
@ControllerAdvice和@RestControllerAdvice這倆註解的功能之一,就是做到Controller層面的異常處理,而兩者的區別,與@Controller和@RestController差不多。
@ExceptionHandler指定需要處理的異常類,針對自定義異常,如果是ajax請求,返回json資訊,如果是普通web請求,返回ModelAndView物件。
/**
* 全域性異常處理器
* @author Summerday
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(CustomException.class)
public Object handle(HttpServletRequest request, CustomException e) {
AjaxResult info = AjaxResult.error(e.getMessage());
log.error(e.getMessage());
// 判斷是否為ajax請求
if (isAjaxRequest(request)) {
return info;
}
ModelAndView mv = new ModelAndView();
mv.setViewName("custom"); // templates/custom.html
mv.addAllObjects(info);
mv.addObject("url", request.getRequestURL());
return mv;
}
private boolean isAjaxRequest(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
}
}
在Controller層,人為定義丟擲異常:
@RestController
public class TestController {
@GetMapping("/ajax")
public AjaxResult ajax() {
double alpha = 0.9;
if (Math.random() < alpha) {
throw new CustomException("自定義異常!");
}
return AjaxResult.ok();
}
}
最後,通過/templates/custom.html
定義的動態模板頁面展示資料:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>自定義介面</title>
</head>
<body>
<p th:text="'msg -->'+ ${msg}"></p>
<p th:text="'code -->'+ ${code}"></p>
<p th:text="'url -->'+ ${url}"></p>
</body>
</html>
原始碼下載
本文內容均為對優秀部落格及官方文件總結而得,原文地址均已在文中參考閱讀處標註。最後,文中的程式碼樣例已經全部上傳至Gitee:https://gitee.com/tqbx/springboot-samples-learn