前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >有鬼!我 throw 的异常,竟然不会中止代码

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

作者头像
一行Java
发布2023-10-25 20:10:47
1150
发布2023-10-25 20:10:47
举报

大家好,我是一航!

今天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节日快乐!

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

本文分享自 一行Java 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题原因
  • 问题复现
    • 原因分析
    • 解决方式
    • 总结
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
    http://www.vxiaotou.com