上周发生了一个BUG,用了一天的时间才解决,记录下过程。
public int getIceTea(int teaId) {
logger.info("getIceTea|access|teaId:" + teaId); // 1
try {
Object obj = getFromCache(teaId);
if (obj == null) {
obj = getFromDb(teaId);
}
logger.info("getIceTea|result|tea:" + obj); // 2
} catch (Exception e) {
logger.error("getIceTea|error",e); // 3
return -1;
}
return 0;
}
一切要从上面这段代码开始说起:这是一个RPC方法,原来的代码已在线上运行了一段时间,随着调用量的增加,希望增加一个缓存层以便提高性能,于是增加了getFormCache()
函数,其中的缓存实现使用Guava的LoadingCache
。大功告成,打包、发布到测试环境,进行接口测试,一切正常。继续发布到正式环境,进行接口测试,不幸的事情发生了,接口报错。“报错嘛,还好还好,我有完备的日志”,打开日志,发现只记录了1的日志,2和3没有。这我就不能理解了,在我认为,1和2或者1和3必须是成对出现的,只有1是什么鬼。
没有想到好的方法,硬着头皮从getFromCache()
层层加入日志,不断调试。偶然想到,会不会抛出了更高层级的异常?于是将Exception
更换其父类为Throwable
,最终发现罪魁祸首,Throwable
的另一个子类Error
:
getIceTea|error:
java.lang.NoSuchMethodError: com.google.common.base.Platform.systemNanoTime()J
at ...
在IDE里搜索Platform
类,发现guava
和google-collections
两个jar包里有相同名字和相同包名的Platform
其中
google-collections
里面的没有systemNanoTime()
方法,可知在测试环境虚拟机正确加载了guava
中的Platform
类所以正常,而正式环境加载了google-collections
中的Platform
类所以抛出NoSuchMethodError
。那么虚拟机加载jar包的顺序是怎样的呢?官方文档里有这样的描述:
The order in which the JAR files in a directory are enumerated in the expanded class path is not specified and may vary from platform to platform and even from moment to moment on the same machine. A well-constructed application should not depend upon any particular order. If a specific order is required, then the JAR files can be enumerated explicitly in the class path
翻译为中文,即:虚拟机加载类路径目录中的各个jar包的顺序是不确定的,在不同平台上不同,甚至同一机器的不同时刻也不相同。一般情况下,JAVA应用不应该依赖于jar包加载顺序。如果必须依赖jar包加载顺序,则应该在类路径CLASS PATH中显式的指定。
可知,开头的代码在测试环境中正常也只是偶然,极有可能下次启动,接口就会发生异常。尝试重启了几次,证明事实正是如此:测试环境也发生了接口异常。找到了原因,解决BUG就很容易了,由于新引入了guava
包,google-collections
就变得冗余了,删去该包即可。
回顾整个过程,解决这个BUG的困难不在于根据NoSuchMethodError
查出jar包污染,而在于定位到异常的源头,也就是,catch (Throwable t)
or catch (Exception e)
?查阅JDK文档,对Error
类有这样的注释:
A method is not required to declare in its throws clause any subclasses of Error that might be thrown during the execution of the method but not caught, since these errors are abnormal conditions that should never occur.
即,JAVA方法不需要在throws
子句中声明方法在执行过程中抛出的任何Error
及其子类也不应该捕获,因为Error
是永远不会发生的异常条件。也就是说,需要捕获RuntimeException
和Checked Exception
,但是永远不要捕获Error
。文档中的提法,是基于这样的考虑:在应用执行过程中如果发生了Error
比如OutOfMemoryError
,那么意味着程序已经不可能再做任何恢复,此时终止执行、退出程序、及时人工介入处理才是合理的做法。
但是,Never say never,某些情况下捕获Error
是很有必要的。想象这样的情况,如果你在开发一个Eclipse类似的App,你设计了插件机制可以由第三方来编写插件扩展功能,当其中某个插件加载错误,抛出比如前文所述的NoSuchMethodError
时,我们期待的是提示插件加载失败而不是退出Eclipse,此时捕获包括Error
在内的Throwable
就显得很有必要。此外,当编写一些框架级别的程序,在代码的最底层捕获Throwable
也很有必要,这样才不会使框架崩溃。比如,Netty中线程NioEventLoop
正是如此处理:
for (;;) {
try {
// process
}catch (Throwable t) {
handleLoopException(t);
}
}
那么,是否需要每次都捕获Throwable
呢?这是最安全的方法,但性能不高,并不提倡。折中的做法是:在最底层代码捕获Throwable
,其他层级代码捕获Exception
。
最后回到开始的问题,抛出Error
的方法并不是RPC框架的底层代码,所以不应该捕获Throwable
。那么,框架的底层是否处理了Throwable
呢,答案是肯定的,和Netty类似,简单的记录日志而不进行任何其他处理。所以,再次遇到这种问题时,需要关注框架级的日志,本例中由于框架日志和普通日志并不在同一路径,导致忽略查看框架日志。
又回到了原点,貌似不一样了。。。
附收集到的关于Exception
和Error
的一些看法: