Spring Boot 統一異常處理最佳實踐 -- 拓展篇
之前一篇文章介紹了基本的統一異常處理思路: Spring MVC/Boot 統一異常處理最佳實踐 .
上篇文章也有許多人提出了一些問題:
- 如何區分 Ajax 請求和普通頁面請求, 以分別返回 JSON 錯誤資訊和錯誤頁面.
- 如何結合 HTTP 狀態碼進行統一異常處理.
今天這篇文章就主要來講講這些, 以及其他的一些拓展點.
區分請求方式
其實 Spring Boot 本身是內建了一個異常處理機制的, 會判斷請求頭的引數來區分要返回 JSON 資料還是錯誤頁面. 原始碼為: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
, 他會處理 /error
請求. 核心處理程式碼如下:
@RequestMapping( produces = {"text/html"} ) // 如果請求頭是 text/html, 則找到錯誤頁面, 並返回 public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { // 1. 獲取 HTTP 錯誤狀態碼 HttpStatus status = this.getStatus(request); // 2. 呼叫 getErrorAttributes 獲取響應的 map 結果集. Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML))); // 3. 設定響應頭的狀態碼 response.setStatus(status.value()); // 4. 獲取錯誤頁面的路徑 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) { // 呼叫 getErrorAttributes 獲取響應的 map 結果集. Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL)); // 獲取 HTTP 錯誤狀態碼 HttpStatus status = this.getStatus(request); // 返回給頁面 JSON 資訊. return new ResponseEntity(body, status); }
這兩個方法的共同點是: 他們都呼叫了 this.getErrorAttributes(…) 方法來獲取響應資訊.
然後來看看他預設情況下對於 AJAX 請求和 HTML 請求, 分別的返回結果是怎樣的:

對於返回錯誤頁面, 其中還呼叫了一個非常重要的方法: this.resolveErrorView(...)
方法, 原始碼我就不帶大家看了, 他的作用就是根據 HTTP 狀態碼來去找錯誤頁面, 如 500 錯誤會去找 /error/500.html
, 403 錯誤回去找 /error/403.html
, 如果找不到則再找 /error/4xx.html
或 /error/5xx.html
頁面. 還找不到的話, 則會去找 /error.html
頁面, 如果都沒有配置, 則會使用 Spring Boot 預設的頁面. 即:
看到這裡, 應該就清楚了, 我們主要需要做四件事:
- 傳送異常後, 重定向到
BasicErrorController
來處理 (既然Spring Boot 都已經寫好了區分請求的功能, 我們就不必要再寫這些判斷程式碼了) - 自定義 HTTP 錯誤狀態碼
- 他返回的資訊格式可能不是我們想要的, 所以必須要改造
getErrorAttributes(...)
方法, 以自定義我們向頁面返回的資料. (自定義錯誤資訊) - 建立我們自己的
/error/4xx.html
或/error/5xx.html
等頁面, (自定義錯誤頁面)
BasicErrorController
第一點很簡單, BasicErrorController
他處理 /error
請求, 我們只需要將頁面重定向到 /error
即可, 在 ControllerAdvice 中是這樣的:
@ControllerAdvice public class WebExceptionHandler { @ExceptionHandler public String methodArgumentNotValid(BindException e) { // do something return "/error"; } }
自定義 HTTP 錯誤狀態碼
我們來看下 this.getStatus(request);
的原始碼, 看他原來時如何獲取錯誤狀態碼的:
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"; } }
自定義錯誤資訊
也就是 getErrorAttributes 方法, 預設的程式碼是這樣的:
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; }
他獲取了時間戳, 錯誤狀態碼, 錯誤資訊, 錯誤路徑等資訊, 和我們之前看到預設的返回內容是一致的:
{ "timestamp": "2019-01-27T07:08:30.011+0000", "status": 500, "error": "Internal Server Error", "message": "/ by zero", "path": "/user/index" }
同樣的思路, 我們將錯誤資訊也放到 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", "引數校驗失敗, xxx"); // do something return "forward:/error"; } }
再繼承 DefaultErrorAttributes
類, 重寫 getErrorAttributes
方法:
//@Component public class MyDefaultErrorAttributes extends DefaultErrorAttributes { @Override //重寫 getErrorAttributes方法-新增自己的專案資料 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; } }
自定義錯誤頁面
我們遵循 SpringBoot 的規則, 在 /error/
下建立 400.html
, 500.html
等頁面細粒度的錯誤, 並配置一個 /error.html
用來處理細粒度未處理到的其他錯誤.
/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>系統出現了錯誤</title> </head> <body> <h1>ERROR PAGE</h1> <h1 th:text="${code}"></h1> <h1 th:text="${message}"></h1> </body> </html>
測試效果
到此位置, 大功告成, 然後來創造一個異常來測試一下效果:

前端 error 處理
現在使用了 HTTP 狀態碼, 所以 Ajax 請求出現錯誤後, 需要在每個 Ajax 請求方法中都寫 error: function() {}
方法, 甚至麻煩. 好在 jQuery 為我們提供了全域性處理 Ajax 的 error 結果的方法 ajaxError() :
$(document).ajaxError(function(event, response){ console.log("錯誤響應狀態碼: ",response.status); console.log("錯誤響應結果: ",response.responseJSON); alert("An error occurred!"); });
結語
回顧一下講到的這些內容:
- 理解
SpringBoot
預設提供的BasicErrorController
- 自定義 HTTP 錯誤狀態碼, (通過 request 域的
javax.servlet.error.status_code
引數) - 自定義錯誤資訊, (將我們自定義的錯誤資訊放到 request 域中, 並重寫
DefaultErrorAttributes
的getErrorAttributes
方法, 從 request 域中獲取這些資訊). - 自定義錯誤頁面, (根據 SpringBoot 查詢錯誤頁面的邏輯來自定義錯誤頁面:
/error/500.html
,/error/400.html
,/error.html
)
可以自己根據文章一步一步走一遍, 或者看我寫好的演示專案先看看效果, 總是動手實踐, 而不是收藏文章並封存。
https://github.com/zhaojun1998/exception-handler-demo