9

今天我花了一个下午分析 NoClassDefFoundError。一次又一次地验证classpath,结果发现有一个类的静态成员抛出了第一次被忽略的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()都隐藏在一个非常大的项目的初始化代码中的某个地方。

问题:

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


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

4

7 回答 7

17

确实,您永远都不应该捕获错误,但这里是您如何在可能发生的任何地方找到初始化程序问题的方法。

这是一个代理,它将使所有 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 都会被打印,即使它被捕获。

于 2010-02-05T23:17:16.090 回答
13

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

大多数(如果不是全部)静态初始化器可以重构为其他形式,并且通常它使问题更易于处理和诊断。正如您所发现的,静态初始化程序被禁止抛出已检查的异常,因此您必须记录并忽略,或记录并重新抛出未检查的异常,这些都不会使诊断工作变得更容易。

此外,大多数类加载器只会一次尝试加载给定的类,如果它第一次失败,并且没有正确处理,问题就会被有效地解决,最终会抛出泛型错误,很少或没有上下文。

于 2010-02-05T22:50:44.303 回答
5

如果您曾经看到具有这种模式的代码:

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

找出谁写了它,然后把它们打败。我是认真的。试着让他们被解雇——他们不了解编程的调试部分,无论是形式还是形式。

我想如果他们是一个学徒程序员,你可能会把他们打得一干二净,然后让他们有第二次机会。

即使是临时代码——也不值得将它以某种方式引入生产代码。

这种代码是由检查异常引起的,一个原本合理的想法变成了一个巨大的语言陷阱,因为在某些时候我们都会看到像上面那样的代码。

解决这个问题可能需要几天甚至几周的时间。所以你必须明白,通过编码,你可能会花费公司数万美元。(还有另一个很好的解决方案,对他们因愚蠢而花费的所有薪水罚款——我敢打赌他们再也不会这样做了)。

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

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

如果我听起来咄咄逼人和生气,那是因为我花了数周时间寻找这样的隐藏错误,而且作为一名顾问,还没有找到任何人来解决它。对不起。

于 2010-02-05T22:53:12.363 回答
1

该错误给出的唯一提示是类的名称,并且在该类的初始化过程中出现了严重错误。因此,无论是在这些静态初始化程序之一、字段初始化中,还是在被调用的构造函数中。

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

定位此类错误的一种有前途的方法是在测试环境中初始化类并调试初始化(单步)代码。然后应该能够找到问题的原因。

于 2010-02-05T22:01:09.863 回答
1

今天我花了一个下午分析 NoClassDefFoundError。一次又一次地验证classpath,结果发现有一个类的静态成员抛出了第一次被忽略的异常

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

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


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

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

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

于 2010-02-05T22:46:54.813 回答
0

我真的不明白你的推理。您询问“从抛出的异常开始查找此错误”,但您发现该错误并忽略它...

于 2010-02-05T22:43:19.783 回答
0

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

于 2016-05-11T09:51:15.943 回答