SpringBoot的異常處理流程是什麼樣的?
一、默認異常處理機制
默認情況下,SpringBoot 提供 /error
請求,來處理所有異常的。
1.瀏覽器客戶端,請求頭裡的屬性是Accept:text/html
。表明它想要一個html類型的文本數據。因此返回的錯誤視圖以HTML格式呈現,也就是響應一個“ whitelabel”錯誤視圖。
2.如果是其他客戶端,請求頭裡的屬性是Accept:/
,默認響應一個json數據 。
二、異常處理流程
介紹異常處理流程前,要先認識HandlerExceptionResolver
:處理器異常解析器接口,可以將異常映射到相應的統一錯誤界面,從而顯示用戶友好的界面(而不是給用戶看到具體的錯誤信息)
public interface HandlerExceptionResolver { //解析處理異常 ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }
在DispatcherServlet
初始化時,已經把所有的HandlerExceptionResolver
處理器異常解析器接口的實現類放到下面的集合裡瞭
public class DispatcherServlet extends FrameworkServlet { //異常解析器集合 private List<HandlerExceptionResolver> handlerExceptionResolvers; }
從上圖可以看出該集合裡有DefaultErrorAttributes
;還有HandlerExceptionResolverComposite
處理器異常解析器組合,這裡面包含瞭三個能真正處理異常的解析器,分別是ExceptionHandlerExceptionResolver
、ResponseStatusExceptionResolver
、DefaultHandlerExceptionResolver
。下面會介紹他們幾個分別用於處理什麼異常。
閱讀doDispatch
()方法的源碼可以看出,Spring MVC對整個doDispatch
()方法用瞭嵌套的try-catch
語句
- 內層的try-catch用於捕獲
HandlerMapping
進行映射查找HandlerExecutionChain
以及HandlerAdapter
執行具體Handler
時的處理異常,並將異常傳入到processDispatchResult
(processedRequest, response, mappedHandler, mv, dispatchException)方法中。外層try-catch
用於捕獲渲染視圖時的異常。 - 通過兩層嵌套的
try-catch
,SpringMVC就能夠捕獲到三大組件在處理用戶請求時的異常,通過這樣的方法能夠很方便的實現統一的異常處理。
//處理分發結果 private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception { //判斷HandlerMapping、HandlerAdapter處理時的異常是否為空 if (exception != null) { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); //異常不為空,處理異常,進行異常視圖的獲取 mv = processHandlerException(request, response, handler, exception); } //隻要存在mv就進行視圖渲染 if (mv != null && !mv.wasCleared()) { //不管視圖是正常視圖還是異常視圖,均進入視圖渲染流程 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } ...略 ...略 ...略 }
從上面代碼可以看出,當出現異常時會進入processHandlerException
()方法進行異常視圖的獲取,處理完成後返回是ModelAndView
對象。接下來不管視圖是正常視圖還是異常視圖,隻要ModelAndView
不為空,均進入視圖渲染流程。下面是如何進行進行異常視圖的獲取的代碼。
//處理異常,進行異常視圖的獲取 protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // Check registered HandlerExceptionResolvers... ModelAndView exMv = null; //遍歷所有的處理器異常解析器handlerExceptionResolvers,看誰能夠處理當前異常 for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) { //解析當前異常 exMv = handlerExceptionResolver.resolveException(request, response, handler, ex); if (exMv != null) { //ModelAndView 不為空時,異常視圖獲取成功,跳出方法,進行異常視圖渲染。 break; } } ...略 ...略 ...略 //所有處理器異常解析器都不能處理該異常,拋出異常 throw ex; }
遍歷所有的處理器異常解析器handlerExceptionResolvers
,看誰能夠處理當前異常
DefaultErrorAttributes
先來處理異常。把異常信息保存到request域,並且返回null,並不能真正解析。HandlerExceptionResolverComposite
會遍歷它包含的三個異常解析器處理異常ExceptionHandlerExceptionResolver
處理器異常解析器支持@ControllerAdvice
+@ExceptionHandler
處理全局異常ResponseStatusExceptionResolver
處理器異常解析器支持@ResponseStatus
+自定義異常DefaultHandlerExceptionResolver
處理器異常解析器支持Spring底層的異常
當沒有任何異常解析器能夠處理異常,異常就會被拋出,最終Tomcat會發送 /error
請求,映射到底層的BasicErrorController
進入默認的異常處理機制。
總結:
當發生異常時,會被catch
。遍歷所有的處理器異常解析器,看誰能夠解析。如果你使用瞭@ControllerAdvice
+@ExceptionHandler
配置瞭全局異常處理,並指定瞭錯誤視圖,那麼該異常會被處理,然後進入視圖渲染流程。如果該異常沒能夠被任何處理器異常解析器處理,就會拋出異常,由Tomcat發送/error
請求,進入默認的異常處理機制,也就是開頭說的,沒有配置錯誤狀態碼頁面,則返回默認我們常見的默認錯誤頁。
訪問的是不存在的路徑,此時不會發生異常,經過處理器映射,處理器適配調用仍然返回的是空的ModelAndView,所以無法進行視圖渲染。Tomcat仍會發送 /error
請求,進入默認的異常處理機制。
三、默認的異常處理機制
要想弄懂錯誤處理原理,首先得看**ErrorMvcAutoConfiguration
:這是錯誤處理的自動配置類**,給容器中添加瞭下面幾個非常重要的組件。
ErrorPageCustomizer
BasicErrorController
DefaultErrorViewResolver
DefaultErrorAttributes
首先我們看ErrorPageCustomizer
組件,此組件是一個靜態內部類,位於ErrorMvcAutoConfiguration
內。它實現瞭ErrorPageRegistrar
接口,該接口提供瞭可以用來註冊ErrorPage
的方法。官方將ErrorPage
描述為:簡單的服務器獨立的錯誤頁面抽象,大致相當於web.xml
中傳統的元素<error-page>
。ErrorPage
裡包含瞭狀態碼、異常、和錯誤控制器映射路徑(server.error.path=/error)。也就是說當發生瞭異常,而且所有的處理器異常解析器都處理不瞭該異常,Tomcat就會發送/error
請求映射到BasicErrorController
。
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { //關於服務器的一些配置,如端口號,編碼方式等。在這裡主要關註server.error.path private final ServerProperties properties; private final DispatcherServletPath dispatcherServletPath; protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) { this.properties = properties; this.dispatcherServletPath = dispatcherServletPath; } //註冊錯誤頁面 @Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { //ErrorPage裡包含瞭狀態碼、異常、和錯誤控制器映射路徑 ErrorPage errorPage = new ErrorPage( //this.properties.getError().getPath()獲取的就是server.error.path=/error(默認) this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())); //註冊ErrorPage errorPageRegistry.addErrorPages(errorPage); } } ====================DispatcherServletPath接口================================= public interface DispatcherServletPath { default String getRelativePath(String path) { String prefix = getPrefix(); if (!path.startsWith("/")) { path = "/" + path; } return prefix + path; } }
下面介紹的就是BasicErrorController
。它裡面有兩個重要的方法,正好對象開頭說的默認處理機制。方法一:如果是瀏覽器請求,則返回HTML響應數據text/html
,方法二:如果是其他客戶端請求,則返回JSON響應數據。
@Controller //server.error.path 為空則用error.path, 再為空,再用/error @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { //1、產生html類型的數據;瀏覽器客戶端發送的請求來到這個方法處理 @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { //狀態碼 HttpStatus status = getStatus(request); //從DefaultErrorAttributes裡獲取可以在頁面顯示的數據 Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); //調用父類裡的方法,尋找錯誤視圖解析器,來解析錯誤視圖 ModelAndView modelAndView = resolveErrorView(request, response, status, model); //!!!如果沒有配置具體的狀態碼錯誤頁面或4xx,5xx這種視圖,視圖解析不成功,就會返回空的ModelAndView對象。此時就會構造一個默認的error錯誤視圖,通過BeanNameViewResolver視圖解析器,根據視圖名(error)作為組件id去容器中找到View對象 return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); } //2、產生json數據,其他客戶端來到這個方法處理 @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<>(status); } Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity<>(body, status); } } ========================AbstractErrorController================================ protected ModelAndView resolveErrorView(HttpServletRequest request,HttpServletResponse response, HttpStatus status, Map<String, Object> model) { for (ErrorViewResolver resolver : this.errorViewResolvers) { //錯誤視圖解析器獲取錯誤視圖,返回ModelAndView ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; }
總結一下,BasicErrorController
主要作用:
處理默認/error
路徑的請求
調用DefaultErrorViewResolver
進行錯誤視圖解析,分為三種情況
1.模板引擎支持解析,就去 /templates/error/
下尋找我們配置的 狀態碼錯誤頁面,例如404.html 或4xx.html。
2.模板引擎找不到這個錯誤頁面,就去靜態資源文件夾【/resources/、/static/、/public/、/META-INF/resources/】下的error文件夾下尋找狀態碼錯誤頁面。
3.靜態資源文件夾下也找不到,則new ModelAndView("error", model)
構造一個默認的錯誤視圖【就是經常見到的 Whitelabel Error Page】。該默認的錯誤視圖在ErrorMvcAutoConfiguration
裡已經註冊到容器裡瞭,並且它在容器中的id就是error。後面就會通過BeanNameViewResolver
視圖解析器,根據視圖邏輯 error,作為組件id去容器中就可以找到默認的錯誤視圖。
=========================ErrorMvcAutoConfiguration======================================== protected static class WhitelabelErrorViewConfiguration { private final StaticView defaultErrorView = new StaticView(); //向容器種註冊默認的錯誤視圖,id為error @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } } //可以看到靜態內部類StaticView,進行視圖渲染的時候,構造瞭我們經常看到的默認錯誤頁面Whitelabel Error Page。 private static class StaticView implements View { @Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (response.isCommitted()) { String message = getMessage(model); logger.error(message); return; } 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(getContentType()); } builder.append("<html><body><h1>Whitelabel Error Page</h1>").append( "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>") .append("<div id='created'>").append(timestamp).append("</div>") .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error"))) .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>"); if (message != null) { builder.append("<div>").append(htmlEscape(message)).append("</div>"); } if (trace != null) { builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>"); } builder.append("</body></html>"); response.getWriter().append(builder.toString()); } }
接下來就是介紹 DefaultErrorViewResolver
,主要就是進行錯誤視圖解析。如果發生錯誤,就會以HTTP的狀態碼 作為視圖地址,找到真正的錯誤頁面。但是註意,首先是精確查找具體的錯誤狀態碼頁面,然後是按照4xx,5xx這種查找。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private static final Map<Series, String> SERIES_VIEWS; static { Map<Series, String> views = new EnumMap<>(Series.class); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); } //解析錯誤視圖,要去的錯誤頁面 @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { //先精確查找錯誤視圖 ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); //精確查找不到,則查找4xx,5xx這種類型的錯誤頁面 if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } //真正解析 viewName是狀態碼 private ModelAndView resolve(String viewName, Map<String, Object> model) { //!!!!默認SpringBoot可以去找到一個頁面 error/404,註意定制錯誤頁面要放在error文件夾下,如error/4xx.html String errorViewName = "error/" + viewName; //模板引擎可以解析這個頁面地址就用模板引擎解析 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { //模板引擎可用的情況下返回到errorViewName指定的視圖地址 return new ModelAndView(errorViewName, model); } //模板引擎不可用,就在靜態資源文件夾下找errorViewName對應的頁面 error/404.html return resolveResource(errorViewName, model); } //模板引擎不可用,就去靜態資源文件夾下尋找 private ModelAndView resolveResource(String viewName, Map<String, Object> model) { //這裡遍歷的是"classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/" for (String location : this.resourceProperties.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; } }
最後介紹DefaultErrorAttributes
,裡面存放瞭錯誤頁面能夠顯示的數據。比如狀態碼、錯誤提示、異常消息等。
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { //幫我們在頁面共享信息 @Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, requestAttributes); addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); addPath(errorAttributes, requestAttributes); return errorAttributes; } }
總結:
- 當發生瞭異常,而且所有的處理器異常解析器都處理不瞭該異常,
ErrorPageCustomizer
就會生效(定制錯誤的響應規則)。Tomcat就會發送/error
請求,然後被HandlerMapping
映射到BasicErrorController
處理。 - 解析錯誤視圖:前提是配置瞭4xx.html、5xx.html錯誤狀態碼頁面,去哪個狀態碼錯誤頁面就由
DefaultErrorViewResolver
解析得到。如果沒有配置錯誤狀態碼頁面,就是默認的錯誤視圖StaticView
,它是位於ErrorMvcAutoConfiguration
裡的一個靜態內部類,被自動註冊到容器中。後面進行視圖渲染的時候,就是StaticView
裡的render
()方法構造瞭我們經常看到的默認錯誤頁面【Whitelabel Error Page】。 - 提取數據:頁面能夠獲取什麼數據是由
DefaultErrorViewResolver
設置的。
四、自定義異常處理
1、自定義異常處理頁
有模板引擎的情況下
- 沒有模板引擎(模板引擎找不到這個錯誤頁面),靜態資源文件夾下找。依然要將錯誤頁面放在error文件夾下。
- error/狀態碼.html,就是將錯誤頁面命名為
狀態碼.html
放在模板引擎文件夾裡面的 error文件夾下;我們可以使用4xx
和5xx
作為錯誤頁面的文件名來匹配這種類型的所有錯誤,不過優先尋找精確的狀態碼.html。 - 以上都沒有找到錯誤頁面,就是默認來到SpringBoot默認的錯誤提示頁面。
錯誤頁面能獲取的信息DefaultErrorAttributes
:
- timestamp:時間戳
- status:狀態碼
- error:錯誤提示
- exception:異常對象
- message:異常消息
- errors:JSR303數據校驗的錯誤都在這裡
2、@ControllerAdvice+@ExceptionHandler處理全局異常
底層是由 ExceptionHandlerExceptionResolver
處理器異常解析器支持的
3、@ResponseStatus+自定義異常
底層是由 ResponseStatusExceptionResolver
處理器異常解析器支持的,但是它解析完成後,調用瞭 **response.sendError(statusCode, resolvedReason);**由tomcat發送的/error請求,進入默認異常處理機制。
Spring底層的異常
如 參數類型轉換異常;由DefaultHandlerExceptionResolver
處理器異常解析器支持的,處理框架底層的異常。但是它解析完成後,調用瞭 response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); 由tomcat發送的/error請求,進入默認處理機制。
擴展:【可不看】
自定義處理器異常解析器、錯誤視圖解析器:
實現 HandlerExceptionResolver
接口自定義處理器異常解析器;可以作為默認的全局異常處理規則
實現ErrorViewResolver
自定義錯誤視圖解析器
到此這篇關於SpringBoot的異常處理流程是什麼樣的?的文章就介紹到這瞭,更多相關SpringBoot異常處理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 詳解SpringBoot異常處理流程及原理
- Java SpringMVC 異常處理SimpleMappingExceptionResolver類詳解
- SpringMVC全局異常處理的三種方式
- 詳解Spring MVC的攔截器與異常處理機制
- SpringBoot多種自定義錯誤頁面方式小結