javascript
Spring Boot Spring MVC异常处理原理分析
一、Spring MVC為處理異常的前期準備
入口類,是一個Servlet,是所有請求的分發點
- 初始化
DispatcherServlet在初始化時會觸發onRefresh()方法,此方法會調用initStrategies方法(初始化九大組件),完成整個DispatcherServlet的初始化工作,其中initHandlerExceptionResolvers()會初始化HandlerExceptionResolvers對象
protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);// 初始化HandlerExceptionResolvers對象initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context); }從Spring容器的ApplicationContext中找出所有HandlerExceptionResolvers對象,將保存到對象屬性handlerExceptionResolvers 中。從這我們也知道如果要在spring mvc中插入自己的HandlerExceptionResolver也比較簡單,只需要類實現接口HandlerExceptionResolver和Ordered,使用類似@Component 的注解注解此類即可
private void initHandlerExceptionResolvers(ApplicationContext context) {this.handlerExceptionResolvers = null;if (this.detectAllHandlerExceptionResolvers) {// 從Spring容器的ApplicationContext中找出所有HandlerExceptionResolvers對象Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);if (!matchingBeans.isEmpty()) {// 將Map轉化為List,保存到屬性handlerExceptionResolvers 中this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values());// 對HandlerExceptionResolvers使用據order接口里值進行排序AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);} } ...到此初始化工作完成
- HandlerExceptionResolver接口
HandlerExceptionResolver是一個接口,用于處理網絡請求過程中拋出的異常,但是不處理異常本身拋出的異常和視圖解析過程中拋出的異常
下圖是Spring MVC默認實現的HandlerExceptionResolver類
- HandlerExceptionResolverComposite
Spring Boot啟動時會默認注冊HandlerExceptionResolverComposite對象。此類只是一個組合類,并不進行真正的異常處理。當他捕獲異常時他只是將異常輪詢委托給注冊到它屬性里的上的HandlerExceptionResolver類來處理異常,如果處理的結果不為null,則轉給下一個處理
默認注冊到HandlerExceptionResolverComposite 的屬性有以下3個HandlerExceptionResolver,按照優先級排列如下:
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
- Spring mvc啟動時,初始化所有HandlerExceptionResolver到Spring 容器中,在Spring boot在啟動時,會初始化WebMvcConfigurationSupport 里配置的Bean, 會創建HandlerExceptionResolverComposite對象,此對象包括3個HandlerExceptionResolver,當他捕獲異常時,會使用這3個HandlerExceptionResolver進行處理,詳細如下:
二、詳細介紹這3個HandlerExceptionResolver的作用
使用@ExceptionHandler注解方法處理異常類,使用注解處理異常就有這個類的功勞。默認情況下,這個HandlerExceptionResolver的優先級是最高。
以下是ExceptionHandlerExceptionResolver運行時屬性值
- 屬性exceptionHandlerAdviceCache :存儲@Controller里@ExceptionHandler的方法
- 屬性exceptionHandlerAdviceCache:存儲@ControllerAdvice里@ExceptionHandler的全局方法
處理異常的關鍵代碼
入口doResolveHandlerMethodException方法會通過 getExceptionHandlerMethod獲取對應的@ExceptionHandler方法,如果有找到則執行此方法
使用@ResponseStatus處理異常,將異常轉化對應的HTTP的狀態碼。@ResponseStatus可以定義在Excpetion的子類的類上,也可以定義在被@ExceptionHandler注解的方法上(不過這個需要小心使用,由于ExceptionHandlerExceptionResolver的優先級高,這種方式可能被ExceptionHandlerExceptionResolver覆蓋掉)
異常處理入口doResolveException方法會先查找異常上的@ResponseStatus注解信息,如果有ResponseStatus ,則按照ResponseStatus 配置的值處理
// protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) {// 獲取異常的@ResponseStatus注解信息ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);if (responseStatus != null) {try {// 如果有ResponseStatus ,則按照ResponseStatus 配置的值處理return resolveResponseStatus(responseStatus, request, response, handler, ex);}catch (Exception resolveEx) {logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx);}}else if (ex.getCause() instanceof Exception) {…}return null; }根據ResponseStatus 的值設置返回的http狀態碼和原因
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex) throws Exception {int statusCode = responseStatus.code().value();String reason = responseStatus.reason();if (!StringUtils.hasLength(reason)) {// 設置返回的http狀態碼response.sendError(statusCode);}else {String resolvedReason = (this.messageSource != null ?this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :reason);// 設置返回的http狀態碼和原因response.sendError(statusCode, resolvedReason);}return new ModelAndView();}默認的HandlerExceptionResolver,將特定異常轉化為標準的HTTP的狀態碼。
詳細如下:左邊是異常名稱,右邊是http的狀態碼
通過代碼解釋此類行為, 只列出NoSuchRequestHandlingMethodException相關的轉換http錯誤碼的代碼,表格里其他異常處理類似
異常處理入口doResolveException方法,如果發現異常是NoSuchRequestHandlingMethodException,則調用方法handleNoSuchRequestHandlingMethod
// 對于NoSuchRequestHandlingMethodException進行轉化http錯誤大碼 protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) {try {if (ex instanceof org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException) {return handleNoSuchRequestHandlingMethod((org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException) ex,request, response, handler);}else if (ex instanceof HttpRequestMethodNotSupportedException) {…}else if … }handleNoSuchRequestHandlingMethod方法返回404錯誤碼和錯誤信息
protected ModelAndView handleNoSuchRequestHandlingMethod(org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException ex,HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {pageNotFoundLogger.warn(ex.getMessage());response.sendError(HttpServletResponse.SC_NOT_FOUND);return new ModelAndView();}每個具體的HandlerExceptionResolver都會實現Ordered接口,來定義執行的順序,order值越小,越是優先執行。
如果要實現自己HandlerExceptionResolver,只需要滿足兩個條件:
- 實現接口HandlerExceptionResolver和Ordered
- 使用類似@Component 的注解注解此類,保證spring啟動時創建此類對應的對象即可
三、異常處理流程
- 當執行@RequestMapping拋出異常,會進入異常處理流程
- 所有的doPost, doGet等do*的方法都會執行到以下方法:找到真正業務的處理邏輯,并進行處理。
下面的代碼是找到本次請求真正要處理的HandlerAdapter 對象,并進行處理,最后調用processDispatchResult對結果進行處理,這是我們關心的內容
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {… try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// Determine handler for the current request.mappedHandler = getHandler(processedRequest);if (mappedHandler == null || mappedHandler.getHandler() == null) {noHandlerFound(processedRequest, response);return;}// Determine handler adapter for the current request.HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// Process last-modified header, if supported by the handler.String method = request.getMethod();boolean isGet = "GET".equals(method);if (isGet || "HEAD".equals(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (logger.isDebugEnabled()) {logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);}if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {// As of 4.3, we're processing Errors thrown from handler methods as well,// making them available for @ExceptionHandler methods and other scenarios.dispatchException = new NestedServletException("Handler dispatch failed", err);}// 處理最后的方法processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);} … }處理業務執行的結果,處理結束可能是 ModelAndView,也可能是Exception。如果結果是Exception,就需要通過本文提到的HandlerExceptionResolver轉化為ModelAndView。然后根據ModelAndView將結果返回給請求方
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {boolean errorView = false;if (exception != null) {if (exception instanceof ModelAndViewDefiningException) {logger.debug("ModelAndViewDefiningException encountered", exception);mv = ((ModelAndViewDefiningException) exception).getModelAndView();}else {// 如果返回值是異常,通過本文提到的HandlerExceptionResolver轉化為ModelAndViewObject handler = (mappedHandler != null ? mappedHandler.getHandler() : null);mv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}// Did the handler return a view to render?// 根據ModelAndView執行后續操作if (mv != null && !mv.wasCleared()) {render(mv, request, response);if (errorView) {WebUtils.clearErrorRequestAttributes(request);}}… }當異常發生時, DispatcherServlet會輪詢調用HandlerExceptionResolver,直到異常被轉化為ModelAndView
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) throws Exception {// Check registered HandlerExceptionResolvers...ModelAndView exMv = null;for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);if (exMv != null) {break;}}…. }四、流程總結
? 默認情況下,Spring Boot提供/error處理所有錯誤的映射
? 對于機器客戶端,它將生成JSON響應,其中包含錯誤,HTTP狀態和異常消息的詳細信息。對于瀏覽器客戶端,響應一個“ whitelabel”錯誤視圖,以HTML格式呈現相同的數據
? 要對其進行自定義,添加View解析為error
? 要完全替換默認行為,可以實現 ErrorController 并注冊該類型的Bean定義,或添加ErrorAttributes類型的組件以使用現有機制但替換其內容。
? error/下的4xx,5xx頁面會被自動解析;
- 自定義錯誤頁
error/404.html error/5xx.html;有精確的錯誤狀態碼頁面就匹配精確,沒有就找4xx.html;如果都沒有就觸發白頁
- @ControllerAdvice+@ExceptionHandler處理全局異常;底層是 ExceptionHandlerExceptionResolver 支持的
- @ResponseStatus+自定義異常 ;底層是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底層調用 response.sendError(statusCode, resolvedReason);tomcat發送的/error
- Spring底層的異常,如 參數類型轉換異常;DefaultHandlerExceptionResolver 處理框架底層的異常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST ,ex.getMessage());
response.sendError會返回tomcat默認的錯誤頁(如下),跟springboot的是不一樣的,但springboot底層有個專門的BasicErrorController來處理“"/error" 請求
- 自定義實現 HandlerExceptionResolver 處理異常;可以作為默認的全局異常處理規則
- ErrorViewResolver 實現自定義處理異常;
- response.sendError 。error請求就會轉給controller
- 你的異常沒有任何人能處理。tomcat底層response.sendError。error請求就會轉給controller
- basicErrorController要去的頁面地址是 ErrorViewResolver ;
- ErrorMvcAutoConfiguration 自動配置異常處理規則
? 容器中的組件:類型:DefaultErrorAttributes -> id:errorAttributes
- public class DefaultErrorAttributes implements ErrorAttributes,HandlerExceptionResolver
- DefaultErrorAttributes:定義錯誤頁面中可以包含哪些數據。
- 容器中的組件:類型:BasicErrorController --> id:basicErrorController(json+白頁 適配響應)
- a.處理默認 /error 路徑的請求;頁面響應 new ModelAndView(“error”, model);
- b.容器中有組件 View->id是error;(響應默認錯誤頁)
- c.容器中放組件 BeanNameViewResolver(視圖解析器);按照返回的視圖名作為組件的id去容器中找View對象。 ?
- 容器中的組件:類型:DefaultErrorViewResolver -> id:conventionErrorViewResolver ?
- a. 如果發生錯誤,會以HTTP的狀態碼 作為視圖頁地址(viewName),找到真正的頁面 ? error/404、5xx.html
- b. 如果想要返回頁面;就會找error視圖【StaticView】。(默認是一個白頁) 寫出去json 錯誤頁
mappedHandler, mv, dispatchException);
a. 遍歷所有的handlerExceptionResolvers,看誰能處理當前異?!綡andlerExceptionResolver處理器異常解析器】
b.系統默認的 異常解析器;
一、DefaultErrorAttributes先來處理異常。把異常信息保存到rrequest域,并且返回null;
二、默認沒有任何人能處理異常,所以異常會被拋出
- 如果沒有任何人能處理最終底層就會發送 /error請求。會被底層的BasicErrorController處理
- 解析錯誤視圖;遍歷所有的 ErrorViewResolver
看誰能解析。
三、默認的 DefaultErrorViewResolver,作用是把響應狀態碼作為錯誤頁的地址,或者匹配5xx,4xx(序列碼)這種形式找到最終頁面,error/500.html
四、模板引擎最終響應這個頁面 error/500.html
攔截時序圖:
圖片來自網絡
參考文章
參考視頻
總結
以上是生活随笔為你收集整理的Spring Boot Spring MVC异常处理原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 投递简历得不到回复,并不是你的简历不好,
- 下一篇: SAAS产业趋势洞察——乘风破浪会有时