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;
}
}
-
首先通过
shouldApplyTo()
判断当前ExceptionResolver是否能处理传入handler所抛出的异常。 -
如果不能处理,返回null,交给下一个ExceptionResolver
-
如果可以,调用
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很类似,主要分为以下几步
-
首先获取一个注释了@ExceptionHandler的可执行的方法。这里需要注意的是,这里只获取一个,规则是优先获取本类定义的方法,如果没有找到,就去找全局的异常处理,全局也没有,就返回Null
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
-
设置参数解析器和返回值解析器
-
包装request,response为ServletWebRequest
-
如果有异常发生,执行该方法并处理返回值
-
如果请求已经完成了(isRequestHandled),直接返回一个空的ModelAndView
-
如果请求还未完成,即有视图需要渲染,就设置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了
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;
}
}
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
去寻找。
通过上面的分析,我们可以知道使用如下的方法可以设置错误页面
- 放置
/error/500.ftl
或者/error/5xx.ftl
(4xx同理) - 在静态资源目录下放置
500.html
(其他同理)
四. 总结
异常处理对我们平常的开发有很大的帮助。springmvc在异常处理上分为了两部分,一部分是业务逻辑的异常,这部分统一用ExceptionHandlerExceptionResolver去处理。另一部分是渲染视图的异常,这部分则提供了默认的StaticView去渲染错误页面。
我们日常开发中使用最多的是业务逻辑的异常。springmvc对于这部分异常的处理则是采用了Adapter类似的逻辑。
- 寻找全局的@ExceptionHandler
- 从本类和全局中找到一个可执行的方法,优先本类
- 执行方法,如果有@ResponseBody注解,则直接返回json数据异常信息
- 如果需要渲染异常视图,则走render渲染
整体的流程还是很清晰的!