Spring Boot統(tǒng)一異常處理最佳實(shí)踐(拓展篇)
前言
之前一篇文章介紹了基本的統(tǒng)一異常處理思路: Spring MVC/Boot 統(tǒng)一異常處理最佳實(shí)踐.
上篇文章也有許多人提出了一些問題:
- 如何區(qū)分 Ajax 請(qǐng)求和普通頁面請(qǐng)求, 以分別返回 JSON 錯(cuò)誤信息和錯(cuò)誤頁面.
- 如何結(jié)合 HTTP 狀態(tài)碼進(jìn)行統(tǒng)一異常處理.
今天這篇文章就主要來講講這些, 以及其他的一些拓展點(diǎn).
區(qū)分請(qǐng)求方式
其實(shí) Spring Boot 本身是內(nèi)置了一個(gè)異常處理機(jī)制的, 會(huì)判斷請(qǐng)求頭的參數(shù)來區(qū)分要返回 JSON 數(shù)據(jù)還是錯(cuò)誤頁面. 源碼為: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController, 他會(huì)處理 /error 請(qǐng)求. 核心處理代碼如下:
@RequestMapping(
produces = {"text/html"}
)
// 如果請(qǐng)求頭是 text/html, 則找到錯(cuò)誤頁面, 并返回
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
// 1. 獲取 HTTP 錯(cuò)誤狀態(tài)碼
HttpStatus status = this.getStatus(request);
// 2. 調(diào)用 getErrorAttributes 獲取響應(yīng)的 map 結(jié)果集.
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
// 3. 設(shè)置響應(yīng)頭的狀態(tài)碼
response.setStatus(status.value());
// 4. 獲取錯(cuò)誤頁面的路徑
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 調(diào)用 getErrorAttributes 獲取響應(yīng)的 map 結(jié)果集.
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
// 獲取 HTTP 錯(cuò)誤狀態(tài)碼
HttpStatus status = this.getStatus(request);
// 返回給頁面 JSON 信息.
return new ResponseEntity(body, status);
}
這兩個(gè)方法的共同點(diǎn)是: 他們都調(diào)用了 this.getErrorAttributes(…) 方法來獲取響應(yīng)信息.
然后來看看他默認(rèn)情況下對(duì)于 AJAX 請(qǐng)求和 HTML 請(qǐng)求, 分別的返回結(jié)果是怎樣的:

對(duì)于返回錯(cuò)誤頁面, 其中還調(diào)用了一個(gè)非常重要的方法: this.resolveErrorView(...) 方法, 源碼我就不帶大家看了, 他的作用就是根據(jù) HTTP 狀態(tài)碼來去找錯(cuò)誤頁面, 如 500 錯(cuò)誤會(huì)去找 /error/500.html, 403 錯(cuò)誤回去找 /error/403.html, 如果找不到則再找 /error/4xx.html 或 /error/5xx.html 頁面. 還找不到的話, 則會(huì)去找 /error.html 頁面, 如果都沒有配置, 則會(huì)使用 Spring Boot 默認(rèn)的頁面. 即:

看到這里, 應(yīng)該就清楚了, 我們主要需要做四件事:
- 發(fā)送異常后, 重定向到 BasicErrorController 來處理 (既然Spring Boot 都已經(jīng)寫好了區(qū)分請(qǐng)求的功能, 我們就不必要再寫這些判斷代碼了)
- 自定義 HTTP 錯(cuò)誤狀態(tài)碼
- 他返回的信息格式可能不是我們想要的, 所以必須要改造
getErrorAttributes(...)方法, 以自定義我們向頁面返回的數(shù)據(jù). (自定義錯(cuò)誤信息) - 創(chuàng)建我們自己的 /error/4xx.html 或 /error/5xx.html 等頁面, (自定義錯(cuò)誤頁面)
BasicErrorController
第一點(diǎn)很簡單, BasicErrorController 他處理 /error 請(qǐng)求, 我們只需要將頁面重定向到 /error 即可, 在 ControllerAdvice 中是這樣的:
@ControllerAdvice
public class WebExceptionHandler {
@ExceptionHandler
public String methodArgumentNotValid(BindException e) {
// do something
return "/error";
}
}
自定義 HTTP 錯(cuò)誤狀態(tài)碼
我們來看下 this.getStatus(request); 的源碼, 看他原來時(shí)如何獲取錯(cuò)誤狀態(tài)碼的:
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
} else {
try {
return HttpStatus.valueOf(statusCode);
} catch (Exception var4) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
}
簡單來說就是從 request 域中獲取 javax.servlet.error.status_code 的值, 如果為 null 或不合理的值, 都返回 500. 既然如何在第一步, 重定向到 /error 之前將其配置到 request 域中即可, 如:
@ControllerAdvice
public class WebExceptionHandler {
@ExceptionHandler
public String methodArgumentNotValid(BindException e, HttpServletRequest request) {
request.setAttribute("javax.servlet.error.status_code", 400);
// do something
return "forward:/error";
}
}
自定義錯(cuò)誤信息
也就是 getErrorAttributes 方法, 默認(rèn)的代碼是這樣的:
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
他獲取了時(shí)間戳, 錯(cuò)誤狀態(tài)碼, 錯(cuò)誤信息, 錯(cuò)誤路徑等信息, 和我們之前看到默認(rèn)的返回內(nèi)容是一致的:
{
"timestamp": "2019-01-27T07:08:30.011+0000",
"status": 500,
"error": "Internal Server Error",
"message": "/ by zero",
"path": "/user/index"
}
同樣的思路, 我們將錯(cuò)誤信息也放到 request 域中, 然后在 getErrorAttributes 中從 request 域中獲取:
@ControllerAdvice
public class WebExceptionHandler {
@ExceptionHandler
public String methodArgumentNotValid(BindException e, HttpServletRequest request) {
request.setAttribute("javax.servlet.error.status_code", 400);
request.setAttribute("code", 1);
request.setAttribute("message", "參數(shù)校驗(yàn)失敗, xxx");
// do something
return "forward:/error";
}
}
再繼承 DefaultErrorAttributes 類, 重寫 getErrorAttributes 方法:
//@Component
public class MyDefaultErrorAttributes extends DefaultErrorAttributes {
@Override
//重寫 getErrorAttributes方法-添加自己的項(xiàng)目數(shù)據(jù)
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace) {
Map<String, Object> map = new HashMap<>();
// 從 request 域中獲取 code
Object code = webRequest.getAttribute("code", RequestAttributes.SCOPE_REQUEST);
// 從 request 域中獲取 message
Object message = webRequest.getAttribute("message", RequestAttributes.SCOPE_REQUEST);
map.put("code", code);
map.put("message", message);
return map;
}
}
自定義錯(cuò)誤頁面
我們遵循 SpringBoot 的規(guī)則, 在 /error/ 下建立 400.html, 500.html 等頁面細(xì)粒度的錯(cuò)誤, 并配置一個(gè) /error.html 用來處理細(xì)粒度未處理到的其他錯(cuò)誤.
/error/400.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>400</title>
</head>
<body>
<h1>400</h1>
<h1 th:text="$[code]"></h1>
<h1 th:text="${message}"></h1>
</body>
</html>
/error/500.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>500</title>
</head>
<body>
<h1>500</h1>
<h1 th:text="$[code]"></h1>
<h1 th:text="${message}"></h1>
</body>
</html>
/error.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系統(tǒng)出現(xiàn)了錯(cuò)誤</title>
</head>
<body>
<h1>ERROR PAGE</h1>
<h1 th:text="$[code]"></h1>
<h1 th:text="${message}"></h1>
</body>
</html>
測試效果
到此位置, 大功告成, 然后來創(chuàng)造一個(gè)異常來測試一下效果:




前端 error 處理
現(xiàn)在使用了 HTTP 狀態(tài)碼, 所以 Ajax 請(qǐng)求出現(xiàn)錯(cuò)誤后, 需要在每個(gè) Ajax 請(qǐng)求方法中都寫 error: function() {} 方法, 甚至麻煩. 好在 jQuery 為我們提供了全局處理 Ajax 的 error 結(jié)果的方法 ajaxError() :
$(document).ajaxError(function(event, response){
console.log("錯(cuò)誤響應(yīng)狀態(tài)碼: ",response.status);
console.log("錯(cuò)誤響應(yīng)結(jié)果: ",response.responseJSON);
alert("An error occurred!");
});
結(jié)語
回顧一下講到的這些內(nèi)容:
- 理解 SpringBoot 默認(rèn)提供的 BasicErrorController
- 自定義 HTTP 錯(cuò)誤狀態(tài)碼, (通過 request 域的
javax.servlet.error.status_code參數(shù)) - 自定義錯(cuò)誤信息, (將我們自定義的錯(cuò)誤信息放到 request 域中, 并重寫 DefaultErrorAttributes 的 getErrorAttributes 方法, 從 request 域中獲取這些信息).
- 自定義錯(cuò)誤頁面, (根據(jù) SpringBoot 查找錯(cuò)誤頁面的邏輯來自定義錯(cuò)誤頁面: /error/500.html, /error/400.html, /error.html)
可以自己根據(jù)文章一步一步走一遍, 或者看我寫好的演示項(xiàng)目先看看效果, 總是動(dòng)手實(shí)踐, 而不是收藏文章并封存。
演示項(xiàng)目地址: https://github.com/zhaojun1998/exception-handler-demo
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
SpringBoot中@Transiactional注解沒有效果的解決
這篇文章主要介紹了SpringBoot中@Transiactional注解沒有效果的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
解決spring中redistemplate不能用通配符keys查出相應(yīng)Key的問題
這篇文章主要介紹了解決spring中redistemplate不能用通配符keys查出相應(yīng)Key的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11
Java HashSet(散列集),HashMap(散列映射)的簡單介紹
這篇文章主要介紹了Java HashSet(散列集),HashMap(散列映射)的簡單介紹,幫助大家更好的理解和學(xué)習(xí)Java集合框架的相關(guān)知識(shí),感興趣的朋友可以了解下2021-01-01
java增強(qiáng)for循環(huán)的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄猨ava增強(qiáng)for循環(huán)的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-09-09

