浅析 Spring 异常处理

如果你去面试,面试官问你 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 接口我们不传任何参数。
query 接口不传参数divide-zero 接口
再看后端日志打印输出了啥

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;
    }

}

仍旧分别调用 querydivide-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,随便找几个常见的,HttpRequestMethodNotSupportedExceptionMissingServletRequestParameterExceptionBindException 等,这个类做的事情就是给 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?

References

  1. 深入理解 Spring 异常处理
  2. Throwable getCause()
posted @ 2019-10-29 21:14  Zhoust9610  阅读(8)  评论(0编辑  收藏  举报