前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >@ControllerAdvice全局异常处理器没生效?生产报错日志看得眼花缭乱...

@ControllerAdvice全局异常处理器没生效?生产报错日志看得眼花缭乱...

作者头像
xiaoyi
发布2024-04-10 16:05:32
980
发布2024-04-10 16:05:32
举报
文章被收录于专栏:小义思小义思

这几天在查看生产日志的时候发现,某个接口打印的报错信息很奇怪,明明是业务异常,却提示接口异常,仔细核查代码,发现原来是@ControllerAdvice全局异常处理器没生效!怎么会这样?本着知根知底的原则,今天就来看看是怎么一回事。

场景复现

简单复现一下代码逻辑,首先创建一个基本控制类BaseController,定义resolveException方法用于捕捉异常。因为Exception是所有异常的父类,所以当程序中抛出异常,添加ExceptionHandler注解的方法中没有具体的异常类型与之相匹配时,那都会被下面的方法捕获处理。

代码语言:javascript
复制
@RestController
public class BaseController {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseResult resolveException(Exception e) {
        return ResponseResult.RESULT_ERROR.format("接口请求失败");
    }
    
}

创建测试控制类,继承于BaseController,定义testV1方法抛出RuntimeException异常进行验证。

代码语言:javascript
复制
@RestController
@RequestMapping("/hand")
public class HandlerController extends BaseController {

    @PostMapping("/v1")
    public ResponseResult testV1() {
        ResponseResult success = new ResponseResult("success");
        throw new RuntimeException("xxx");
    }

}

再创建一个@ControllerAdvice配置的全局异常处理器。

代码语言:javascript
复制
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseResult resolveException(Exception e) {
        return ResponseResult.RESULT_ERROR.format("全局提醒-接口请求失败");
    }

    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public ResponseResult resolveRuntimeException(Exception e) {
        return ResponseResult.RESULT_ERROR.format("全局提醒-接口运行失败");
    }
}

运行程序,调用/hand/v1接口发现,GlobalExceptionHandler类中的方法并没有执行,而是返回的BaseController里面的结果:接口请求失败。去掉继承类,则返回:全局提醒-接口运行失败。从结果来看,BaseController里面的ExceptionHandler已经捕获了异常,所以全局异常处理器的就给忽略了。

原理分析

打断点进行追踪,如果@PostMapping方法抛出异常,在DispatcherServlet前置控制器的doDispatch方法中,会进入processDispatchResult,判断入参Exception不为null,代表发生异常,调用processHandlerException处理。

processHandlerException方法代码位置:org.springframework.web.servlet.DispatcherServlet#processHandlerException

代码语言:javascript
复制
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
   @Nullable Object handler, Exception ex) throws Exception {

  // Success and error responses may use different content types
  request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

  // 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;
    }
   }
  }
 //省略其余代码...
}

this指当前对象dispatchServlet,handlerExceptionResolvers有三个,分别是ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver,都是Spring框架帮注册的,具体的作用分别如下:

ExceptionHandlerExceptionResolver

是HandlerExceptionResolver接口的一个实现类,该类提供了@ExceptionHandler注解用来捕获指定类型的异常。

DefaultHandlerExceptionResolver

Springmvc默认装配的异常解析器,会对一些特殊的异常,如NoSuchRequestHandlingMethodException、HttpRequestMethodNotSupportedException、HttpMediaTypeNotSupportedException、HttpMediaTypeNotAcceptableException等进行处理。

ResponseStatusExceptionResolver

用于处理ResponseStatusException异常,该类根据异常的状态码和原因消息来生成一个相应的HTTP响应。可以通过ResponseStatusExceptionResolver类提供的@ResponseStatus注解来定制页面异常信息。

言归正传,下面继续追踪代码,在getExceptionHandlerMethod方法中,可以看到@ExceptionHandler注解的执行顺序。

getExceptionHandlerMethod代码位置:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#getExceptionHandlerMethod

代码语言:javascript
复制
 /**
  * Find an {@code @ExceptionHandler} method for the given exception. The default
  * implementation searches methods in the class hierarchy of the controller first
  * and if not found, it continues searching for additional {@code @ExceptionHandler}
  * methods assuming some {@linkplain ControllerAdvice @ControllerAdvice}
  * Spring-managed beans were detected.
  * @param handlerMethod the method where the exception was raised (may be {@code null})
  * @param exception the raised exception
  * @return a method to handle the exception, or {@code null} if none
  */
 @Nullable
 protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
   @Nullable HandlerMethod handlerMethod, Exception exception) {

  Class<?> handlerType = null;

  if (handlerMethod != null) {
   // Local exception handler methods on the controller class itself.
   // To be invoked through the proxy, even in case of an interface-based proxy.
   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);
   }
   // For advice applicability check below (involving base packages, assignable types
   // and annotation presence), use target class instead of interface-based proxy.
   if (Proxy.isProxyClass(handlerType)) {
    handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
   }
  }

  for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
   ControllerAdviceBean advice = entry.getKey();
   if (advice.isApplicableToBeanType(handlerType)) {
    ExceptionHandlerMethodResolver resolver = entry.getValue();
    Method method = resolver.resolveMethod(exception);
    if (method != null) {
     return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
    }
   }
  }

  return null;
 }

@Controller+@ExceptionHandler的信息缓存在ExceptionHandlerExceptionResolver的exceptionHandlerCache中,而@ControllerAdvice+@ExceptionHandler的信息缓存在ExceptionHandlerExceptionResolver的exceptionHandlerAdviceCache中。从上面代码的执行顺序中不难看出,当exceptionHandlerCache中有值时,自然就不会往下运行去判断exceptionHandlerAdviceCache缓存里面的东西了。

总结

当程序中抛出一个异常时,Spring 框架会捕获这个异常,并依次按照如下顺序开始寻找合适的@ExceptionHandler方法来处理这个异常。

  1. Spring 会从当前控制器类中查找是否有匹配的@ExceptionHandler方法。如果有多个@ExceptionHandler方法,Spring 会根据异常类型和方法参数来选择最匹配的一个。
  2. 如果在当前控制器类中没有找到匹配的@ExceptionHandler方法,Spring 会向上搜索控制器的父类,看看是否有匹配的方法。
  3. 当在控制器的继承链中都没有找到匹配的@ExceptionHandler方法时,Spring会继续查找@ControllerAdvice注解配置的类或HandlerExceptionResolver接口实现的全局异常处理器来处理异常。
  4. 如果全局异常处理器也没有处理这个异常,那么 Spring 就会把异常传递给容器,由容器来决定如何处理这个异常。通常,容器会把异常信息写入到响应中,并返回一个错误状态码。

通过实现HandlerExceptionResolver接口来实现的全局异常处理器,执行的优先级比起@Controller+@ExceptionHandler和@ControllerAdvice+@ExceptionHandler是最低的,而且相比于前两种处理起来要麻烦得多,感兴趣的小伙伴可以自行尝试。

本文参与?腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-02-28,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 程序员小义 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体同步曝光计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 场景复现
  • 原理分析
    • ExceptionHandlerExceptionResolver
      • DefaultHandlerExceptionResolver
        • ResponseStatusExceptionResolver
        • 总结
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
        http://www.vxiaotou.com