springmvc源码分析-handlerExceptionResolver原理

异常解析器也是springmvc的一个非常重要组件,在编写程序代码时,若抛出异常了,那么springmvc是通过异常解析器来获取异常并进行后续的处理

看下接口定义的方法

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

入参除了request和response,还有发生异常所在的handler,以及异常对象。

在WebMvcSupport中,定义了异常解析器的bean

@Bean
    public HandlerExceptionResolver handlerExceptionResolver(
            @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
        List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
        configureHandlerExceptionResolvers(exceptionResolvers);
        if (exceptionResolvers.isEmpty()) {
            addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
        }
        extendHandlerExceptionResolvers(exceptionResolvers);
        HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
        composite.setOrder(0);
        composite.setExceptionResolvers(exceptionResolvers);
        return composite;
    }

这个bean方法返回的是HandlerExceptionResolverComposite,内部维护着spring容器的异常解析器列表,这里有两个地方需要关注,首先是addDefaultHandlerExceptionResolvers,就是spring会默认添加三个异常解析器,一个是ExceptionHandlerExceptionResolver,这个处理的是程序中注解了@ExceptionHandler的,第二个DefaultHandlerExceptionResolver,这个是处理一些通常的异常,具体可查看官方文档。第三个是较少用的ResponseStatusExceptionResolver

另一个要关注的是extendHandlerExceptionResolvers,这个是留给子类重写,扩展使用的。

来看下最常用的ExceptionHandlerExceptionResolver

protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers,
        ContentNegotiationManager mvcContentNegotiationManager) {

    ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
    exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager);
    exceptionHandlerResolver.setMessageConverters(getMessageConverters());
    exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
    exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
    if (jackson2Present) {
        exceptionHandlerResolver.setResponseBodyAdvice(
                Collections.singletonList(new JsonViewResponseBodyAdvice()));
    }
    if (this.applicationContext != null) {
        exceptionHandlerResolver.setApplicationContext(this.applicationContext);
    }
    exceptionHandlerResolver.afterPropertiesSet();
    exceptionResolvers.add(exceptionHandlerResolver);

    ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
    responseStatusResolver.setMessageSource(this.applicationContext);
    exceptionResolvers.add(responseStatusResolver);

    exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}

通过这可以看到这个解析器的很多属性和HandlerAdapter的一样,有ArgumentResolvers、ReturnValueHandlers、JsonViewResponseBodyAdvice,这是因为假如程序抛出异常被@ExceptionHandler捕获了,那么程序会执行这个方法,整个流程相当于和@RequestMapping方法执行的处理逻辑一致。因此,还会发现ExceptionHandlerExceptionResolver内部也会维护@ControllerAdvice注解的ResponseBodyAdvice,这个跟进afterPropertiesSet方法便可得知

private void initExceptionHandlerAdviceCache() {

    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);
        }
    }
}

ExceptionHandlerExceptionResolver内部两个map exceptionHandlerAdviceCache维护的是注解了@ControllerAdvice的bean,且存在注解了@ExceptionHandler的方法,responseBodyAdvice维护的就是注解了@ControllerAdvice的ResponseBodyAdvice了

以上便是spring容器对异常解析器的一个实例化过程。接下来,看下程序发生异常springmvc是如何处理的

直接看http的入口DispatcherServlet的doDispatch,当发生异常时,会执行这行代码

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

一直跟进代码会走到HandlerExceptionResolverComposite的resolveException,

    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;
    }

这里就是循环执行spring容器默认添加的三个异常解析器的resolveException方法,重点关注下最常用的ExceptionHandlerExceptionResolver

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 {

        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) {
        
    }

    if (mavContainer.isRequestHandled()) {
        return new ModelAndView();
    }
}

这个方法的逻辑和HandlerAdapter执行Handler的很像,先构造ServletInvocableHandlerMethod ,再添加argumentResolvers和returnValueHandlers,最后再调用ServletInvocableHandlerMethod 的invokeAndHandle方法。

因此我们重点放在第一个步骤构造ServletInvocableHandlerMethod 对象上。

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

    Class<?> handlerType = null;

    if (handlerMethod != null) {
//发生异常方法所在的Class对象
        handlerType = handlerMethod.getBeanType();
//根据Class对象查询是否有对应的ExceptionHandlerMethodResolver对象
        ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
        if (resolver == null) {
//没有的话,生成一个新的ExceptionHandlerMethodResolver对象,并放入缓存中
            resolver = new ExceptionHandlerMethodResolver(handlerType);
            this.exceptionHandlerCache.put(handlerType, resolver);
        }
//解析exception对象,若发送异常的Class对象有对应的方法(注解有@ExceptionHandler的,且类型匹配的)
        Method method = resolver.resolveMethod(exception);
        if (method != null) {
//若有这样的方法,那么直接封装返回
            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());
        }
    }
//程序走到这里,说明发生异常的controller没有注解@ExceptionHandler方法,需要通过全局异常处理器来执行
    for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
        ControllerAdviceBean advice = entry.getKey();
        if (advice.isApplicableToBeanType(handlerType)) {
            ExceptionHandlerMethodResolver resolver = entry.getValue();
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
                return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
            }
        }
    }

    return null;
}

这个方法主要有两种情况,一种是发生异常所在controller便有注解了@ExceptionHandler方法,;另一种是通过全局异常处理器(@ControllerAdvice)的@ExceptionHandler方法。

对于第一种的,springmvc内部有个缓存exceptionHandlerCache,key是controller的Class对象,value是ExceptionHandlerMethodResolver,防止程序每次发生异常都需要重新new一个ExceptionHandlerMethodResolver对象,ExceptionHandlerMethodResolver根据异常类型获取到对应的method对象。

假如第一种情况没匹配上,那么会执行全局异常处理器,当然了,会判断全局异常处理器是否有拦截了当前发生异常的Handler,advice.isApplicableToBeanType(handlerType)
假如有拦截,那么也会根据ExceptionHandlerMethodResolver 来解析出处理异常的method对象。

当拿到ServletInvocableHandlerMethod 对象后,剩下的执行逻辑可参考HandlerAdapter执行正常程序代码,未抛出异常的逻辑

假如所有的异常解析器都没匹配到此次http请求发生的异常,那么springmvc容器会抛出NestedServletException,程序又会重新走一次DispatcherServlet的doDispatch方法,springmvc会返回Internal Server Error这样的错误信息

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335

推荐阅读更多精彩内容