如何分析由忽略的ExceptionInInitializerError引起的NoClassDefFoundError?

今天我花了我的下午分析一个NoClassDefFoundError。 在一次又一次地validation类路径之后,结果发现有一个类的静态成员抛出了第一次被忽略的exception。 之后,每次使用该类都会抛出一个没有有意义的堆栈跟踪的NoClassDefFoundError:

Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class InitializationProblem$A at InitializationProblem.main(InitializationProblem.java:19) 

就这样。 没有更多的线条。

减少到这一点,这是问题:

 public class InitializationProblem { public static class A { static int foo = 1 / 0; static String getId() { return "42"; } } public static void main( String[] args ) { try { new A(); } catch( Error e ) { // ignore the initialization error } // here an Error is being thrown again, // without any hint what is going wrong. A.getId(); } } 

为了使它不那么容易,除了A.getId()的最后一次调用之外的所有内容都隐藏在一个非常大的项目的初始化代码中。

题:

现在我在经过数小时的试验和错误后发现了这个错误,我想知道是否有一种直接的方法从抛出的exception开始找到这个bug。 关于如何做到这一点的任何想法?


我希望这个问题能为其他人分析一个莫名其妙的NoClassDefFoundError提示。

我的建议是尽可能避免使用静态初始化器来避免这个问题。 因为这些初始化程序在类加载过程中执行,所以许多框架都不能很好地处理它们,实际上较老的VM也不能很好地处理它们。

大多数(如果不是全部)静态初始化器可以重构为其他forms,并且通常它使问题更容易处理和诊断。 正如您所发现的那样,静态初始化器被禁止抛出已检查的exception,因此您必须进行日志记录和忽略,或者记录并重新抛出未选中的任何一个,这些都不会使诊断工作变得更容易。

此外,大多数类加载器只进行一次尝试加载给定的类,如果它第一次失败,并且处理不当,问题会被有效压缩,最终会抛出generics错误,很少或没有上下文。

真的,你永远不应该捕获错误,但是这里是你可以在任何地方发现初始化问题的方法。

这是一个代理,它将使所有ExceptionInInitializerErrors在创建时打印堆栈跟踪:

 import java.lang.instrument.*; import javassist.*; import java.io.*; import java.security.*; public class InitializerLoggingAgent implements ClassFileTransformer { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new InitializerLoggingAgent(), true); } private final ClassPool pool = new ClassPool(true); public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { try { if (className.equals("java/lang/ExceptionInInitializerError")) { CtClass klass = pool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtConstructor[] ctors = klass.getConstructors(); for (int i = 0; i < ctors.length; i++) { ctors[i].insertAfter("this.printStackTrace();"); } return klass.toBytecode(); } else { return null; } } catch (Throwable t) { return null; } } } 

它使用javassist来修改类。 编译并将其放在带有javassist类和以下MANIFEST.MF的jar文件中

 Manifest-Version: 1.0 Premain-Class: InitializerLoggingAgent 

使用java -javaagent:agentjar.jar MainClass运行你的应用程序,即使被捕获,也会打印每个ExceptionInInitializerError。

如果您看到使用此模式的代码:

 } catch(...) { // no code } 

找出谁写了它并击败他们的CRAP。 我是认真的。 尝试让他们被解雇 – 他们无法以任何方式,形状或forms理解编程的调试部分。

我猜如果他们是一名学徒程序员,你可能会打败他们,然后让他们有一次机会。

即使对于临时代码 – 它也永远不值得以某种方式被提交到生产代码中。

这种代码是由经过检查的exception引起的,这是一个非常合理的想法,因为在某些时候我们都会看到如上所述的代码。

如果不是WEEKS可以花费DAYS来解决这个问题。 所以你必须明白,通过编码,你可能会使公司花费数万美元。 (还有另一个很好的解决方案,因为那种愚蠢而花费了所有的工资 – 我打赌他们再也不会这样做了)。

如果您确实期望(捕获)给定错误并处理它,请确保:

  1. 您知道您处理的错误是该exception的唯一可能来源。
  2. 偶然捕获的任何其他exception/原因要么被重新抛出,要么被记录。
  3. 你没有抓住广泛的例外(Exception或Throwable)

如果我听起来很咄咄逼人,那就是因为我花了几周的时间才发现这样的隐患,作为一名顾问,没有找到任何人把它拿出去。 抱歉。

错误给出的唯一提示是类的名称,并且在该类的初始化期间出现了严重错误。 因此,无论是在其中一个静态初始化器,字段初始化还是在一个被调用的构造函数中。

第二个错误已被抛出,因为在调用A.getId()时类尚未初始化。 第一次初始化被中止。 捕获该错误对于工程团队来说是一个很好的测试;-)

找到这种错误的有希望的方法是在测试环境中初始化类并调试初始化(单步)代码。 然后人们应该能够找到问题的原因。

今天我花了我的下午分析一个NoClassDefFoundError。 在一次又一次地validation类路径之后, 结果发现有一个类的静态成员抛出了第一次被忽略的exception

有你的问题! 永远不要捕捉并忽略错误(或Throwable)。 永远不会。

如果您inheritance了可能会执行此操作的狡猾代码,请使用您喜欢的代码搜索工具/ IDE来查找和销毁有问题的catch子句。


现在我在经过数小时的试验和错误后发现了这个错误,我想知道是否有一种直接的方法从抛出的exception开始找到这个bug。

不,没有。 有复杂/英雄的方式……比如使用java代理做聪明的事情来动态地破解运行时系统……但不是典型的Java开发人员可能在他们的“工具箱”中拥有的那种东西。

这就是为什么上面的建议如此重要。

我真的不明白你的推理。 你问“从抛出的exception中找到这个bug”然后你就抓住了这个错误而忽略了它……

如果您可以重现问题(甚至偶尔),并且可以在调试下运行应用程序,那么您可以在调试器中为(所有3个构造函数)ExceptionInInitializerError设置一个断点,并查看它们何时命中。