基本概念
这里讲的异常处理,具体来说是控制器(controller)请求处方法的异常。
在 springmvc 中可能存在着多个控制器,各个控制器又存在着众多请求处理方法。若在每个方法上都进行异常处理,那样实在是过于繁琐。
因此 springmvc 提供了 HandlerExceptionResolver 接口来统一处理异常。
public interface HandlerExceptionResolver { ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);}
springmvc 中默认实现该接口的类有:
原理解释
1.流程
首先来看请求处理方法的异常是如何被捕获的。找到 DispatcherServlet 的 doDispatch 方法,该方法负责实现 springmvc 的请求转发,同时也包含了调用控制器的请求处理方法。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { try { ModelAndView mv = null; Exception dispatchException = null; try { mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); } catch (Exception ex) { dispatchException = ex; } 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) { 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
观察代码,发现当它捕获到异常之后,会对异常的类型进行判断。
接着来看 processHandlerException 这个方法,绝大多数的异常(除 ModelAndViewDefiningException 外)会在这里被处理。
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ModelAndView exMv = null; for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) { exMv = handlerExceptionResolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } if (exMv != null) { if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } 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; } }
配置到spring配置文件中,或者加上@Component注解。
<bean class="com.resolver.MyExceptionResolver"/>
2.添加 @ExceptionHandler 注解
首先来看它的注解定义:
Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ExceptionHandler { Class<? extends Throwable>[] value() default {};}
在控制器中使用,可以定义不同方法来处理不同类型的异常。
public abstract class BaseController { @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
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) { 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) { 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) { 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 方法
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) { 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; 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; 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;}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) { if (this.statusCodes.containsKey(viewName)) { return this.statusCodes.get(viewName); } return this.defaultStatusCode;}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