Servlet規範規定了當web應用發生異常時必須可以指明, 並肯定了該如何處理, 規定了錯誤信息應該包含的內容和展現頁面的方式.(詳細能夠參考servlet規範文檔)html
<error-code>
<exception-type>
<location>
全部的請求必然以某種方式轉化爲響應.java
@ResponseStatus
註解能夠映射某一異常到特定的HTTP狀態碼@ExceptionHandler
註解使其用來處理異常@ControllerAdvice
方式能夠統一的方式處理全局異常一.接口HandlerExceptionResolver
ios
該接口定義了Spring中該如何處理異常. 它只有一個方法resolveException()
, 接口源碼以下:web
// 由對象實現的接口,這些對象能夠解決在處理程序映射或執行期間引起的異常,在典型的狀況下是錯誤視圖。在應用程序上下文中,實現器一般被註冊爲bean。 // 錯誤視圖相似於JSP錯誤頁面,可是能夠與任何類型的異常一塊兒使用,包括任何已檢查的異常,以及針對特定處理程序的潛在細粒度映射。 public interface HandlerExceptionResolver { @Nullable ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex); }
Spring 爲該接口提供了若干實現類以下:spring
HandlerExceptionResolverComposite 委託給其餘HandlerExceptionResolver的實例列表 AbstractHandlerExceptionResolver 抽象基類 AbstractHandlerMethodExceptionResolver 支持HandlerMethod處理器的抽象基類 ExceptionHandlerExceptionResolver 經過 @ExceptionHandler 註解的方式實現的異常處理 DefaultHandlerExceptionResolver 默認實現, 處理spring預約義的異常並將其對應到錯誤碼 ResponseStatusExceptionResolver 經過 @ResponseStatus 註解映射到錯誤碼的異常 SimpleMappingExceptionResolver 容許將異常類映射到視圖名
二. DefaultHandlerExceptionResolver
spring-mvc
這個類是Spring提供的默認實現, 用於將一些常見異常映射到特定的狀態碼. 這些狀態碼定義在接口HttpServletResponse
中, 下面是幾個狀態碼的代碼片斷mvc
public interface HttpServletResponse extends ServletResponse { ... public static final int SC_OK = 200; public static final int SC_MOVED_PERMANENTLY = 301; public static final int SC_MOVED_TEMPORARILY = 302; public static final int SC_FOUND = 302; public static final int SC_UNAUTHORIZED = 401; public static final int SC_INTERNAL_SERVER_ERROR = 500; ... }
實際上, DefaultHandlerExceptionResolver
中並無直接實現接口的resolveException
方法, 而是實現了抽象類AbstractHandlerExceptionResolver
的doResolveException()
方法, 後者則在實現了接口的方法中委託給抽象方法doResolveException
, 這個方法由子類去實現.app
AbstractHandlerExceptionResolver
的resolveException
方法代碼以下:ide
@Override @Nullable public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { // 判斷是否當前解析器可用於handler if (shouldApplyTo(request, handler)) { prepareResponse(ex, response); ModelAndView result = doResolveException(request, response, handler, ex); if (result != null) { // Print warn message when warn logger is not enabled... if (logger.isWarnEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) { logger.warn("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result)); } // warnLogger with full stack trace (requires explicit config) logException(ex, request); } return result; } else { return null; } }
接下來咱們看DefaultHandlerExceptionResolver
實現的doResolveException
方法. 代碼以下;spring-boot
@Override @Nullable protected ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { try { if (ex instanceof HttpRequestMethodNotSupportedException) { return handleHttpRequestMethodNotSupported( (HttpRequestMethodNotSupportedException) ex, request, response, handler); } else if (ex instanceof HttpMediaTypeNotSupportedException) { return handleHttpMediaTypeNotSupported( (HttpMediaTypeNotSupportedException) ex, request, response, handler); } .... else if (ex instanceof NoHandlerFoundException) { return handleNoHandlerFoundException( (NoHandlerFoundException) ex, request, response, handler); } ..... } catch (Exception handlerEx) { if (logger.isWarnEnabled()) { logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx); } } return null; }
能夠看到代碼中使用了大量的分支語句, 其實是將方法傳入的異常類型經過instanceof運算符測試, 經過測試的轉化爲特定的異常. 並調用處理該異常的特定方法. 咱們挑一個好比處理NoHandlerFoundException
這個異常類的方法, 這個方法將異常映射爲404錯誤.
protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { pageNotFoundLogger.warn(ex.getMessage()); response.sendError(HttpServletResponse.SC_NOT_FOUND); //設置爲404錯誤 return new ModelAndView(); //返回個空視圖 }
上面分析了Spring默認的異常處理實現類DefaultHandlerExceptionResolver
.它處理的異常是Spring預約義的幾種常見異常, 它將異常對應到HTTP的狀態碼. 而對於不屬於這些類型的其餘異常, 咱們可使用ResponseStatusExceptionResolver
來處理, 將其對應到HTTP狀態碼.
三. ResponseStatusExceptionResolver
如何使用?
@GetMapping("/responseStatus") @ResponseBody public String responseStatus() throws MyException { throw new MyException(); } @ResponseStatus(code = HttpStatus.BAD_GATEWAY) public class MyException extends Exception{}
只須要在異常上使用@ResponseStatus
註解便可將特定的自定義異常對應到Http的狀態碼.
四. ExceptionHandlerExceptionResolver
使用相似於普通的controller方法, 使用@ExceptionHandler
註解的方法將做爲處理該註解參數中異常的handler. 好比, 在一個controller中, 咱們定義一個處理NPE的異常處理handler方法, 能夠用來處理該controller中拋出的NPE. 代碼以下:
@GetMapping("/npe1") @ResponseBody public String npe1() throws NullPointerException { throw new NullPointerException(); } @GetMapping("/npe2") @ResponseBody public String npe2() throws NullPointerException { throw new NullPointerException(); } @ExceptionHandler(value = {NullPointerException.class}) @ResponseBody public String npehandler(){ return "test npe handler"; }
不管是請求/npe1仍是請求/npe2, 系統都會拋出異常, 並交給對應的處理程序npehandler
去處理. 使用@ExceptionHandler(value = {NullPointerException.class})
註解的方法能夠處理本controller範圍內的全部方法排除的npe異常, 若是要將其做爲應用中全部controller的異常處理器, 就要將其定義在@ControllerAdvice
註解的類中.
@ControllerAdvice public class ControllerAdvicer { @ExceptionHandler(value = {NullPointerException.class}) @ResponseBody public String npehandler(){ return "test npe handler in advice"; } }
要了解其原理, 須要查看ExceptionHandlerExceptionResolver
中的方法doResolveHandlerMethodException
@Override @Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { // 獲取異常對用的處理器, 就是@ExceptionHandler註解的方法包裝, 注意參數handlerMethod, 在方法內部, 它將用來獲取所在Controller的信息 ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); if (exceptionHandlerMethod == null) { return null; } if (this.argumentResolvers != null) { exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ServletWebRequest webRequest = new ServletWebRequest(request, response); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); try { if (logger.isDebugEnabled()) { logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod); } Throwable cause = exception.getCause(); // 調用異常處理handler的方法. if (cause != null) { // Expose cause as provided argument as well exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod); } else { // Otherwise, just the given exception as-is exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod); } } catch (Throwable invocationEx) { // Any other than the original exception is unintended here, // probably an accident (e.g. failed assertion or the like). if (invocationEx != exception && logger.isWarnEnabled()) { logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx); } // Continue with default processing of the original exception... return null; } if (mavContainer.isRequestHandled()) { return new ModelAndView(); } else { ModelMap model = mavContainer.getModel(); HttpStatus status = mavContainer.getStatus(); ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); mav.setViewName(mavContainer.getViewName()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } return mav; } }
能夠看到在兩個中文註釋的地方, 其一是方法的開始部分獲取到了異常的handler, 其二是調用這個handler的方法. 調用方法應該很好理解, 咱們接下來查看方法getExceptionHandlerMethod
.
// 找到給定異常對應的@ExceptionHandler註解方法, 默認先在controller類的繼承結構中查找, 不然繼續在@ControllerAdvice註解的 bean中查找. @Nullable protected ServletInvocableHandlerMethod getExceptionHandlerMethod( @Nullable HandlerMethod handlerMethod, Exception exception) { Class<?> handlerType = null; if (handlerMethod != null) { // Local exception handler methods on the controller class itself. // To be invoked through the proxy, even in case of an interface-based proxy. handlerType = handlerMethod.getBeanType(); ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); if (resolver == null) { resolver = new ExceptionHandlerMethodResolver(handlerType); this.exceptionHandlerCache.put(handlerType, resolver); } Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method); } // For advice applicability check below (involving base packages, assignable types // and annotation presence), use target class instead of interface-based proxy. if (Proxy.isProxyClass(handlerType)) { handlerType = AopUtils.getTargetClass(handlerMethod.getBean()); } } // 在@ControllerAdvice註解的類中遍歷查找 for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { ControllerAdviceBean advice = entry.getKey(); if (advice.isApplicableToBeanType(handlerType)) { ExceptionHandlerMethodResolver resolver = entry.getValue(); Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(advice.resolveBean(), method); } } } return null; }
咱們能夠看到,它會首先查找controller中的方法, 若是找不到纔去查找@ControllerAdvice註解的bean. 也就是說controller中的handler的優先級要高於advice.
上面咱們瞭解了幾個Exceptionresolver的使用, 並經過源代碼簡單看了他們各自處理的原理. 但這些Resolver如何加載咱們還不知道, 接下來咱們重點看下他們是如何加載進去的.
四. ExceptionResolver的加載
在本系列的上一篇Spring系列(六) Spring Web MVC 應用構建分析中, 咱們大體提到了DispatcherServlet的啓動調用關係以下:
整理下調用關係: DispatcherServlet
initHandlerMappings <-- initStrategies <-- onRefresh <--
FrameworkServlet
initWebApplicationContext <-- initServletBean <--
HttpServletBean
init <--
GenericServlet
init(ServletConfig config)
最後的GenericServlet
是servlet Api的.
正是在initStrategies
方法中, DispatcherServlet
作了啓動的一系列工做, 除了initHandlerMappings
還能夠看到一個initHandlerExceptionResolvers
的方法, 其源碼以下:
// 初始化HandlerExceptionResolver, 若是沒有找到任何命名空間中定義的bean, 默認沒有任何resolver private void initHandlerExceptionResolvers(ApplicationContext context) { this.handlerExceptionResolvers = null; if (this.detectAllHandlerExceptionResolvers) { // 找到全部ApplicationContext中定義的 HandlerExceptionResolvers 包括在上級上下文中. Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values()); // 保持有序. AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers); } } else { try { HandlerExceptionResolver her = context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class); this.handlerExceptionResolvers = Collections.singletonList(her); } catch (NoSuchBeanDefinitionException ex) { // Ignore, no HandlerExceptionResolver is fine too. } } // 確保有Resolver, 不然使用默認的 if (this.handlerExceptionResolvers == null) { this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class); if (logger.isTraceEnabled()) { logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties"); } } }
好了, 如今咱們加載了應用程序中全部定義的Resolver. 當有請求到達時, DispatcherServlet
的doDispatch
方法使用請求特定的handler處理, 當handler發生異常時, 變量dispatchException
的值賦值爲拋出的異常, 並委託給方法processDispatchResult
doDispatch的代碼, 只摘錄出與本議題有關的.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { .... try { ModelAndView mv = null; mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); }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); .... } // 處理handler的結果 private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; // 異常處理 if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } // handler是否返回了view if (mv != null && !mv.wasCleared()) { render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isTraceEnabled()) { logger.trace("No view rendering, null ModelAndView returned."); } } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Concurrent handling started during a forward return; } if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(request, response, null); } }
從processDispatchResult
方法中能夠看到, 若是參數exception
不爲null, 則會處理異常, 對於ModelAndViewDefiningException
類型的異常單獨處理, 對於其餘類型的異常, 轉交給processHandlerException
方法處理, 這個方法就是異常處理邏輯的核心.
@Nullable protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { // Success and error responses may use different content types request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); // 使用註冊的Resolver處理 ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } } if (exMv != null) { if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } // We might still need view name translation for a plain error model... if (!exMv.hasView()) { String defaultViewName = getDefaultViewName(request); if (defaultViewName != null) { exMv.setViewName(defaultViewName); } } if (logger.isTraceEnabled()) { logger.trace("Using resolved error view: " + exMv, ex); } if (logger.isDebugEnabled()) { logger.debug("Using resolved error view: " + exMv); } WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); return exMv; } throw ex; }
從上面代碼能夠看到, this.handlerExceptionResolvers
就是在程序啓動時初始化註冊的, spring經過遍歷Resolver列表的方式處理異常, 若是返回結果不爲null, 說明處理成功, 就跳出循環.
Spring的異常解析器實現所有繼承自接口ResponseStatusExceptionResolver
, 上面咱們詳細瞭解了該接口在Spring中的幾種實現, 好比處理預約義異常的DefaultHandlerExceptionResolver
, 能夠映射異常到狀態碼的ResponseStatusExceptionResolver
, 還有功能更爲強大的ExceptionHandlerExceptionResolver
. 同時也簡單瞭解了其使用方式,使用@ExceptionHandler
來將方法標記爲異常處理器, 結合@ControllerAdvice
處理全局異常.
最後咱們探究了異常處理器的加載和處理方式, 咱們知道了其經過 DispatcherServlet
的初始化方法initHandlerMappings
完成加載器列表的註冊初始化, 而且在具體處理請求的doDispatch
中檢測異常, 最終processDispatchResult
方法委託給processHandlerException
, 該方法循環註冊的異常處理器列表完成處理過程.