07-HandlerExceptionResolver

2022-07-18

07-HandlerExceptionResolver

springmvc之HandlerExceptionResolver

一. 概述

HandlerExceptionResolver用于解析请求处理过程中所产生的异常。

public interface HandlerExceptionResolver {

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

}

异常解析过程主要包含两部分内容:给ModelAndView设置相应内容、设置response的相关属性。当然还可能有一些辅助功能,如记录日志等,在自定义的ExceptionHandler里还可以做更多的事情。

二. 源码分析

2.1 AbstractHandlerExceptionResolver

该类是所有异常解析类的父类。提供了一些共有的方法,定义了通用的解析流程,子类只需要实现其中的模板方法即可。

@Override
	@Nullable
	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);
			if (result != null) {
				// Print debug message when warn logger is not enabled.
				if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
					logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
				}
				// Explicitly configured warn logger in logException method.
				logException(ex, request);
			}
			return result;
		}
		else {
			return null;
		}
	}
  1. 首先通过shouldApplyTo()判断当前ExceptionResolver是否能处理传入handler所抛出的异常。
  2. 如果不能处理,返回null,交给下一个ExceptionResolver
  3. 如果可以,调用prepareResponse设置response,接着调用doResolverException设置ModelAndView。最后记录日志,返回mav

shouldApplyTo

protected boolean shouldApplyTo(HttpServletRequest request, @Nullable 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);
}

这里有两个属性:

@Nullable
private Set<?> mappedHandlers;

@Nullable
private Class<?>[] mappedHandlerClasses;
  • mappedHandlers:如果设置了该属性,那么只有该集合包含此handler,该resolver才能解析该Handler抛出的异常
  • mappedHandlerClasses:含义同上。只不过是以Class类型表示的

如果两个都不设置,那么代表可以解析全部handler

prepareResponse

protected void prepareResponse(Exception ex, HttpServletResponse response) {
  if (this.preventResponseCaching) {
    preventCaching(response);
  }
}

protected void preventCaching(HttpServletResponse response) {
  response.addHeader(HEADER_CACHE_CONTROL, "no-store");
}

如果阻止response缓存,则给response设置响应头Cache-Control: no-store,默认为false,即不阻止

doResolveException则是一个模板方法,交给子类实现

2.2 AbstractHandlerMethodExceptionResolver

重写了shouldApplyTo方法

@Override
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
  if (handler == null) {
    return super.shouldApplyTo(request, null);
  }
  else if (handler instanceof HandlerMethod) {
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    handler = handlerMethod.getBean();
    return super.shouldApplyTo(request, handler);
  }
  else {
    return false;
  }
}

这种是针对于HandlerMethod类型的专门定制。如果handler是HandlerMethod类型,获取其所在的bean交给父类处理。其余返回false

也就意味着只支持null和HandlerMethod类型的处理器

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

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

将Handler强转为HandlerMethod,交给子类的模板方法去处理

2.3 ExceptionHandlerExceptionResolver

这个类和RequestMappingHandlerAdapter很类似,其实也是一个执行异常处理方法的过程

2.3.1 初始化

@Override
public void afterPropertiesSet() {
  // Do this first, it may add ResponseBodyAdvice beans
  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);
  }
}
private void initExceptionHandlerAdviceCache() {
		if (getApplicationContext() == null) {
			return;
		}

		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		for (ControllerAdviceBean adviceBean : adviceBeans) {
			Class<?> beanType = adviceBean.getBeanType();
			if (beanType == null) {
				throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
			}
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
			if (resolver.hasExceptionMappings()) {
				this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
			}
			if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
				this.responseBodyAdvice.add(adviceBean);
			}
		}
	}

从容器中获取@ControllerAdvice注释的类,然后将注释了@ExceptionHandler注解的方法保存起来。同时还保存了注释了@ResponseBody的方法

这个也就是保存全局的异常处理

2.3.2 处理异常

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

  ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
  if (exceptionHandlerMethod == null) {
    return null;
  }

  if (this.argumentResolvers != null) {
    exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
  }
  if (this.returnValueHandlers != null) {
    exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
  }

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

  try {
    if (logger.isDebugEnabled()) {
      logger.debug("Using @ExceptionHandler " + 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 (or its cause) is unintended here,
    // probably an accident (e.g. failed assertion or the like).
    if (invocationEx != exception && invocationEx != exception.getCause() && logger.isWarnEnabled()) {
      logger.warn("Failure in @ExceptionHandler " + 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();
      RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
    }
    return mav;
  }
}

处理流程和RequestMappingHandlerAdapter很类似,主要分为以下几步

  1. 首先获取一个注释了@ExceptionHandler的可执行的方法。这里需要注意的是,这里只获取一个,规则是优先获取本类定义的方法,如果没有找到,就去找全局的异常处理,全局也没有,就返回Null

      ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    
  2. 设置参数解析器和返回值解析器

  3. 包装request,response为ServletWebRequest

  4. 如果有异常发生,执行该方法并处理返回值

  5. 如果请求已经完成了(isRequestHandled),直接返回一个空的ModelAndView

  6. 如果请求还未完成,即有视图需要渲染,就设置viewName,view,重定向请求还会设置FlashMap

三. 使用技巧

3.1 自定义全局异常处理

我们可以在@ControllerAdvice中定义@ExceptionHandler注释的方法,来实现全局的异常处理

@ControllerAdvice
@Slf4j
public class MyExceptionHandler {

  @ExceptionHandler(Exception.class)
  @ResponseBody
  public ResponseResult handleException(Exception ex, HttpServletRequest request) throws Exception {
    ResponseResult result = new ResponseResult();
    if (ex instanceof ServiceException) {
      ServiceException e = (ServiceException) ex;
      result.setCode(e.getCode());
      result.setMsg(e.getMessage());
    }else {
      result.setCode(MessageCode.SYSTEM_INTERNAL_ERROR.getCode());
      result.setMsg(MessageCode.SYSTEM_INTERNAL_ERROR.getMessage());
    }
    log.error("system error:", ex);
    return result;
  }
}

需要注意的是,这种只能处理业务逻辑执行的异常,处理不了渲染视图抛出的异常。因为在springmvc中,这两部分的异常是分开处理的。对于前者,采用ExceptionResolver,对于后者则是抛出去交给tomcat。

tomcat对于异常,会转发/error请求,在springboot中定义了对于/error的controller,并且自动配置了处理这种异常的类

3.2 Whitelabel Error Page

通过前面的分析,可以看到对于渲染视图过程中抛出的异常(比如找不到对应的viewName),springmvc是不处理的,那么下面看看/error请求如何处理

3.2.1 BasicErrorController

这是spring为我们提供的一个处理异常的Controller

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {}

可以看到映射路径为/error

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

可以看到,对于异常请求,设置了一些model之后,通过resolveErrorView来获取ModelAndView,如果没有获取到,则返回一个默认的error。这里默认情况下是null,则返回error

3.2.2 BeanNameViewResolver

上面的Controller处理完成之后,来到了视图解析器。在默认情况下,BeanNameViewResolver是排在第一位的视图解析器。

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
  ApplicationContext context = obtainApplicationContext();
  if (!context.containsBean(viewName)) {
    // Allow for ViewResolver chaining...
    return null;
  }
  if (!context.isTypeMatch(viewName, View.class)) {
    if (logger.isDebugEnabled()) {
      logger.debug("Found bean named '" + viewName + "' but it does not implement View");
    }
    // Since we're looking into the general ApplicationContext here,
    // let's accept this as a non-match and allow for chaining as well...
    return null;
  }
  return context.getBean(viewName, View.class);
}

这里的逻辑是,直接从容器取viewName命名的bean,如果能取到,则使用该View,上面我们返回的viewName是error,那么这里可以取到吗,我们下面看看springboot是否给我们注入了叫做error的bean

3.2.3 WhitelabelErrorViewConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

  private final StaticView defaultErrorView = new StaticView();

  @Bean(name = "error")
  @ConditionalOnMissingBean(name = "error")
  public View defaultErrorView() {
    return this.defaultErrorView;
  }
}

在这个自动注入类中,注入了名为error的bean,正好是我们需要的!这是一个StaticView类型的类

private static class StaticView implements View {

		private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);

		private static final Log logger = LogFactory.getLog(StaticView.class);

		@Override
		public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
				throws Exception {
			if (response.isCommitted()) {
				String message = getMessage(model);
				logger.error(message);
				return;
			}
			response.setContentType(TEXT_HTML_UTF8.toString());
			StringBuilder builder = new StringBuilder();
			Date timestamp = (Date) model.get("timestamp");
			Object message = model.get("message");
			Object trace = model.get("trace");
			if (response.getContentType() == null) {
				response.setContentType(getContentType());
			}
			builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
					"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
					.append("<div id='created'>").append(timestamp).append("</div>")
					.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
					.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
			if (message != null) {
				builder.append("<div>").append(htmlEscape(message)).append("</div>");
			}
			if (trace != null) {
				builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
			}
			builder.append("</body></html>");
			response.getWriter().append(builder.toString());
		}
	}

从这个类的render方法中,我们找到了熟悉的Whitelabel Error Page,这也就是这个页面生成的原理。

3.3 自定义页面异常

上面分析了springboot自动生成的Whitelabel Error Page,那么我们如何去更改这个配置呢,有如下的几种方法

3.3.1 覆盖error

@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
  return this.defaultErrorView;
}

这个注入上面有一个注解ConditionalOnMissingBean(name = "error"),因此我们可以自己定义一个叫做error的bean,实现覆盖。

public class CustomView implements View {

  @Override
  public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    response.setContentType("text/html;charset=UTF-8");
    Object trace = model.get("trace");
    StringBuilder sb = new StringBuilder();
    sb.append("<div>出错啦!!!</div>");
    if (trace != null) {
      sb.append("<div>").append(trace.toString()).append("</div>");
    }
    response.getWriter().append(sb.toString());
  }
}
@Bean("error")
public CustomView error() {
  return new CustomView();
}

这样当发生错误时,就能看到我们自定义的view了

image-20210618171015535

3.3.2 设置error.ftl

如果使用了Freemarker渲染页面,可以写一个默认的error.ftl,同时关闭Whitelabel Error Page

server.error.whitelabel.enabled=false
<div>出错啦!!!!</div>
<div>${trace}</div>

3.3.3 自定义错误属性

在前面的ErrorController中有这样一行代码

Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));

getErrorAtttributes用来给错误页面的model里面设置属性

默认情况下生效的是DefaultErrorAttributes

@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
  Map<String, Object> errorAttributes = new LinkedHashMap<>();
  errorAttributes.put("timestamp", new Date());
  addStatus(errorAttributes, webRequest);
  addErrorDetails(errorAttributes, webRequest, includeStackTrace);
  addPath(errorAttributes, webRequest);
  return errorAttributes;
}

ErrorMvcAutoConfiguration中,自动注入了这个类

@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
  return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}

同样的,我们可以覆盖这个类,实现自定义的错误属性注入。这样在页面上就可以使用这些自定义的属性了

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        map.put("author", "M˚Haonan");
        map.put("ext", webRequest.getAttribute("ext", 0));
        return map;
    }
}

image-20210618174948871

3.3.4 设置ErrorViewResolver

ModelAndView modelAndView = resolveErrorView(request, response, status, model);这段代码是这种方法生效的关键。

/error请求中,首先是使用ErrorViewResolver来解析错误。在springmvc中,默认的为DefaultErrorViewResolver

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
  ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
  if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
  }
  return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
  String errorViewName = "error/" + viewName;
  TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
                                                                                         this.applicationContext);
  if (provider != null) {
    return new ModelAndView(errorViewName, model);
  }
  return resolveResource(errorViewName, model);
}

将状态码作为viewName,尝试获取ModelAndView。即首先viewName = "error/500"(如果是500错误)

先使用TemplateAvailabilityProvider去寻找,如果是Freemaker就会使用freemaker配置的。也就是说会找error/500.ftl作为错误页面。因此我们可以放一个error/500.ftl作为错误页面

如果没有找到,那么会去寻找静态资源

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
		for (String location : this.resourceProperties.getStaticLocations()) {
			try {
				Resource resource = this.applicationContext.getResource(location);
				resource = resource.createRelative(viewName + ".html");
				if (resource.exists()) {
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			}
			catch (Exception ex) {
			}
		}
		return null;
	}

即寻找"classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/"目录下有没有500.html文件。如果有作为view返回

如果状态码没有找到,就使用5xx或者4xx去寻找。

通过上面的分析,我们可以知道使用如下的方法可以设置错误页面

  1. 放置/error/500.ftl或者/error/5xx.ftl(4xx同理)
  2. 在静态资源目录下放置500.html(其他同理)

3.4 全局的ExceptionHandler和本类的区别

众所周知,全局的ExceptionHandler和本类最大的区别就是前者可以处理所有Controller的异常,而后者只能处理本类的异常。

除此之外,第二个区别是 会优先查找本类是否有ExceptionHandler,如果找到了就不会用全局的。

这个特性也导致了下面一个不容易被发现的区别

对于MaxUploadSizeExceededException这个异常,只有全局的才能处理,而本类的无法处理。这是为什么呢?

  1. 这个异常是上传文件的异常,发生的时机在根据request查找Handler之前。也就是说,当发生这个异常的时候,handler还未找到,为null
  2. 上面对于这个组件的分析中可以看到,对于本类的异常,如果handler为null,那么本类上的ExceptionHandler就找不到(这个显而易见,handler都不知道,哪知道是哪个类呢?)
  3. 而对于全局的异常来说。就不关心类是谁了,总能找到

所以,归根到底,还是由于这个异常发生的时机在寻找handler之前,导致handler为null

四. 总结

​ 异常处理对我们平常的开发有很大的帮助。springmvc在异常处理上分为了两部分,一部分是业务逻辑的异常,这部分统一用ExceptionHandlerExceptionResolver去处理。另一部分是渲染视图的异常,这部分则提供了默认的StaticView去渲染错误页面。

​ 我们日常开发中使用最多的是业务逻辑的异常。springmvc对于这部分异常的处理则是采用了Adapter类似的逻辑。

  1. 寻找全局的@ExceptionHandler
  2. 从本类和全局中找到一个可执行的方法,优先本类
  3. 执行方法,如果有@ResponseBody注解,则直接返回json数据异常信息
  4. 如果需要渲染异常视图,则走render渲染

整体的流程还是很清晰的!


标题:07-HandlerExceptionResolver
作者:mahaonan
地址:https://mahaonan.fun/articles/2022/07/18/1658147015931.html