SpringBoot錯(cuò)誤處理流程深入詳解
一、錯(cuò)誤處理
默認(rèn)情況下,Spring Boot提供/error處理所有錯(cuò)誤的映射
對(duì)于機(jī)器客戶端(例如PostMan),它將生成JSON響應(yīng),其中包含錯(cuò)誤,HTTP狀態(tài)和異常消息的詳細(xì)信息(如果設(shè)置了攔截器,需要在請(qǐng)求頭中塞入Cookie相關(guān)參數(shù))
對(duì)于瀏覽器客戶端,響應(yīng)一個(gè)“ whitelabel”錯(cuò)誤視圖,以HTML格式呈現(xiàn)相同的數(shù)據(jù)
另外,templates下面error文件夾中的4xx,5xx頁(yè)面會(huì)被自動(dòng)解析
二、底層相關(guān)組件
那么Spring Boot是怎么實(shí)現(xiàn)上述的錯(cuò)誤頁(yè)相關(guān)功能的呢?
我們又要來(lái)找一下相關(guān)源碼進(jìn)行分析了
首先我們先了解一個(gè)概念:@Bean配置的類的默認(rèn)id是方法的名稱,但是我們可以通過(guò)value或者name給這個(gè)bean取別名,兩者不可同時(shí)使用
我們進(jìn)入ErrorMvcAutoConfiguration
,看這個(gè)類名應(yīng)該是和錯(cuò)誤處理的自動(dòng)配置有關(guān),我們看下這個(gè)類做了什么
向容器中注冊(cè)類型為DefaultErrorAttributes
,id為errorAttributes
的bean(管理錯(cuò)誤信息,如果要自定義錯(cuò)誤頁(yè)面打印的字段,就自定義它),這個(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)頁(yè)面中的message、error等字段有關(guān)
向容器中注冊(cè)一個(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ò)誤頁(yè)
聲明一個(gè)靜態(tài)內(nèi)部類WhitelabelErrorViewConfiguration
,它與錯(cuò)誤視圖配置相關(guān),這個(gè)類中聲明了一個(gè)id為error的視圖對(duì)象提供給basicErrorController
中使用,還定義了視圖解析器BeanNameViewResolver
,它會(huì)根據(jù)返回的視圖名作為組件的id去容器中找View對(duì)象
@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
/** * 測(cè)試報(bào)錯(cuò)信息 * @return 跳轉(zhuǎn)錯(cuò)誤頁(yè)面 */ @GetMapping(value = "/test_error") public String testError() { int a = 1/0; return String.valueOf(a); }
然后放置一個(gè)錯(cuò)誤頁(yè)面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>
然后我們開(kāi)啟debug模式,發(fā)送請(qǐng)求
首先,我們的斷點(diǎn)還是來(lái)到DispatcherServlet
類下的doDispatch()
方法
經(jīng)過(guò)mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
調(diào)用目標(biāo)方法之后,他會(huì)返回相關(guān)錯(cuò)誤信息,并將其塞入dispatchException這個(gè)對(duì)象
然后調(diào)用this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
處理調(diào)度結(jié)果
然后他會(huì)在processDispatchResult()
中經(jīng)過(guò)判斷是否存在異常,異常不為空,調(diào)用processHandlerException()
方法,這里它會(huì)遍歷系統(tǒng)中所有的異常處理解析器,哪個(gè)解析器返回結(jié)果不為null,就結(jié)束循環(huán)
在調(diào)用DefaultErrorAttributes
時(shí),它會(huì)將錯(cuò)誤中的信息放入request請(qǐng)求域中(我們后面模板引擎頁(yè)面解析會(huì)用到)
遍歷完所有解析器,我們發(fā)現(xiàn)他們都不能返回一個(gè)不為空的ModelAndView
對(duì)象,于是它會(huì)繼續(xù)拋出異常
當(dāng)系統(tǒng)發(fā)現(xiàn)沒(méi)有任何人能處理這個(gè)異常時(shí),底層就會(huì)發(fā)送 /error 請(qǐng)求,它就會(huì)被我們上面介紹的BasicErrorController
下的errorHtml()
方法處理
這個(gè)方法會(huì)通過(guò)ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
去遍歷系統(tǒng)中所有的錯(cuò)誤視圖解析器,如果調(diào)用解析器的resolveErrorView()
方法返回結(jié)果不為空就結(jié)束循環(huán)
系統(tǒng)中只默認(rèn)注冊(cè)了一個(gè)錯(cuò)誤視圖解析器,也就是我們上面介紹的DefaultErrorViewResolver
,跟隨debug斷點(diǎn)我們得知,這個(gè)解析器會(huì)把error+響應(yīng)狀態(tài)碼
作為錯(cuò)誤頁(yè)的地址,最終返回給我們的視圖地址為error/5xx.html
四、定制錯(cuò)誤處理邏輯
1、自定義錯(cuò)誤頁(yè)面
error下的4xx.html和5xx.html,根據(jù)我們上面了解的DefaultErrorViewResolver
類可以,它的resolveErrorView()
方法在進(jìn)行錯(cuò)誤頁(yè)解析時(shí),如果有精確的錯(cuò)誤狀態(tài)碼頁(yè)面就匹配精確,沒(méi)有就找 4xx.html,如果都沒(méi)有就轉(zhuǎn)到系統(tǒng)默認(rèn)的錯(cuò)誤頁(yè)
2、使用注解或者默認(rèn)的異常處理
@ControllerAdvice+@ExceptionHandler處理全局異常,我們結(jié)合一個(gè)demo來(lái)了解一下用法
首先我們創(chuàng)建一個(gè)類用來(lái)處理全局異常
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ò)誤信息后,返回登錄頁(yè) return "login"; } }
我們還是使用上面的會(huì)發(fā)生算術(shù)運(yùn)算異常的接口/test_error
進(jìn)行測(cè)試
請(qǐng)求接口后發(fā)現(xiàn),頁(yè)面跳轉(zhuǎn)到登錄頁(yè)了
為什么沒(méi)有再走到5xx.html呢?
因?yàn)锧ControllerAdvice+@ExceptionHandler的底層是ExceptionHandlerExceptionResolver
來(lái)處理的
這樣在進(jìn)入DispatcherServlet
類下的processHandlerException()
方法時(shí),就會(huì)調(diào)用ExceptionHandlerExceptionResolver
這個(gè)異常處理解析器,從而跳轉(zhuǎn)到我們自己創(chuàng)建的異常處理類進(jìn)行異常處理,然后返回不為null的ModelAndView對(duì)象給它,終止遍歷,不會(huì)再發(fā)送/error
請(qǐng)求
@ResponseStatus+自定義異常
首先我們自定義一個(gè)異常類
package com.decade.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; // code對(duì)應(yīng)錯(cuò)誤碼,reason對(duì)應(yīng)message @ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED, reason = "自定義異常") public class CustomException extends RuntimeException { public CustomException() { } public CustomException(String message) { super(message); } }
然后寫一個(gè)接口去拋出自定義異常
/** * 測(cè)試報(bào)錯(cuò)信息 * @return 跳轉(zhuǎn)錯(cuò)誤頁(yè)面 */ @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()
方法開(kāi)始看
當(dāng)我們拋出自定義異常時(shí),由于前面@ControllerAdvice+@ExceptionHandler修飾的類沒(méi)有指定處理這個(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請(qǐng)求,入?yún)锧ResponseStatus注解的code和reason response.sendError(statusCode, resolvedReason); } // 返回一個(gè)modelAndView return new ModelAndView(); }
經(jīng)過(guò)debug我們知道,ResponseStatusExceptionResolver
這個(gè)異常處理解析器返回了一個(gè)空的ModelAndView對(duì)象給我們,而且還通過(guò)response.sendError(statusCode, resolvedReason);
發(fā)送了/error請(qǐng)求
這樣就又走到了上面的第三節(jié)處理/error請(qǐng)求的流程中,從而帶著我們@ResponseStatus注解的code和reason跳轉(zhuǎn)到了4xx.html頁(yè)面,這樣就能解釋為什么4xx.html頁(yè)面中的狀態(tài)碼和message都是我們自定義的了
如果沒(méi)有使用上述2種方法處理指定異?;蛱幚砦覀冏约鹤远x的異常,那么系統(tǒng)就會(huì)按照Spring底層的異常進(jìn)行處理,如 請(qǐng)求方法不支持異常等,都是使用DefaultHandlerExceptionResolver
這個(gè)異常處理解析器進(jìn)行處理的
我們分析這個(gè)類的doResolveException()
方法得知,它最后也會(huì)發(fā)送/error
請(qǐng)求,從而轉(zhuǎn)到4xx.html或者5xx.html頁(yè)面
3、自定義異常處理解析器
使用@Component注解,并實(shí)現(xiàn)HandlerExceptionResolver
接口來(lái)自定義一個(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)先級(jí)提到第一位,Order越小,優(yōu)先級(jí)越高,所以我們這里設(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)先級(jí)提到最高時(shí),前面的那些異常處理解析器都會(huì)失效,這時(shí)我們的自定義異常處理解析器可以作為默認(rèn)的全局異常處理規(guī)則
值得注意的是,當(dāng)代碼走到response.sendError
時(shí),就會(huì)觸發(fā)/error
請(qǐng)求,當(dāng)你的異常沒(méi)有人能處理時(shí),也會(huì)走tomcat底層觸發(fā)response.sendError
,發(fā)送/error
請(qǐng)求
到此這篇關(guān)于SpringBoot錯(cuò)誤處理流程深入詳解的文章就介紹到這了,更多相關(guān)SpringBoot錯(cuò)誤處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot2.1.4中的錯(cuò)誤處理機(jī)制
- Springboot實(shí)現(xiàn)自定義錯(cuò)誤頁(yè)面的方法(錯(cuò)誤處理機(jī)制)
- Springboot異常錯(cuò)誤處理解決方案詳解
- Springboot錯(cuò)誤處理機(jī)制實(shí)現(xiàn)原理解析
- SpringBoot錯(cuò)誤處理機(jī)制以及自定義異常處理詳解
- SpringBoot 錯(cuò)誤處理機(jī)制與自定義錯(cuò)誤處理實(shí)現(xiàn)詳解
- springboot 錯(cuò)誤處理小結(jié)
- SpringBoot自定義錯(cuò)誤處理邏輯詳解
相關(guān)文章
詳解idea中web.xml默認(rèn)版本問(wèn)題解決
這篇文章主要介紹了詳解idea中web.xml默認(rèn)版本問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12使用IntelliJ IDEA 進(jìn)行代碼對(duì)比的方法(兩種方法)
這篇文章給大家?guī)?lái)了兩種IntelliJ IDEA 進(jìn)行代碼對(duì)比的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01Java中調(diào)用SQL Server存儲(chǔ)過(guò)程詳解
這篇文章主要介紹了Java中調(diào)用SQL Server存儲(chǔ)過(guò)程詳解,本文講解了使用不帶參數(shù)的存儲(chǔ)過(guò)程、使用帶有輸入?yún)?shù)的存儲(chǔ)過(guò)程、使用帶有輸出參數(shù)的存儲(chǔ)過(guò)程、使用帶有返回狀態(tài)的存儲(chǔ)過(guò)程、使用帶有更新計(jì)數(shù)的存儲(chǔ)過(guò)程等操作實(shí)例,需要的朋友可以參考下2015-01-01使用Runtime 調(diào)用Process.waitfor導(dǎo)致的阻塞問(wèn)題
這篇文章主要介紹了使用Runtime 調(diào)用Process.waitfor導(dǎo)致的阻塞問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12