有鬼!我 throw 的异常,竟然不会中止代码

2023-10-25 20:10:47 浏览数 (2)

大家好,我是一航!

今天1024程序员节日,在这里祝大家节日快乐

近期的一个需求开发中,遇到了一个非常诡异的小bug,忍不住要分享一下;第一眼看到这个bug时,满脑子就是曹老板的那句:不可能,绝对不可能。问题总结起来就一句话:明明一个方法执行 throw 了一个异常,调用方也没有 try-catch 捕获,结果异常后,代码依然很丝滑的往下继续执行了

看一段示例伪代码

一个用于验证请求的工具类

代码语言:javascript复制
@Slf4j
@Component
public class VerifyUtil {

    public void userVerify(Integer userId){
        log.info("验证失败!抛出异常");
        throw new BaseException(UserErrStatusCode.ERR_2000);
    }

}

去掉了无关的逻辑,目前调用这个方法就只会抛个异常

一个测试接口

代码语言:javascript复制
@Slf4j
@RequestMapping("test")
@RestController
public class TestController extends BaseController {

    @Autowired
    VerifyUtil verifyUtil;

    @GetMapping("get/{userId}")
    public BaseResponceDto getUserInfo(@PathVariable Integer userId) {
        log.info("接收用户ID:{}", userId);

        log.info("开始验证用户:{}", userId);

        verifyUtil.userVerify(userId);

        log.info("验证完成,返回数据!");
        return ReturnUtils.success();
    }
}

当代码跑起来之后,我们调用接口http://127.0.0.1:8085/test/get/1,哪怕你是刚入行 java 的同学,也能很容易看明白这段代码,最后的执行结果肯定会在verifyUtil.userVerify(userId);这里抛出个异常,并响应前端错误,后续流程不会继续执行!

我作为一个练习时长两年半的 javaer ,自然也是这么认为的,可执行结果却是:

代码语言:javascript复制
com.ehang.responce.rest.TestController   : 接收用户ID:1
com.ehang.responce.rest.TestController   : 开始验证用户:1
com.ehang.responce.rest.VerifyUtil       : 验证失败!抛出异常
com.ehang.responce.rest.TestController   : 验证完成,返回数据!

我写的代码,他居然在异常之后,还继续执行了后续的代码。

问题原因

事出反常必有妖...

经过一圈的排查,发现这里的代码并没有问题;导致这个bug的主要是因为一个不太规范的AOP操作,拦截了异常,使得异常虽然抛是抛了,但是抛了个寂寞,后续的流程依然继续在执行;

问题复现

项目中的所有接口都放在一个rest的目录下,为了规范前后端的交互,确保前端的每次请求,无论是正常还是异常,都能够拿到一个友好的 JSON 应答,于是项目中使用了AOP来切了所有的 Controller 接口,做了一些未处理异常的拦截操作,代码如下:

代码语言:javascript复制
@Slf4j
@Aspect
@Component
public class ExAop {

    @Around("(execution(public * com.ehang.*.rest..*.*(..)))")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        try {
            return pjp.proceed();
        } catch (BaseException e) {
            return ReturnUtils.error(e);
        } catch (Exception e) {
            return ReturnUtils.error(BaseStatusCode.ERR_9999);
        }
    }
}

切面通过 @Around("(execution(public * com.ehang.*.rest..*.*(..)))") 设置了rest目录下的所有方法为切点,一旦方法执行异常,且没有处理成自定义 BaseException 异常,就统一响应一个未知错误的应答。

就到目前为止,这样写是没有任何问题的;

现在来一个新的需求,在 Controller 接收到参数就需要做一些特殊的校验,因为是只在 Controller 中处理,开发的时候,就顺手在rest目录下建了个util目录,写了个校验的 Util 工具类,来校验参数,在不满足条件的情况下抛出异常;

伪代码和目录结构如下:

这么一写,就出现了文章一开头说的问题了,这个Controller里面的校验方法不管怎么抛异常,都能正常执行后续的代码;

原因分析

就单从细节上来说,无论是AOP,还是 Controller ,都是正常的;但是把AOP切点表达式:(execution(public * com.ehang.*.rest..*.*(..)));和新增加的 util 目录结合在一起,问题就出现了;

这个表达式指定了 rest 及其子目录下所有类的所有方法;那么这一刀下去,不光切了 rest 目录下的 Contrller ,连 util 的所有方法也一并切了,当执行verifyUtil.userVerify(userId) 并throw 异常之后,ExAop 拦截了异常,并执行了return ReturnUtils.error(e);,将异常处理并返回了一个对象,由于本身userVerify无返回参数,最终的效果就是verifyUtil.userVerify成功执行并继续执行了后续的代码。

相当于变成如下代码:

代码语言:javascript复制
try {
    verifyUtil.userVerify(userId);
} catch (BaseException exception) {
    exception.printStackTrace();
}

虽然这个try - catch 我并没有写,但是AOP的代理增强帮我做了这个事情,这么说的话,这个 bug 的出现似乎就可以解释通了。

解决方式

只要分析出了原因,要解决起来还是比较容易的,方法也有很多种:

将rest目录下的 util 挪走

既然是AOP增强了rest目录下所有类的所有方法,那 rest 目录下就不要放Controller 以外的无关东西;

将 Util 工具类定义成静态方法

将工具类的方法变成静态方法之后,就不会被AOP增强;

细化AOP的切点表达式

代码语言:javascript复制
@Around("(execution(public * com.ehang.*.rest..*.*Controller(..)))")

统一Controller的类名格式为xxxController,然后在表达式中限定,只切类名为Controller后缀下的所有方法;

总结

这种多个正常的设计结合,化学反应后产生的问题,排查起来就会有些头疼;如果你不太了解AOP、或者是接手了一个新的项目,不熟悉项目结构,这种违背常理的问题出现,还是有些让人匪夷所思的...

最后,再次祝所有的同行,1024节日快乐!

0 人点赞