基本概念
在 SpringMVC 中 HandlerExceptionResolver 接口负责统一异常处理。
内部构造
下面来看它的源码:
1public interface HandlerExceptionResolver { 2 3 ModelAndView resolveException( 4 HttpServletRequest request, HttpServletResponse response, 5 Object handler, Exception ex); 6}
AbstractHandlerMethodExceptionResolver
该类是实现了 HandlerExceptionResolver 接口的抽象实现类。关键来看 resolveException 方法:
1public ModelAndView resolveException(HttpServletRequest request, 2 HttpServletResponse response, Object handler, Exception ex) { 3 4 // 1.判断是否支持该处理器 5 if (shouldApplyTo(request, handler)) { 6 7 // 省略部分源码... 8 9 // 2.预处理响应消息,让请求头取消缓存 10 prepareResponse(ex, response); 11 12 // 3.异常处理,空方法 13 ModelAndView mav = doResolveException(request, response, handler, ex); 14 if (mav != null) { 15 logException(ex, request); 16 } 17 18 return mav; 19 20 } else { 21 return null; 22 } 23}
接着来看 shouldApplyTo 方法:
1protected boolean shouldApplyTo(HttpServletRequest request, Object handler) { 2 if (handler != null) { 3 // 分别比对 mappedHandlers 、mappedHandlerClasses 4 if (this.mappedHandlers != null && 5 this.mappedHandlers.contains(handler)) { 6 return true; 7 } 8 9 if (this.mappedHandlerClasses != null) { 10 for (Class<?> handlerClass : this.mappedHandlerClasses) { 11 if (handlerClass.isInstance(handler)) { 12 return true; 13 } 14 } 15 } 16 } 17 18 return (this.mappedHandlers == null && 19 this.mappedHandlerClasses == null); 20}
SimpleMappingExceptionResolver
它继承了 AbstractHandlerMethodExceptionResolver 。实现了真正的异常处理。
来看该类的 doResolveException 方法:
1protected ModelAndView doResolveException(HttpServletRequest request, 2 HttpServletResponse response, Object handler, Exception ex) { 3 4 // 1.决定的视图 5 String viewName = determineViewName(ex, request); 6 7 if (viewName != null) { 8 // 2.决定错误状态码 9 Integer statusCode = determineStatusCode(request, viewName); 10 if (statusCode != null) { 11 3.设置错误状态码 12 applyStatusCodeIfPossible(request, response, statusCode); 13 } 14 15 // 4.返回错误页面 16 return getModelAndView(viewName, ex, request); 17 } else { 18 return null; 19 } 20}
1.决定视图
1// 被过滤的异常集合 2private Class<?>[] excludedExceptions; 3 4// 被处理的异常集合 5private Properties exceptionMappings; 6 7// 默认的错误显示页面 8private String defaultErrorView; 9 10protected String determineViewName(Exception ex, HttpServletRequest request) { 11 12 String viewName = null; 13 14 // 1.判断属于被过滤的异常? 15 if (this.excludedExceptions != null) { 16 for (Class<?> excludedEx : this.excludedExceptions) { 17 if (excludedEx.equals(ex.getClass())) { 18 return null; 19 } 20 } 21 } 22 23 // 2.判断属于被处理的异常? 24 if (this.exceptionMappings != null) { 25 // 找到匹配的页面 26 viewName = findMatchingViewName(this.exceptionMappings, ex); 27 } 28 29 // 3.为空则使用默认的错误页面 30 if (viewName == null && this.defaultErrorView != null) { 31 viewName = this.defaultErrorView; 32 } 33 return viewName; 34}
接着来看 findMatchingViewName 方法:
1protected String findMatchingViewName(Properties exceptionMappings, Exception ex) { 2 String viewName = null; 3 String dominantMapping = null; 4 int deepest = Integer.MAX_VALUE; 5 6 // 遍历 exceptionMappings 7 for (Enumeration<?> names = exceptionMappings.propertyNames(); 8 names.hasMoreElements();) { 9 10 String exceptionMapping = (String) names.nextElement(); 11 12 // 关键 -> 匹配异常 13 int depth = getDepth(exceptionMapping, ex); 14 15 if (depth >= 0 && 16 ( depth < deepest || 17 (depth == deepest && 18 dominantMapping != null && 19 exceptionMapping.length() > dominantMapping.length() ) )) { 20 21 deepest = depth; 22 dominantMapping = exceptionMapping; 23 24 viewName = exceptionMappings.getProperty(exceptionMapping); 25 } 26 } 27 28 // 省略代码... 29 30 return viewName; 31}
继续来看 getDepth 方法:
1protected int getDepth(String exceptionMapping, Exception ex) { 2 // 匹配返回 0,不匹配返回 -1 ,depth 越低的好 3 return getDepth(exceptionMapping, ex.getClass(), 0); 4} 5 6private int getDepth(String exceptionMapping, Class<?> exceptionClass, int depth) { 7 if (exceptionClass.getName().contains(exceptionMapping)) { 8 return depth; 9 } 10 11 if (exceptionClass == Throwable.class) { 12 return -1; 13 } 14 15 return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1); 16}
2.决定错误状态码
1// 在配置文件中定义 2private Map<String, Integer> statusCodes = new HashMap<String, Integer>(); 3 4protected Integer determineStatusCode(HttpServletRequest request, String viewName) { 5 if (this.statusCodes.containsKey(viewName)) { 6 return this.statusCodes.get(viewName); 7 } 8 return this.defaultStatusCode; 9}
3.设置错误状态码
1public static final String ERROR_STATUS_CODE_ATTRIBUTE = 2 "javax.servlet.error.status_code"; 3 4protected void applyStatusCodeIfPossible(HttpServletRequest request, 5 HttpServletResponse response, int statusCode) { 6 7 if (!WebUtils.isIncludeRequest(request)) { 8 // 省略代码... 9 10 // 设置错误状态码,并添加到 request 的属性 11 response.setStatus(statusCode); 12 request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, statusCode); 13 } 14}
4.返回错误页面
1protected ModelAndView getModelAndView(String viewName, Exception ex, 2 HttpServletRequest request) { 3 return getModelAndView(viewName, ex); 4} 5 6protected ModelAndView getModelAndView(String viewName, Exception ex) { 7 ModelAndView mv = new ModelAndView(viewName); 8 if (this.exceptionAttribute != null) { 9 // 省略代码... 10 11 mv.addObject(this.exceptionAttribute, ex); 12 } 13 return mv; 14}
实例探究
下面来看 springmvc 中常见的统一异常处理方法。
1.实现 HandlerExceptionResolver 接口
首先需要实现 HandlerExceptionResolver 接口。
1public class MyExceptionResolver implements HandlerExceptionResolver { 2 @Override 3 public ModelAndView resolveException(HttpServletRequest request, 4 HttpServletResponse response, Object handler, Exception ex) { 5 6 // 异常处理... 7 8 // 视图显示专门的错误页 9 ModelAndView modelAndView = new ModelAndView("error"); 10 return modelAndView; 11 } 12}
配置到 spring 配置文件中,或者加上@Component 注解。
<bean class="com.resolver.MyExceptionResolver"/>
2.添加 @ExceptionHandler 注解
首先来看它的注解定义:
1// 只能作用在方法上,运行时有效 2Target(ElementType.METHOD) 3Retention(RetentionPolicy.RUNTIME) 4@Documented 5public @interface ExceptionHandler { 6 7 // 这里可以定义异常类型,为空表示匹配任何异常 8 Class<? extends Throwable>[] value() default {}; 9}
在控制器中使用,可以定义不同方法来处理不同类型的异常。
1public abstract class BaseController { 2 // 处理 IO 异常 3 @ExceptionHandler(IOException.class) 4 public ModelAndView handleIOException(HttpServletRequest request, HttpServletResponse response, Exception e) { 5 6 // 视图显示专门的错误页 7 ModelAndView modelAndView = new ModelAndView("error"); 8 9 return modelAndView; 10 } 11 12 // 处理空指针异常 13 @ExceptionHandler(NullPointerException.class) 14 public ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Exception e) { 15 16 // 视图显示专门的错误页 17 ModelAndView modelAndView = new ModelAndView("error"); 18 19 return modelAndView; 20 } 21}
使用 @ExceptionHandler 注解实现异常处理有个缺陷就是只对该注解所在的控制器有效。
想要让所有的所有的控制器都生效,就要通过继承来实现。
如上所示(定义了一个抽象的基类控制器) ,可以让其他控制器继承它实现异常处理。
public class HelloController extends BaseController
3.利用 SimpleMappingExceptionResolver 类
该类实现了 HandlerExceptionResolver 接口,是 springmvc 默认实现的类,通过它可以实现灵活的异常处理。
只需要在 xml 文件中进行如下配置:
1<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> 2 <property name="exceptionMappings"> 3 <props> 4 <prop key="NullPointerException">nullpointpage</prop> 5 <prop key="IOException">iopage</prop> 6 <prop key="NumberFormatException">numberpage</prop> 7 </props> 8 </property> 9 <property name="statusCodes"> 10 <props> 11 <prop key="nullpointpage">400</prop> 12 <prop key="iopage">500</prop> 13 </props> 14 </property> 15 <property name="defaultErrorView" value="errorpage"/> 16 <property name="defaultStatusCode" value="404"/> 17</bean>
-exceptionMappings:定义 springmvc 要处理的异常类型和对应的错误页面;
-statusCodes:定义错误页面和response 中要返回的错误状态码
-defaultErrorView:定义默认错误显示页面,表示处理不了的异常都显示该页面。在这里表示 springmvc 处理不了的异常都跳转到 errorpage页面。
-defaultStatusCode:定义 response 默认返回的错误状态码,表示错误页面未定义对应的错误状态码时返回该值;在这里表示跳转到 errorpage、numberpage 页面的 reponse 状态码为 404。
4.ajax 异常处理
对于页面 ajax 的请求产生的异常不就适合跳转到错误页面,而是应该是将异常信息显示在请求回应的结果中。
实现方式也很简单,需要继承了 SimpleMappingExceptionResolver ,重写它的异常处理流程(下面会详细分析)。
1public class CustomSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver { 2 3 @Override 4 protected ModelAndView doResolveException(HttpServletRequest request, 5 HttpServletResponse response, Object handler, Exception ex) { 6 7 // 判断是否 Ajax 请求 8 if ((request.getHeader("accept").indexOf("application/json") > -1 || 9 (request.getHeader("X-Requested-With") != null && 10 request.getHeader("X-Requested-With").indexOf("XMLHttpRequest") > -1))){ 11 12 try { 13 response.setContentType("text/html;charset=UTF-8"); 14 response.setCharacterEncoding("UTF-8"); 15 PrintWriter writer = response.getWriter(); 16 writer.write(ex.getMessage()); 17 writer.flush(); 18 writer.close(); 19 } catch (Exception e) { 20 LogHelper.info(e); 21 } 22 return null; 23 } 24 25 return super.doResolveException(request, response, handler, ex); 26 } 27}
配置文件如上,只是将注入的 Bean 替换成我们自己的定义的 CustomSimpleMappingExceptionResolver 。