SpringBoot原始碼解析-ExceptionHandler處理異常的原理

吾乃上將軍邢道榮發表於2019-04-10

在專案中,經常會使用ExceptionHandler來作為全域性性的異常處理中心。那麼ExceptionHandler處理異常的原理是什麼呢,今天就來分析一下。

ExceptionHandler使用示例

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(value = RuntimeException.class)
    public String handle(){
        return "error";
    }
}
複製程式碼

使用還是很簡單的,在類上面新增ControllerAdvice註解,在方法上面新增ExceptionHandler註解,就可以在方法裡處理相應的異常資訊了。

原理剖析

ControllerAdvice和ExceptionHandler註解的作用

異常處理的核心類是ExceptionHandlerExceptionResolver,進入該類。檢視afterPropertiesSet方法。

	public void afterPropertiesSet() {
		// Do this first, it may add ResponseBodyAdvice beans
		initExceptionHandlerAdviceCache();
		...
	}

	private void initExceptionHandlerAdviceCache() {
		if (getApplicationContext() == null) {
			return;
		}
		//這行程式碼會找出所有標記了ControllerAdvice註解的類
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		AnnotationAwareOrderComparator.sort(adviceBeans);

		for (ControllerAdviceBean adviceBean : adviceBeans) {
			Class<?> beanType = adviceBean.getBeanType();
			if (beanType == null) {
				throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
			}
			//遍歷這些類,找出有ExceptionHandler註解標註的方法。
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
			if (resolver.hasExceptionMappings()) {
				this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
			}
			if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
				this.responseBodyAdvice.add(adviceBean);
			}
		}
		...
	}
複製程式碼

通過上述程式碼可以看出,在ExceptionHandlerExceptionResolver類中,該類掃描了所有標註有ExceptionHandler註解的方法,並將他們存入了exceptionHandlerAdviceCache中。

異常處理的原理

看過了ControllerAdvice和ExceptionHandler註解的作用後,我們來看一下異常處理的原理。進入DispatcherServlet的doDispatch方法

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
				...
				//處理controller方法
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				...
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			//異常處理中心
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		...
	}
複製程式碼

從doDispatch方法中可以看出,程式先處理了controller層的業務邏輯,對於業務邏輯丟擲的異常,程式統一做了封裝,然後進入了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) {
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}
		...
	}

	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;
		if (this.handlerExceptionResolvers != null) {
		//遍歷handlerExceptionResolvers處理異常資訊
			for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
				exMv = resolver.resolveException(request, response, handler, ex);
				if (exMv != null) {
					break;
				}
			}
		}
		...
	}
複製程式碼

那這邊的handlerExceptionResolvers是哪裡來的呢?

	private void initHandlerExceptionResolvers(ApplicationContext context) {
		this.handlerExceptionResolvers = null;

		if (this.detectAllHandlerExceptionResolvers) {
			// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
			Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
					.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
			...
		}
		...
	}
複製程式碼

在DispatcherServlet初始化的時候,會去容器中找HandlerExceptionResolver型別的類。而剛剛的ExceptionHandlerExceptionResolver類就是繼承了HandlerExceptionResolver介面,所以這個地方就將他放入了DispatcherServlet中。所以上面的遍歷handlerExceptionResolvers處理異常資訊的地方,就是呼叫了ExceptionHandlerExceptionResolver的resolveException方法。所以我們進入該方法。

	public ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

		if (shouldApplyTo(request, handler)) {
			prepareResponse(ex, response);
			ModelAndView result = doResolveException(request, response, handler, ex);
			...
		}
	}

	protected final ModelAndView doResolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

		return doResolveHandlerMethodException(request, response, (HandlerMethod) handler, ex);
	}

	protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
			HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {

		ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
		...
			else {
				// Otherwise, just the given exception as-is
				exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
			}
		}
		...
	}

	public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

		Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
		...
	}

	public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {
		//獲取方法的引數
		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
		if (logger.isTraceEnabled()) {
			logger.trace("Arguments: " + Arrays.toString(args));
		}
		//執行方法
		return doInvoke(args);
	}
複製程式碼

整個異常的執行邏輯如上面的程式碼,簡單點說就是找到相應的異常處理方法,執行他。這個地方getMethodArgumentValues裡面的邏輯和 SpringBoot原始碼解析-controller層引數的封裝 是一樣的,但是他們能處理的引數型別卻不一樣。

檢視ExceptionHandlerExceptionResolver類的afterPropertiesSet方法:

	public void afterPropertiesSet() {
		...
		if (this.argumentResolvers == null) {
			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		...
	}

	protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

		// Annotation-based argument resolution
		resolvers.add(new SessionAttributeMethodArgumentResolver());
		resolvers.add(new RequestAttributeMethodArgumentResolver());

		// Type-based argument resolution
		resolvers.add(new ServletRequestMethodArgumentResolver());
		resolvers.add(new ServletResponseMethodArgumentResolver());
		resolvers.add(new RedirectAttributesMethodArgumentResolver());
		resolvers.add(new ModelMethodProcessor());

		// Custom arguments
		if (getCustomArgumentResolvers() != null) {
			resolvers.addAll(getCustomArgumentResolvers());
		}

		return resolvers;
	}
複製程式碼

這邊就是ExceptionHandler方法中可以接收的引數型別了。看一下,要比controller那邊的型別少了許多,使用的時候注意一下即可。


返回目錄

相關文章