從源碼看全局異常處理器@ExceptionHandler&@ExceptionHandler的生效原理
- 2021 年 12 月 30 日
- 筆記
- springboot, SpringMVC
1.開頭在前
日常開發中,幾乎我們的項目都會用到異常處理器,我們通常會訂製屬於自己的異常處理器,來處理項目中大大小小、各種各樣的異常。配置異常處理器目前最常用的方式應該是使用@ControllerAdvice+@ExceptionHandler的組合來實現,當然還有其他的方式,例如實現HandlerExceptionResolver介面等等等。這次我們來看看再@ControllerAdvice和@ExceptionHandler模式下Spring是如何識別我們配置的處理器按照我們的配置進行處理,以及我們配置了兩個異常處理器,而這兩個異常又是父子繼承關係,那麼當程式中拋出子異常時,Spring又是如何選擇的呢,是以子異常處理器處理還是父異常處理器處理?下面我們來跟蹤源碼來看看到底是怎麼實現的
2.案例展示
如下程式碼,配置了一個全局異常處理器,針對於RuntimeException,ArithmeticException,Exception三個異常進行了特殊處理,其中ArithmeticException是RuntimeException的子類,RuntimeException是Exception的子類
@RestControllerAdvice public class YuqiExceptionHandler { @ExceptionHandler(RuntimeException.class) public String handleRuntimeException(RuntimeException e) { return "handle runtimeException"; } @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException"; } @ExceptionHandler(Exception.class) public String handleException(Exception e) { return "handle Exception"; } }
如下程式碼,是我們的測試類,通過訪問該請求,會觸發除0的算術異常。
@RestController @RequestMapping("/test") public class TestController { @GetMapping("/exception") public void testException() { int i = 1 / 0; } }
3.源碼跟蹤
首先我們來看下前端控制器DispatcherServlet的執行邏輯,核心doDispatch()方法,前端請求由dispatcherServlet攔截後,經handlerMapping找到匹配的handler後轉換成適合的handlerAdapter進行實際方法的調用。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); 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) { noHandlerFound(processedRequest, response); return; } //根據handler找到合適的適配器 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 進行方法實際的調用 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); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
然後我們再來看下processDispatchResult()方法看看是如何處理方法調用結果的
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
//判斷當前異常是否為ModelAndViewDefiningException的實例
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
//獲取當前執行的handler(這裡的時候即controller)
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
//調用processHandlerException()進行進一步的異常處理
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
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) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
接下來看processHandlerException()是如何對異常進一步進行處理的
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); // Check registered HandlerExceptionResolvers... ModelAndView exMv = null;
//如果spring容器中的handlerExceptionResolver列表不為null的話,則進行遍歷,當前容器是有兩個handlerExceptionResolver,如下面圖所展示 if (this.handlerExceptionResolvers != null) { for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
//遍歷每一個handlerExceptionResolver,並且調用其resolveException()方法嘗試對當前異常進行解決 exMv = resolver.resolveException(request, response, handler, ex);
//如果返回的ModelAndView不為null,說明該handlerExceptionResolver成功解決了該異常,那麼就退出循環 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); } else if (logger.isDebugEnabled()) { logger.debug("Using resolved error view: " + exMv); } WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); return exMv; } throw ex; }
容器持有的handlerExceptionResolver如下,重點就是第二個,HandlerExceptionResolverComposite,我們來看看它是如何解決的
那麼下一步來看HandlerExceptionResolverComposite的resolveException()方法是如何解決異常的,可以看到HandlerExceptionResolverComposite內部又包含著三個異常解析器,通過遍歷該列表繼續重複之前disPatcherServlet的方法中的操作,即遍歷每個解析器看是否能解決這個異常。最重要的就是ExceptionHandlerExceptionResolver這個解析器,下面我們來看看它的resolveException()方法是如何處理這個異常的
ExceptionHandlerExceptionResolver本身並沒有實現resolveException方法,而是直接使用它的超類AbstractHandlerExceptionResolver實現的resolveException方法,下面我們來看下內部具體邏輯
@Override @Nullable public ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { //判斷當前resolver是否支援對該handler的處理 if (shouldApplyTo(request, handler)) { prepareResponse(ex, response);
//進行異常處理,該方法由它的子類AbstractHandlerMethodExceptionResolver來提供 ModelAndView result = doResolveException(request, response, handler, ex); if (result != null) { // Print debug message when warn logger is not enabled. if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) { logger.debug(buildLogMessage(ex, request) + (result.isEmpty() ? "" : " to " + result)); } // Explicitly configured warn logger in logException method. logException(ex, request); } return result; } else { return null; } }
來看下AbstractHandlerMethodExceptionResolver的doResolveException()方法
protected final ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { //將handler轉換成handlerMehtod HandlerMethod handlerMethod = (handler instanceof HandlerMethod ? (HandlerMethod) handler : null);
//調用該方法進行對異常進行處理,這裡就開始真正調用ExceptionHandlerExceptionResolver的方法了 return doResolveHandlerMethodException(request, response, handlerMethod, ex); }
看下核心解析器ExceptionHandlerExceptionResolver的doResolveHandlerMethodException()方法
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { //核心方法,獲取處理該異常的HandlerMehtod,也就是前面我們配置的方法,就是說在這裡真正要決定用哪個方法來處理這個異常了這裡因為我們之前的@ControllerAdvice也會跟@Controller一樣將類轉為Handler,
裡面的方法對應的也都轉換為HandlerMethod 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(); ArrayList<Throwable> exceptions = new ArrayList<>(); try { if (logger.isDebugEnabled()) { logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod); } // Expose causes as provided arguments as well Throwable exToExpose = exception; while (exToExpose != null) { exceptions.add(exToExpose); Throwable cause = exToExpose.getCause(); exToExpose = (cause != exToExpose ? cause : null); } Object[] arguments = new Object[exceptions.size() + 1]; exceptions.toArray(arguments); // efficient arraycopy call in ArrayList arguments[arguments.length - 1] = handlerMethod; exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); } catch (Throwable invocationEx) { // Any other than the original exception (or a cause) is unintended here, // probably an accident (e.g. failed assertion or the like). if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { logger.warn("Failure in @ExceptionHandler " + 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; } }
來看下ExceptionHandlerExceptionResolver是如何獲取處理異常的指定方法的
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.
//獲取該HandlerMehtod實際的Bean類型,即我們Controller的類型--TestController handlerType = handlerMethod.getBeanType();
//根據該類型去excpetionHandler的快取Map中獲取解析器,此時快取map中沒有數據 ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); if (resolver == null) {
//如果快取中沒有該類型的解析器就根據該類型創建一個新的解析器 resolver = new ExceptionHandlerMethodResolver(handlerType);
//放入到快取Map中 this.exceptionHandlerCache.put(handlerType, resolver); }
//通過異常獲取方法,這裡獲取為null Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext); } // For advice applicability check below (involving base packages, assignable types // and annotation presence), use target class instead of interface-based proxy
//判斷該Bean類型是否為代理類 if (Proxy.isProxyClass(handlerType)) {
//如果是代理類我們要獲取到到實際的被代理類的類型 handlerType = AopUtils.getTargetClass(handlerMethod.getBean()); } } //遍歷當前容器內controllerAdvice Bean的快取集合,快取中當前只有一個controllerAdvice 就是我們之前配置的全局異常處理器 for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { ControllerAdviceBean advice = entry.getKey();
//判斷該controller類型是否需要被增強處理,這裡主要是判斷@ControllerAdvice上的三個屬性basePackages即該類是否被基包路徑覆蓋,
assignableTypes或者被該類特殊指定,annotations以及該類上有特殊指定的註解,這三種情況有一個滿足都為true,如果三個屬性都為null,那麼也返回true if (advice.isApplicableToBeanType(handlerType)) {
//獲取該controllerAdvice對應的解析器 ExceptionHandlerMethodResolver resolver = entry.getValue();
//獲取解決異常的方法 Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext); } } } return null; }
ExceptionHandlerMethodResolver.resolveMethod()方法中內部嵌套調用了好幾個方法,最主要的我們來看getMapperMethod
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();
//遍歷該解析器下對應的異常處理方法的key,我們測試類的應該是有三個方法,對應著Exception,RuntimeException,ArithmeticException三個異常 for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
//判斷當前遍歷的異常是否等於傳入的異常,或者為他的父類或超類,很顯然,我們遍歷的這三個都符合 if (mappedException.isAssignableFrom(exceptionType)) {
//符合就添加進列表中 matches.add(mappedException); } }
//如果列表不為空,說明有符合條件的對應方法 if (!matches.isEmpty()) {
//符合條件的方法大於1個,就對列表進行排序,這裡就是確定當存在多個能夠處理同一異常的方法如何選擇 if (matches.size() > 1) {
//來重點看下這個自定義排序器的內部排序規則 matches.sort(new ExceptionDepthComparator(exceptionType)); }
//返回排序完列表的第一個元素,也就是深度最低的一個,跟傳入異常關係最近的一個,可以理解為就近原則。 return this.mappedMethods.get(matches.get(0)); } else {
return NO_MATCHING_EXCEPTION_HANDLER_METHOD; } }
ExceptionDepthComparator的排序規則,可以看到就是計算每個異常的深度,這個深度就是和傳入的異常進行比較,如何是同一個異常深度就是0,不等就深度+1然後繼續用該異常的父類判斷,不斷遞歸,把列表中所有異常相對於傳入異常的深度都計算完,然後升序排序。
public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) { int depth1 = getDepth(o1, this.targetException, 0); int depth2 = getDepth(o2, this.targetException, 0); return (depth1 - depth2); } private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) { if (exceptionToMatch.equals(declaredException)) { // Found it! return depth; } // If we've gone as far as we can go and haven't found it... if (exceptionToMatch == Throwable.class) { return Integer.MAX_VALUE; } return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1); }
到這裡我們就看完了@ControllerAdvice+@ExceptionHandler執行流程的核心程式碼,完整流程的源碼較多,本文沒有全部展示,但是這些核心部分應該可以讓我們了解SpringMVC是如何在拋出異常後,找到合適異常處理器進行處理的。
4. 拓展
在日常開發中,可能有需要到某個類的異常要這麼處理,另外個類的異常要那麼處理,這就相當於需要一些訂製化處理,這個時候我們系統中只有一個全局異常處理器就難以滿足需求,這時候我們可以訂製多個全局異常處理器,在@ControllerAdvice屬性上可以指定覆蓋的範圍,如下示例
我們在程式中定義了兩個異常處理器,其中Yuqi2ExceptionHandler在@ControllerAdvice註解上的assignableTypes屬性上指定了TestController,這樣的話當TestController中拋出算術異常時,就會走Yuqi2ExceptionHandler的處理。這樣就實現了對特殊類的訂製化處理
@RestControllerAdvice public class YuqiExceptionHandler { @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException"; } } @RestControllerAdvice(assignableTypes = {TestController.class}) public class Yuqi2ExceptionHandler { @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException v2"; } }
那當我們有兩個訂製異常處理類都對訂製類有特殊處理,這個時候SpringMVC會怎麼選擇呢,如下示例,兩個異常處理器都都指定了生效範圍為TestController,這個時候TestController觸發異常時,經測試都是走Yuqi2這個處理器,debug源碼看到,因為在遍歷ControllerAdviceBean時,Yuqi2在Yuqi3的位置上面,所以遍歷的時候直接就在Yuqi2結束處理了,我們可以在處理器中手動加@Order註解,指定它們的載入屬性,例如Yuqi2上加@Order(2),Yuqi3上加@Order(1),這樣Yuqi3的載入順序就優先,再次觸發異常時,就會走Yuqi3的處理。
@RestControllerAdvice(assignableTypes = {TestController.class}) public class Yuqi2ExceptionHandler { @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException v2"; } } @RestControllerAdvice(assignableTypes = {TestController.class}) public class Yuqi3ExceptionHandler { @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException v3"; } }
5. 最後
相信跟著流程看了一遍源碼後,大家對@ControllerAdvice和@ExceptionHandler的執行原理有了更加清晰的認識,通過源碼重新過了一遍同時也是我自己學習了一遍,由於筆者水平有限,文中可能有一些不對的地方,希望大家能夠指出,共同進步。