第七篇:控制器方法异常处理

第七篇:控制器方法异常处理

集中式的异常处理

得益于所有的请求都在一个 servlet 也就是 DispatcherServlet 中处理,所以全局的异常处理得以容易地做到,下面我们来简单看一下如何在 DispatcherServlet 中进行集中的异常处理

DispatcherServlet 中的源码分析

DispatcherServletdoDispatch 方法中,存在两个嵌套的 try-catch 代码块,内部的代码块包含了主要逻辑,比如获取拦截器,调用控制器方法获取视图等方法等,在这个的 try-catch 外,会调用 processDispatchResult 来处理控制器调用的结果,因为此方法在 try-catch 代码块的外面,因此即使控制器调用过程中报错了,依然会执行,现在我们来看看 processDispatchResult 方法,

其中有一点需要注意,关于错误的处理,我们需要先判断错误对象的类型,

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {

    boolean errorView = false;

    // 判断是否报错,如果报错,则需要处理爆出的异常 
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            // 如果是 ModelAndViewDefiningException 类型的错误,表示此时应该转发到具有特定模型的特定视图。此异常可以在控制器方法处理请求期间的任何时候抛出。 
            logger.debug("ModelAndViewDefiningException encountered", exception);
            // 直接从 exception 对象中获取 ModelAndView 进行返回,看来在抛出异常的时候,就应该将ModelAndView填充进去。
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            // 使用场景如下,一个表单有很多字段,我们希望不同的字段错误,跳转到不同的视图展示错误信息,就可以通过抛出 ModelAndViewDefiningException 达到目标
        }
        else {
            // 如果不是ModelAndViewDefiningException类型的错误,就需要调用 processHandlerException 进一步的处理,以得到ModelAndView
            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()) {
        // 不管结果ModelAndView来自于控制器方法直接返回,还是从exception中解析得出,都需要进行渲染
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("No view rendering, null ModelAndView returned.");
        }
    }

    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        // Concurrent handling started during a forward
        return;
    }

    // 执行控制器方法的所有的拦截器的  afterCompletion 方法,详情请看《第二篇》中的HandlerInterceptor相关的章节
    if (mappedHandler != null) {
        // Exception (if any) is already handled..
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

我们接着看 processHandlerException 方法,这个方法首先依次调用所有的注册了的 HandlerExceptionResolver ,对错误进行解析,然后返回 ModelAndView,

注意,排在前面的 HandlerExceptionResolver 解析成功了的话,会直接返回,所以 HandlerExceptionResolver 的注册顺序很重要

这跟之前的查看过的视图解析过程,内容协商过程的处理模式一模一样

如果 HandlerExceptionResolver 解析出来的 ModelAndView,没有视图(为 "" 都会正常解析,为 null 才表示没有视图),那么我们就会尝试从请求路径中解析出默认的一个默认的视图名。

getDefaultViewName 方法默认调用 RequestToViewNameTranslator 的 getViewName 方法根据请求返回一个默认的视图名称

RequestToViewNameTranslator 的默认实现是 DefaultRequestToViewNameTranslator ,会直接把请求的 URI 去掉应用上下文部分,去掉请求的拓展后缀,返回中间那一段,例如

http://localhost:8080/gamecast/display.html » display

http://localhost:8080/gamecast/displayShoppingCart.html » displayShoppingCart

http://localhost:8080/gamecast/admin/index.html » admin/index

关于 getDefaultViewName 方法的更多细节,请看《SpringMVC- 第五篇:视图》

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {

    // Success and error responses may use different content types
    request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

    // Check registered HandlerExceptionResolvers...
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        // 依次调用所有的注册了的 HandlerExceptionResolver ,对错误进行解析,然后返回 ModelAndView
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            // HandlerExceptionResolver 是一个 函数式接口,只有一个方法 resolveException,返回的就是 ModelAndView
            exMv = resolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                // 解析器返回的 ModelAndView 为 null 表示解析不成功,但凡返回不为null的 ModelAndView 即使是没有内容的 ModelAndView 对象,也表示解析成功
                // 注意,排在前面的 HandlerExceptionResolver 解析成功了的话,会直接返回,所以 HandlerExceptionResolver 的注册顺序很重要。
                // 这跟之前的查看过的视图解析、内容协商策略的处理模式一模一样
                break;
            }
        }
    }
    if (exMv != null) {
        if (exMv.isEmpty()) {
            request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
            return null;
        }
        // We might still need view name translation for a plain error model...
        if (!exMv.hasView()) {
            // 如果 HandlerExceptionResolver 解析出来的 ModelAndView,没有视图,那么我们就会尝试从请求路径中解析出默认的一个默认的视图名
            // getDefaultViewName方法默认调用 RequestToViewNameTranslator 的getViewName方法根据请求返回一个默认的视图名称
            // RequestToViewNameTranslator的默认实现是 DefaultRequestToViewNameTranslator ,会直接把请求的URI去掉应用上下文部分,去掉请求的拓展后缀,返回中间那一段,例如
            // http://localhost:8080/gamecast/display.html » display
            // http://localhost:8080/gamecast/displayShoppingCart.html » displayShoppingCart
            // http://localhost:8080/gamecast/admin/index.html » admin/index
            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);
        }
        WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
        return exMv;
    }

    throw ex;
}

默认的 HandlerExceptionResolver

通过测试,可以知道系统中默认注册的就有三个 HandlerExceptionResolver

默认就有三个 HandlerExceptionResolver 的 bean 被注册,按顺序是

  1. ExceptionHandlerExceptionResolver

  2. ResponseStatusExceptionResolver

  3. DefaultHandlerExceptionResolver,默认的一般都作为最终兜底的放在最后面,比如处理静态资源的 org.apache.catalina.servlets.DefaultServlet

初始化过程

初始化的具体过程,请看 DispatcherServlet#initStrategies 中调用的 DispatcherServlet#initHandlerExceptionResolvers

如何通过 XML 配置具体的 HandlerExceptionResolver 实例,在《SpringMVC- 第九篇:基于 XML 配置 SpringMVC》的 AnnotationDrivenBeanDefinitionParser 小节中的 HandlerExceptionResolver 小节中有详细描述

如何通过 Java 配置具体的 HandlerExceptionResolver 实例,请看 WebMvcConfigurationSupport#handlerExceptionResolver 方法,文章请看《SpringMVC- 第十篇:基于注解配置 SpringMVC.md》的 configureHandlerExceptionResolvers & extendHandlerExceptionResolvers 小节

HandlerExceptionResolver 及其实现类的源码分析

HandlerExceptionResolver接口是一个函数式接口,只有一个 resolveException 方法,功能也很简单,传入 HttpServletRequestHttpServletResponse 对象,还有控制器方法(或者控制器方法执行链)对象,还有异常对象,输出一个 ModelAndView 视图,用于展示异常的详细信息。

AbstractHandlerExceptionResolver实现了 HandlerExceptionResolver 接口,是一个抽象类,主要功能是指定了当前 HandlerExceptionResolver 实现类应用的控制器方法的范围(指定 mappedHandlers 或者 mappedHandlerClasses,当前 HandlerExceptionResolver 实现类只会处理这个范围内的控制器方法的异常,如果没有指定这个范围,则默认还是应用到所有的控制器方法。范围判定的具体实现方法是 shouldApplyTo 方法。

AbstractHandlerMethodExceptionResolver继承了 AbstractHandlerExceptionResolver,添加了对 HandlerMethod 类型的 handler 的支持,具体体现在对 shouldApplyTo 方法的重写。同时将 doResolveException 方法委托给 doResolveHandlerMethodException 方法,需要子类重写

HandlerMethod 其实就是对控制器方法的信息的封装,包含方法名和所在控制器的信息。

大部分情况下,我们处理的 handler 都是 HandlerMethod 类型,我们一般也都会直接把 handle 对象称为控制器方法

ExceptionHandlerExceptionResolver - 首要的 HandlerExceptionResolver

ExceptionHandlerExceptionResolver继承 AbstractHandlerMethodExceptionResolver,通过@ExceptionHandler 注解修饰的方法解析错误,同时支持在调用@ExceptionHandler 注解修饰的方法时添加自定义的参数解析器和返回值解析器

我们先看初始化方法

ExceptionHandlerExceptionResolver 实现了 InitializingBean 接口,在 afterPropertiesSet 方法中进行初始化

public void afterPropertiesSet() {
    // Do this first, it may add ResponseBodyAdvice beans
    // 对IOC容器中所有的@ControllerAdvice修饰的类进行封装,封装成 ExceptionHandlerMethodResolver
    // 如果其中包含@ExceptionHandler修饰的方法,则将ExceptionHandlerMethodResolver添加到 exceptionHandlerAdviceCache 缓存中
    // 如果 @ControllerAdvice修饰的类还同时实现了ResponseBodyAdvice接口,就放到 responseBodyAdvice 缓存中
    // 初始化  exceptionHandlerAdviceCache 缓存和 responseBodyAdvice 缓存
    initExceptionHandlerAdviceCache();

    // 初始化异常处理方法的 默认的参数解析器和返回值解析器,通过添加这些解析器,我们就可以使用在 @RequestMapping注解修饰的方法中可以使用的那些参数 
    // 返回值也是类似的
    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);
    }
}

initExceptionHandlerAdviceCache 方法很简单,查找 IOC 容器中所有的@ControllerAdvic,如果其中包含@ExceptionHandler 修饰的方法,则将 ExceptionHandlerMethodResolver 添加到 exceptionHandlerAdviceCache 缓存中,留到 getExceptionHandlerMethod 方法中使用

private void initExceptionHandlerAdviceCache() {
    if (getApplicationContext() == null) {
        return;
    }

    // 查找IOC容器中所有的@ControllerAdvice修饰的类
    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
        ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
        if (resolver.hasExceptionMappings()) {
            // 如果其中包含@ExceptionHandler修饰的方法,则将ExceptionHandlerMethodResolver添加到 exceptionHandlerAdviceCache 缓存中
            this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
        }
        if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
            // 如果 @ControllerAdvice修饰的类同时还实现了 ResponseBodyAdvice 接口,就放到 responseBodyAdvice 缓存中
            this.responseBodyAdvice.add(adviceBean);
        }
    }

    ......

}

这个过程跟在 RequestMappingHandlerAdapter 中的 initControllerAdviceCache 方法缓存 @ControllerAdvice 类中的 @ModelAttribute 方法 和@InitBinder 方法到 modelAttributeAdviceCacheinitBinderAdviceCache 中很像,具体请查看《SpringMVC- 第八篇:跨控制器间共享》

getDefaultArgumentResolvers 方法也很简单,就是添加对各种类型的参数的支持,跟 @RequestMapping 注解修饰的方法可以添加各种类型参数的原理是一样的。

protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

    // Annotation-based argument resolution
    // 添加对@RequestAttribute参数的支持
    resolvers.add(new SessionAttributeMethodArgumentResolver());
    // 添加对@SessionAttribute参数的支持
    resolvers.add(new RequestAttributeMethodArgumentResolver());

    // Type-based argument resolution
    // 添加对HttpServletRequest类型的参数的支持
    resolvers.add(new ServletRequestMethodArgumentResolver());
    // 添加对HttpServletResponse类型的参数的支持
    resolvers.add(new ServletResponseMethodArgumentResolver());
    resolvers.add(new RedirectAttributesMethodArgumentResolver());
    // 添加对Model类型的参数的支持
    resolvers.add(new ModelMethodProcessor());

    // Custom arguments
    // 自定义添加对其他类型的参数的支持
    if (getCustomArgumentResolvers() != null) {
        resolvers.addAll(getCustomArgumentResolvers());
    }

    return resolvers;
}

因此,@ExceptionHandler 注解修饰的方法默认支持这些参数

@ExceptionHandler(ArithmeticException.class)
    public String handleArithmeticException( ArithmeticException ex,
                                                HandlerMethod handlerMethod,
                                                HttpServletRequest request,
                                                HttpServletResponse response,
                                                @RequestAttribute String requestName,
                                                @SessionAttribute String sessionName,
                                                Model model) {
    return "error";
}

其中,对 Exception 和 HandlerMethod 这两种参数的支持,是因为在 ExceptionHandlerExceptionResolverdoResolveHandlerMethodException 方法中通过 exceptionHandlerMethod.invokeAndHandle 调用异常处理方法方法(@ExceptionHandler 注解修饰的方法)的时候,添加了这两种类型的参数的值,所以在@ExceptionHandler 注解修饰的方法中使用这两种参数的时候,可以正常解析。

@ExceptionHandler 方法参数的 官方文档

@ExceptionHandler 方法的返回值的 官方文档

getDefaultReturnValueHandlers 方法就不解释了,差不多。

其实本质上来说,通过@ExceptionHandler 注解修饰的方法解析错误,跟@RequestMapping 注解修饰的方法处理请求,没有啥本质的区别,包括从参数和返回值的解析到其他各个方面都非常相像。

控制器方法报错之后,请求将继续由异常处理器进行处理,有点像服务器内部转发。

继续看 doResolveHandlerMethodException 方法,其实流程很简单,查找错误对应的异常处理方法,然后调用之,获取视图,然后返回,就这么简单,我们重点看 getExceptionHandlerMethod 方法,这是异常处理的关键

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

    // 找到此错误对应的异常处理方法 
    ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    if (exceptionHandlerMethod == null) {
        // 没找到异常处理方法,就表示解析失败,返回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()) {
        // 异常对应的 @ExceptionHandler方法 有 @ResponseBody 修饰
        return new ModelAndView();
    }
    else {
        // 异常对应的 @ExceptionHandler方法 没有 @ResponseBody 修饰
        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 方法有没有配置 @ResponseBody 注解的区别,在 @ResponseBody 注解的处理类 RequestResponseBodyMethodProcessorhandleReturnValue 方法中,设置了 ModelAndViewContainerrequestHandled 属性为 true,表示请求已经被完全处理完,不需要再进行视图解析。

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    // 设置 ModelAndViewContainer 的 requestHandled 属性为 true 
    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

    // Try even with null return value. ResponseBodyAdvice could get involved.
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

因此会直接返回一个空的 ModelAndView 对象,而没有配置 @ResponseBody 注解的时候,如果 @ExceptionHandler 方法的返回值为 void,根据 DispatcherServletprocessHandlerException 方法中的判断,这两种情况都会触发默认视图解析

默认视图解析请查看《SpringMVC- 第五篇:视图》

接下来看 getExceptionHandlerMethod 方法,这个方法的作用是为特定的异常查找 @ExceptionHandler 方法。首先在当前控制器或者控制器的父类中搜索处理当前异常的方法,如果没有找到,同时 IOC 容器中存在@ControllerAdvice 修饰的 bean,则继续在其中搜索可处理当前异常的@ExceptionHandler 方法。

initExceptionHandlerAdviceCache 中初始化的 exceptionHandlerAdviceCache 缓存就是用在此处

@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod( @Nullable HandlerMethod handlerMethod, Exception exception) {

    Class<?> handlerType = null;

    if (handlerMethod != null) {
        // Local exception handler methods on the controller class itself.
        // To be invoked through the proxy, even in case of an interface-based proxy.
        handlerType = handlerMethod.getBeanType();
        // 直接将当前控制器方法所在的控制器转化为 ExceptionHandlerMethodResolver ,
        ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
        if (resolver == null) {
            resolver = new ExceptionHandlerMethodResolver(handlerType);
            this.exceptionHandlerCache.put(handlerType, resolver);
        }
        // 尝试用中解析出 @ExceptionHandler 注解修饰的方法
        Method method = resolver.resolveMethod(exception);
        if (method != null) {
            // 如果当前控制器中已经有了 @ExceptionHandler 修饰的异常处理方法,且与当前错误匹配,则直接返回
            // 否则,继续查找
            return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
        }
        // For advice applicability check below (involving base packages, assignable types
        // and annotation presence), use target class instead of interface-based proxy.
        // 控制器如果进行了代理,那么就需要找到被代理类,方便后续使用
        if (Proxy.isProxyClass(handlerType)) {
            handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
        }
    }

    // 从 exceptionHandlerAdviceCache 中获取缓存的 ControllerAdviceBean
    for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
        ControllerAdviceBean advice = entry.getKey();
        // 判断当前 ControllerAdviceBean 是否能用于此控制器
        // 因为 @ControllerAdvice 可以通过 basePackageClasses basePackageClasses assignableTypes Annotation 配置适用范围
        // 不在此范围内则ControllerAdviceBean不能对其生效
        // 不配置的话就是对所有控制器生效
        // 详情请看 <第八章> 中的 @ControllerAdvice
        if (advice.isApplicableToBeanType(handlerType)) {
            ExceptionHandlerMethodResolver resolver = entry.getValue();
            // 查找@ControllerAdvice类中合适的 @ExceptionHandler 方法
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
                return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
            }
        }
    }

    return null;
}

所以异常处理器的优先级是当前控制器中的 @ExceptionHandler 方法 > @ControllerAdvice 中包含的 @ExceptionHandler 方法。

ExceptionHandlerMethodResolver

这里我们有必要详细了解一下 ExceptionHandlerMethodResolverresolveMethod 方法,正是这个方法在众多异常处理方法中,返回唯一的最终处理方法。

构造方法,在构造 ExceptionHandlerMethodResolver 的时候,就会检测传入类中的所有的 @ExceptionHandler 方法,并将 @ExceptionHandler 注解的 value 值(可以有多个),也就是匹配的异常类型和 @ExceptionHandler 修饰的方法放入一个类型为 Map<Class<? extends Throwable>, Method> 名为 mappedMethods 的 Map 集合的字段中,在 getMappedMethod 方法中会有使用。

public ExceptionHandlerMethodResolver(Class<?> handlerType) {
    for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
        for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
            addExceptionMapping(exceptionType, method);
        }
    }
}

然后就是 resolveMethod 方法的实现,直接委托给 resolveMethodByThrowable 方法

@Nullable
public Method resolveMethod(Exception exception) {
    return resolveMethodByThrowable(exception);
}

resolveMethodByThrowable 方法,调用 resolveMethodByExceptionType 先根据当前报错的异常类型进行匹配,如果没找到,再根据当前异常的根异常(也就是引发当前异常的父异常)进行递归运行,直到找到异常对应的处理方法。

@Nullable
public Method resolveMethodByThrowable(Throwable exception) {
    // 根据当前报错的异常类型进行匹配,
    Method method = resolveMethodByExceptionType(exception.getClass());
    // 没有找到,再找当前异常的根异常
    if (method == null) {
        Throwable cause = exception.getCause();
        if (cause != null) {
            // 递归
            method = resolveMethodByThrowable(cause);
        }
    }
    return method;
}

resolveMethodByExceptionType 方法也没有直接查找,而是经过了一遍缓存,最终调用 getMappedMethod 方法

@Nullable
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
    // 在通过当前异常类型进行查找的时候,
    // 首先查缓存,缓存里有直接返回
    Method method = this.exceptionLookupCache.get(exceptionType);
    if (method == null) {
        // 缓存里没有,就得手动查找了
        method = getMappedMethod(exceptionType);
        this.exceptionLookupCache.put(exceptionType, method);
    }
    return method;
}

getMappedMethod 方法中,如果实际报错的类型,是@ExceptionHandler 注解声明的异常类型的子类,则这个@ExceptionHandler 修饰的方法可以处理当前异常,编译一遍之后,把所有符合条件的@ExceptionHandler 注解声明的异常类型都聚起来,然后根据继承路径最短原则进行排序,返回继承路径最短的异常类型对应的@ExceptionHandler 方法(即异常处理方法)。

这里用到了构造函数中构建的缓存 mappedMethods

private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
    List<Class<? extends Throwable>> matches = new ArrayList<>();
    for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
        // 如果实际报错的类型,是@ExceptionHandler注解声明的异常类型的子类,则这个@ExceptionHandler修饰的方法可以处理当前异常
        // 这里只需要记录符合条件的异常类型即可,因为我们已经有了mappedMethods,记录这异常类型和实际异常处理方法的映射
        if (mappedException.isAssignableFrom(exceptionType)) {
            matches.add(mappedException);
        }
    }
    // 符合条件的异常类型方法,会有多个,我们只需要一个,所以需要排序,然后取第一个
    if (!matches.isEmpty()) {
        // ExceptionDepthComparator这个比较器的原理很简单
        // 看所有的候选方法的@ExceptionHandler注解声明的异常类型距离当前异常类型,有几层,层数小的排在前面,
        // 层数计算逻辑,比如,是当前实际报错类型的父类,就是一层,父类的父类,就是两层,
        // 也就是说,ExceptionDepthComparator会将离当前爆出的异常类型最近的父类放在最前面
        // 具体逻辑请看 ExceptionDepthComparator 比较器的内容
        matches.sort(new ExceptionDepthComparator(exceptionType));
        // 通过mappedMethods找到最终的异常处理方法
        return this.mappedMethods.get(matches.get(0));
    }
    else {
        return null;
    }
}
简单总结执行过程

先在当前控制器中的 @ExceptionHandler 方法这个范围内查找,如果没找到,就按照按照 @ControllerAdvice 的排序依次在 @ControllerAdvice 中包含的 @ExceptionHandler 方法中查找。

拿如何在一个类中查找处理当前异常呢?

先根据当前抛出的异常的类型进行匹配,按照继承路径最短原则,遍历所有的异常处理方法的 @ExceptionHandler 注解中配置的异常类型,查找异常类型就是当前爆出的异常类型或者为当前爆出的异常类型的父类,同时继承层级最短的一个,如果找不到,就查找当前抛出的异常的根异常,然后继续以这个根异常来进行类型匹配,直到找到合适的异常,如果还是没找到,那就没有了,换下一个类。

所以,只要在当前类中能找到一个能处理当前爆出的异常的 @ExceptionHandler 方法,都不会继续从下一个类中查找,也就是说,只有当前类中完全不包含能处理当前爆出的异常的 @ExceptionHandler 方法,才会去下一个类中查找。

我们最开始从爆出异常的控制器方法所在的控制器中查找,然后按照 Order,从匹配的 @ControllerAdvice 中查找。

关于 @ControllerAdvice 的使用方式,详情请看《SpringMVC- 第八篇:跨控制器间共享》

简单实践

在 SpringMVC.xml 中,我们默认添加了一个 StringHttpMessageConverter,这个 MessageConverter 会排在系统中所有的 MessageConverter 的第一个,用于处理处理响应中文内容乱码,同时在客户端没有指定响应中需要的 MediaType 时(例如 Accept=*/* 的时候),这个 MessageConverter 的第一个 supportedMediaTypes,会作为服务器响应的消息体的默认 MediaType。

此时,默认的内容协商格式为 json

<mvc:annotation-driven>
    <mvc:message-converters>
        <!-- 处理响应中文内容乱码 -->
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <property name="defaultCharset" value="UTF-8"/>
            <property name="supportedMediaTypes">
                <list>
                    <value>application/json</value>
                    <value>text/html</value>
                </list>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

控制器方法

@Controller
@RequestMapping("/Exception")
public class TestExceptionHandler {

    @RequestMapping(value = "/arithmetic")
    public String testArithmeticException(Model model) {
        System.out.println(1 / 0);
        return "success";
    }


    @ResponseBody
    @ExceptionHandler(Exception.class)
    public String handleException(ArithmeticException ex,
                                  HandlerMethod handlerMethod,
                                  HttpServletRequest request,
                                  HttpServletResponse response,
                                  @RequestAttribute String requestName,
                                  @SessionAttribute String sessionName,
                                  Model model) {
        return "json return from Exception method";
    }

    @ResponseBody
    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(ArithmeticException ex,
                                         HandlerMethod handlerMethod,
                                         HttpServletRequest request,
                                         HttpServletResponse response,
                                         @RequestAttribute String requestName,
                                         @SessionAttribute String sessionName,
                                         Model model) {
        return "json return from RuntimeException method";
    }

    @ResponseBody
    @ExceptionHandler(ArithmeticException.class)
    public String handleArithmeticException(ArithmeticException ex,
                                            HandlerMethod handlerMethod,
                                            HttpServletRequest request,
                                            HttpServletResponse response,
                                            @RequestAttribute String requestName,
                                            @SessionAttribute String sessionName,
                                            Model model) {
        return "json return from ArithmeticException method";
    }


}

测试类

@SpringJUnitWebConfig(locations = "classpath:springMVC.xml")
class TestExceptionHandlerTest {
    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    void testArithmeticException() throws Exception {
        mockMvc.perform(get("/Exception/arithmetic")
                .requestAttr("requestName", "xiashuo")
                .sessionAttr("sessionName", "xiashuo")
        ).andDo(print());
    }
}

日志:可以看到确实是从 handleArithmeticException 方法中返回的。

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /Exception/arithmetic
       Parameters = {}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {sessionName=xiashuo}

Handler:
             Type = xyz.xiashuo.springmvcrequestmapping.TestExceptionHandler
           Method = xyz.xiashuo.springmvcrequestmapping.TestExceptionHandler#testArithmeticException(Model)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = java.lang.ArithmeticException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", Content-Length:"43"]
     Content type = application/json
             Body = json return from ArithmeticException method
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

然后我们将 handleArithmeticException 方法注释掉,再次运行测试,我们会发现输出的就是 @ExceptionHandler(RuntimeException.class) 注解修饰的方法的日志,这就是查找的顺序:先按当前类或者父类找,找不到,再按 cause 来找的查找顺序,

配合@ControllerAdvice

请看《SpringMVC- 第八篇:跨控制器间共享》

完整的例子,看这个章节

ResponseStatusExceptionResolver

继承 AbstractHandlerExceptionResolver。

主要解析继承了 ResponseStatusException 或者被 @ResponseStatus 注解修饰的异常类实例,直接将异常转化为 HTTP 状态码,然后返回一个空的 ModelAndView 对象,具体可查看 ResponseStatusExceptionResolver 的 doResolveException 方法,不管当前抛出的异常类是继承了 ResponseStatusException 还是被@ResponseStatus 注解修饰,最终都是调用 applyStatusAndReason 方法

protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
        throws IOException {

    // 最终调用的都是  response.sendError 方法,设置 响应的 HTTP 状态码
    if (!StringUtils.hasLength(reason)) {
        response.sendError(statusCode);
    }
    else {
        // 如果 messageSource 不为空,则还是通过  reason 来解析错误信息
        String resolvedReason = (this.messageSource != null ?
                this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
                reason);
        response.sendError(statusCode, resolvedReason);
    }
    // 最终返回一个空的 ModelAndView 对象
    return new ModelAndView();
}

关于 messageSource 的初始化请看上文中的 初始化过程 小节,messageSource 的具体原理,请看《Spring-MessageSource 相关解析.md

主要是通过 response.sendError 来设置响应的状态码,最终返回的是一个空的 ModelAndView 对象,会触发默认的视图解析,回看前面的 processHandlerException 方法。

@ResponseStatus 的作用与继承 ResponseStatusException 的作用一样,都是为了存储异常的状态码和自定义信息

构造响应的方式与 DefaultHandlerExceptionResolver 其实差别不大。

DefaultHandlerExceptionResolver

继承 AbstractHandlerExceptionResolver。

直接根据抛出的 Exception 的类型决定定制响应的内容,主要是通过 response.sendError 来设置响应的状态码,最终返回空的 ModelAndView 对象,

构造响应的方式与 ResponseStatusExceptionResolver 其实差别不大。

SimpleMappingExceptionResolver - 非默认,但是常用

继承 AbstractHandlerExceptionResolver。

简单地说,核心功能就是,当出现特定的异常类型的时候,直接返回对应的视图,简单粗暴,即配置 exceptionMappings 属性,同时还需要配置视图对应的 HTTP 状态码,即 statusCodes 属性,实际上,视图与异常对应,所以这个 HTTP 状态码实际上是与异常对应的,

此外再添加一些额外的配置,比如在匹配之前就排除某些控制器不参与匹配:excludedExceptions,默认视图名:defaultErrorView,默认状态码 defaultStatusCode,异常实例存放到返回的 ModelAndView 的属性名 exceptionAttribute

源码分析

直接看核心的 doResolveException 方法,总共分三步,查找当前异常对应的视图,然后根据返回的视图,查找视图对应的 HTTP 状态码,并设置到响应中,最终,以视图名作为 view,同时将当前错误对象作为一个名为 "exception" 的属性设置到 model 中,初始化一个 ModelAndView,返回。如果没有找到视图,则直接返回 null。

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

    // Expose ModelAndView for chosen error view.
    // 查找当前异常对应的视图
    String viewName = determineViewName(ex, request);
    if (viewName != null) {
        // Apply HTTP status code for error views, if specified.
        // Only apply it if we're processing a top-level request.
        // 查找 statusCodes 中存储的当前视图对应的HTTP状态码
        // 如果没有的话,使用默认的 HTTP状态码 ,defaultStatusCode
        Integer statusCode = determineStatusCode(request, viewName);
        if (statusCode != null) {
            // 如果有的话,设置到响应对象中,
            // 注意,如果当前请求时 include请求(转发的一种),则不设置code
            applyStatusCodeIfPossible(request, response, statusCode);
        }
        // 以视图名作为view,同时将当前错误对象作为一个名为"exception"的属性设置到model中,初始化一个 ModelAndView
        return getModelAndView(viewName, ex, request);
    }
    else {
        return null;
    }
}

这里我们重点看根据异常查找视图的方法 determineViewName,determineViewName 方法的逻辑很简单,首先判断当前异常是不是在排除列表中,然后再开始再异常类型和视图名称的映射中查找视图,注意,这里的使用的是跟 ExceptionHandlerMethodResolver 一样的 继承路径最短原则,如果最终没有找到当前异常类型对应的视图,同时默认错误视图不为空,就会使用默认异常视图名

这里再复制一遍继承路径最短原则

如果实际报错的类型,是 exceptionMappings 中配置的异常类型的子类,那么遍历 exceptionMappings 中所有的异常类型,查找异常类型为当前爆出的异常类型的父类,同时继承层级最短的一个

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;
            }
        }
    }
    // Check for specific exception mappings.
    if (this.exceptionMappings != null) {
        // 然后再开始再异常类型和视图名称的映射中查找视图
        // 注意,这里的使用的是跟 ExceptionHandlerMethodResolver 一样的 继承路径最短原则
        viewName = findMatchingViewName(this.exceptionMappings, ex);
    }
    // Return default error view else, if defined.
    if (viewName == null && this.defaultErrorView != null) {
        if (logger.isDebugEnabled()) {
            logger.debug("Resolving to default view '" + this.defaultErrorView + "'");
        }
        // 如果最终没有找到当前异常类型对应的视图,同时默认错误视图不为空,就会使用默认异常视图名
        viewName = this.defaultErrorView;
    }
    return viewName;
}
简单实践

添加此解析器之后,解析器的列表是

我们前面在分析 DispatcherServletprocessHandlerException 方法的时候得出过结论,

排在前面的 HandlerExceptionResolver 解析成功了的话,会直接返回,所以 HandlerExceptionResolver 的注册顺序很重要

SimpleMappingExceptionResolver 想要生效,那么前面几个 ExceptionResolver 就不能成功解析,即,解析结果 ModelAndView 必须为 null,

了解清楚了,开始实践

SpringMVC 配置文件中注册 SimpleMappingExceptionResolver

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
            <!--
                properties的键表示处理器方法执行过程中出现的异常
                properties的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面
            -->
            <prop key="java.lang.ArithmeticException">error</prop>
        </props>
    </property>
    <!--
        exceptionAttribute属性设置一个属性名,将出现的异常信息在Model中进行共享
    -->
    <property name="exceptionAttribute" value="ex"></property>
</bean>

同时,项目中不要存在添加监听 ArithmeticException@ExceptionHandler 方法

添加控制器方法

@Controller
@RequestMapping("/SimpleMappingHandler")
public class TestSimpleMappingExceptionHandler {

    @RequestMapping(value = "/arithmetic")
    public String testArithmeticException(Model model) {
        System.out.println(1 / 0);
        return "success";
    }

}

添加测试类

@SpringJUnitWebConfig(locations = "classpath:springMVC.xml")
class TestSimpleMappingExceptionHandlerTest {
    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    void testArithmeticException() throws Exception {
        mockMvc.perform(get("/SimpleMappingHandler/arithmetic")
                .requestAttr("requestName", "xiashuo")
                .sessionAttr("sessionName", "xiashuo")
        ).andDo(print());
    }
}

错误页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>错误页面</title>
</head>
<body>
<h1>this is a error page</h1>
<p th:text="${ex}"></p>
</body>
</html>

日志解析,可以看到 error 页面中的 Model 中保存的异常属性 ex 正常显示了

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /SimpleMappingHandler/arithmetic
       Parameters = {}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {sessionName=xiashuo}

Handler:
             Type = xyz.xiashuo.springmvcrequestmapping.TestSimpleMappingExceptionHandler
           Method = xyz.xiashuo.springmvcrequestmapping.TestSimpleMappingExceptionHandler#testArithmeticException(Model)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = java.lang.ArithmeticException

ModelAndView:
        View name = error
             View = null
        Attribute = ex
            value = java.lang.ArithmeticException: / by zero

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Language:"en", Content-Type:"text/html;charset=UTF-8"]
     Content type = text/html;charset=UTF-8
             Body = <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>错误页面</title>
</head>
<body>
<h1>this is a error page</h1>
<p>java.lang.ArithmeticException: / by zero</p>
</body>
</html>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

异常处理的总结和最佳实践

默认的加上常用的异常处理器总共也就是四个,顺序如下:

  1. ExceptionHandlerExceptionResolver 常用

  2. ResponseStatusExceptionResolver

  3. DefaultHandlerExceptionResolver

  4. SimpleMappingExceptionResolver 常用

其中,只有 ExceptionHandlerExceptionResolver 是可以异常处理的时候进行业务逻辑的逻辑的,剩下的集中异常处理器都无法做到这一点,因此,我们应该尽量将异常处理放到 ExceptionHandlerExceptionResolver 中完成,此外,SimpleMappingExceptionResolver 其实也只是前三个异常处理器的一个补充,他们处理不了的日常,才会轮到 SimpleMappingExceptionResolver 来处理,但有些异常我们只需要跳转页面,而不需要进行业务处理的时候,可以尝试将其处理逻辑从 ExceptionHandlerExceptionResolver 挪到 SimpleMappingExceptionResolver