1. 程式人生 > >SpringMVC原始碼剖析(七)- HandlerExceptionResolver異常解析器家族揭祕

SpringMVC原始碼剖析(七)- HandlerExceptionResolver異常解析器家族揭祕

在Spring MVC中,所有用於處理在請求處理過程中丟擲的異常,都要實現HandlerExceptionResolver介面。HandlerExceptionResolver是Spring MVC提供的非常好的通用異常處理工具,不過需要注意的是,它只能處理請求過程中丟擲的異常,異常處理本身所丟擲的異常和檢視解析過程中丟擲的異常它是不能處理的。AbstractHandlerExceptionResolver實現該介面和Orderd介面,是HandlerExceptionResolver類的實現的基類。ResponseStatusExceptionResolver等具體的異常處理類均在AbstractHandlerExceptionResolver之上,實現了具體的異常處理方式。一個基於Spring MVC的Web應用程式中,可以存在多個實現了HandlerExceptionResolver的異常處理類,他們的執行順序,由其order屬性決定, order值越小,越是優先執行, 在執行到第一個返回不是null的ModelAndView的Resolver時,不再執行後續的尚未執行的Resolver的異常處理方法。

<mvc:annotation-driven/>會自動將ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver配置到Spring MVC中,並且其中ExceptionHandlerExceptionResolver優先順序最高,ResponseStatusExceptionResolver第二,DefaultHandlerExceptionResolver第三。如果你想使用SimpleMappingExceptionResolver,你需要自己將SimpleMappingExceptionResolver配置到Spring MVC中。另外ExceptionHandlerExceptionResolver不僅可以解析處理器類中註解的@

ExceptionHandler的方法,還可以使用@ControllerAdvice註解的類裡的有@ExceptionHandler註解的全域性異常處理方法。

我們首先看一下HandlerExceptionResolver家族體系的結構:


其中HandlerExceptionResolverComposite作為容器使用,可以封裝別的Resolver,它並不會解析具體的異常,而是呼叫其他的異常解析器處理異常。這裡我們不需要去研究它。

而AnnotationMethodHandlerExceptionResolver已經被棄用了,所以不需要解析。剩下的是我們研究的重點:

1. AbstractHandlerMethodExceptionResolver和ExceptionHandlerExceptionResolver一起使用,完成使用@ExceptionHandler註釋的方法進行對異常的解析。

2. ResponseStatusExceptionResolver: 解析有@ResponseStatus註解的異常。

3. DefaultHandlerExceptionResolver:按照不同的型別分別對異常進行解析。

4. SimpleMappingExceptionResolver: 通過配置的異常類和view的對應關係來解析異常。

下面我們具體的分析這些異常解析器。

首先我們HandlerExceptionResolver介面的原始碼,我去掉了原始碼中不關心的註釋的部分:

public interface HandlerExceptionResolver {

	ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
Spring MVC的異常處理只是處理請求處理過程中異常,既然是"請求處理過程中"的異常,我們必然會知道本次請求的handler物件,以及被丟擲的Exception物件,既然是本次請求,肯定還會出現與請求有關的request和response物件。這就很好的說明了為什麼resolveException方法中會出現這四個引數。

為什麼我一直都說Spring MVC的異常處理體系只是處理請求處理過程的異常呢?我們來分析下原始碼,下面的原始碼出自DispatcherServlet的doDispatch方法,我省略了部分不重要的內容:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		try {
			//請求處理的程式碼
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		finally {

		}
	}
在請求處理過程中發生的異常,都會進入到processDispatchResult方法中,我去掉了不關心的部分:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
		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);
			}
		}
	}
會發現在這個方法實際呼叫的是processHandlerException方法,我去掉了不關心的部分:
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
			Object handler, Exception ex) throws Exception {
		ModelAndView exMv = null;
		for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
			exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
			if (exMv != null) {
				break;
			}
		}
		throw ex;
	}
會發現這個方法遍歷了handlerExceptionResolvers異常解析器列表,這個handlerExceptionResolvers異常解析器列表DispatcherServlet初始化的時候建立的。通常handlerExceptionResolvers異常解析器列表裡面包含了上面我們所講述的ExceptionHandlerExceptionResolver和ResponseStatusExceptionResolver以及DefaultHandlerExceptionResolver。

HandlerExceptionResolver異常體系中使用到模板設計模式,HandlerExceptionResolver介面定義了處理異常的標準API,而AbstractHandlerExceptionResolver則定義了處理異常的步驟。下面是AbstractHandlerExceptionResolver原始碼:

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

		if (shouldApplyTo(request, handler)) {
			// Log exception, both at debug log level and at warn level, if desired.
			if (logger.isDebugEnabled()) {
				logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
			}
			logException(ex, request);
			prepareResponse(ex, response);
			return doResolveException(request, response, handler, ex);
		}
		else {
			return null;
		}
	}
第一步:判斷當前所用的異常處理器是否可以處理當前的handler。如果不可以,就返回null, 這樣在DispatcherServlet裡面就會繼續遍歷handlerExceptionResolvers異常解析器列表,尋找下一個異常解析器來處理當前的handler。如果可以,就繼續進行下面的第二步。那麼它是如何判斷的呢?我們來研究下shouldApplyTo方法:
protected boolean shouldApplyTo(HttpServletRequest request, Object handler) {
		if (handler != null) {
			if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) {
				return true;
			}
			if (this.mappedHandlerClasses != null) {
				for (Class<?> handlerClass : this.mappedHandlerClasses) {
					if (handlerClass.isInstance(handler)) {
						return true;
					}
				}
			}
		}
		// Else only apply if there are no explicit handler mappings.
		return (this.mappedHandlers == null && this.mappedHandlerClasses == null);
	}
this.mappedHandlers代表當前的異常處理器可以處理哪些handler,如果目標handler非空,並且異常處理器可以處理的handler包含目標handler,我們就說這個異常處理器可以處理目標handler的異常。下面的if條件的內容與此相似。

第二步:logException就是將exception打印出來。

第三步:prepareResponse方法根據preventResponseCaching標誌判斷是否給response設定禁用快取的屬性。

第四步:doResolveException方法是模板方法,至於具體的如何處理異常,應該交給具體的異常解析器來進行處理。

ExceptionHandlerExceptionResolver

ExceptionHandlerExceptionResolver的父類AbstractHandlerMethodExceptionResolver重寫了shouldApplyTo方法:

protected boolean shouldApplyTo(HttpServletRequest request, Object handler) {
		if (handler == null) {
			return super.shouldApplyTo(request, handler);
		}
		else if (handler instanceof HandlerMethod) {
			HandlerMethod handlerMethod = (HandlerMethod) handler;
			handler = handlerMethod.getBean();
			return super.shouldApplyTo(request, handler);
		}
		else {
			return false;
		}
	}
在這裡我們可以看出,如果我們的handler為空,就呼叫父類的shouldApplyTo方法,如果handler非空,就判斷handler是否是屬於HandlerMethod型別,如果是就獲取HandlerMethod處理器的型別資訊,賦給當前的handler,然後還是呼叫父類的shouldApplyTo方法進行處理,否則返回false。

AbstractHandlerMethodExceptionResolver的作用就相當於一個介面卡,一般的處理器是類的形式,但是HandlerMethod其實是講方法作為處理器來使用的,所以需要適配。

AbstractHandlerMethodExceptionResolver裡面的doResolveException將處理傳遞給了doResolveHandlerMethodException方法具體處理,而doResolveHandlerMethodException是一個模板方法,由ExceptionHandlerExceptionResolver具體實現。

ResponseStatusExceptionResolver

現在我們看下ResponseStatusExceptionResolver的doResolveException方法:

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

		ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
		if (responseStatus != null) {
			try {
				return resolveResponseStatus(responseStatus, request, response, handler, ex);
			}
			catch (Exception resolveEx) {
				logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx);
			}
		}
		return null;
	}
首先獲取當前的exception是否存在@ResponseStatus註解,如果存在,就使用resolveResponseStatus方法處理.
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,
			HttpServletResponse response, Object handler, Exception ex) throws Exception {

		int statusCode = responseStatus.value().value();
		String reason = responseStatus.reason();
		if (this.messageSource != null) {
			reason = this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale());
		}
		if (!StringUtils.hasLength(reason)) {
			response.sendError(statusCode);
		}
		else {
			response.sendError(statusCode, reason);
		}
		return new ModelAndView();
	}
獲取@ResponseStatus註解中的value和reason的值,然後設定到當前的response中去。

DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver是根據異常的具體型別來進行處理:

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

		try {
			if (ex instanceof NoSuchRequestHandlingMethodException) {
				return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, request, response,
						handler);
			}
			else if (ex instanceof HttpRequestMethodNotSupportedException) {
				return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, request,
						response, handler);
			}
			else if (ex instanceof HttpMediaTypeNotSupportedException) {
				return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, request, response,
						handler);
			}
			else if (ex instanceof HttpMediaTypeNotAcceptableException) {
				return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response,
						handler);
			}
			else if (ex instanceof MissingServletRequestParameterException) {
				return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request,
						response, handler);
			}
			else if (ex instanceof ServletRequestBindingException) {
				return handleServletRequestBindingException((ServletRequestBindingException) ex, request, response,
						handler);
			}
			else if (ex instanceof ConversionNotSupportedException) {
				return handleConversionNotSupported((ConversionNotSupportedException) ex, request, response, handler);
			}
			else if (ex instanceof TypeMismatchException) {
				return handleTypeMismatch((TypeMismatchException) ex, request, response, handler);
			}
			else if (ex instanceof HttpMessageNotReadableException) {
				return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, request, response, handler);
			}
			else if (ex instanceof HttpMessageNotWritableException) {
				return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, request, response, handler);
			}
			else if (ex instanceof MethodArgumentNotValidException) {
				return handleMethodArgumentNotValidException((MethodArgumentNotValidException) ex, request, response, handler);
			}
			else if (ex instanceof MissingServletRequestPartException) {
				return handleMissingServletRequestPartException((MissingServletRequestPartException) ex, request, response, handler);
			}
			else if (ex instanceof BindException) {
				return handleBindException((BindException) ex, request, response, handler);
			}
			else if (ex instanceof NoHandlerFoundException) {
				return handleNoHandlerFoundException((NoHandlerFoundException) ex, request, response, handler);
			}
		}
		catch (Exception handlerException) {
			logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
		}
		return null;
	}

各種異常解析器的使用

在我們自定義的異常上使用ResponseStatus註解。當我們的Controller丟擲異常,並且沒有被處理的時候,他將返回HTTP STATUS 為指定值的 HTTP RESPONSE,比如:

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order")  // 404
    public class OrderNotFoundException extends RuntimeException {
        // ...
    }

我們的Controller為:

 @RequestMapping(value="/orders/{id}", method=GET)
    public String showOrder(@PathVariable("id") long id, Model model) {
        Order order = orderRepository.findOrderById(id);
        if (order == null) throw new OrderNotFoundException(id);
        model.addAttribute(order);
        return "orderDetail";
    }
這時候會返回404,轉到404頁面而不是錯誤頁面。
  1. 發生異常後,改變Response status,一般而言,發生異常返回HTTP STATUS 500.我們可以變為其他。
  2. 發生錯誤後轉到錯誤頁面
  3. 可以為不同異常定義不同處理(如不同的錯誤頁面,不同的Response status)

舉例說明:

@Controller
public class ExceptionHandlingController {

  // 我們標註了@RequestMapping的方法
  ...
  
  //處理異常的方法。
  
  // 把我們定義的異常轉換為特定的Http status code
  @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation")  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public void conflict() {
    // Nothing to do
  }
  

  // 捕獲到SQLException,DataAccessException異常之後,轉到特定的頁面。
  @ExceptionHandler({SQLException.class,DataAccessException.class})
  public String databaseError() {
    //僅僅轉到錯誤頁面,我們在頁面上得不到這個Exception的值,要得到值,我們可以通過下面的方法得到
    return "databaseError";
  }

  // 通過ModelAndView返回頁面,以及往頁面傳相應的值
  @ExceptionHandler(Exception.class)
  public ModelAndView handleError(HttpServletRequest req, Exception exception) {
    logger.error("Request: " + req.getRequestURL() + " raised " + exception);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", exception);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName("error");
    return mav;
  }
}


class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
    
   //轉到特定頁面 。。。。。
}

如果我們要處理程式中所有的異常可以這麼做:
@ControllerAdvice
class GlobalDefaultExceptionHandler {
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        // If the exception is annotated with @ResponseStatus rethrow it and let
        // the framework handle it - like the OrderNotFoundException example
        // at the start of this post.
        // AnnotationUtils is a Spring Framework utility class.
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
            throw e;
        }
        // Otherwise setup and send the user to a default error-view.
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    }
}


參考文獻: