Spring MVC 是如何做统一异常处理?

Spring MVC 统一异常处理方式解析

 作者简介:大家好,我是码炫码哥,前中兴通讯、美团架构师,现任某互联网公司CTO,兼职码炫课堂主讲源码系列专题


代表作:《jdk源码&多线程&高并发》,《深入tomcat源码解析》,《深入netty源码解析》,《深入dubbo源码解析》,《深入springboot源码解析》,《深入spring源码解析》,《深入redis源码解析》等


联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬。码炫课堂的个人空间-码炫码哥个人主页-面试,源码等

回答

Spring MVC 提供了三种方式来进行统一异常处理:

  1. 使用 @ExceptionHandler :使用 @ExceptionHandler 标注在 Controller 的方法上面,当 Controller 中的方法抛出指定异常时,Spring 会自动调用标记了 @ExceptionHandler 的方法进行处理。但是该方式只对特定Controller 有效。
  2. 实现 HandlerExceptionResolver 接口:HandlerExceptionResolver 是 Spring MVC 提供的一个异常处理接口,它允许我们再应用层处理所有异常,当一个 Controller 抛出异常时,Spring 会通过 HandlerExceptionResolver 来决定如何处理该异常,我们可以实现该接口来定制全局的异常处理逻辑。
  3. 使用 @ControllerAdvice + @ExceptionHandler:目前最主流的处理方式。@ControllerAdvice 允许我们定义一个全局异常处理器,结合 @ExceptionHandler ,@ControllerAdvice 就可以捕获所有 Controller 中的异常并统一处理。

详解

使用 @ExceptionHandler

示例如下:

@Controller
public class SkJavaController {

    @RequestMapping("/mx")
    public String mx() {
        if (true) {
            throw new RuntimeException("抛个异常看看...");
        }
        return "success";
    }

    // 使用 @ExceptionHandler 注解处理 RuntimeException 异常
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
        return new ResponseEntity<>("发生了 RuntimeException: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

当 mx() 抛出 RuntimeException 异常时,Spring MVC 会 handleRuntimeException() 方法处理该异常,并返回一个带有错误信息和状态码的响应。但是这种方式至对特定的 Controller 有效。如果想要为整个应用提供全局异常处理,则需要使用下面两种方式。

当然,这种方式也不是一无是处,当我们只需要在某些特定的 Controller 中处理特定的异常时,@ExceptionHandler 还是非常合适的。

实现 HandlerExceptionResolver 接口

HandlerExceptionResolver 是 Spring MVC 提供的一个异常处理接口,它允许我们再应用层处理所有异常,当一个 Controller 抛出异常时,Spring 会通过 HandlerExceptionResolver 来决定如何处理该异常,我们可以实现该接口来定制全局的异常处理逻辑。

示例:

@Component
public class SkJavaExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 在这里可以根据异常类型返回不同的视图或响应
        ModelAndView modelAndView = new ModelAndView();
        if (ex instanceof NullPointerException) {
            modelAndView.setViewName("errorPage");
            modelAndView.addObject("message", "空指针异常发生");
        } else {
            modelAndView.setViewName("genericError");
            modelAndView.addObject("message", "系统发生了错误");
        }
        return modelAndView;
    }
}

这种方式可以全局捕获所有异常,并且能够根据异常的不同可以灵活定制不同的处理逻辑。

使用 @ControllerAdvice + @ExceptionHandler

这是目前统一异常处理的最主流方式。

@ControllerAdvice 是一个类级别注解,允许我们定义一个全局异常处理器。结合 @ExceptionHandler ,@ControllerAdvice 就可以捕获所有控制器中的异常,并统一处理。@ControllerAdvice 本质上是全局异常处理的一个封装器。

@ControllerAdvice 可以作用于整个应用,处理所有控制器层抛出的异常,而不需要在每个控制器中都单独写异常处理方法。它使得异常处理代码得以集中管理,提升了代码的清晰度和维护性。

示例:

@ControllerAdvice
public class GlobalExceptionHandler {

    // 捕获所有异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception ex) {
        return new ResponseEntity<>("发生了异常:" + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // 捕获特定异常
    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity<String> handleNullPointerException(NullPointerException ex) {
        return new ResponseEntity<>("空指针异常:" + ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

@ControllerAdvice + @ExceptionHandler 原理分析

@ControllerAdvice 和 @ExceptionHandler 的初始化

@ControllerAdvice 的定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
  //..
}

使用 @Component ,所以 @ControllerAdvice 本质上就是一个 Spring 组件,Spring 容器在启动时会扫描到它并将其注册到 Spring 容器中。@ControllerAdvice 的处理逻辑在 ExceptionHandlerExceptionResolver 中。

  public void afterPropertiesSet() {
    // Do this first, it may add ResponseBodyAdvice beans
    initExceptionHandlerAdviceCache();

    // ...
  }

调用 initExceptionHandlerAdviceCache() 初始化 ExceptionHandler:

  private void initExceptionHandlerAdviceCache() {
    if (getApplicationContext() == null) {
      return;
    }
    
    // 委托 ControllerAdviceBean 来扫描 @ControllerAdvice
    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    for (ControllerAdviceBean adviceBean : adviceBeans) {
      Class<?> beanType = adviceBean.getBeanType();
      if (beanType == null) {
        throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
      }
      
      // 封装为 ExceptionHandlerMethodResolver 
      ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
      if (resolver.hasExceptionMappings()) {
        // 将 ExceptionHandlerMethodResolver 存储到 exceptionHandlerAdviceCache
        this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
      }
      if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
        this.responseBodyAdvice.add(adviceBean);
      }
    }

   // ...
  }

委托 ControllerAdviceBean 来扫描 @ControllerAdvice

  public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
    // ...
    List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
    for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Object.class)) {
      if (!ScopedProxyUtils.isScopedTarget(name)) {
        // 扫描 @ControllerAdvice
        ControllerAdvice controllerAdvice = beanFactory.findAnnotationOnBean(name, ControllerAdvice.class);
        if (controllerAdvice != null) {
          // Use the @ControllerAdvice annotation found by findAnnotationOnBean()
          // in order to avoid a subsequent lookup of the same annotation.
          adviceBeans.add(new ControllerAdviceBean(name, beanFactory, controllerAdvice));
        }
      }
    }
    OrderComparator.sort(adviceBeans);
    return adviceBeans;
  }

当扫描所有的 @ControllerAdvice 标注的类后,ExceptionHandlerExceptionResolver 会对其进行遍历,然后封装为 ExceptionHandlerMethodResolver,并保存到 exceptionHandlerAdviceCache

ExceptionHandlerMethodResolver 是 Spring MVC 的异常解析器,其初始化如下:

  public ExceptionHandlerMethodResolver(Class<?> handlerType) {
    for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
      for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
        addExceptionMapping(exceptionType, method);
      }
    }
  }
  
 public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);

ExceptionHandlerMethodResolver 会查找 @ControllerAdvice 标注的类中包含有 @ExceptionHandler 的方法,同时对 @ExceptionHandler 进行解析,获取其 exceptionType ,将他们添加到 mappedMethods 中,其中 key 为 exceptionType ,value 为对应的 Method。

@ControllerAdvice 和 @ExceptionHandler 处理异常

DispatcherServlet 负责请求的处理流程,其中 processHandlerException() 负责处理异常:

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

    // ...

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

一次调用 HandlerExceptionResolver 的 resolveException(),我们一路跟踪,最终会到 ExceptionHandlerExceptionResolver 中的 doResolveHandlerMethodException()

  protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
      HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
    
    // 获取具体的异常处理方法
    ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    
    // ...

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

    ArrayList<Throwable> exceptions = new ArrayList<>();
    try {
      // ...
      // 执行异常处理方法
      exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
    }
    catch (Throwable invocationEx) {
     // ...
    }

    // ...
  }

调用 getExceptionHandlerMethod() 获取具体的异常处理 HandlerMethod:

  protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
      @Nullable HandlerMethod handlerMethod, Exception exception) {

    Class<?> handlerType = null;

    if (handlerMethod != null) {
      
      // 从 exceptionHandlerCache 中获取 ExceptionHandlerMethodResolver
      handlerType = handlerMethod.getBeanType();
      ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
      if (resolver == null) {
         // 没有的话,就封装,加入缓存
        resolver = new ExceptionHandlerMethodResolver(handlerType);
        this.exceptionHandlerCache.put(handlerType, resolver);
      }
      
      // 提取对应的处理方法
      Method method = resolver.resolveMethod(exception);
      if (method != null) {
        return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
      }
      // ...
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值