Spring基礎知識(31)- Spring Boot (十二)
預設異常處理、全域性異常處理
1. 預設異常處理
在Web 開發中,往往需要一個統一的異常處理機制,來保證客戶端能接收較為友好的提示。Spring Boot 同樣提供了一套預設的異常處理機制。
1) Spring Boot 預設異常處理機制
Spring Boot 提供了一套預設的異常處理機制,一旦程式中出現了異常,Spring Boot 會自動識別客戶端的型別(瀏覽器或客戶端APP),並根據客戶端的不同,以不同的形式展示異常資訊。
例如,訪問一個不存在的頁面,結果如下。
(1) 瀏覽器,Spring Boot 會響應一個 “whitelabel” 錯誤檢視,以 HTML 格式呈現錯誤資訊
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Tue Apr 19 20:01:40 CST 2022
There was an unexpected error (type=Not Found, status=404).
(2) 客戶端APP,Spring Boot 將生成 JSON 響應,來展示異常訊息
{
"timestamp": "2022-04-19T12:03:08.474+00:00",
"status": 404,
"error": "Not Found",
"path": "/test"
}
2) Spring Boot 異常處理自動配置原理
Spring Boot 通過配置類 ErrorMvcAutoConfiguration 對異常處理提供了自動配置,該配置類向容器中注入了以下 4 個元件。
ErrorPageCustomizer:該元件會在在系統發生異常後,預設將請求轉發到 “/error” 上;
BasicErrorController:處理預設的 “/error” 請求;
DefaultErrorViewResolver:預設的錯誤檢視解析器,將異常資訊解析到相應的錯誤檢視上;
DefaultErrorAttributes:用於頁面上共享異常資訊。
(1) ErrorPageCustomizer
ErrorMvcAutoConfiguration 向容器中注入了一個名為 ErrorPageCustomizer 的元件,它主要用於定製錯誤頁面的響應規則。
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
ErrorPageCustomizer 通過 registerErrorPages() 方法來註冊錯誤頁面的響應規則。當系統中發生異常後,ErrorPageCustomizer 元件會自動生效,並將請求轉發到 “/error”上,交給 BasicErrorController 進行處理,其部分程式碼如下。
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
// 將請求轉發到 /errror(this.properties.getError().getPath())上
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
// 註冊錯誤頁面
errorPageRegistry.addErrorPages(errorPage);
}
(2) BasicErrorController
ErrorMvcAutoConfiguration 還向容器中注入了一個錯誤控制器元件 BasicErrorController,程式碼如下。
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
Spring Boot 通過 BasicErrorController 進行統一的錯誤處理(例如預設的“/error”請求)。Spring Boot 會自動識別發出請求的客戶端的型別(瀏覽器客戶端或機器客戶端),並根據客戶端型別,將請求分別交給 errorHtml() 和 error() 方法進行處理。
方法 | 描述 |
ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) | 瀏覽器訪問返回 text/html(錯誤頁面) |
ResponseEntity<Map<String, Object>> error(HttpServletRequest request) | 客戶端APP(例如安卓、IOS、Postman 等等)訪問返回 JSON |
(3) DefaultErrorViewResolver
ErrorMvcAutoConfiguration 還向容器中注入了一個預設的錯誤檢視解析器元件 DefaultErrorViewResolver,程式碼如下。
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
當發出請求的客戶端為瀏覽器時,Spring Boot 會獲取容器中所有的 ErrorViewResolver 物件(錯誤檢視解析器),並分別呼叫它們的 resolveErrorView() 方法對異常資訊進行解析,其中自然也包括 DefaultErrorViewResolver(預設錯誤資訊解析器)。
DefaultErrorViewResolver 解析異常資訊的步驟如下:
a) 根據錯誤狀態碼(例如 404、500、400 等),生成一個錯誤檢視 error/status,例如 error/404、error/500、error/400;
b) 嘗試使用模板引擎解析 error/status 檢視,即嘗試從 classpath 類路徑下的 templates 目錄下,查詢 error/status.html,例如 error/404.html、error/500.html、error/400.html;
c) 若模板引擎能夠解析到 error/status 檢視,則將檢視和資料封裝成 ModelAndView 返回並結束整個解析流程,否則跳轉到第 4 步;
d) 依次從各個靜態資原始檔夾中查詢 error/status.html,若在靜態資料夾中找到了該錯誤頁面,則返回並結束整個解析流程,否則跳轉到第 5 步;
e) 將錯誤狀態碼(例如 404、500、400 等)轉換為 4xx 或 5xx,然後重複前 4 個步驟,若解析成功則返回並結束整個解析流程,否則跳轉第 6 步;
f) 處理預設的 “/error ”請求,使用 Spring Boot 預設的錯誤頁面(Whitelabel Error Page)。
(4) DefaultErrorAttributes
ErrorMvcAutoConfiguration 還向容器中注入了一個元件預設錯誤屬性處理工具 DefaultErrorAttributes,程式碼如下。
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
DefaultErrorAttributes 是 Spring Boot 的預設錯誤屬性處理工具,它可以從請求中獲取異常或錯誤資訊,並將其封裝為一個 Map 物件返回。
在 Spring Boot 預設的 Error 控制器(BasicErrorController)處理錯誤時,會呼叫 DefaultErrorAttributes 的 getErrorAttributes() 方法獲取錯誤或異常資訊,並封裝成 model 資料(Map 物件),返回到頁面或 JSON 資料中。該 model 資料主要包含以下屬性:
timestamp:時間戳;
status:錯誤狀態碼
error:錯誤的提示
exception:導致請求處理失敗的異常物件
message:錯誤/異常訊息
trace: 錯誤/異常棧資訊
path:錯誤/異常丟擲時所請求的URL路徑
注:所有通過 DefaultErrorAttributes 封裝到 model 資料中的屬性,都可以直接在頁面或 JSON 中獲取。
2. 全域性異常處理
Spring Boot 提供了一套預設的異常處理機制,但是 Spring Boot 提供的預設異常處理機制卻並不一定適合我們實際的業務場景,因此,我們通常會根據自身的需要對 Spring Boot 全域性異常進行統一定製,例如定製錯誤頁面,定製錯誤資訊等。
我們可以通過以下 3 種方式定製 Spring Boot 錯誤頁面:
(1) 自定義 error.html
(2) 自定義動態錯誤頁面
(3) 自定義靜態錯誤頁面
注:前兩種方式需要在 Spring Boot 整合 Thymeleaf 模板(或其它 Spring Boot 支援的模版)的基礎上實現,第三種不需要整合 Thymeleaf 模板也可以實現。
1) 自定義 error.html
可以直接在模板資料夾 resources/templates 下建立 error.html ,覆蓋 Spring Boot 預設的錯誤檢視頁面(Whitelabel Error Page)。
示例,建立 src/main/resources/templates/error.html 檔案
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>自定義 error.html</title> 6 </head> 7 <body> 8 <h1>自定義 error.html</h1> 9 <p>status:<span th:text="${status}"></span></p> 10 <p>error:<span th:text="${error}"></span></p> 11 <p>timestamp:<span th:text="${timestamp}"></span></p> 12 <p>message:<span th:text="${message}"></span></p> 13 <p>path:<span th:text="${path}"></span></p> 14 </body> 15 </html>
2) 自定義動態錯誤頁面
如果 Sprng Boot 專案使用了模板,當程式發生異常時,Spring Boot 的預設錯誤檢視解析器(DefaultErrorViewResolver)就會解析模板資料夾 resources/templates 下 error 目錄中的錯誤檢視頁面。
(1) 精確匹配
可以根據錯誤狀態碼(例如 404、500、400 等等)的不同,分別建立不同的動態錯誤頁面(例如 404.html、500.html、400.html 等等),並將它們存放在模板引擎資料夾下的 error 目錄中。當發生異常時,Spring Boot 會根據其錯誤狀態碼精確匹配到對應的錯誤頁面上。
示例,建立 src/main/resources/templates/error/404.html 檔案
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>自定義動態錯誤頁面 404.html</title> 6 </head> 7 <body> 8 <h1>自定義動態錯誤頁面 404.html</h1> 9 <p>status:<span th:text="${status}"></span></p> 10 <p>error:<span th:text="${error}"></span></p> 11 <p>timestamp:<span th:text="${timestamp}"></span></p> 12 <p>message:<span th:text="${message}"></span></p> 13 <p>path:<span th:text="${path}"></span></p> 14 </body> 15 </html>
(2) 模糊匹配
可以使用 4xx.html 和 5xx.html 作為動態錯誤頁面的檔名,並將它們存放在模板引擎資料夾下的 error 目錄中,來模糊匹配對應型別的所有錯誤,例如 404、400 等錯誤狀態碼以“4”開頭的所有異常,都會解析到動態錯誤頁面 4xx.html 上。
示例,建立 src/main/resources/templates/error/4xx.html 檔案
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>自定義動態錯誤頁面 4xx.html</title> 6 </head> 7 <body> 8 <h1>自定義動態錯誤頁面 4xx.html</h1> 9 <p>status:<span th:text="${status}"></span></p> 10 <p>error:<span th:text="${error}"></span></p> 11 <p>timestamp:<span th:text="${timestamp}"></span></p> 12 <p>message:<span th:text="${message}"></span></p> 13 <p>path:<span th:text="${path}"></span></p> 14 </body> 15 </html>
3) 自定義靜態錯誤頁面
若 Sprng Boot 專案沒有使用模板,當程式發生異常時,Spring Boot 的預設錯誤檢視解析器(DefaultErrorViewResolver)則會解析靜態資原始檔夾下 error 目錄中的靜態錯誤頁面。
(1) 精確匹配
可以根據錯誤狀態碼(例如 404、500、400 等等)的不同,分別建立不同的靜態錯誤頁面(例如 404.html、500.html、400.html 等等),並將它們存放在靜態資原始檔夾下的 error 目錄中。當發生異常時,Spring Boot 會根據錯誤狀態碼精確匹配到對應的錯誤頁面上。
示例,建立 src/main/resources/static/error/404.html 檔案
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>自定義靜態錯誤頁面 404.html</title> 6 </head> 7 <body> 8 <h1>自定義靜態錯誤頁面 404.html</h1> 9 <p>status:<span th:text="${status}"></span></p> 10 <p>error:<span th:text="${error}"></span></p> 11 <p>timestamp:<span th:text="${timestamp}"></span></p> 12 <p>message:<span th:text="${message}"></span></p> 13 <p>path:<span th:text="${path}"></span></p> 14 </body> 15 </html>
注:這裡的 404.html 是個靜態頁面,頁面裡不顯示錯誤資訊。
(2) 模糊匹配
可以使用 4xx.html 和 5xx.html 作為靜態錯誤頁面的檔名,並將它們存放在靜態資原始檔夾下的 error 目錄中,來模糊匹配對應型別的所有錯誤,例如 404、400 等錯誤狀態碼以 “4” 開頭的所有錯誤,都會解析到靜態錯誤頁面 4xx.html 上。
示例,建立 src/main/resources/static/error/4xx.html 檔案
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>自定義靜態錯誤頁面 4xx.html</title> 6 </head> 7 <body> 8 <h1>自定義靜態錯誤頁面 4xx.html</h1> 9 <p>status:<span th:text="${status}"></span></p> 10 <p>error:<span th:text="${error}"></span></p> 11 <p>timestamp:<span th:text="${timestamp}"></span></p> 12 <p>message:<span th:text="${message}"></span></p> 13 <p>path:<span th:text="${path}"></span></p> 14 </body> 15 </html>
4) 錯誤頁面優先順序
以上 5 種方式均可以定製 Spring Boot 錯誤頁面,且它們的優先順序順序為:
自定義動態錯誤頁面(精確匹配)> 自定義靜態錯誤頁面(精確匹配)> 自定義動態錯誤頁面(模糊匹配)> 自定義靜態錯誤頁面(模糊匹配)> 自定義 error.html
當遇到錯誤時,Spring Boot 會按照優先順序由高到低,依次查詢解析錯誤頁,一旦找到可用的錯誤頁面,則直接返回客戶端展示。
5) 定製錯誤資訊
Spring Boot 提供了一套預設的異常處理機制,其主要流程如下:
(1) 發生異常時,將請求轉發到 “/error”,交由 BasicErrorController(Spring Boot 預設的 Error 控制器) 進行處理;
(2) BasicErrorController 根據客戶端的不同,自動適配返回的響應形式,瀏覽器返回錯誤頁面,客戶端APP返回 JSON 資料。
(3) BasicErrorController 處理異常時,會呼叫 DefaultErrorAttributes(預設的錯誤屬性處理工具) 的 getErrorAttributes() 方法獲取錯誤資料。
在預設的異常處理機制上,做一些調整,可以定製 Spring Boot 的錯誤資訊,具體步驟如下。
(1) 自定義異常處理類(使用 @ControllerAdvice 註解),將請求轉發到 “/error”,交由 Spring Boot 底層(BasicErrorController)進行處理,自動適配瀏覽器客和客戶端APP;
(2) 通過繼承 DefaultErrorAttributes 來定義一個錯誤屬性處理工具,並在原來的基礎上新增自定義的錯誤資訊。
注:被 @ControllerAdvice 註解的類可以用來實現全域性異常處理,這是 Spring MVC 中提供的功能,在 Spring Boot 中可以直接使用。
示例,在 “Spring基礎知識(27)- Spring Boot (八)” 裡 SpringbootWeb 專案整合 Thymeleaf 的基礎上,程式碼如下。
(1) 建立 src/main/java/com/example/exception/PageNotFoundException.java 檔案
1 package com.example.exception; 2 3 public class PageNotExistException extends RuntimeException { 4 public PageNotExistException() { 5 super("頁面不存在"); 6 } 7 }
(2) 建立 src/main/java/com/example/controller/PageNotExistExceptionHandler.java 檔案
1 package com.example.controller; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 import javax.servlet.http.HttpServletRequest; 6 7 import org.springframework.web.bind.annotation.ControllerAdvice; 8 import org.springframework.web.bind.annotation.ExceptionHandler; 9 10 import com.example.exception.PageNotExistException; 11 12 @ControllerAdvice 13 public class PageNotExistExceptionHandler { 14 @ExceptionHandler(PageNotExistException.class) 15 public String handleException(Exception e, HttpServletRequest request) { 16 17 Map<String, Object> map = new HashMap<>(); 18 request.setAttribute("javax.servlet.error.status_code", 404); 19 map.put("code", "PageNotExist"); 20 map.put("message", e.getMessage()); 21 request.setAttribute("ext", map); 22 return "forward:/error"; 23 } 24 }
(3) 建立 src/main/java/com/example/componet/CustomErrorAttributes.java 檔案
1 package com.example.componet; 2 3 import java.util.Map; 4 import org.springframework.stereotype.Component; 5 import org.springframework.web.context.request.WebRequest; 6 import org.springframework.boot.web.error.ErrorAttributeOptions; 7 import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 8 9 @Component 10 public class CustomErrorAttributes extends DefaultErrorAttributes { 11 @Override 12 public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 13 Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options); 14 15 // 新增自定義的錯誤資料 16 errorAttributes.put("custom", "Custom Error Attributes"); 17 // 獲取 PageNotExistExceptionHandler 傳入 request 域中的錯誤資料 18 Map ext = (Map) webRequest.getAttribute("ext", 0); 19 errorAttributes.put("ext", ext); 20 return errorAttributes; 21 } 22 }
(4) 建立 src/main/resources/templates/error/404.html 檔案
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>自定義動態錯誤頁面 404.html</title> 6 </head> 7 <body> 8 <h1>自定義靜態錯誤頁面 404.html</h1> 9 <p>status:<span th:text="${status}"></span></p> 10 <p>error:<span th:text="${error}"></span></p> 11 <p>timestamp:<span th:text="${timestamp}"></span></p> 12 <p>message:<span th:text="${message}"></span></p> 13 <p>path:<span th:text="${path}"></span></p> 14 15 <h3>定製錯誤資訊:</h3> 16 <p>custom:<span th:text="${custom}"></span></p> 17 <p>ext.code:<span th:text="${ext.code}"></span></p> 18 <p>ext.message:<span th:text="${ext.message}"></span></p> 19 </body> 20 </html>
(5) 建立 src/main/com/example/controller/IndexController.java 檔案
1 package com.example.controller; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.ResponseBody; 6 7 import com.example.exception.PageNotExistException; 8 9 @Controller 10 public class IndexController { 11 @ResponseBody 12 @RequestMapping("/test") 13 public String testErr(String action) { 14 15 if ("error".equals(action)) { 16 throw new PageNotExistException(); 17 } 18 return "Test"; 19 } 20 }
訪問:http://localhost:9090/test?action=error
自定義動態錯誤頁面 404.html
status:404
error:Not Found
timestamp:Wed Apr 20 20:49:52 CST 2022
message:
path:/test
定製錯誤資訊:
custom:Custom Error Attributes
ext.code:PageNotExist
ext.message:頁面不存在