乐趣区

关于spring-mvc:SpringMVC-异常处理体系深入分析

@[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;}
}
  1. 首先调用 shouldApplyTo 办法判断以后解析器是否能够解决传入的处理器所抛出的异样,如果不反对,则间接返回 null,这个异样将交给下一个 HandlerExceptionResolver 去解决。
  2. 调用 prepareResponse 办法解决 response。
  3. 调用 doResolveException 办法理论解决异样,这是一个模版办法,具体的实现在子类中。
  4. 调用 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;
    }
}

这个办法尽管比拟长,然而很好了解:

  1. 首先查找到带有 @ExceptionHandler 注解的办法,封装成一个 ServletInvocableHandlerMethod 对象(对于 ServletInvocableHandlerMethod 对象,松哥在之前的文章中曾经介绍过了,具体参见:Spring Boot 定义接口的办法是否能够申明为 private?)。
  2. 如果找到了对应的办法,则为 exceptionHandlerMethod 配置参数解析器、视图解析器等,对于这些解析器,参考松哥之前的文章:SpringBoot 中如何自定义参数解析器?、深入分析 SpringMVC 参数解析器、Spring Boot 中如何对立 API 接口响应格局?。
  3. 接下来定义一个 exceptions 数组,如果产生的异样存在异样链,则将整个异样链存入 exceptions 数组中。
  4. exceptions 数组再加上 handlerMethod,独特组成办法参数,调用 exceptionHandlerMethod.invokeAndHandle 实现自定义异样办法的执行,执行后果被保留再 mavContainer 中。
  5. 如果申请到此结束,则间接结构一个 ModelAndView 返回。
  6. 否则从 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;}
}
  1. 首先调用 determineViewName 办法确定视图的名称。
  2. 接下来调用 determineStatusCode 查看视图是否有对应的 statusCode。
  3. 调用 applyStatusCodeIfPossible 办法将 statusCode 设置到 response 上,这个办法很简略,不多说。
  4. 调用 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;
}
  1. 如果以后异样蕴含在 excludedExceptions 中,则间接返回 null(意思是以后异样被疏忽解决了,间接依照默认形式来)。
  2. 如果 exceptionMappings 不为 null,则间接调用 findMatchingViewName 办法查找异样对应的视图名(exceptionMappings 变量就是后面咱们配置的映射关系),具体的查找形式就是遍历咱们后面配置的映射表。
  3. 如果没找到对应的 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 中的异样解决体系,整体来说并不难,小伙伴们能够认真品一品。

退出移动版