淺析 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()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章