SpringMVC源碼(五)-異常處理

SpringMVC除了對請求URL的路由處理特別方便外,還支持對異常的統一處理機制,能夠對業務操做時拋出的異常,unchecked異常以及狀態碼的異常進行統一處理。SpringMVC既提供簡單的配置類,也提供了細粒度的異常控制機制。java

SpringMVC中全部的異常處理經過接口HandlerExceptionResolver來實現,接口中只定義了一個方法web

public interface HandlerExceptionResolver {

	ModelAndView resolveException(
		HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}

方法中接受request和response信息,以及當前的處理Handler,和拋出的異常對象。而且提供抽象類AbstractHandlerExceptionResolver,實現resolveException方法,支持前置判斷和處理,將實際處理抽象出doResolveException方法由子類來實現。ajax

1.SimpleMappingExceptionResolver

SimpleMappingExceptionResolver是SpringMVC提供的一個很是便捷的簡易異常處理方式,在XML中進行配置便可使用。spring

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
	<!-- 默認異常視圖 -->
	<property name="defaultErrorView" value="error"/>
	<!-- 視圖中獲取exception信息變量名 -->
	<property name="exceptionAttribute" value="ex"></property>
	<!-- 異常同視圖映射關係 -->
	<property name="exceptionMappings">
		<props>
			<prop key="com.lcifn.springmvc.exception.BusinessException">businessEx</prop>
		</props>
	</property>
</bean>

這是極簡的一種配置,exceptionMappings配置的是異常同視圖之間的映射關係,它是一個Properties對象,key-value分別是異常的類路徑和視圖名稱。defaultErrorView表示默認異常視圖,若是拋出的異常沒有匹配到任何視圖,即會走默認異常視圖。exceptionAttribute表示在視圖中獲取exception信息變量名,默認爲exception。還有一些其餘配置能夠查看SimpleMappingExceptionResolver的源碼來使用。mvc

2.@ExceptionHandler

SpringMVC提供了一種註解方式來靈活地配置異常處理,@ExceptionHandler中能夠配置要處理的異常類型,而後定義在處理此種異常的方法上,方法只要寫在Controller中,便可對Controller中全部請求方法有效。app

咱們定義一個BaseController,而且將須要處理的異常經過@ExceptionHandler定義好處理方法,這樣業務Controller只須要繼承這個基類就能夠了。處理方法中支持Request/Response/Sessioin等相關的參數綁定。async

[@Controller](https://my.oschina.net/u/1774615)
public class BaseController {

	@ExceptionHandler(RuntimeException.class)
	public ModelAndView handleRuntimeException(HttpServletRequest req, HttpServletResponse resp, RuntimeException ex){
		return new ModelAndView("error");
	}
}

可是繼承的方式仍是對業務代碼形成侵入,Spring很是重要的特性就是非侵入性,於是SpringMVC提供了@ControllerAdvice,簡單來講就是Controller的切面,支持對可選擇的Controller進行統一配置,用於異常處理簡直再合適不過了,咱們只須要將BaseController稍稍改一下。ide

@ControllerAdvice
public class AdviceController {

	@ExceptionHandler(RuntimeException.class)
	public ModelAndView handleRuntimeException(HttpServletRequest req, HttpServletResponse resp, RuntimeException ex){
		return new ModelAndView("error");
	}
}

只須要在統一配置類上加上@ControllerAdvice註解,支持包路徑,註解等過濾方式,便可完成對全部業務Controller進行控制,而業務Controller不用作anything。函數

若是請求爲ajax方式,須要其餘格式返回異常,在方法上加上@ResponseBody便可。this

3.異常處理原理

上面介紹了經常使用的兩種異常處理的配置方式,所謂知其然要知其因此然,SpringMVC怎麼在請求處理的過程當中完成對異常的統一處理的呢?咱們從源碼來深度解讀。

回到DispatcherServlet的doDispatcher方法

try {
	processedRequest = checkMultipart(request);
	multipartRequestParsed = (processedRequest != request);

	// Determine handler for the current request.
	mappedHandler = getHandler(processedRequest);
	if (mappedHandler == null || mappedHandler.getHandler() == null) {
		noHandlerFound(processedRequest, response);
		return;
	}

	// Determine handler adapter for the current request.
	HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

	if (!mappedHandler.applyPreHandle(processedRequest, response)) {
		return;
	}

	// Actually invoke the handler.
	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

	if (asyncManager.isConcurrentHandlingStarted()) {
		return;
	}

	applyDefaultViewName(processedRequest, mv);
	mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
	dispatchException = ex;
}
catch (Throwable err) {
	dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

能夠看到對請求處理的核心處理使用一個大的try/catch,若是出現異常,統一封裝成dispatchException交給processDispatchResult方法進行處理。咱們知道processDispatchResult方法用來對返回視圖進行操做,而同時也對異常進行統一處理。

在processDispatchResult中,首先對異常進行判斷。

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

若是不是特殊的ModelAndViewDefiningException,則由processHandlerException來操做。

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
		Object handler, Exception ex) throws Exception {

	// Check registered HandlerExceptionResolvers...
	ModelAndView exMv = null;
	// 遍歷全部註冊的異常處理器,由異常處理器進行處理
	for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
		exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
		if (exMv != null) {
			break;
		}
	}
	// 若是異常視圖存在,則轉向異常視圖
	if (exMv != null) {
		if (exMv.isEmpty()) {
			request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
			return null;
		}
		// We might still need view name translation for a plain error model...
		if (!exMv.hasView()) {
			exMv.setViewName(getDefaultViewName(request));
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
		}
		WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
		return exMv;
	}

	throw ex;
}

咱們主要關注異常處理器對異常的處理,SpringMVC經過HandlerExceptionResolver的resolveException調用實現類的實際實現方法doResolveException。

SimpleMappingExceptionResolver

來看SimpleMappingExceptionResolver的實現:

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

	// Expose ModelAndView for chosen error view.
	// 根據request和異常對象獲取異常視圖名稱
	String viewName = determineViewName(ex, request);
	if (viewName != null) {
		// Apply HTTP status code for error views, if specified.
		// Only apply it if we're processing a top-level request.
		Integer statusCode = determineStatusCode(request, viewName);
		if (statusCode != null) {
			applyStatusCodeIfPossible(request, response, statusCode);
		}
		// 組裝異常視圖模型ModelAndView
		return getModelAndView(viewName, ex, request);
	}
	else {
		return null;
	}
}

determineViewName方法決定異常視圖名稱,getModelAndView方法返回ModelAndView對象

protected String determineViewName(Exception ex, HttpServletRequest request) {
	String viewName = null;
	if (this.excludedExceptions != null) {
		for (Class<?> excludedEx : this.excludedExceptions) {
			if (excludedEx.equals(ex.getClass())) {
				return null;
			}
		}
	}
	// Check for specific exception mappings.
	if (this.exceptionMappings != null) {
		viewName = findMatchingViewName(this.exceptionMappings, ex);
	}
	// Return default error view else, if defined.
	if (viewName == null && this.defaultErrorView != null) {
		if (logger.isDebugEnabled()) {
			logger.debug("Resolving to default view '" + this.defaultErrorView + "' for exception of type [" +
					ex.getClass().getName() + "]");
		}
		viewName = this.defaultErrorView;
	}
	return viewName;
}

在determineViewName方法中,咱們配置的defaultErrorView和exceptionMappings都起了做用。更細節的就不深刻了,有興趣能夠本身去看。

ExceptionHandlerExceptionResolver

ExceptionHandlerExceptionResolver支持了@ExceptionHandler註解的實現。它的抽象基類AbstractHandlerMethodExceptionResolver繼承了AbstractHandlerExceptionResolver,doResolveException方法實際調用ExceptionHandlerExceptionResolver的doResolveHandlerMethodException方法。

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

	// 根據HandlerMethod和exception獲取異常處理的Method
	ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
	if (exceptionHandlerMethod == null) {
		return null;
	}

	// 設置異常處理方法的參數解析器和返回值解析器
	exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
	exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);

	ServletWebRequest webRequest = new ServletWebRequest(request, response);
	ModelAndViewContainer mavContainer = new ModelAndViewContainer();

	// 執行異常處理方法
	try {
		if (logger.isDebugEnabled()) {
			logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
		}
		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) {
		// Any other than the original exception is unintended here,
		// probably an accident (e.g. failed assertion or the like).
		if (invocationEx != exception && logger.isWarnEnabled()) {
			logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, 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());
		if (!mavContainer.isViewReference()) {
			mav.setView((View) mavContainer.getView());
		}
		if (model instanceof RedirectAttributes) {
			Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
			request = webRequest.getNativeRequest(HttpServletRequest.class);
			RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
		}
		return mav;
	}
}

咱們主要關注的是如何匹配到異常處理方法的

protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
	Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);

	// 從當前Controller中匹配異常處理Method
	if (handlerMethod != null) {
		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);
		}
	}

	// 從ControllerAdvice中匹配異常處理Method
	for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
		if (entry.getKey().isApplicableToBeanType(handlerType)) {
			ExceptionHandlerMethodResolver resolver = entry.getValue();
			Method method = resolver.resolveMethod(exception);
			if (method != null) {
				return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
			}
		}
	}

	return null;
}

匹配異常處理方法的來源有兩個,一個是當前Controller,一個是全部@ControllerAdvice類。能夠看到兩種方式都使用了cache的方式,那麼ExceptionHandlerMethod的信息怎麼初始化的呢?

當前Controller

對每一個請求HandlerMethod的Controller類型,都實例化一個ExceptionHandlerMethodResolver來處理異常。ExceptionHandlerMethodResolver的構造函數中初始化了當前Controller中的異常處理配置。

public ExceptionHandlerMethodResolver(Class<?> handlerType) {
	for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
		// detectExceptionMappings方法執行探查
		for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
			addExceptionMapping(exceptionType, method);
		}
	}
}

private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
	List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
	// 探查全部ExceptionHandler註解的方法
	detectAnnotationExceptionMappings(method, result);
	if (result.isEmpty()) {
		for (Class<?> paramType : method.getParameterTypes()) {
			if (Throwable.class.isAssignableFrom(paramType)) {
				result.add((Class<? extends Throwable>) paramType);
			}
		}
	}
	if (result.isEmpty()) {
		throw new IllegalStateException("No exception types mapped to " + method);
	}
	return result;
}

protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
	ExceptionHandler ann = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
	result.addAll(Arrays.asList(ann.value()));
}

@ControllerAdvice類

對@ControllerAdvice統一切面類的處理,則是在ExceptionHandlerExceptionResolver的初始化方法afterPropertiesSet中進行處理。

public void afterPropertiesSet() {
	// Do this first, it may add ResponseBodyAdvice beans
	// 初始化@ControllerAdvice中的@ExceptionHandler
	initExceptionHandlerAdviceCache();

	if (this.argumentResolvers == null) {
		List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
		this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	if (this.returnValueHandlers == null) {
		List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
		this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
	}
}

initExceptionHandlerAdviceCache方法遍歷上下文中全部有@ControllerAdvice註解的Bean,而後實例化成ExceptionHandlerMethodResolver類,在構造函數中初始化全部@ExceptionHandler。

private void initExceptionHandlerAdviceCache() {
	if (getApplicationContext() == null) {
		return;
	}
	if (logger.isDebugEnabled()) {
		logger.debug("Looking for exception mappings: " + getApplicationContext());
	}

	// 查詢全部@ControllerAdvice的Bean
	List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
	AnnotationAwareOrderComparator.sort(adviceBeans);

	// 遍歷,實例化ExceptionHandlerMethodResolver
	for (ControllerAdviceBean adviceBean : adviceBeans) {
		ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
		if (resolver.hasExceptionMappings()) {
			this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
			if (logger.isInfoEnabled()) {
				logger.info("Detected @ExceptionHandler methods in " + adviceBean);
			}
		}
		
		if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) {
			this.responseBodyAdvice.add(adviceBean);
			if (logger.isInfoEnabled()) {
				logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean);
			}
		}
	}
}

匹配到exceptionHandlerMethod後,設置一些方法執行的環境,而後調用ServletInvocableHandlerMethod中的invokeAndHandle去執行,這個調用過程和正常請求的調用就是一致了。這裏也不向下擴展了,能夠參看SpringMVC源碼(四)-請求處理

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

	// 執行請求方法
	Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
	setResponseStatus(webRequest);

	if (returnValue == null) {
		if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
			mavContainer.setRequestHandled(true);
			return;
		}
	}
	else if (StringUtils.hasText(getResponseStatusReason())) {
		mavContainer.setRequestHandled(true);
		return;
	}

	mavContainer.setRequestHandled(false);
	try {
		this.returnValueHandlers.handleReturnValue(
				returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
	}
	catch (Exception ex) {
		if (logger.isTraceEnabled()) {
			logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex);
		}
		throw ex;
	}
}

至此ExceptionHandlerExceptionResolver的異常處理已經基本完成。SpringMVC還內置了ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver來對狀態碼異常和常見的請求響應異常進行統一處理。

4.web.xml的配置

某些狀況下,SpringMVC的處理並無異常出現,但在最終的視圖輸出時找不到視圖文件,就會顯示404錯誤頁面,很是影響用戶體驗。咱們能夠在web.xml中對未捕獲的異常以及此種4xx或5xx的異常經過<error-page>進行處理。

<error-page>
	<error-code>404</error-code>
	<location>/404</location>
</error-page>
<error-page>
	<exception-type>java.lang.Throwable</exception-type>
	<location>/500</location>
</error-page>

一般對於系統中的異常,業務相關的儘可能自定義異常處理方式,而一些系統異常經過統一錯誤頁面進行處理。

相關文章
相關標籤/搜索