SpringMVC源码解析---ExceptionHandler异常处理实现原理

1.概述

通常在一个web应用当中,当用户的操作不当,或者程序出现bug,就会出现大量的异常。其中有些异常是需要暴露给用户的,返回一个异常处理界面给用户,而不是直接出现404、500等不人性化的界面。

这就需要有统一的调度,统一的处理这些异常。将程序的异常转换为用户可读的异常。

这篇博客将从异常处理的使用到原理实现进行讲解。

2.ExceptionHandler的使用

比较常用的两种方式,一种是通过xml配置SimpleMappingExceptionResolver,根据异常的类型返回异常处理的视图。另一种是@ExceptionHandler注解,配合@ControllerAdvice使用,编写异常处理类。

2.1 SimpleMappingExceptionResolver 配置

通过xml的配置方式,当出现某个异常的时候,可以返回指定的异常处理界面。配置的方式如下:

		<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
            <property name="defaultErrorView" value="error"/>
        <property name="exceptionMappings" >
            <props>
                <prop key="java.io.IOException">IoError</prop>
                <prop key="java.lang.IllegalArgumentException">IllegalError</prop>
            </props>
        </property>
    </bean>

配置属性 key是异常的名称,标签中的值就是异常处理对应的页面

例如上面异常为java.io.IOException 它的异常处理的对应页面就是IoError.jsp

2.2 @ExceptionHandler

当一个Controller中有方法加了@ExceptionHandler之后,这个Controller其他方法中没有捕获的异常就会以参数的形式传入加了@ExceptionHandler注解的那个方法中。

例如,在下面的方法中处理异常并返回ModelAndView。

@ExceptionHandler
public ModelAndView handleException(Exception ex) {
    System.out.println(ex);
    ModelAndView mv = new ModelAndView();
    mv.addObject("exception", new RuntimeException("这个是由@ExceptionHandler捕捉的异常"));
    mv.setViewName("error");
    return mv;
}

@ExceptionHandler 还提供了一个value属性,该属性是一个数组,可以指定多个异常的名称。这样就可以写多个handleException,并且通过value来指定要处理的异常的名称,处理不同的异常。

例如

@ExceptionHandler(value = {IllegalArgumentException.class, IOException.class})

但是@ExceptionHandler 只能拦截这一个Controller中的方法抛出的异常。

所以Spring来提供了一个注解@ControllerAdvice,该注解是加在类上的。表明该类中的ExceptionHandler可以拦截所有Controller中的异常。

@Controller
@ControllerAdvice
public class MethodHandler {

    @ExceptionHandler(value = {IllegalArgumentException.class, IOException.class})
    public ModelAndView handleException(Exception ex) {
        System.out.println(ex);
        ModelAndView mv = new ModelAndView();
        mv.addObject("exception", new RuntimeException("这个是由@ExceptionHandler捕捉的异常"));
        mv.setViewName("error");
        return mv;
    }
}

所以可以专门写一个Controller 用来写ExceptionHandler,进行统一的异常拦截处理。

3.ExceptionHandler的初始化

上面讲了通过xml配置的方式或者@ExceptionHandler注解的方式可以得到ExceptionHandler。是如何做到的呢?

Spring容器初始化阶段,在初始化ExceptionHandlerExceptionResolver的时候,会执行afterPropertiesSet()方法,这是Bean生命周期中的一步。进一步的初始化Bean。在ExceptionHandlerExceptionResolver的afterPropertiesSet()方法中,会调用initExceptionHandlerAdviceCache()代码如下:

private void initExceptionHandlerAdviceCache() {
   if (getApplicationContext() == null) {
      return;
   }
   // 扫描 @ControllerAdvice 注解的Bean,并进行排序
   List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
   // 遍历 ControllerAdviceBean 数组
   for (ControllerAdviceBean adviceBean : adviceBeans) {
      Class<?> beanType = adviceBean.getBeanType();
      if (beanType == null) {
         throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
      }
      // 扫描该 ControllerAdviceBean 对应的类型
      ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
      // 有 @ExceptionHandler 注解,则添加到 exceptionHandlerAdviceCache 中
      if (resolver.hasExceptionMappings()) {
         this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
      }
      // 如果该 beanType 类型是 ResponseBodyAdvice 子类,则添加到 responseBodyAdvice 中
      if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
         this.responseBodyAdvice.add(adviceBean);
      }
   }

   if (logger.isDebugEnabled()) {
      int handlerSize = this.exceptionHandlerAdviceCache.size();
      int adviceSize = this.responseBodyAdvice.size();
      if (handlerSize == 0 && adviceSize == 0) {
         logger.debug("ControllerAdvice beans: none");
      }
      else {
         logger.debug("ControllerAdvice beans: " +
               handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
      }
   }
}
  1. 首先找到加了注解@ControllerAdvice的Bean

  2. 遍历找到的所有的Bean,根据Bean类型构建ExceptionHandlerMethodResolver对象。ExceptionHandlerMethodResolver(beanType)方法如下:

    public ExceptionHandlerMethodResolver(Class<?> handlerType) {
       // 遍历 @ExceptionHandler 注解的方法
       for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
          // 遍历处理的异常集合
          for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
             // 添加到 mappedMethods 中
             addExceptionMapping(exceptionType, method);
          }
       }
    }
    
    • 逻辑很简单,就是遍历这个类中的所有的方法,找到加了注解@ExceptionHandler的方法。然后遍历这个方法中的异常类型的映射。也就是该方法可以处理的异常类型。将它添加到mappedMethods当中。key是异常类型,value是对应的异常处理的方法。
  3. 这样一个类就对应这一个ExceptionHandlerMethodResolver对象,保存在exceptionHandlerAdviceCache缓存当中。就可以做到不仅一个Controller可以使用了。单例池中结果如下:在这里插入图片描述

  4. 之后在DispatcherServlet初始化的时候,会调用initHandlerExceptionResolvers(),该方法从spring容器中找HandlerExceptionResolver类型的Bean,添加到成员变量handlerExceptionResolvers当中。并排序。

对应xml配置的就更为简单了。

容器初始化的时候,扫描xml配置文件,解析Bean标签,构建SimpleMappingExceptionResolver对象。填充xml中配置的属性。就完成了SimpleMappingExceptionResolver的初始化。

4.ExceptionHandler的触发时机

在doDispatch中有一个局部变量Exception dispatchException = null,用于存储catch到的异常。并在调用processDispatchResult的时候,会将这个局部变量传入。processDispatchResult代码如下:

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
      @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
      @Nullable 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);
      }
   }

   // Did the handler return a view to render?
   if (mv != null && !mv.wasCleared()) {
      render(mv, request, response);//解析视图 分发结果
      if (errorView) {
         WebUtils.clearErrorRequestAttributes(request);
      }
   }
   else {
      if (logger.isTraceEnabled()) {
         logger.trace("No view rendering, null ModelAndView returned.");
      }
   }

   if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
      // Concurrent handling started during a forward
      return;
   }

   if (mappedHandler != null) {
      // Exception (if any) is already handled..
      mappedHandler.triggerAfterCompletion(request, response, null);//触发拦截器完成处理
   }
}

调用processHandlerException来调用异常处理器处理异常,代码如下:

public ModelAndView resolveException(
      HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
   // 判断是否可以应用
   if (shouldApplyTo(request, handler)) {
      // 阻止缓存
      prepareResponse(ex, response);
      // 执行解析异常,返回 ModelAndView 对象 由子类实现
      ModelAndView result = doResolveException(request, response, handler, ex);
      // 如果 ModelAndView 对象非空,则进行返回
      if (result != null) {
         // Print debug message when warn logger is not enabled.
         if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
            logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
         }
         // Explicitly configured warn logger in logException method.
         // 打印异常日志
         logException(ex, request);
      }
      // 返回 ModelAndView 对象
      return result;
   }
   else {
      return null;
   }
}

真正的异常处理是调用doResolveException,由子类实现,根据不同类型的HandlerExceptionResolver执行不同的逻辑。

根据上面异常处理的时机,可以得出的结论是异常处理拦截的是视图解析之前的逻辑。也就是从getHandler开始到执行了拦截器后置处理的地方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值