SpringMVC 异常处理 - HandlerExceptionResolver

来源:互联网 发布:淘宝一分钱群 编辑:程序博客网 时间:2024/05/16 07:39

基本概念

这里讲的异常处理,具体来说是控制器(controller)请求处方法的异常。

在 springmvc 中可能存在着多个控制器,各个控制器又存在着众多请求处理方法。若在每个方法上都进行异常处理,那样实在是过于繁琐。

因此 springmvc 提供了 HandlerExceptionResolver 接口来统一处理异常。

public interface HandlerExceptionResolver {    // 关键 -> 子类通过重写该方法进行具体的异常处理    // 返回值 ModelAndView 可以设置发生异常时要显示的页面    ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

springmvc 中默认实现该接口的类有:

这里写图片描述


原理解释

1.流程

首先来看请求处理方法的异常是如何被捕获的。找到 DispatcherServlet 的 doDispatch 方法,该方法负责实现 springmvc 的请求转发,同时也包含了调用控制器的请求处理方法。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {    // 省略部分代码...    try {        ModelAndView mv = null;        Exception dispatchException = null;        try {            // 省略部分代码...            // 调用控制器(controller )匹配请求路径的处理方法            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());            // 省略部分代码...        }        catch (Exception ex) {            // 将捕获的异常赋给 dispatchException            dispatchException = ex;        }        // 处理请求结果(观察参数 dispatchException  -> 异常会进入该方法进行处理)        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);    }    catch (Exception ex) {        // 省略部分代码...    }    catch (Error err) {        // 省略部分代码...    }    finally {        // 省略部分代码...    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

观察代码,发现控制器的请求处理方法产生的异常会在 doDispatch 中通过 try-catch 捕获,然后被传递给 processDispatchResult方法处理。


再来看 processDispatchResult 这个方法,该方法负责处理请求结果,并渲染视图。

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,    HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {    boolean errorView = false;    // 异常处理    if (exception != null) {        // 判断是否是 ModelAndViewDefiningException 异常,是的话直接处理        if (exception instanceof ModelAndViewDefiningException) {            logger.debug("ModelAndViewDefiningException encountered", exception);            mv = ( (ModelAndViewDefiningException) exception ).getModelAndView();        } else {            Object handler = ( mappedHandler != null ? mappedHandler.getHandler() : null );            // 若不是,进入该方法            mv = processHandlerException(request, response, handler, exception);            errorView = ( mv != null );        }    }    // 省略部分代码...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

观察代码,发现当它捕获到异常之后,会对异常的类型进行判断。

  • 如果是异常类型是 ModelAndViewDefiningException 会直接进行处理。

  • 若不是,则将异常传递到 processHandlerException方法处理。


接着来看 processHandlerException 这个方法,绝大多数的异常(除 ModelAndViewDefiningException 外)会在这里被处理。

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,    Object handler, Exception ex) throws Exception {    ModelAndView exMv = null;    // 遍历 IOC 容器中所有的 handlerExceptionResolver     for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {        // 关键 --> 执行 resolveException 并返回 ModelAndView        exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);        if (exMv != null) {            break;        }    }    // 如果异常中 exMv 不为空则返回指定页面    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()) {            exMv.setViewName(getDefaultViewName(request));        }        if (logger.isDebugEnabled()) {            logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);        }        WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());        return exMv;    }    throw ex;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

观察代码,发现在这里会遍历 Ioc 容器中的所有实现 handlerExceptionResolver 接口的类,然后调用它的 resolveException 方法进行异常处理,并返回具体的error page(错误页面)。


2.总结

最后再来总结下 springmvc 的异常处理过程:

  • 请求转发过程中(doDispatch)中调用匹配请求路径的控制器请求处理方法(handle);

  • 请求处理方法产生异常,通过 catch 捕获异常并传递给 processDispatchResult;

  • 判断异常类型是否是 ModelAndViewDefiningException,是的话直接处理,否则异常被传递给 processHandlerException ;

  • 遍历 Ioc 的 handlerExceptionResolver ,调用 resolveException 方法进行异常处理,并返回指定的错误接main。

简单流程如下:

  • doDispatch -> handle -> processHandlerException -> resolveException

实例探究

下面来看 springmvc 中常见的统一异常处理方法。

1.实现 HandlerExceptionResolver 接口

首先需要实现 HandlerExceptionResolver 接口。

public class MyExceptionResolver implements HandlerExceptionResolver {    @Override    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {        // 异常处理...        // 视图显示专门的错误页        ModelAndView modelAndView = new ModelAndView("error");        return modelAndView;    }   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

配置到spring配置文件中,或者加上@Component注解。

<bean  class="com.resolver.MyExceptionResolver"/>
  • 1
  • 1

2.添加 @ExceptionHandler 注解

首先来看它的注解定义:

// 只能作用在方法上,运行时有效Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ExceptionHandler {    // 这里可以定义异常类型,为空表示匹配任何异常    Class<? extends Throwable>[] value() default {};}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在控制器中使用,可以定义不同方法来处理不同类型的异常。

public abstract class BaseController {    // 处理 IO 异常    @ExceptionHandler(IOException.class)    public ModelAndView handleIOException(HttpServletRequest request, HttpServletResponse response, Exception e) {        // 视图显示专门的错误页        ModelAndView modelAndView = new ModelAndView("error");        return modelAndView;    }    // 处理空指针异常    @ExceptionHandler(NullPointerException.class)    public ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Exception e) {        // 视图显示专门的错误页        ModelAndView modelAndView = new ModelAndView("error");        return modelAndView;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

使用 @ExceptionHandler 注解实现异常处理有个缺陷就是只对该注解所在的控制器有效

想要让所有的所有的控制器都生效,就要通过继承来实现。如上所示(定义了一个抽象的基类控制器) ,可以让其他控制器继承它实现异常处理。

public class HelloController extends BaseController 
  • 1
  • 1

3.利用 SimpleMappingExceptionResolver 类

该类实现了 HandlerExceptionResolver 接口,是 springmvc 默认实现的类,通过它可以实现灵活的异常处理。

只需要在 xml 文件中进行如下配置:

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">    <property name="exceptionMappings">        <props>            <prop key="NullPointerException">nullpointpage</prop>            <prop key="IOException">iopage</prop>            <prop key="NumberFormatException">numberpage</prop>        </props>    </property>     <property name="statusCodes">        <props>              <prop key="nullpointpage">400</prop>            <prop key="iopage">500</prop>          </props>      </property>      <property name="defaultErrorView" value="errorpage"/>    <property name="defaultStatusCode" value="404"/></bean>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • exceptionMappings:定义 springmvc 要处理的异常类型对应的错误页面

  • statusCodes:定义错误页面response 中要返回的错误状态码

  • defaultErrorView:定义默认错误显示页面,表示处理不了的异常都显示该页面。在这里表示 springmvc 处理不了的异常都跳转到 errorpage页面。

  • defaultStatusCode:定义 response 默认返回的错误状态码,表示错误页面未定义对应的错误状态码时返回该值;在这里表示跳转到 errorpage、numberpage 页面的 reponse 状态码为 404。


4.ajax 异常处理

对于页面 ajax 的请求产生的异常不就适合跳转到错误页面,而是应该是将异常信息显示在请求回应的结果中。

实现方式也很简单,需要继承了 SimpleMappingExceptionResolver ,重写它的异常处理流程(下面会详细分析)。

public class CustomSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver {    @Override    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {        // 判断是否 Ajax 请求           if ((request.getHeader("accept").indexOf("application/json") > -1 ||                   (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").indexOf("XMLHttpRequest") > -1))){            try {                response.setContentType("text/html;charset=UTF-8");                response.setCharacterEncoding("UTF-8");                PrintWriter writer = response.getWriter();                writer.write(ex.getMessage());                writer.flush();                writer.close();            } catch (Exception e) {                LogHelper.info(e);            }            return null;        }        return super.doResolveException(request, response, handler, ex);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

配置文件如上,只是将注入的 bean 替换成我们自己的定义的 CustomSimpleMappingExceptionResolver 。


源码分析

1.AbstractHandlerMethodExceptionResolver

AbstractHandlerMethodExceptionResolver 实现了 HandlerExceptionResolver 接口。

通过上面的分析可以知道异常处理发生在 resolveException 这个方法。

因此来看该类 resolveException 方法,发现这里定义了 doResolveException 这个抽象方法,留给子类去做具体实现。

说明真正的异常处理从 resolveException 被转移到了 doResolveException 。

@Overridepublic ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {    // 判断是否支持该处理器    if (shouldApplyTo(request, handler)) {        // 日志记录...        // 预处理回复消息:让请求头取消缓存        prepareResponse(ex, response);        // 关键 -> 处理异常(抽象方法,留给子类实现)        ModelAndView mav = doResolveException(request, response, handler, ex);        if (mav != null) {            // Log exception message at warn log level            logException(ex, request);        }        return mav;    } else {        return null;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

2.SimpleMappingExceptionResolver

首先来看继承关系,发现它继承了 AbstractHandlerMethodExceptionResolver 这个类。

通过上面的分析可以知道具体的异常处理被 AbstractHandlerMethodExceptionResolver 留给子类通过 doResolveException 作具体的展开。

因此首先来看 doResolveException 这个方法。

protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {    // ①决定发生异常时的错误显示页面    String viewName = determineViewName(ex, request);    if (viewName != null) {        // ②决定发生异常的错误状态码,并在 response 中设置        Integer statusCode = determineStatusCode(request, viewName);        if (statusCode != null) {            applyStatusCodeIfPossible(request, response, statusCode);        }        // ③ 返回错误页面        return getModelAndView(viewName, ex, request);    } else {        return null;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

接着来看页面匹配,determineViewName 方法

// 决定不同异常的错误显示页面(配置的异常类型来自于配置文件中 exceptionMappings)protected String determineViewName(Exception ex, HttpServletRequest request) {    String viewName = null;    // 判断【发生的异常】是否属于 excludedExceptions(被过滤的异常),若是则不处理    if (this.excludedExceptions != null) {        for (Class<?> excludedEx : this.excludedExceptions) {            if (excludedEx.equals(ex.getClass())) {                return null;            }        }    }    // 若配置文件中 exceptionMappings 不为空    if (this.exceptionMappings != null) {        // 找到指定异常的错误显示页面        viewName = findMatchingViewName(this.exceptionMappings, ex);    }    // 若找不到该异常类型的错误页面,采用默认的错误显示页面(如果有,对应配置文件的 defaultErrorView)    if (viewName == null && this.defaultErrorView != null) {        if (logger.isDebugEnabled()) {            logger.debug("Resolving to default view '" + this.defaultErrorView + "' for exception of type [" + ex.getClass().getName() + "]");        }        viewName = this.defaultErrorView;    }    return viewName;}// 找到异常类型匹配的错误页面protected String findMatchingViewName(Properties exceptionMappings, Exception ex) {    String viewName = null;    String dominantMapping = null;    int deepest = Integer.MAX_VALUE;    // 遍历配置文件的 exceptionMappings 元素,这里 names 代表就是【配置的异常类型】    for (Enumeration<?> names = exceptionMappings.propertyNames(); names.hasMoreElements();) {        String exceptionMapping = (String) names.nextElement();        // 关键 -> 匹配异常        int depth = getDepth(exceptionMapping, ex);        if (depth >= 0 && ( depth < deepest || ( depth == deepest && dominantMapping != null && exceptionMapping.length() > dominantMapping.length() ) )) {            deepest = depth;            dominantMapping = exceptionMapping;            // 若匹配则配置文件 exceptionMappings 元素定义的【错误页面】            viewName = exceptionMappings.getProperty(exceptionMapping);        }    }    if (viewName != null && logger.isDebugEnabled()) {        logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() + "], based on exception mapping ["+ dominantMapping + "]");    }    return viewName;}// 匹配返回 0,不匹配返回 -1 ,depth 越低的好protected int getDepth(String exceptionMapping, Exception ex) {    return getDepth(exceptionMapping, ex.getClass(), 0);}private int getDepth(String exceptionMapping, Class<?> exceptionClass, int depth) {    if (exceptionClass.getName().contains(exceptionMapping)) {        return depth;    }    if (exceptionClass == Throwable.class) {        return -1;    }    return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

再来看错误状态码处理流程,determineStatusCode ,applyStatusCodeIfPossible方法

// 决定错误页面匹配的错误状态码protected Integer determineStatusCode(HttpServletRequest request, String viewName) {    // 判断配置文件中 statusCodes 的 key 是否匹配 viewName    if (this.statusCodes.containsKey(viewName)) {        return this.statusCodes.get(viewName);    }    return this.defaultStatusCode;}// 在 response 中设置错误的状态码protected void applyStatusCodeIfPossible(HttpServletRequest request, HttpServletResponse response, int statusCode) {    if (!WebUtils.isIncludeRequest(request)) {        if (logger.isDebugEnabled()) {            logger.debug("Applying HTTP status code " + statusCode);        }        // 关键        response.setStatus(statusCode);        request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, statusCode);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

返回指定的异常页面

protected ModelAndView getModelAndView(String viewName, Exception ex, HttpServletRequest request) {    return getModelAndView(viewName, ex);}protected ModelAndView getModelAndView(String viewName, Exception ex) {    ModelAndView mv = new ModelAndView(viewName);    if (this.exceptionAttribute != null) {        if (logger.isDebugEnabled()) {            logger.debug("Exposing Exception as model attribute '" + this.exceptionAttribute + "'");        }        mv.addObject(this.exceptionAttribute, ex);    }    return mv;}
0 0
原创粉丝点击