如果你去面試,面試官問你 Spring 異常處理時,想必你一定能回答上,“如果某個 Controller 有
@ExceptionHandler
註解的方法,就走這個局部異常處理;沒有的話就走那個@ControllerAdvice
類中@ExceptionHandler
修飾的方法。”面試官說:“嗯,沒毛病非常正確,但是在多問一句,Spring 是如何實現的呢? 啥?忙於業務開發沒思考過?額,抱歉,我們這個崗位不適合你~”
我們先假設有一下兩個接口,然後逐步揭開 Spring 異常處理的神祕面紗。相關版本:JDK8、Spring Boot 2.1.5.RELEASE、Spring 5.1.7.RELEASE
@RestController
public class MockHealth {
/**
* 測試缺少 userId 會產生的異常情況,會導致 Http 響應碼 400
* @param userId
* @return
*/
@GetMapping("/query")
public String query(@RequestParam("userId") String userId) {
return "userId = " + userId;
}
}
@RestController
public class TestController {
/**
* 測試全局異常和 Controller 內部的 ExceptionHandler
* @param num
* @return
*/
@GetMapping("/divide-zero")
public GenericResponse<Integer> divide(Integer num) {
return GenericResponse.success(num/0);
}
}
在我們沒有任何異常處理的情況下,嘗試訪問這兩個接口,看看會得到什麼結果。注意這裏只演示異常處理情況,第一個 query 接口我們不傳任何參數。
再看後端日誌打印輸出了啥
2019-11-30 16:53:26.853 WARN 67187 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'userId' is not present]
2019-11-30 17:04:04.056 ERROR 67187 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
at com.zst.provider.controller.TestController.divide(TestController.java:88)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:123)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:200)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:836)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1747)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
先簡單分析下,第一個 /query
接口請求參數 userId
是必傳的,而我們沒有傳這個參數,導致響應碼是 400。並且我們也可以第一行日誌看出來,產生的 MissingServletRequestParameterException
異常被 DefaultHandlerExceptionResolver
類處理了。 第二個 /divide-zero
接口我們故意產生了一個算數異常。
我們試着在 MockHealth 類中添加局部異常處理,同時也添加一個全局異常處理類 —— GlobalExceptionHandler,TestController 類則不發送任何變化,代碼更新如下:
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler(Throwable.class)
public GenericResponse errorHandler(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Exception e) {
int responseCode = httpServletResponse.getStatus();
log.error("Unhandled Exception! url is {}, response code is {}, msg is {}", httpServletRequest.getRequestURI(), responseCode, e.getMessage(), e);
return GenericResponse.failed("服務器內部異常!from [" + this.getClass().getCanonicalName() + "]");
}
}
@Slf4j
@RestController
public class MockHealth {
@ExceptionHandler(Throwable.class)
public GenericResponse errorHandler(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Exception e) {
int responseCode = httpServletResponse.getStatus();
log.error("Unhandled Exception! url is {}, response code is {}, msg is {}", httpServletRequest.getRequestURI(), responseCode, e.getMessage(), e);
return GenericResponse.failed("服務器內部異常!from [" + this.getClass().getCanonicalName() + "]");
}
/**
* 測試缺少 userId 會產生的異常情況,會導致 Http 響應碼 400
* @param userId
* @return
*/
@GetMapping("/query")
public String query(@RequestParam("userId") String userId) {
return "userId = " + userId;
}
}
仍舊分別調用 query
和 divide-zero
接口,觀察局部異常和全局異常處理兩種情況。根據我們經驗得知 query
接口產生的異常由其所在類中的局部異常處理方法捕獲,而 divide-zero
接口拋出的異常則交給全局異常類 GlobalExceptionHandler 處理,返回結果也正是我們預料的那樣。
我們通過上面幾步驗證了之前的說法,現在讓我們一起來看 Spring 是如何做到的。
Spring HandlerExceptionResolver
先不用考慮什麼是 HandlerExceptionResolver
,我們就從第一次測試,產生的第一行異常日誌開始下手,在 DispatcherServlet#processDispatchResult
打斷點,看看這個日誌是在怎麼出來的,我們可以看到 MissingServletRequestParameterException
這個異常。很顯然它不是 ModelAndViewDefiningException
,繼續往下走。
流程執行到 processHandlerException
方法,這裏出現了日誌中的 DefaultHandlerExceptionResolver
類。
我們重點看 HandlerExceptionResolverComposite
這個類(額,因爲 DefaultErrorAttributes
確實也沒幹啥),繼續往下調試就到了 HandlerExceptionResolverComposite#resolveException
,如果 resolvers 中有一個類返回了 ModelAndView
,就不再往後遍歷了。
到這裏我們需要重點分析的幾個類都已經找到了,先暫時放一下,回頭來看一眼 HandlerExceptionResolver
接口,因爲上面這幾個類都實現了該接口,這個接口裏面就只有一個方法。從註釋可瞭解到,HandlerExceptionResolver
接口的實現類,都用來處理在映射或程序執行過程中產生的異常。 Spring 尿性就是一個功能,管他三七二十一先給你來個接口,再來個抽象類稍微意思意思,最後再整幾個實際幹活兒的類,整一套下來,就擊敗了不少喜歡刨根問底拋 Spring 源碼的人。看破不說破~
/**
* Interface to be implemented by objects that can resolve exceptions thrown during
* handler mapping or execution, in the typical case to error views. Implementors are
* typically registered as beans in the application context.
*
* <p>Error views are analogous to JSP error pages but can be used with any kind of
* exception including any checked exception, with potentially fine-grained mappings for
* specific handlers.
*
* @since 22.11.2003
*/
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
ExceptionHandlerExceptionResolver —— 全局異常與局部異常處理的關鍵
Spring 通過封裝繼承,最終上面 HandlerExceptionResolver#resolveException(request, response, handler, ex)
首先調用了 ExceptionHandlerExceptionResolver 類的 doResolveHandlerMethodException(request, response, handler, ex)
方法。我們看這個方法幹啥了,把方法中的異常處理邏輯幹掉,只看主要流程,精簡後代碼如下:
/**
* Find an {@code @ExceptionHandler} method and invoke it to handle the raised exception.
*/
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
Throwable cause = exception.getCause();
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) {
//如果說異常處理再發生異常,繼續往下走,讓其他處理器處理之前的異常
//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());
//...
return mav;
}
}
doResolveHandlerMethodException(request, response, handler, ex)
方法就是要找到一個 @ExceptionHandler
註解的方法,如果沒有或者這個方法也出現異常了,就讓後面的處理器去處理異常。如果能找到異常處理方法,就調用該方法去進行異常處理。那麼是怎麼找到這個被 @ExceptionHandler
註解修飾的方法的?這纔是重點啊!廢話少說,就在第一行getExceptionHandlerMethod(handlerMethod, exception)
,我們再來看這個方法幹啥了,代碼加註釋都沒幾行:
//緩存了所有散佈在各個 Controller 層中的異常處理方法
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<>(64);
//緩存了全局異常處理類中的方法
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
new LinkedHashMap<>();
@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());
}
}
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;
}
ExceptionHandlerExceptionResolver 類中有兩個 Map,分別緩存 local exception handler 和全局異常處理。當嘗試獲取異常對應的處理器時,會先從該異常產生的類中尋找,看看該類中有沒有異常處理方法,沒有就再試試能不能找到全局異常處理,這兩個都找不到才輪到之後的其他 HandlerExceptionResolver 類。
再回頭驗證一下,第一次直接訪問 /query
接口,我們什麼異常處理都沒有添加,走到 ExceptionHandlerExceptionResolver 時就直接返回 null,後面的 DefaultHandlerExceptionResolver 纔得到機會去處理。
DefaultHandlerExceptionResolver
這個類繼承了 AbstractHandlerExceptionResolver,它的 doResolveException(request, response, handler, ex)
方法處理了好多異常狀態碼的 exception,隨便找幾個常見的,HttpRequestMethodNotSupportedException
、MissingServletRequestParameterException
、BindException
等,這個類做的事情就是給 HttpResponse 設置異常狀態碼,然後 new 一個 ModelAndView 對象返回。
我們再來看一下,調用 /query
接口,不傳 userId
參數會報 MissingServletRequestParameterException
,經過我們上面分析的流程,最終交給 DefaultHandlerExceptionResolver 處理。下面這一行日誌是怎麼來的?
2019-11-30 16:53:26.853 WARN 67187 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'userId' is not present]
答案就在 DefaultHandlerExceptionResolver 父類 AbstractHandlerExceptionResolver 的 logException(Exception ex, HttpServletRequest request)
方法。
// AbstractHandlerExceptionResolver 類
protected void logException(Exception ex, HttpServletRequest request) {
if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) {
this.warnLogger.warn(buildLogMessage(ex, request));
}
}
protected String buildLogMessage(Exception ex, HttpServletRequest request) {
return "Resolved [" + ex + "]";
}
HandlerExceptionResolverComposite 是怎麼來的
從上面的截圖中我們可以看到,不管是 ExceptionHandlerExceptionResolver 還是 DefaultHandlerExceptionResolver,他們都包含在 HandlerExceptionResolverComposite 類 resolvers 屬性中(類型是 List),下面我們就看看 Spring Boot 是如何將這些 HandlerExceptionResolver 裝載到 HandlerExceptionResolverComposite 裏面的,着手點就在 HandlerExceptionResolverComposite 的 setOrder(o)
方法。
我們找到了這個類 WebMvcConfigurationSupport,handlerExceptionResolver()
方法負責初始化這個 Bean,addDefaultHandlerExceptionResolvers(list)
方法負責加載那些 HandlerExceptionResolver,具體代碼如下:
/**
* Returns a {@link HandlerExceptionResolverComposite} containing a list of exception
* resolvers obtained either through {@link #configureHandlerExceptionResolvers} or
* through {@link #addDefaultHandlerExceptionResolvers}.
* <p><strong>Note:</strong> This method cannot be made final due to CGLIB constraints.
* Rather than overriding it, consider overriding {@link #configureHandlerExceptionResolvers}
* which allows for providing a list of resolvers.
*/
@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
configureHandlerExceptionResolvers(exceptionResolvers);
if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers);
}
extendHandlerExceptionResolvers(exceptionResolvers);
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
}
/**
* A method available to subclasses for adding default
* {@link HandlerExceptionResolver HandlerExceptionResolvers}.
* <p>Adds the following exception resolvers:
* <ul>
* <li>{@link ExceptionHandlerExceptionResolver} for handling exceptions through
* {@link org.springframework.web.bind.annotation.ExceptionHandler} methods.
* <li>{@link ResponseStatusExceptionResolver} for exceptions annotated with
* {@link org.springframework.web.bind.annotation.ResponseStatus}.
* <li>{@link DefaultHandlerExceptionResolver} for resolving known Spring exception types
* </ul>
*/
protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager());
exceptionHandlerResolver.setMessageConverters(getMessageConverters());
exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
exceptionHandlerResolver.setResponseBodyAdvice(
Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
if (this.applicationContext != null) {
exceptionHandlerResolver.setApplicationContext(this.applicationContext);
}
exceptionHandlerResolver.afterPropertiesSet();
exceptionResolvers.add(exceptionHandlerResolver);
ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
responseStatusResolver.setMessageSource(this.applicationContext);
exceptionResolvers.add(responseStatusResolver);
exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}
存疑點
對於那些 404 異常,爲什麼沒有交給 DefaultHandlerExceptionResolver 處理?在 DispatcherServlet 中發現,即使 404 也能找到對應的 Handler,不拋 NoHandlerFoundException 異常就不會觸發異常處理,自然就輪不到 DefaultHandlerExceptionResolver 上場了。
爲啥明明 404 了,DispatcherServlet 還能找到 Handler?