如果你去面试,面试官问你 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?