捕获throwable还是exception?

上周发生了一个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类,发现guavagoogle-collections两个jar包里有相同名字和相同包名的Platform

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是永远不会发生的异常条件。也就是说,需要捕获RuntimeExceptionChecked 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类似,简单的记录日志而不进行任何其他处理。所以,再次遇到这种问题时,需要关注框架级的日志,本例中由于框架日志和普通日志并不在同一路径,导致忽略查看框架日志。
又回到了原点,貌似不一样了。。。

附收集到的关于ExceptionError的一些看法:

  • Differences between Exception and Error
  • When to catch java.lang.Error?

你可能感兴趣的:(捕获throwable还是exception?)