Spring MVC更多家族成员---框架内异常处理与HandlerExceptionResolver---09

2022-08-23 11:03:51 浏览数 (1)

Spring MVC更多家族成员---框架内异常处理与HandlerExceptionResolver---09

  • 引言
    • 源码体现
    • HandlerExceptionResolver
      • AbstractHandlerExceptionResolver
      • 默认加载的HandlerExceptionResolver
        • DefaultHandlerExceptionResolver
        • ResponseStatusExceptionResolver
      • HandlerExceptionResolver排序问题
  • 小结

引言

我们先来简单回滚一些之前讲过的Controller的定义:

代码语言:javascript复制
@FunctionalInterface
public interface Controller {
	@Nullable
	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

在Effictive Java一书中,作者对异常处理提出了如下的一段陈述:

总是要单独的声明被检查的异常,并且利用Javadoc的@throws:标记准确地记录下每个异常被抛出的条件。如果一个方法可能会抛出多个异常类,那么不要使用“快捷方式”,即声明它会抛出这些异常类的某个基类。作为一个极端的例子,永远不要声明一个方法“throws Exception”,或者更差的做法,“throws Throwable”。这样的声明没有为你的客户提供关于“这个方法能够抛出哪些异常”的任何指导信息,而且大大地妨碍了该方法的使用,因为它实际上掩盖了在同样的执行环境中该方法可能会抛出的任何其他异常。

对于上面的Handler处理方法定义来说,直接抛出异常的做法看起来直接违反了这段描述所倡导的异常处理最佳实践标准,而且框架开发者也承认这一点。不过,让我们换一个角度再来看这样的接口设计。

作为框架类的Handler,其应用的场景可能千差万别,而且在处理各个场景的Web请求的过程中,Handler自身或者Handler所依赖的各种业务对象所可能抛出的checked exception"也是不一而足。

试想,如何让Handler来预知那些可能抛出的异常类型呢?

就好像牛顿当年只能感慨“我能计算出天体的运行轨迹,却难以预料到…”一样,如果让Handler还去走最佳实践的那条路,显然后果也好不到哪里去。而且,应该根本就达不到最初所设想的目的。所以,框架实现者可能不得不“退而求其次”,转而throws Exception。

而且,这并非尽是坏处,现在的Handler接口不会对所有可能抛出的异常类型做任何的限制。

虽然最为顶层的Handler接口定义直接throws Exception,但如果愿意,我们依然可以通过覆写Handler的子类来进一步限定处理方法可能抛出的异常类型,例如:

代码语言:javascript复制
public class ExController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)throws IllegalArgumentException{
        throw new IllegalArgumentException();
    }
}

对于类似的子类实现,使用它们的客户端代码同样可以明确所要处理的具体异常情况。

当然,Handler接口定义能够如此设计和实现,其背后最强大的支持者当属即将登场的HandlerExceptionResolver。是它提供的框架内统一的异常处理方式,让throws Exception看起来更加“理直气壮”。

org.springframework.web.servlet,HandlerExceptionResolver对异常的处理范围仅限于Handler查找以及Handler执行期间,也就是下图中矩形所圈定的范围。

HandlerExceptionResolver和Handler的关系最不一般,它们就好像双子座两兄弟一样,如果Handler执行过程中没有任何异常,将以ModelAndview的形式返回后继流程要用的视图和模型数据信息,而一旦出现异常情况,HandlerExceptionResolver将接手异常情况的处理,处理完成后,将同样以ModelAndviewl的形式返回后继处理流程要使用的视图和模型数据信息。

只不过,HandlerExceptionResolver所返回的ModelAndview中所包含的信息是错误信息页面和相关异常的信息。


源码体现

让我们深入DispathcerServlet的doDispatch源码,查看一下HandlerExceptionResolver具体工作时机。

代码语言:javascript复制
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		....
		//两层try
		try {
			ModelAndView mv = null;
			Exception dispatchException = null;
            //内层try包裹的范围就是HandlerExceptionResolver进行异常处理的范围
			try {
				processedRequest = checkMultipart(request);
				....
				mappedHandler = getHandler(processedRequest);
			    if (mappedHandler == null) {
					//如果当前请求不存在对应处理的handler,默认是通过返回404的方式进行响应
					//而不是抛出异常的方式
					noHandlerFound(processedRequest, response);
					return;
				}
				....
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
				...
				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
				....
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			//内层try包括的范围就是查找和执行的handler的范围
			//进入全局异常处理和视图渲染流程
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		//如果在全局异常处理和视图渲染过程中出现异常,会被外层try接收,然后触发相关拦截器的后处理逻辑
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			 ...
		}
	}
  • 进入全局异常处理和视图渲染流程
代码语言:javascript复制
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {

		boolean errorView = false;
        //如果查找和执行的handler过程中抛出了异常,那么就进入全局异常处理逻辑
		if (exception != null) {
		   //如果异常类型为ModelAndViewDefiningException,那么直接取出该异常内部的ModelAndView进行错误渲染
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
			    //否则执行正常的全局异常渲染逻辑--mappedHandler是HandlerExcuteChain
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				//进入全部异常处理逻辑
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}

		// Did the handler return a view to render?
		//进入视图渲染逻辑
		if (mv != null && !mv.wasCleared()) {
			render(mv, request, response);
			//如果是错误视图渲染的话,情况当前请求中相关错误属性
			if (errorView) {
				WebUtils.clearErrorRequestAttributes(request);
			}
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace("No view rendering, null ModelAndView returned.");
			}
		}
		 ....
	}
  • 这些错误属性会在进行全局异常解析的时候,被加入request对象中
代码语言:javascript复制
	public static void clearErrorRequestAttributes(HttpServletRequest request) {
		request.removeAttribute(ERROR_STATUS_CODE_ATTRIBUTE);
		request.removeAttribute(ERROR_EXCEPTION_TYPE_ATTRIBUTE);
		request.removeAttribute(ERROR_MESSAGE_ATTRIBUTE);
		request.removeAttribute(ERROR_EXCEPTION_ATTRIBUTE);
		request.removeAttribute(ERROR_REQUEST_URI_ATTRIBUTE);
		request.removeAttribute(ERROR_SERVLET_NAME_ATTRIBUTE);
	}
  • processHandlerException就是全部异常处理的核心了,也是本文的重点
代码语言:javascript复制
	@Nullable
	protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
			@Nullable Object handler, Exception ex) throws Exception {

		// Success and error responses may use different content types
		//成功响应和错误响应使用的响应数据类型可能是不同的,因此这里将之前请求的响应类型从request中移除
		request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

		// Check registered HandlerExceptionResolvers...
		ModelAndView exMv = null;
		//挨个遍历DispathcerServlet中所有的handlerExceptionResolvers 
		if (this.handlerExceptionResolvers != null) {
			for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
			//尝试挨个调用每个HandlerExceptionResolver,如果某个HandlerExceptionResolver对当前异常解析完后,返回了非null的ModelAndView,那么就直接中断返回
				exMv = resolver.resolveException(request, response, handler, ex);
				if (exMv != null) {
					break;
				}
			}
		}
		//如果上面存在某一个HandlerExceptionResolver能够成功解析当前handler执行过程中抛出的异常
		if (exMv != null) {
		    //view和model都为空的话---说明HandlerExceptionResolvern内部自行处理了视图逻辑的渲染工作
			if (exMv.isEmpty()) {
			    //在request中设置当前ex
				request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
				return null;
			}
			// We might still need view name translation for a plain error model...
			//如果HandlerExceptionResolver返回的ModelAndView中没有直接设置View对象或者ViewName
			if (!exMv.hasView()) {
			//尝试借助viewNameTranslator获取一个默认的视图名
				String defaultViewName = getDefaultViewName(request);
				if (defaultViewName != null) {
					exMv.setViewName(defaultViewName);
				}
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Using resolved error view: "   exMv, ex);
			}
			else if (logger.isDebugEnabled()) {
				logger.debug("Using resolved error view: "   exMv);
			}
			//将各种错误信息暴露到当前request属性集合中去
			WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
			return exMv;
		}
       //如果查找和执行handler过程中抛出的异常,没有HandlerExceptionResolver能够处理,那么最后还是会正常抛出该异常
		throw ex;
	}
代码语言:javascript复制
	public static void exposeErrorRequestAttributes(HttpServletRequest request, Throwable ex,
			@Nullable String servletName) {

		exposeRequestAttributeIfNotPresent(request, ERROR_STATUS_CODE_ATTRIBUTE, HttpServletResponse.SC_OK);
		exposeRequestAttributeIfNotPresent(request, ERROR_EXCEPTION_TYPE_ATTRIBUTE, ex.getClass());
		exposeRequestAttributeIfNotPresent(request, ERROR_MESSAGE_ATTRIBUTE, ex.getMessage());
		exposeRequestAttributeIfNotPresent(request, ERROR_EXCEPTION_ATTRIBUTE, ex);
		exposeRequestAttributeIfNotPresent(request, ERROR_REQUEST_URI_ATTRIBUTE, request.getRequestURI());
		if (servletName != null) {
			exposeRequestAttributeIfNotPresent(request, ERROR_SERVLET_NAME_ATTRIBUTE, servletName);
		}
	}

HandlerExceptionResolver

HandlerExceptionResolver的定义如下:

代码语言:javascript复制
public interface HandlerExceptionResolver {
	@Nullable
	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}

HandlerExceptionResolver仅定义了一个resolveException方法,负费处理与相关Handler所关联的某种异常(如参数Exception ex所示),异常处理完成后,将处理结果以ModelAndviewl的形式返回。这当然包括将要跳转到的错误信息页面,以及该页面所要显示的必要信息。

至于HandlerExceptionResolver所返回的ModelAndview的后继处理,与Handler处理后返回的ModelAndView应该说是“殊途同归”了。

下面DispatcherServlet将寻求ViewResolver和view对这些返回的信息进行处理。

HandlerExceptionResolver的继承图谱如下所示:

AbstractHandlerExceptionResolver基本上是所有HandlerExceptionResolver实现类的父类,因此,我们先来看看AbstractHandlerExceptionResolver。


AbstractHandlerExceptionResolver

AbstractHandlerExceptionResolver主要是将handler匹配过滤逻辑和响应缓存设置进行了统一处理,对于核心的异常解析行为,还是交给了子类去实现:

代码语言:javascript复制
	public ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
        //判断当前AbstractHandlerExceptionResolver能否解析当前handler
		if (shouldApplyTo(request, handler)) {
		    //做响应缓存处理---设置相关响应头
			prepareResponse(ex, response);
			//子类实现真正的异常解析逻辑
			ModelAndView result = doResolveException(request, response, handler, ex);
			//异常日志记录
			if (result != null) {
				// Print debug message when warn logger is not enabled.
				if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
					logger.debug(buildLogMessage(ex, request)   (result.isEmpty() ? "" : " to "   result));
				}
				// Explicitly configured warn logger in logException method.
				logException(ex, request);
			}
			return result;
		}
		else {
		//如果当前AbstractHandlerExceptionResolver不会对当前handler类型进行处理,那么直接返回null
			return null;
		}
	}

在讲解shouldApplyTo方法之前,需要先来认识一下AbstractHandlerExceptionResolver内部用于识别自己可以处理handler类型的工具:

代码语言:javascript复制
    //按照具体的handler对象进行匹配
	@Nullable
	private Set<?> mappedHandlers;
    //按照类型进行匹配
	@Nullable
	private Class<?>[] mappedHandlerClasses;

下面再来看看shouldApplyTo的实现:

代码语言:javascript复制
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
       //如果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;
					}
				}
			}
		}
		//如果我们没有设置实例和类型限制的话,那就默认当前HandlerExceptionResolver可以解析所有handler抛出的异常
		return !hasHandlerMappings();
	}
	 
	 protected boolean hasHandlerMappings() {
		return (this.mappedHandlers != null || this.mappedHandlerClasses != null);
	} 

默认加载的HandlerExceptionResolver

DispathcerServlet.properties文件规定了Spring mvc默认使用的组件列表,对于HandlerExceptionResolver来说,会默认加载下面三个实现类:


DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver是Spring mvc默认加载的三个HandlerExceptionResolver其中一个,DefaultHandlerExceptionResolver提供了spring内部抛出的异常以及对应的响应错误码处理。

  • 支持处理的异常列表如下:
代码语言:javascript复制
Exception                                HTTP Status Code
HttpRequestMethodNotSupportedException   405 (SC_METHOD_NOT_ALLOWED)
HttpMediaTypeNotSupportedException       415 (SC_UNSUPPORTED_MEDIA_TYPE)
HttpMediaTypeNotAcceptableException      406 (SC_NOT_ACCEPTABLE)
MissingPathVariableException             500 (SC_INTERNAL_SERVER_ERROR)
MissingServletRequestParameterException  400 (SC_BAD_REQUEST)
ServletRequestBindingException           400 (SC_BAD_REQUEST)
ConversionNotSupportedException          500 (SC_INTERNAL_SERVER_ERROR)
TypeMismatchException                    400 (SC_BAD_REQUEST)
HttpMessageNotReadableException          400 (SC_BAD_REQUEST)
HttpMessageNotWritableException          500 (SC_INTERNAL_SERVER_ERROR)
MethodArgumentNotValidException          400 (SC_BAD_REQUEST)
MissingServletRequestPartException       400 (SC_BAD_REQUEST)
BindException                            400 (SC_BAD_REQUEST)
NoHandlerFoundException                  404 (SC_NOT_FOUND)
AsyncRequestTimeoutException             503 (SC_SERVICE_UNAVAILABLE)
  • 核心方法如下:
代码语言:javascript复制
@Override
	@Nullable
	protected ModelAndView doResolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

		try {
			if (ex instanceof HttpRequestMethodNotSupportedException) {
				return handleHttpRequestMethodNotSupported(
						(HttpRequestMethodNotSupportedException) ex, request, response, handler);
			}
			else if (ex instanceof HttpMediaTypeNotSupportedException) {
				return handleHttpMediaTypeNotSupported(
						(HttpMediaTypeNotSupportedException) ex, request, response, handler);
			}
			....
		}
		catch (Exception handlerEx) {
			if (logger.isWarnEnabled()) {
				logger.warn("Failure while trying to resolve exception ["   ex.getClass().getName()   "]", handlerEx);
			}
		}
		return null;
	}
  • 针对每一个异常进行处理的方法如下,大体思路类似,这里只列举一个:
代码语言:javascript复制
	protected ModelAndView handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
        
		String[] supportedMethods = ex.getSupportedMethods();
		if (supportedMethods != null) {
			response.setHeader("Allow", StringUtils.arrayToDelimitedString(supportedMethods, ", "));
		}
		//直接通过reponse对象发送对应的错误码
		response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, ex.getMessage());
		//返回一个空的ModelAndView实现---表示当前HandlerExceptionResolver自行完成了错误视图的渲染
		return new ModelAndView();
	}

ResponseStatusExceptionResolver

ResponseStatusExceptionResolver作用如下:

  • 如果所发生异常是ResponseStatusException,则从中解析status,reason,然后调用response.sendError;
  • 否则如果所发生异常上使用了注解@ResponseStatus,则从中解析status,reason,然后调用response.sendError;
  • 否则如果所发生异常的cause也是一个异常,则对其递归执行该流程;

以上主要功能主要体现在其方法#doResolveException中。

代码语言:javascript复制
	protected ModelAndView doResolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

		try {
			if (ex instanceof ResponseStatusException) {
				return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
			}

			ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
			if (status != null) {
				return resolveResponseStatus(status, request, response, handler, ex);
			}

			if (ex.getCause() instanceof Exception) {
				return doResolveException(request, response, handler, (Exception) ex.getCause());
			}
		}
		catch (Exception resolveEx) {
			if (logger.isWarnEnabled()) {
				logger.warn("Failure while trying to resolve exception ["   ex.getClass().getName()   "]", resolveEx);
			}
		}
		return null;
	}

如果ResponseStatusExceptionResolver能够处理某个异常,最终调用了response.sendError,则可以说ResponseStatusExceptionResolver处理完了这个异常,它会返回一个空ModelAndView对象。

另外,ResponseStatusExceptionResolver实现了接口MessageSourceAware,也就是说它会接收一个MessageSource用于解析reason对应的消息,这也是一种国际化/本地化消息处理的一种体现。

代码语言:javascript复制
	protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
			throws IOException {

		if (!StringUtils.hasLength(reason)) {
			response.sendError(statusCode);
		}
		else {
			String resolvedReason = (this.messageSource != null ?
					this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
					reason);
			response.sendError(statusCode, resolvedReason);
		}
		return new ModelAndView();
	}

HandlerExceptionResolver排序问题

在Spring MVC框架中,我们可以按照优先级顺序指定多个HandlerMapping以及ViewResolverl的实例来帮助我们细化相应关注点的处理,而HandlerExceptionResolver则是框架内第三个拥有这种能力的“人”也就是说,如 果我们在DispatcherServlet的WebApplicationContext中指定多个HandlerExceptionResolver实例的话,DispatcherServlet将根据它们的优先级顺序选取合适的实例进行异常处理。

当然啦,优先级的控制方式依然是通过Ordered接口来进行。

AbstractHandlerExceptionResolver父基类默认已经实现了Ordered接口,子类就无需实现了。

代码语言:javascript复制
	public void setOrder(int order) {
		this.order = order;
	}

只需要通过父类提供的setOrder方法来设置当前HandlerExceptionResolver的顺序即可,


DispathcerServlet初始化HandlerExceptionResolver的方法如下:

代码语言:javascript复制
	private void initHandlerExceptionResolvers(ApplicationContext context) {
		this.handlerExceptionResolvers = null;
        //默认按照类型去容器中查询所有HandlerExceptionResolver 
		if (this.detectAllHandlerExceptionResolvers) {
			Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
					.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
			if (!matchingBeans.isEmpty()) {
				this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
				//按照order进行排序
				AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
			}
		}
		//如果我们手动将detectAllHandlerExceptionResolvers设置为false
		else {
		//那么会去容器中查询名字为handlerExceptionResolver的HandlerExceptionResolver
			try {
				HandlerExceptionResolver her =
						context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
				this.handlerExceptionResolvers = Collections.singletonList(her);
			}
			catch (NoSuchBeanDefinitionException ex) {
				// Ignore, no HandlerExceptionResolver is fine too.
			}
		}

		//如果用户没有向容器中提供HandlerExceptionResolver,那么会去读取配置文件中默认给出的三个实现类
		if (this.handlerExceptionResolvers == null) {
			this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
			if (logger.isTraceEnabled()) {
				logger.trace("No HandlerExceptionResolvers declared in servlet '"   getServletName()  
						"': using default strategies from DispatcherServlet.properties");
			}
		}
	}

小结

本文对Spring mvc为我们提供的全局异常处理体系进行了详细的介绍,可能细心的小伙伴会发现并没有列举出我们开发中常用的注解版本HandlerExceptionResolver实现,至于注解版本的实现,会在后续介绍Spring mvc提供的注解支持时进行介绍。

0 人点赞