DEBUG 怪诞之背后的秘密
缘起
昨天接到同事高禹的报告,说 eql 从0.0.72升级到0.0.96版本后,原来可以用的代码,有一处不能用了,但是如果把参数名从 table 改为 table1,那就没问题。凭直觉感觉这应该是一个很有意思的 BUG,我就他那边把出 BUG 的 SQL 和代码都要过来了,先添加一个 新的测试用例。然后我就直接RUN这个测试用例,发现果然不通过。很纳闷,开始单点调试(DEBUG)看看,但是结果竟然反转了,竟然能通过了。多次试验皆如此。有意思有意思,简直百思不得其解了。
重现
随着对代码更进一步的跟踪与分析,我逐渐发现了原因了。为了简单的重现原因,我截了两个图,第一张图是加了两个断点跑的图,结果是v1和v2相等的,都是同样的一串内容。如下:
第二张图是去掉了第一个断点,只保留第二个断点,结果是v1和v2完全是不一样的东西。如下:
匹夫无罪,怀璧(Map 动态代理)其罪
到底是为什么呢?从两张图上的右下角的 console 输出差别可以看出来,在 debug 断点时,IDE 为了展示变量的概要信息,其实是做了一些调用的。在第一张图停在第一个断点的时候,IDE 其实在背后调用了 Map.isEmpty 和 Map.size 方法,然后才是被调试程序调用了 Map.get 方法。而在第二张图只有一个断点的时候,IDE 在 Map.get 方法调用之前,并没有调用 Map.isEmpty 和 Map.size 方法。
问题是,这个 Map 不是普通的 Map,是一个 JAVA 的动态代理,如下:
所以,这个代理实现上有缺陷,取 map 中属性为 table 的值时,又去找了名字为 table 的属性,结果 HashMap 中刚好有这个属性,于是取出来了,v1 的值就成了 HashMap$Node 类型了(见下图),导致了问题的存在,所以改成 table1 就没这个问题,但是如果使用 entrySet 肯定有问题。
问题定位了,修复起来就简单得多,只需要在 if 判断 Map 类型之外,取属性/方法的时候 加一个 else 就行了。
为什么要用动态代理
当初为什么要做一个动态代理呢,其实这个跟 eql 的参数传递实现有关系。一开始是并没有这个动态代理的,直到某一天,生产环境上老是报告空指针异常,然后异常的堆栈就指向了 eql 的内部实现。我一查,都是使用 JavaBean 传递参数遭遇问题,因为 JavaBean 里面有一些属性并没有被 sql 用到,却调用 get 方法,导致空指针异常。比如下面这样:
public class Person {
// ...
public String getName() {
return name;
}
public String getImage() {
throw new NullPointerException();
}
}
-- [findPersonByName] SELECT NAME,SEX,EMAIL,ADDRESS FROM PERSON WHERE NAME = #name#
eql 的使用者感觉很无奈,sql 里面只用到了 name,并没有使用 image,为什么要调用 image 方法呢。确实是这样子,刚开始时,我是为了省事,把 Bean 的所有属性都转换成 Map 使用了,而不会管这个属性在 sql 中有没有实际使用到。(注:JavaBean 的 getter/setter 方法应该只是简单的 get 和 set,抛出异常是违反约定的)。所以为了避免这种无用的调用,我就开挂了 Map 的动态代理,只有等真正需要读取属性的时候,才去调用(Map 就用 get 取,JavaBean 用 getter 或者 filed 取)。然后这就种下了本次问题的因了。
总结
Debug 断点模式下,IDE 为了展示堆栈上变量的信息,经常会调用一些方法,比如 toString() 方法,比如 Map.isEmpty,Map.size 等等。这些方法一般情况下都不会有问题,所以 DEBUG 与非 DEBUG 行为上一般都是一致的。但是在某些特殊情况下,却可能产生不一致的行为,比如本案所示,比如当初我在跟踪 druid 的 sql 解析代码时,就发现 IDE 的 DEBUG 窗口的一些变量直接就报告了空指针异常,然后我发现原来作者对变量的 toString() 方法实现没有特别考虑和测试。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 我是这些常数收藏爱好者
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论