2024-12-16  阅读(8)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1147481945

回答

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("/skjava")
    public String skjava() {
        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);
    }
}

skjava() 抛出 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);
      }
      // ...
  }

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文