spring mvc 异常处理机制

1、原理

springmvc 通过HandlerExceptionResolver 接口来进行异常处理。任何实现该接口的类通过配置即可进行异常处理。

HandlerExceptionResolver的resolveException方法用来处理异常。接口如下:

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

使用

使用方法包括@ExceptionHandler和自己实现HandlerExceptionResolver接口。这里不做详细说明,请自行查阅资料。

那么这2中方法有什么区别?

 自己实现HandlerExceptionResolver是自己处理异常(废话)。
 @ExceptionHandler使用spring自带的类ExceptionHandlerExceptionResolver来处理异常。(具体如何处理后面详细说)

spring mvc 异常处理类介绍

到这里,我们先来暂停一下,先来了解spring mvc自身的异常处理类,这样才能继续深入。

1. HandlerExceptionResolver接口
SpringMVC异常处理核心接口。最开始所说的基础类,所有异常处理类都要实现该类。

2. AbstractHandlerExceptionResolver抽象类
实现HandlerExceptionResolver接口的抽象类。
其中属性order默认为最低优先级(int最大值),order代表整个exception handler链处理顺序,值越小,处理顺序越靠前。

    private int order = Ordered.LOWEST_PRECEDENCE;

   **//异常处理方法**
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
        Object handler, Exception ex) {
            // shouldApplyTo方法是用来判断当前exceptionHandler能否处理handler(handler是抛出异常的类或者方法)
    if (shouldApplyTo(request, handler)) {
        // Log exception, both at debug log level and at warn level, if desired.
        if (logger.isDebugEnabled()) {
            logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
        }
        logException(ex, request);
        prepareResponse(ex, response);
           ** //最终调用doResolveException抽象方法,需要子类实现**
        return doResolveException(request, response, handler, ex);
    }
    else {
        return null;
    }
}

3. AbstractHandlerMethodExceptionResolver抽象类

继承AbstractHandlerExceptionResolver抽象类的抽象类。 该类主要就是为HandlerMethod类服务,既handler参数是HandlerMethod类型。

该类重写了shouldApplyTo方法:主要母的是将handlerMethod转换为handler

    protected boolean shouldApplyTo(HttpServletRequest request, Object handler) {
    if (handler == null) {
        return super.shouldApplyTo(request, handler);
    }
    else if (handler instanceof HandlerMethod) {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        handler = handlerMethod.getBean();
        return super.shouldApplyTo(request, handler);
    }
    else {
        return false;
    }
}

4. ExceptionHandlerExceptionResolver类
继承自AbstractHandlerMethodExceptionResolver,该类主要处理Controller中用@ExceptionHandler注解定义的方法。这个类也是上面所说的2中实现方式之一的处理类。

但是我不想过于深入源码,我觉得这样会导致文章过长反而不利于轻松的理解更加重要的东西,只做一些简单介绍。
我希望未来自己忘记整个内容的时候,能在几分钟内对整个exception的处理有个整体理解,并对一些重要的注意事项进行记录。

无论如何还是要看一下最重要的方法doResolveHandlerMethodException:

@Override
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
        HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {
             //*** getExceptionHandlerMethod方法封装了如何寻找@exception注解的内容。 ***
    ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    if (exceptionHandlerMethod == null) {
        return null;
    }

    exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    ModelAndViewContainer mavContainer = new ModelAndViewContainer();

    try {
        if (logger.isDebugEnabled()) {
            logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
        }
                  //该处即是交给异常处理方法进行处理。
        exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception);
    }
    catch (Exception invocationEx) {
        if (logger.isErrorEnabled()) {
            logger.error("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
        }
        return null;
    }

    if (mavContainer.isRequestHandled()) {
        return new ModelAndView();
    }
    else {
        ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
        mav.setViewName(mavContainer.getViewName());
        if (!mavContainer.isViewReference()) {
            mav.setView((View) mavContainer.getView());
        }
        return mav;
    }
}

该方法第一行 getExceptionHandlerMethod(handlerMethod, exception);封装了获取处理handler的异常处理方法(这里指的是@ExceptionHandler注解的方法)。
该方法中包括很多内容,缓存,验证等信息。
这里的缓存指的handler对应的异常处理resolver的mapping;

验证会验证一个handler中的相同@Exceptionhandler是否出现了多次,如果出现多次会抛出

        throw new IllegalStateException(
                "Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" +
                oldMethod + ", " + method + "}.");

简单说就是一个controller中针对一个异常类型,只能使用一个@Exceptionhandler,当然如果处理的异常类型有多个,则可以有多个。
比如下面将不合法,应为@ExceptionHandler(Exception.class)有2个,如果将其中一个@ExceptionHandler(Exception.class)修改为其他则可行。

@ExceptionHandler(Exception.class)
@ResponseBody
public CMCCApiResponse processMethod(Exception ex, HttpServletRequest request, HttpServletResponse response) {
    CMCCApiResponse cmccApiResponse = new CMCCApiResponse();
    cmccApiResponse.setCode(CMCCBaseResultCodeConstant.FAIL);
    logger.error(request.getRequestURL() + "?" + request.getQueryString());
    logger.error("ApiBaseController:ExceptionHandler:", ex);
    if (!EcovacsValidateException.class.isInstance(ex)) {
        //内部异常
        cmccApiResponse.setMsg("消息接收异常");
    } else {

        cmccApiResponse.setMsg(ex.getMessage());
    }
    return cmccApiResponse;
}

@ExceptionHandler(Exception.class)
@ResponseBody
public CMCCApiResponse processMethod2(Exception ex, HttpServletRequest request, HttpServletResponse response) {
    CMCCApiResponse cmccApiResponse = new CMCCApiResponse();
    cmccApiResponse.setCode(CMCCBaseResultCodeConstant.FAIL);
    logger.error(request.getRequestURL() + "?" + request.getQueryString());
    logger.error("ApiBaseController:ExceptionHandler:", ex);
    if (!EcovacsValidateException.class.isInstance(ex)) {
        //内部异常
        cmccApiResponse.setMsg("消息接收异常");
    } else {

        cmccApiResponse.setMsg(ex.getMessage());
    }
    return cmccApiResponse;
}

5. DefaultHandlerExceptionResolver类
继承自AbstractHandlerExceptionResolver抽象类。该类封装了对很多不同异常的处理。列举部分:

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

    try {
        if (ex instanceof NoSuchRequestHandlingMethodException) {
            return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, request, response,
                    handler);
        }
        else 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);
        }

6. ResponseStatusExceptionResolver类

继承自AbstractHandlerExceptionResolver抽象类。该类的doResolveException方法主要在异常及异常父类中找到@ResponseStatus注解,然后使用这个注解的属性进行处理。这一点和@ExceptionHandler类似.

7. SimpleMappingExceptionResolver类
继承自AbstractHandlerExceptionResolver抽象类,是一个简单的处理,试图映射的类。很多人会继承该类来实现自己的异常处理handler。

8. @ResponseStatus注解
让1个方法或异常有状态码(status)和理由(reason)返回。这个状态码是http响应的状态码。

异常处理的生命周期(关键之处)

上面介绍了一些基础的类,但是依然有疑惑。为何配置了@ExceptionHandler注解就可以进行异常处理?
虽然我们知道了ExceptionHandlerExceptionResolver类对配置了@ExceptionHandler注解的类进行处理,但是这是如何发生的?或者说ExceptionHandlerExceptionResolver在何时初始化(配置),何时调用?

答案是:<annotation-driven/>

我们看下<annotation-driven/>解析类AnnotationDrivenBeanDefinitionParser中部分代码片段:

    RootBeanDefinition exceptionHandlerExceptionResolver = new RootBeanDefinition(ExceptionHandlerExceptionResolver.class);
    exceptionHandlerExceptionResolver.setSource(source);
    exceptionHandlerExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    exceptionHandlerExceptionResolver.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
    exceptionHandlerExceptionResolver.getPropertyValues().add("messageConverters", messageConverters);
    exceptionHandlerExceptionResolver.getPropertyValues().add("order", 0);
    addResponseBodyAdvice(exceptionHandlerExceptionResolver);

    String methodExceptionResolverName =
            parserContext.getReaderContext().registerWithGeneratedName(exceptionHandlerExceptionResolver);

    RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class);
    responseStatusExceptionResolver.setSource(source);
    responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    responseStatusExceptionResolver.getPropertyValues().add("order", 1);
    String responseStatusExceptionResolverName =
            parserContext.getReaderContext().registerWithGeneratedName(responseStatusExceptionResolver);

    RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class);
    defaultExceptionResolver.setSource(source);
    defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    defaultExceptionResolver.getPropertyValues().add("order", 2);
    String defaultExceptionResolverName =

上面代码中定义了3个异常处理类及其顺序。所以答案在这里得到解决。

顺序order
如果你的项目中有时候出现抛出异常,但是无法进入自定义的异常处理类的时候,就需要当心这个陷阱。

上面代码中定义了异常处理的默认顺序,所以如果我们自己实现自己的异常处理类的时候需要注意自己的order属性,如果不设置默认将会是排在末尾,配置多个将会依次根据顺序排序。
这里需要注意的是:如果希望自己优先处理所有异常,应该将order设置为-1,并且我也推荐使用。应为如果不讲自己的异常处理类设置为最优先的话,我们将对一些异常无法处理。比如封装spring mvc属性的时候出现类型不匹配,jsp中的异常,或者其他异常(这里记得DefaultHandlerExceptionResolver的处理吗?这个类封装了很多异常处理)。所以要当心当出现异常的时候,自己的异常处理类,无法接到任何异常信息。这一点笔者接触的一些公司经常会犯这样的错误,导致一些异常难以排查(应为线上日志几乎不会出现DEBUG模式,而JSP的一些异常却是需要DEBUG才会打印日志)。

异常处理类的调用
观察spring mvc的核心类 DispatcherServlet
整个流程如下:
doService()调用doDispatch->processDispatchResult->processHandlerException。
dodispatch如下(这类忽略掉了其他代码):

   try{
            xxxxxxxxx
    }catch (Exception ex) {
            dispatchException = ex;
    }
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

processHandlerException 通过循环来交给异常处理类处理:

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

    // Check registered HandlerExceptionResolvers...
    ModelAndView exMv = null;
    for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
        exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
        if (exMv != null) {
            break;
        }
    }

异常处理类中如果抛出异常
这个问题很有趣,如果处理异常的方法自己抛出异常,将会如何处理?这里不妨脑洞一些,如果抛出的异常依然可以被异常处理链路来处理,那么就会出现死循环!!!那么如果不处理,这里抛出的异常该怎么办呢?
结论是:如果抛出异常,将会沿着调用栈向上抛,最终抛出到servlet进行处理,然后抛出servlet异常。也就是会500。

    try {
        doService(request, response);
    }
    catch (ServletException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (IOException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    }

2、总结

本文讲解了spring mvc的异常处理流程。重要的地方发包括:
1、spring mvc 通过<annotation-driven/>默认加入了3个异常处理器。
2、@ExceptionHandler注解由ExceptionHandlerExceptionResolver类来处理。
3、同一个类中不能使用@ExceptionHandler注解解释相同异常2次。并且@Order注解毫无意义,应为@ExceptionHandler不是一个异常处理器,只是让异常处理器能识别到应该如何处理。
4、通过配置文件可以配置多个异常处理器,处理顺序根据order来设置,如果要自己处理所有异常,需要将order设置成小于0。并且强烈推荐如此做。否则一些异常会被spring mvc自身处理,而达不到想要的效果。
5、异常处理器中抛出的异常,将会一直往上抛,直到servlet捕获并处理,最终抛出ServletException,或者其他Exception。其实只有3中ServletException exception 和NestedServletException。

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

推荐阅读更多精彩内容