SpringBoot錯(cuò)誤處理流程深入詳解
一、錯(cuò)誤處理
默認(rèn)情況下,Spring Boot提供/error處理所有錯(cuò)誤的映射
對于機(jī)器客戶端(例如PostMan),它將生成JSON響應(yīng),其中包含錯(cuò)誤,HTTP狀態(tài)和異常消息的詳細(xì)信息(如果設(shè)置了攔截器,需要在請求頭中塞入Cookie相關(guān)參數(shù))

對于瀏覽器客戶端,響應(yīng)一個(gè)“ whitelabel”錯(cuò)誤視圖,以HTML格式呈現(xiàn)相同的數(shù)據(jù)

另外,templates下面error文件夾中的4xx,5xx頁面會(huì)被自動(dòng)解析
二、底層相關(guān)組件
那么Spring Boot是怎么實(shí)現(xiàn)上述的錯(cuò)誤頁相關(guān)功能的呢?
我們又要來找一下相關(guān)源碼進(jìn)行分析了
首先我們先了解一個(gè)概念:@Bean配置的類的默認(rèn)id是方法的名稱,但是我們可以通過value或者name給這個(gè)bean取別名,兩者不可同時(shí)使用
我們進(jìn)入ErrorMvcAutoConfiguration,看這個(gè)類名應(yīng)該是和錯(cuò)誤處理的自動(dòng)配置有關(guān),我們看下這個(gè)類做了什么
向容器中注冊類型為DefaultErrorAttributes,id為errorAttributes的bean(管理錯(cuò)誤信息,如果要自定義錯(cuò)誤頁面打印的字段,就自定義它),這個(gè)類實(shí)現(xiàn)了ErrorAttributes, HandlerExceptionResolver(異常處理解析器接口), Ordered三個(gè)接口
@Bean
@ConditionalOnMissingBean(
value = {ErrorAttributes.class},
search = SearchStrategy.CURRENT
)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
點(diǎn)進(jìn)去后發(fā)現(xiàn),這個(gè)類是和我們響應(yīng)頁面中的message、error等字段有關(guān)

向容器中注冊一個(gè)id為basicErrorController的控制器bean(管理錯(cuò)誤相應(yīng)邏輯,不想返回json或者錯(cuò)誤視圖,就自定義它)
@Bean
@ConditionalOnMissingBean(
value = {ErrorController.class},
search = SearchStrategy.CURRENT
)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
這個(gè)控制器就和前面我們返回json或者錯(cuò)誤視圖有關(guān)

聲明類型為DefaultErrorViewResolver,id為conventionErrorViewResolver的bean(管理錯(cuò)誤視圖跳轉(zhuǎn)路徑,如果要改變跳轉(zhuǎn)路徑,就自定義它)
@Configuration(
proxyBeanMethods = false
)
@EnableConfigurationProperties({WebProperties.class, WebMvcProperties.class})
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final Resources resources;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, WebProperties webProperties) {
this.applicationContext = applicationContext;
this.resources = webProperties.getResources();
}
@Bean
@ConditionalOnBean({DispatcherServlet.class})
@ConditionalOnMissingBean({ErrorViewResolver.class})
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
}
這個(gè)類中,解釋了為什么前面會(huì)根據(jù)不同的狀態(tài)碼轉(zhuǎn)向不同的錯(cuò)誤頁

聲明一個(gè)靜態(tài)內(nèi)部類WhitelabelErrorViewConfiguration,它與錯(cuò)誤視圖配置相關(guān),這個(gè)類中聲明了一個(gè)id為error的視圖對象提供給basicErrorController中使用,還定義了視圖解析器BeanNameViewResolver ,它會(huì)根據(jù)返回的視圖名作為組件的id去容器中找View對象
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
prefix = "server.error.whitelabel",
name = {"enabled"},
matchIfMissing = true
)
@Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class})
protected static class WhitelabelErrorViewConfiguration {
private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView();
protected WhitelabelErrorViewConfiguration() {
}
@Bean(
name = {"error"}
)
@ConditionalOnMissingBean(
name = {"error"}
)
public View defaultErrorView() {
return this.defaultErrorView;
}
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(2147483637);
return resolver;
}
}另外還聲明了一個(gè)靜態(tài)內(nèi)部類StaticView,這里面涉及錯(cuò)誤視圖的渲染等相關(guān)操作
private static class StaticView implements View {
private static final MediaType TEXT_HTML_UTF8;
private static final Log logger;
private StaticView() {
}
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (response.isCommitted()) {
String message = this.getMessage(model);
logger.error(message);
} else {
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(this.getContentType());
}
...三、異常處理流程
為了了解Spring Boot的異常處理流程,我們寫一個(gè)demo進(jìn)行debug
首先寫一個(gè)會(huì)發(fā)生算術(shù)運(yùn)算異常的接口/test_error
/**
* 測試報(bào)錯(cuò)信息
* @return 跳轉(zhuǎn)錯(cuò)誤頁面
*/
@GetMapping(value = "/test_error")
public String testError() {
int a = 1/0;
return String.valueOf(a);
}
然后放置一個(gè)錯(cuò)誤頁面5xx.html于templates下的error文件夾中
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="">
<meta name="author" content="ThemeBucket">
<link rel="shortcut icon" href="#" rel="external nofollow" rel="external nofollow" type="image/png">
<title>500 Page</title>
<link href="css/style.css" rel="external nofollow" rel="stylesheet">
<link href="css/style-responsive.css" rel="external nofollow" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="js/html5shiv.js"></script>
<script src="js/respond.min.js"></script>
<![endif]-->
</head>
<body class="error-page">
<section>
<div class="container ">
<section class="error-wrapper text-center">
<h1><img alt="" src="images/500-error.png"></h1>
<h2>OOOPS!!!</h2>
<h3 th:text="${message}">Something went wrong.</h3>
<p class="nrml-txt" th:text="${trace}">Why not try refreshing you page? Or you can <a href="#" rel="external nofollow" rel="external nofollow" >contact our support</a> if the problem persists.</p>
<a class="back-btn" href="index.html" rel="external nofollow" th:text="${status}"> Back To Home</a>
</section>
</div>
</section>
<!-- Placed js at the end of the document so the pages load faster -->
<script src="js/jquery-1.10.2.min.js"></script>
<script src="js/jquery-migrate-1.2.1.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/modernizr.min.js"></script>
<!--common scripts for all pages-->
<!--<script src="js/scripts.js"></script>-->
</body>
</html>然后我們開啟debug模式,發(fā)送請求
首先,我們的斷點(diǎn)還是來到DispatcherServlet類下的doDispatch()方法
經(jīng)過mv = ha.handle(processedRequest, response, mappedHandler.getHandler());調(diào)用目標(biāo)方法之后,他會(huì)返回相關(guān)錯(cuò)誤信息,并將其塞入dispatchException這個(gè)對象
然后調(diào)用this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);處理調(diào)度結(jié)果

然后他會(huì)在processDispatchResult()中經(jīng)過判斷是否存在異常,異常不為空,調(diào)用processHandlerException()方法,這里它會(huì)遍歷系統(tǒng)中所有的異常處理解析器,哪個(gè)解析器返回結(jié)果不為null,就結(jié)束循環(huán)
在調(diào)用DefaultErrorAttributes時(shí),它會(huì)將錯(cuò)誤中的信息放入request請求域中(我們后面模板引擎頁面解析會(huì)用到)
遍歷完所有解析器,我們發(fā)現(xiàn)他們都不能返回一個(gè)不為空的ModelAndView對象,于是它會(huì)繼續(xù)拋出異常

當(dāng)系統(tǒng)發(fā)現(xiàn)沒有任何人能處理這個(gè)異常時(shí),底層就會(huì)發(fā)送 /error 請求,它就會(huì)被我們上面介紹的BasicErrorController下的errorHtml()方法處理


這個(gè)方法會(huì)通過ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);去遍歷系統(tǒng)中所有的錯(cuò)誤視圖解析器,如果調(diào)用解析器的resolveErrorView()方法返回結(jié)果不為空就結(jié)束循環(huán)

系統(tǒng)中只默認(rèn)注冊了一個(gè)錯(cuò)誤視圖解析器,也就是我們上面介紹的DefaultErrorViewResolver,跟隨debug斷點(diǎn)我們得知,這個(gè)解析器會(huì)把error+響應(yīng)狀態(tài)碼作為錯(cuò)誤頁的地址,最終返回給我們的視圖地址為error/5xx.html

四、定制錯(cuò)誤處理邏輯
1、自定義錯(cuò)誤頁面
error下的4xx.html和5xx.html,根據(jù)我們上面了解的DefaultErrorViewResolver類可以,它的resolveErrorView()方法在進(jìn)行錯(cuò)誤頁解析時(shí),如果有精確的錯(cuò)誤狀態(tài)碼頁面就匹配精確,沒有就找 4xx.html,如果都沒有就轉(zhuǎn)到系統(tǒng)默認(rèn)的錯(cuò)誤頁
2、使用注解或者默認(rèn)的異常處理
@ControllerAdvice+@ExceptionHandler處理全局異常,我們結(jié)合一個(gè)demo來了解一下用法
首先我們創(chuàng)建一個(gè)類用來處理全局異常
package com.decade.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
@Slf4j
public class MyExceptionHandler {
// 指定該方法處理某些指定異常,@ExceptionHandler的value可以是數(shù)組,這里我們指定該方法處理數(shù)學(xué)運(yùn)算異常和空指針異常
@ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class})
public String handleArithmeticException(Exception exception) {
log.error("異常信息為:{}", exception);
// 打印完錯(cuò)誤信息后,返回登錄頁
return "login";
}
}我們還是使用上面的會(huì)發(fā)生算術(shù)運(yùn)算異常的接口/test_error進(jìn)行測試
請求接口后發(fā)現(xiàn),頁面跳轉(zhuǎn)到登錄頁了

為什么沒有再走到5xx.html呢?
因?yàn)锧ControllerAdvice+@ExceptionHandler的底層是ExceptionHandlerExceptionResolver來處理的
這樣在進(jìn)入DispatcherServlet類下的processHandlerException()方法時(shí),就會(huì)調(diào)用ExceptionHandlerExceptionResolver這個(gè)異常處理解析器,從而跳轉(zhuǎn)到我們自己創(chuàng)建的異常處理類進(jìn)行異常處理,然后返回不為null的ModelAndView對象給它,終止遍歷,不會(huì)再發(fā)送/error請求
@ResponseStatus+自定義異常
首先我們自定義一個(gè)異常類
package com.decade.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
// code對應(yīng)錯(cuò)誤碼,reason對應(yīng)message
@ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED, reason = "自定義異常")
public class CustomException extends RuntimeException {
public CustomException() {
}
public CustomException(String message) {
super(message);
}
}然后寫一個(gè)接口去拋出自定義異常
/**
* 測試報(bào)錯(cuò)信息
* @return 跳轉(zhuǎn)錯(cuò)誤頁面
*/
@GetMapping(value = "/test_responseStatus")
public String testResponseStatus(@RequestParam("param") String param) {
if ("test_responseStatus".equals(param)) {
throw new CustomException();
}
return "main";
}
最后我們調(diào)用接口,可以得到,跳轉(zhuǎn)到了4xx.html,但是狀態(tài)碼和message都和我們自己定義的匹配

那么原理是什么呢?我們還是從DispatcherServlet類下的processHandlerException()方法開始看
當(dāng)我們拋出自定義異常時(shí),由于前面@ControllerAdvice+@ExceptionHandler修飾的類沒有指定處理這個(gè)異常,所以循環(huán)走到下一個(gè)異常處理解析器ResponseStatusExceptionResolver
我們分析一下這里的代碼
@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof ResponseStatusException) {
return this.resolveResponseStatusException((ResponseStatusException)ex, request, response, handler);
}
// 由于我們自定義異常類使用了@ResponseStatus注解修飾,所以我們這里獲取到的status信息不為空
ResponseStatus status = (ResponseStatus)AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
return this.resolveResponseStatus(status, request, response, handler, ex);
}
...
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
// 獲取@ResponseStatus注解的code和reason作為狀態(tài)碼和message
int statusCode = responseStatus.code().value();
String reason = responseStatus.reason();
return this.applyStatusAndReason(statusCode, reason, response);
}
protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response) throws IOException {
if (!StringUtils.hasLength(reason)) {
response.sendError(statusCode);
} else {
String resolvedReason = this.messageSource != null ? this.messageSource.getMessage(reason, (Object[])null, reason, LocaleContextHolder.getLocale()) : reason;
// 發(fā)送/error請求,入?yún)锧ResponseStatus注解的code和reason
response.sendError(statusCode, resolvedReason);
}
// 返回一個(gè)modelAndView
return new ModelAndView();
}
經(jīng)過debug我們知道,ResponseStatusExceptionResolver這個(gè)異常處理解析器返回了一個(gè)空的ModelAndView對象給我們,而且還通過response.sendError(statusCode, resolvedReason);發(fā)送了/error請求
這樣就又走到了上面的第三節(jié)處理/error請求的流程中,從而帶著我們@ResponseStatus注解的code和reason跳轉(zhuǎn)到了4xx.html頁面,這樣就能解釋為什么4xx.html頁面中的狀態(tài)碼和message都是我們自定義的了
如果沒有使用上述2種方法處理指定異常或處理我們自己自定義的異常,那么系統(tǒng)就會(huì)按照Spring底層的異常進(jìn)行處理,如 請求方法不支持異常等,都是使用DefaultHandlerExceptionResolver這個(gè)異常處理解析器進(jìn)行處理的
我們分析這個(gè)類的doResolveException()方法得知,它最后也會(huì)發(fā)送/error請求,從而轉(zhuǎn)到4xx.html或者5xx.html頁面


3、自定義異常處理解析器
使用@Component注解,并實(shí)現(xiàn)HandlerExceptionResolver接口來自定義一個(gè)異常處理解析器
package com.decade.exception;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
// 將優(yōu)先級提到第一位,Order越小,優(yōu)先級越高,所以我們這里設(shè)置int的最小值
@Order(Integer.MIN_VALUE)
@Component
public class CustomExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
response.sendError(500, "自己定義的異常");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}當(dāng)我們把優(yōu)先級提到最高時(shí),前面的那些異常處理解析器都會(huì)失效,這時(shí)我們的自定義異常處理解析器可以作為默認(rèn)的全局異常處理規(guī)則

值得注意的是,當(dāng)代碼走到response.sendError時(shí),就會(huì)觸發(fā)/error請求,當(dāng)你的異常沒有人能處理時(shí),也會(huì)走tomcat底層觸發(fā)response.sendError,發(fā)送/error請求
到此這篇關(guān)于SpringBoot錯(cuò)誤處理流程深入詳解的文章就介紹到這了,更多相關(guān)SpringBoot錯(cuò)誤處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用IntelliJ IDEA 進(jìn)行代碼對比的方法(兩種方法)
這篇文章給大家?guī)砹藘煞NIntelliJ IDEA 進(jìn)行代碼對比的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01
使用Runtime 調(diào)用Process.waitfor導(dǎo)致的阻塞問題
這篇文章主要介紹了使用Runtime 調(diào)用Process.waitfor導(dǎo)致的阻塞問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12

