@[toc]
SpringMVC 中针对异样问题有一套残缺的解决体系,这套体系十分好用,明天松哥就花点工夫来和大家聊一聊 SpringMVC 中的异样解决体系,咱们把 SpringMVC 中的异样体系从头到尾梳理一下。
1. 异样解析器概览
在 SpringMVC 的异样体系中,处于最顶层的大 Boss 是 HandlerExceptionResolver,这是一个接口,里边只有一个办法:
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
resolveException 办法就用来解析申请处理过程中所产生的异样,并最终返回一个 ModelAndView。
咱们来看下 HandlerExceptionResolver 的实现类:
间接实现 HandlerExceptionResolver 接口的类有三个:
- HandlerExceptionResolverComposite:这个一看又是一个组合,在最近的源码剖析中咱们曾经屡次见到 xxxComposite 了,这里就不再赘述。
- DefaultErrorAttributes:这个用来保留异样属性。
-
AbstractHandlerExceptionResolver:这个的子类比拟多:
- SimpleMappingExceptionResolver:通过提前配置好的异样类和 View 之间的对应关系来解析异样。- AbstractHandlerMethodExceptionResolver:解决应用 `@ExceptionHandler` 注解自定义的异样类型。- DefaultHandlerExceptionResolver:依照不同类型来解决异样。- ResponseStatusExceptionResolver:解决含有 `@ResponseStatus` 注解的异样。
在 SpringMVC 中,大抵的异样解析器就是这些,接下来咱们来一一学习这些异样解析器。
2.AbstractHandlerExceptionResolver
AbstractHandlerExceptionResolver 是真正干活的异样解析器的父类,咱们就先从他的 resolveException 办法开始看起。
@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) {logException(ex, request);
}
return result;
}
else {return null;}
}
- 首先调用 shouldApplyTo 办法判断以后解析器是否能够解决传入的处理器所抛出的异样,如果不反对,则间接返回 null,这个异样将交给下一个 HandlerExceptionResolver 去解决。
- 调用 prepareResponse 办法解决 response。
- 调用 doResolveException 办法理论解决异样,这是一个模版办法,具体的实现在子类中。
- 调用 logException 办法记录异样日志信息。
记录异样日志没啥好说的,doResolveException 则是一个空的模版办法,所以这里对咱们来说次要就是两个办法:shouldApplyTo 和 prepareResponse,咱们别离来看。
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;}
}
}
}
return !hasHandlerMappings();}
这里波及到了两个对象:mappedHandlers 和 mappedHandlerClasses:
- mappedHandlers:存储的是处理器对象(Controller 或者 Controller 中的办法)
- mappedHandlerClasses:存储的是处理器的 Class。
咱们在配置异样解析器的时候能够配置这两个对象,进而实现该异样处理器只为某一个处理器服务,然而一般来说没这种需要,所以大家仅做理解即可。
如果开发者一开始配置了 mappedHandlers 或者 mappedHandlerClasses,则用这两个和处理器去比拟,否则就间接返回 true,示意反对该异样解决。
prepareResponse
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");
}
这是 AbstractHandlerExceptionResolver 的大抵内容,能够看到还是十分 easy 的,接下来咱们来看它的实现类。
2.1 AbstractHandlerMethodExceptionResolver
AbstractHandlerMethodExceptionResolver 次要是重写了 shouldApplyTo 办法和 doResolveException 办法,一个一个来看。
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 if (hasGlobalExceptionHandlers() && hasHandlerMappings()) {return super.shouldApplyTo(request, handler);
}
else {return false;}
}
这块感觉没啥好说的,判断逻辑基本上都还是调用父类的 shouldApplyTo 办法去解决。
doResolveException
@Override
@Nullable
protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {HandlerMethod handlerMethod = (handler instanceof HandlerMethod ? (HandlerMethod) handler : null);
return doResolveHandlerMethodException(request, response, handlerMethod, ex);
}
@Nullable
protected abstract ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception ex);
doResolveException 是具体的异样解决办法,然而它里边却没有实质性操作,具体的事件交给 doResolveHandlerMethodException 办法去做了,而该办法是一个形象办法,具体的实现在子类中。
2.1.1 ExceptionHandlerExceptionResolver
AbstractHandlerMethodExceptionResolver 只有一个子类就是 ExceptionHandlerExceptionResolver,来看下它的 doResolveHandlerMethodException 办法:
@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();
ArrayList<Throwable> exceptions = new ArrayList<>();
try {if (logger.isDebugEnabled()) {logger.debug("Using @ExceptionHandler" + exceptionHandlerMethod);
}
// Expose causes as provided arguments as well
Throwable exToExpose = exception;
while (exToExpose != null) {exceptions.add(exToExpose);
Throwable cause = exToExpose.getCause();
exToExpose = (cause != exToExpose ? cause : null);
}
Object[] arguments = new Object[exceptions.size() + 1];
exceptions.toArray(arguments); // efficient arraycopy call in ArrayList
arguments[arguments.length - 1] = handlerMethod;
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
}
catch (Throwable invocationEx) {// Any other than the original exception (or a cause) is unintended here,
// probably an accident (e.g. failed assertion or the like).
if (!exceptions.contains(invocationEx) && 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;
}
}
这个办法尽管比拟长,然而很好了解:
- 首先查找到带有
@ExceptionHandler
注解的办法,封装成一个 ServletInvocableHandlerMethod 对象(对于 ServletInvocableHandlerMethod 对象,松哥在之前的文章中曾经介绍过了,具体参见:Spring Boot 定义接口的办法是否能够申明为 private?)。 - 如果找到了对应的办法,则为 exceptionHandlerMethod 配置参数解析器、视图解析器等,对于这些解析器,参考松哥之前的文章:SpringBoot 中如何自定义参数解析器?、深入分析 SpringMVC 参数解析器、Spring Boot 中如何对立 API 接口响应格局?。
- 接下来定义一个 exceptions 数组,如果产生的异样存在异样链,则将整个异样链存入 exceptions 数组中。
- exceptions 数组再加上 handlerMethod,独特组成办法参数,调用
exceptionHandlerMethod.invokeAndHandle
实现自定义异样办法的执行,执行后果被保留再 mavContainer 中。 - 如果申请到此结束,则间接结构一个 ModelAndView 返回。
- 否则从 mavContainer 中取出各项信息,构建新的 ModelAndView 返回。同时,如果存在重定向参数,也将之保留下来(对于重定向参数,参见:SpringMVC 中的参数还能这么传递?涨姿态了!)。
这就是 ExceptionHandlerExceptionResolver 的大抵工作流程,能够看到,还是十分 easy 的。
2.2 DefaultHandlerExceptionResolver
这个看名字就晓得是一个默认的异样处理器,用来解决一些常见的异样类型,咱们来看一下它的 doResolveException 办法:
@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);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response, handler);
}
else if (ex instanceof MissingPathVariableException) {
return handleMissingPathVariable((MissingPathVariableException) 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);
}
else if (ex instanceof AsyncRequestTimeoutException) {
return handleAsyncRequestTimeoutException((AsyncRequestTimeoutException) ex, request, response, handler);
}
}
catch (Exception handlerEx) { }
return null;
}
能够看到,这里实际上就是依据不同的异样类型,而后调用不同的类去解决该异样。这里相干的解决都比拟容易,以 HttpRequestMethodNotSupportedException 为例,异样解决就是对 response 对象做一些配置,如下:
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, ","));
}
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, ex.getMessage());
return new ModelAndView();}
配置响应头,而后 sendError,最初返回一个空的 ModelAndView 对象。
其实这里哥哥异样解决办法都大同小异,松哥就不再赘述啦。
2.3 ResponseStatusExceptionResolver
这个用来解决 ResponseStatusException 类型的异样,或者应用了 @ResponseStatus
注解标记的一般异样类。咱们来看下它的 doResolveException 办法:
@Override
@Nullable
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) { }
return null;
}
能够看到,首先判断异样类型是不是 ResponseStatusException,如果是,则间接调用 resolveResponseStatusException 办法进行异样信息处理,如果不是,则去查找到异样类上的 @ResponseStatus
注解,并从中查找出相干的异样信息,而后调用 resolveResponseStatus 办法进行解决。
能够看到,ResponseStatusExceptionResolver 解决的异样类型有两种:
- 间接继承自 ResponseStatusException 的异样类,这种异样类能够间接从里边提取进去想要的信息。
- 通过
@ResponseStatus
注解的一般异样类,这种状况下异样信息从@ResponseStatus
注解中提取进去。
这个比较简单,没啥好说的。
2.4 SimpleMappingExceptionResolver
SimpleMappingExceptionResolver 则是依据不同的异样显示不同的 error 页面。可能有的小伙伴还没用过 SimpleMappingExceptionResolver,所以松哥这里先简略说一下用法。
SimpleMappingExceptionResolver 的配置非常简单,间接提供一个 SimpleMappingExceptionResolver 的实例即可,如下:
@Bean
SimpleMappingExceptionResolver simpleMappingExceptionResolver() {SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.put("java.lang.ArithmeticException", "11");
mappings.put("java.lang.NullPointerException", "22");
resolver.setExceptionMappings(mappings);
Properties statusCodes = new Properties();
statusCodes.put("11", "500");
statusCodes.put("22", "500");
resolver.setStatusCodes(statusCodes);
return resolver;
}
在 mappings 中配置异样和 view 之间的对应关系,要写异样类的全门路,前面的 11、22 则示意视图名称;statusCodes 中配置了视图和响应状态码之间的映射关系。配置实现后,如果咱们的我的项目在运行时抛出了 ArithmeticException 异样,则会展现出 11 视图,如果咱们的我的项目在运行时抛出了 NullPointerException 异样,则会展现出 22 视图。
这是用法,理解了用法之后咱们再来看源码,就容易了解了,咱们间接来看 doResolveException 办法:
@Override
@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {String viewName = determineViewName(ex, request);
if (viewName != null) {Integer statusCode = determineStatusCode(request, viewName);
if (statusCode != null) {applyStatusCodeIfPossible(request, response, statusCode);
}
return getModelAndView(viewName, ex, request);
}
else {return null;}
}
- 首先调用 determineViewName 办法确定视图的名称。
- 接下来调用 determineStatusCode 查看视图是否有对应的 statusCode。
- 调用 applyStatusCodeIfPossible 办法将 statusCode 设置到 response 上,这个办法很简略,不多说。
- 调用 getModelAndView 办法结构一个 ModelAndView 对象返回,在结构时,同时设置异样参数,异样的信息的 key 默认就是 exception。
在下面这个过程中,有两个比拟长的办法,松哥这里须要和大家额定多说两句。
determineViewName
这个就是依据异样类型找到视图名,咱们来看下具体的查找形式:
@Nullable
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;}
}
}
if (this.exceptionMappings != null) {viewName = findMatchingViewName(this.exceptionMappings, ex);
}
if (viewName == null && this.defaultErrorView != null) {viewName = this.defaultErrorView;}
return viewName;
}
- 如果以后异样蕴含在 excludedExceptions 中,则间接返回 null(意思是以后异样被疏忽解决了,间接依照默认形式来)。
- 如果 exceptionMappings 不为 null,则间接调用 findMatchingViewName 办法查找异样对应的视图名(exceptionMappings 变量就是后面咱们配置的映射关系),具体的查找形式就是遍历咱们后面配置的映射表。
- 如果没找到对应的 viewName,并且用户配置了 defaultErrorView,则将 defaultErrorView 赋值给 viewName,并将 viewName 返回。
determineStatusCode
@Nullable
protected Integer determineStatusCode(HttpServletRequest request, String viewName) {if (this.statusCodes.containsKey(viewName)) {return this.statusCodes.get(viewName);
}
return this.defaultStatusCode;
}
这个就比拟容易,间接去 statusCodes 中查看是否有视图对应的状态码,如果有则间接返回,如果没有,就返回一个默认的。
3.HandlerExceptionResolverComposite
最初,还有一个 HandlerExceptionResolverComposite 须要和大家介绍下,这是一个组合的异样处理器,用来代理哪些真正干活的异样处理器。
@Override
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {if (this.resolvers != null) {for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {return mav;}
}
}
return null;
}
它的 resolveException 办法就比较简单了,这种写法咱们曾经见到过很屡次了,不再赘述。
4. 小结
好啦,明天就和大家简略聊一聊 SpringMVC 中的异样解决体系,整体来说并不难,小伙伴们能够认真品一品。