SpringBoot異常處理 1.基本介紹 預設情況下,SpringBoot提供/error處理所有錯誤的映射,也就是說當出現錯誤時,SpringBoot底層會請求轉發到/error這個映射路徑所關聯的頁面或者控制器方法。(預設異常處理機制) 要驗證這個點,我們只需要設置一個攔截器,當每次請求時都在 ...
SpringBoot異常處理
1.基本介紹
預設情況下,SpringBoot提供/error
處理所有錯誤的映射,也就是說當出現錯誤時,SpringBoot底層會請求轉發到/error
這個映射路徑所關聯的頁面或者控制器方法。(預設異常處理機制)
要驗證這個點,我們只需要設置一個攔截器,當每次請求時都在preHandle()中列印請求URI。在瀏覽器訪問不存在的路徑映射時:
-
瀏覽器:SpringBoot會響應一個"whitelabel"的錯誤視圖,並以HTML格式呈現
-
伺服器:後臺輸出請求的URI為
-
整個過程:當瀏覽器訪問不存在的路徑映射時,就產生了錯誤。這時 SpringBoot(底層由預設錯誤視圖解析器 DefaultErrorViewResolver 請求轉發到
/error
路徑關聯的頁面,該路徑若沒有被排除,也會經過攔截器處理)會立即去請求/error
映射關聯的資源(預設為"whitelabel" 錯誤視圖),然後返回給瀏覽器,我們就看到了這個錯誤視圖。
2.預設錯誤視圖解析器
在SpringBoot發現瀏覽器請求的路徑不存在後,底層發生了一系列的操作:
(1)當瀏覽器訪問不存在的路徑映射時,就產生了錯誤
(2)底層由 DefaultErrorViewResolver (預設錯誤視圖解析器)先去遍歷項目中設置的所有靜態資源路徑,嘗試從這些靜態資源路徑中找到/error/404.html
文件
//第一次執行resolve()方法:
//這裡的viewName就是狀態碼
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;//第一次:errorViewName=error/404
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);//進入resolveResource()方法
}
遍歷項目中設置的所有靜態資源路徑,嘗試從這些靜態資源路徑中找到/error/404.html
文件
//第一次執行resolveResource()方法
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//resources.getStaticLocations()就是項目中的靜態資源路徑,根據你的設置而變化
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
//如果找到了,就返回這個視圖
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
(3)如果找不到,退而求其次,再遍歷項目中設置的所有靜態資源路徑,嘗試從這些靜態資源路徑中找到/error/4xx.html
文件
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//再一次調用resolve()方法
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
//第二次執行resolve()方法:
//這裡的viewName就是狀態碼
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;//第二次:errorViewName=/error/4xx
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);//進入resolveResource()方法
}
遍歷項目中設置的所有靜態資源路徑,嘗試從這些靜態資源路徑中找到/error/4xx.html
文件
//第二次執行resolveResource()方法
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//遍歷項目中設置的所有靜態資源路徑
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
//如果找到了就返回
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
示例:
(4)以上兩輪都找不到匹配的視圖,就執行下麵的方法(位於AbstractErrorController.java):
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
如果以上方法仍然返回null,然後去BasicErrorController.java里執行如下方法:
@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 = resolveErrorView(request, response, status, model);
//modelAndView為null,產生一個預設的新的視圖,並返回
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
(5)此時瀏覽器接收到的視圖就是這個預設的視圖
3.攔截器VS過濾器
-
使用範圍不同
(1)過濾器實現的是javax.servlet.Filter介面,而這個介面是在Servlet規範中定義的,也就是說過濾器Filter的使用依賴於Tomcat等容器,Filter只能在web程式中使用
(2)攔截器是一個Spring組件,由Spring容器管理,並不依賴Tomcat等容器,是可以單獨使用的。它不僅能應用在web程式中,也能用於Application等程式中。
-
兩者的觸發時機也不同
(1)過濾器Filter在請求進入容器後,在進入Servlet之前進行預處理。請求結束則是在servlet處理完以後。
(2)攔截器Interceptor是在請求進入Servlet之後(即DispatcherServlet,前端控制器),在進入Controller之前進行預處理的,Controller中渲染了對應的視圖之後請求結束。
-
過濾器不會處理請求轉發,攔截器會處理請求轉發(前提是攔截器沒有放行此請求)。原因是過濾器處理的是容器接收過來的外部請求,而請求轉發是伺服器內部,Servlet之間的處理。但攔截器是Spring的一個組件,依然會去處理請求轉發,除非請求轉發的路徑被攔截器放行了。
例子演示1--過濾器與攔截器的執行順序
我們分別在SpringBoot項目中配置一個過濾器和攔截器,測試它們的執行順序
(1)創建過濾器
package com.li.thymeleaf.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
/**
* @author 李
* @version 1.0
*/
@Component
@Slf4j
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter的init()被調用...");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("MyFilter的doFilter()被調用...");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
log.info("MyFilter的destroy()被調用...");
}
}
(2)創建攔截器
package com.li.thymeleaf.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author 李
* @version 1.0
*/
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("MyInterceptor的preHandle()被執行...");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("MyInterceptor的postHandle()被執行...");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("MyInterceptor的afterCompletion()被執行...");
}
}
在配置類中註冊攔截器,註入到spring容器中
package com.li.thymeleaf.config;
import com.li.thymeleaf.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 李
* @version 1.0
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//addInterceptor註冊自定義攔截器
//addPathPatterns指定攔截器規則(攔截所有請求/**)
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/**");
}
}
(3)瀏覽器請求某一個資源,後臺輸出如下:
例子演示2--過濾器不會處理請求轉發,攔截器會處理請求轉發
我們在瀏覽器請求一個不存在的資源如http://localhost:8080/xxx
,後臺輸出如下:
這是因為SpringBoot底層處理了/error
,進行了請求轉發(預設錯誤視圖解析器請求轉發到 /error
路徑關聯的頁面)。而過濾器不會處理請求轉發,因此可以看到途中只有攔截器被調用了兩次。過濾器只在外部請求資源/xxx
的時候被調用了一次。
4.自定義異常頁面
4.1自定義異常頁面說明
Spring Boot Reference Documentation
如果要顯示給定狀態代碼的自定義 HTML 錯誤頁,可以將文件添加到目錄中。 錯誤頁面可以是靜態 HTML(即,添加到任何靜態資源目錄下),也可以是使用模板構建的。 文件名應為確切的狀態代碼或系列掩碼。/error
例如,要映射到靜態 HTML 文件,目錄結構如下所示:404
src/
+- main/
+- java/
| + <source code>
+- resources/
+- public/
+- error/
| +- 404.html
+- <other public assets>
要使用 Mustache 模板映射所有錯誤,目錄結構如下所示:5xx
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 5xx.mustache
+- <other templates>
- 如果發生了404錯誤,優先匹配 404.html,如果沒有,則匹配 4xx.html,再沒有則使用預設方式顯示錯誤
- 500.html 和 5xx.html 是一樣的邏輯
4.2應用實例
需求:自定義404.html、500.html、4xx.html、5xx.html,當發生相應錯誤時,顯示自定義的頁面信息。
(1)4xx.html,使用thymeleaf標簽取出狀態碼和錯誤信息,其他頁面同理
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>4xx</title>
</head>
<body bgcolor="#cedafe">
<div style="text-align: center">
<br/><br/><hr/>
<h1>4xx.html :)</h1>
狀態碼:<h1 th:text="${status}"></h1>
錯誤信息:<h1 th:text="${error}"></h1>
<hr/>
</div>
</body>
</html>
(2)5xx.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>5xx</title>
</head>
<body bgcolor="#cedafe">
<div style="text-align: center">
<br/><br/><hr/>
<h1>5xx.html :(</h1>
狀態碼:<h1 th:text="${status}"></h1>
錯誤信息:<h1 th:text="${error}"></h1>
<hr/>
</div>
</body>
</html>
(3)404.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>404</title>
</head>
<body bgcolor="#cedafe">
<div style="text-align: center">
<br/><br/><hr/>
<h1>404 Not Found~~</h1>
狀態碼:<h1 th:text="${status}"></h1>
錯誤信息:<h1 th:text="${error}"></h1>
<hr/>
</div>
</body>
</html>
(4)500.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>500</title>
</head>
<body bgcolor="#cedafe">
<div style="text-align: center">
<br/><br/><hr/>
<h1>500 伺服器內部發生錯誤 :(</h1>
狀態碼:<h1 th:text="${status}"></h1>
錯誤信息:<h1 th:text="${error}"></h1>
<hr/>
</div>
</body>
</html>
(5)模擬500和405錯誤
package com.li.thymeleaf.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
/**
* @author 李
* @version 1.0
*/
@Controller
public class MyErrorController {
//模擬一個伺服器內部錯誤500
@GetMapping("/abc")
public String abc() {
int i = 10 / 0;
return "manage";
}
//如果get方式請求此路徑,會產生405的客戶端錯誤
@PostMapping("/xyz")
public String xyz() {
return "manage";
}
}
(6)瀏覽器訪問不存在的資源時,顯示的是404.html。因為發生錯誤時首先會在靜態資源目錄中按照:404.html-->4xx.html的順序尋找視圖。
(7)伺服器內部錯誤-500錯誤:
(8)如果出現出現的是4開頭的錯誤,就會返回4xx.html
5.全局異常處理
5.1全局異常說明
在 Java 程式發生異常時,可以通過全局異常來捕獲處理異常。
在 SpringBoot 中通過 @ControllerAdvice(修飾的類即為全局異常處理器)加上@ExceptionHandler(修飾方法)處理全局異常,它的底層由 ExceptionHandlerExceptionResolver 類支撐。
- 預設異常處理機制,是通過不同的狀態碼(status),確定要返回的頁面
- 而全局異常的處理,則是根據 Java異常種類,來顯式指定返回的錯誤頁面
- 全局異常處理優先順序 > 預設異常處理優先順序
5.2全局異常-應用實例
需求:演示全局異常使用,當發生類似 ArithmeticException、NullPointException 時,不使用預設異常機制(通過狀態碼)匹配的 xxx.html,而是通過全局異常機制顯式指定的錯誤頁面。
(1)創建全局異常處理器 GlobalExceptionHandler.java
package com.li.thymeleaf.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @author 李
* @version 1.0
*/
@ControllerAdvice //標識一個全局異常處理器/對象,標識的類將會被註入到spring容器中
@Slf4j
public class GlobalExceptionHandler {
/**
* 編寫方法,處理指定異常(這裡要處理的異常由你指定)
* @param e 表示發生異常後傳遞的異常對象
* @param model 將異常信息放入model,傳遞給下一個頁面
* @return
*/
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handleException(Exception e, Model model) {
log.info("異常信息={}", e.getMessage());
//model的數據會自動放入request域中
model.addAttribute("msg", e.getMessage());
return "/error/global";//指定轉發到global.html
}
}
(2)global.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>全局異常</title>
</head>
<body bgcolor="#cedafe">
<div style="text-align: center">
<br/><br/><hr/>
<h1>發生了全局異常/錯誤 :(</h1>
錯誤信息:<h1 th:text="${msg}"></h1>
<hr/>
</div>
</body>
</html>
(3)模擬一個500錯誤
(4)瀏覽器訪問,可以看到顯示的是global.html而不是500.html,因為全局異常處理的優先順序高於預設異常處理
5.3拓展
全局異常的兩個註解是通過 ExceptionHandlerExceptionResolver 類來支撐的,該類有一個重要方法:doResolveHandlerMethodException()
執行上述方法時,會獲取異常發生的方法以及發生的異常。併在返回的模型和視圖類也會帶有這兩個數據,我們可以根據這兩個信息進行日誌輸出,在異常發生時可以迅速定位處理異常:
日誌輸出:
6.自定義異常的處理
6.1自定義異常說明
-
如果SpringBoot提供的異常不能滿足開發需求,我們也可以自定義異常。
-
自定義異常處理(若採用預設的處理機制) = 自定義異常類 + @ResponseStatus
@ResponseStatus 底層是
ResponseStatusExceptionResolve
,底層調用response.sendError(statusCode,resolvedReason);
-
自定義異常的處理方式:
- 當拋出自定義異常時,仍然會根據狀態碼,去匹配使用 xxx.html 顯示(預設的異常處理機制)。
- 或者將自定義異常類,放在你創建的全局異常處理器中進行處理(全局異常處理機制)
6.2自定義異常-應用實例
需求:自定義一個異常類 AccessException,當用戶訪問某個無權訪問的路徑時,拋出該異常,顯示自定義異常的狀態碼。
(1)自定義異常類:AccessException.java
package com.li.thymeleaf.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* @author 李
* @version 1.0
* 自定義的異常類
* (1)如果繼承Exception,屬於編譯異常
* (2)如果繼承RuntimeException,屬於運行異常(一般來說都是繼承RuntimeException)
* (3)@ResponseStatus(value = HttpStatus.FORBIDDEN)
* 指定發生此異常時,通過http協議返回的的狀態碼(403-Forbidden)
*/
@ResponseStatus(value = HttpStatus.FORBIDDEN)
public class AccessException extends RuntimeException {
public AccessException() {
}
//提供一個構造器,可以指定信息
public AccessException(String message) {
super(message);
}
}
(2)在Controller中模擬發生異常
//模擬發生 AccessException
@GetMapping("/errTest")
public String test(String name) {
if (!"tom".equals(name)) {
throw new AccessException();
}
return "manage";//視圖地址
}
(3)4xx.html(略)
(4)瀏覽器訪問localhost:8080/errTest?name=jack
,返回的頁面如下:
因為返回的狀態碼為403,根據預設的異常處理機制,這裡會尋找4xx.html頁面返回給瀏覽器。如果是全局異常處理機制的話就會走全局異常處理的流程。
6.3註意事項和使用細節
如果把自定義異常類放在全局異常處理器,因為全局異常處理優先順序高,因此會走全局異常的處理機制。
比如在上一個例子中,添加如下代碼:
package com.li.thymeleaf.exception;
import ...
/**
* @author 李
* @version 1.0
* 全局異常處理器
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({AccessException.class})
public String handleException(Exception e, Model model,HandlerMethod handlerMethod) {
log.info("出現異常的方法={}", handlerMethod.getMethod());
log.info("異常信息={}", e.getMessage());
//model的數據會自動放入request域中
model.addAttribute("msg", e.getMessage());
return "/error/global";//指定轉發到global.html
}
}
瀏覽器訪問localhost:8080/errTest?name=jack
,返回的頁面如下:(global.html)