今天有一次比较苦逼的查错经历。
最近接过一离职同事的项目,是某个系统的管理界面(Java Web)。刚好有一个任务,需要修改一下读取某个配置的代码。开发的过程只是改动就三、四行后台的代码,想着对系统没有什么影响的,打包部署到Tomcat后,点了几个页面,读取配置正确,没发现什么不妥,就提交给QA了。
结果是QA发现大量的问题,很多页面看着正常,但一旦提交表单就出错,而且出的错误都是NPE。
首先看了QA的截图和日志的异常堆栈。大致上确定了出错的类和方法。于是分析了一下这里的代码逻辑:
定义了一些Bean类存放数据,还有很多内部类(接口),内部类上定义了很多常量。这个容易理解,很多人会借助接口来定义常量。
不可理解的地方是这里:写了一个方法getFieldValue,根据类名,通过反射机制去获取某个属性(或者内部类的常量)。例如,输入:com.example.Clazz.InnerClazz.type_name,可以获取内部类的type_name的值。这个方法被做成一个标签,在jsp页面上大量使用。(把常量写到资源文件,不更好?不过既然是接手别人的项目,那只能跟着别人的思路走)getFieldValue方法返回了null,导致了大量的NPE被抛出。
既然找到了切入口,就开始调试程序,先写了个测试用例——简单地调用了一下getFiedlValue方法,然后判断返回值。测试用例顺利通过,一切正常。显然它没有受到我的改动影响——本来也是八杆子打不着的两个功能。
既然调试,启动Tomcat、在Idea设置好断点,打开远程调试、提交表单、一步一步跟踪getFieldValue方法,终于发现问题的元凶在这里:
tmpClazz.getDeclaredClasses();
上面的代码返回了空数组。我赶紧检查了一下tmpClazz的值,确实是正确的类,再看其类定义,内部类好好的躺在上面,怎么可能返回空数组?
我用这个类在测试用例上调试,完全正常,getDeclaredClasses()返回了预期的内部类数组。为什么部署到Tomcat上就会出错?
Tomcat执行反射的行为会不一样?或者有安全方面的设置,限制了反射机制?不可能的,Tomcat没有这本事。
是IDE用的JDK版本跟Tomcat的JRE不一致?我赶紧核对了一下,是完全一样的JRE。
我还换了几个版本的JDK编译代码,结果在Tomcat都一样的返回空数组。
正郁闷呢,眼睛盯到ant的脚本(build.xml)文件上。直觉最有可能出问题的地方就是Proguard的配置。我对这个东西不熟悉,混淆代码给我带来过不只一次的麻烦。(我对混淆代码一向非常抵触,至少它影响了我查看异常堆栈。)
为了能在jsp页面获得内部类的常量,我是加了这样的配置,让Proguard排除了某些类:
-keepclass=com.xxx.**$* {*;}
我把混淆过的jar包解开,检查里面的”.class”文件:com.xxx.Class、com.xxx.Class$InnerClass等文件都乖乖地躺在那里。名字没有被混淆。
看起来一切正常,为什么运行就不正常呢?百思不得骑姐啊~
这里面肯定有问题,但我一时想不到答案。抱着试试的态度,把jar包反编译了来看,好像也没有什么问题...除了Class里面没有InnerClass。等等,这很奇怪!
我赶紧把Idea自动编译的class搬出来,也反编译了看,Class里面是有InnerClass的!
找到根源了!Proguard混淆代码时,把内部类给拽出来,变成外部类,而且名字成了Class$InnerClass了!上网搜索,果然有这样的事情!唉,谁让我对Proguard不熟悉!浪费这么多时间。知道原因就好办了,继续搜索,得到解决方法,给Proguard加上参数:
keepattributes InnerClasses
这样内部类就不会被修改了。重新打包、部署上Tomcat测试,OK!问题解决!
最后还是有一个疑问的,离职的同事以前是怎样打包的?如果他用的build脚本跟我一样,肯定会有同样的问题。难道他改过build脚本,但是没有提交代码?有机会再见他,一定不能放过!