【SpringBoot】当AOP引发的异常与@RestControllerAdvice擦肩而过:异常处理的盲点揭秘
  ssbY8JrwAR4j 2024年02月19日 273 0

各位上午/下午/晚上好呀!

今天在写bug的时候发现一个这样的问题:

AOP抛出的异常竟然没有被@RestControllerAdvice注解修饰的异常统一处理类处理。

 

需求是这样子滴:对某些加了自定义注解的方法进行切面处理,通过条件判断是否有权限执行该方法。

伪代码大概长这个样子:

    @Around("pointcut()") public Object aroundScheduledMethod(ProceedingJoinPoint joinPoint) throws Throwable { if (!isAccess()) { throw new PException(); } joinPoint.proceed(); } 

其中,PException就是个Exception的子类,没什么特别的:

public class PException extends Exception { }

在@RestControllerAdvice的统一异常处理类中处理了这个异常:

    @ExceptionHandler({PException.class}) public Object handleAuthFailedException(PException e, HttpServletResponse response) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return "Permission denied"; }

到这里应该一切正常吧?讲道理@ExceptionHandler({PException.class})会处理AOP抛出的PException,并返回401

可是啪一跑,就不出意外的出意外了。

执行发现当权限不足时,也就是满足if条件时,主动抛出的异常并没有被@ExceptionHandler({PException.class})方法处理,而是返回了Internal Server Error。

Internal Server Error是哪里来的?别急,@RestControllerAdvice注解修饰的统一异常处理类中确实有这个,是用来捕获Exception类的,就是给整个Controller的异常处理兜底。代码如下:

    @ExceptionHandler({Exception.class}) public Object handleException(Exception e, HttpServletResponse response) { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); log.error("Internal Server Error", e); return "Internal Server Error"; }

虽然我觉得上面这个东西的出现并不合理:

这个异常兜底可能会导致部分由Spring框架本身处理的异常被屏蔽掉,比如当接口返回的HttpRequestMethodNotSupportedException异常,实际上的response code是由Spring来设置成405的。如果在@RestControllerAdvice中没有处理HttpRequestMethodNotSupportedException,就会由兜底返回Internal Server Error。

不过有的项目确实希望把所有的异常控制在一定的范围内,不对外暴露,比如像下面这样:

    @ExceptionHandler({Exception.class}) public Object handleException(Exception e, HttpServletResponse response) { response.setStatus(HttpStatus.BAD_REQUEST.value()); log.error("Internal Server Error", e); return "Bad Request"; }

日志中会完整的输出Exception信息,但是对外只会报错400 Bad Request来隐藏服务的异常。

扯远了,我们在这里不讨论兜底处理Exception的合理性,只讨论PException到底哪里去了,为什么没有被@ExceptionHandler({PException.class})方法处理,反而被@ExceptionHandler({Exception.class})方法处理了

问题分析:

 

首先看日志:

发现我们定义的PException信息确实打印出来了,不过抛出了一个UndeclaredThrowableException,嗯?这样异常哪里来的呢?跟我们定义的PException之间又是什么关系?

Debug断点看看到底什么时候处理了PException这个异常:

 继续

然后就会走到这么一个地方,好像看到日志中那个UndeclaredThrowableException异常了,继续

 哎,确实走到了throw new UndeclaredThrowableException这行代码,而且我们本身的PException被作为一个参数传进去了。

 追踪这个方法,发现传入的PException被赋值给了一个undeclaredThrowable变量,这个变量有什么用,我们一会儿再说,先记下它

 也就是说,在AOP中抛出的异常最终被包装成了一个UndeclaredThrowableException,但是有一个前提:

通过反射判断是否属于切点执行方法的声明的Exception,如果不是且不属于RuntimeException,那么将会使用UndeclaredThrowableException包装原始的Exception。

判断方法如下:

抛出UndeclaredThrowableException后由@ExceptionHandler({Exception.class})方法捕获处理。

问题已经定位到了,解决方案也显而易见:

1.让PException继承RuntimeException,不过在某些情况下未必合理。根据业务来。

2.在切点执行方法中声明抛出PException,即使在该方法中根本不会抛出该异常,但只要在AOP中可以抛出,那就需要声明。

3.去掉兜底的@ExceptionHandler({Exception.class}),让SpringBoot处理。

 

到这,问题已经解决了。吗?

你难道一点儿都不好奇?

SpringBoot为什么可以正常的处理PException?

 

具体调试流程就不展开了,最终在SpringBoot的ExceptionHandlerExceptionResolver中找到了答案,看下面的422和423行代码,cause调用了Throwable的getCause()方法:

看下这个方法在UndeclaredThrowableException中的实现:

 到这是不是都说的通了呀?还记得之前提到的undeclaredThrowable变量吗?

 

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2024年02月19日 0

暂无评论

推荐阅读
  2Vtxr3XfwhHq   2024年05月17日   55   0   0 Java
  Tnh5bgG19sRf   2024年05月20日   114   0   0 Java
  8s1LUHPryisj   2024年05月17日   49   0   0 Java
  aRSRdgycpgWt   2024年05月17日   47   0   0 Java
ssbY8JrwAR4j