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 中可以直接使用。
示例,在 “ Springboot基礎知識(08)- spring-boot-starter-web(Web啟動器)” 里 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:頁面不存在