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

发布于 2024-08-20 14:47:32 字数 1133 浏览 5 评论 0原文

今天我花了一个下午的时间来分析 NoClassDefFoundError。反复验证类路径后,发现有一个类的静态成员抛出了第一次被忽略的异常。之后,每次使用该类都会抛出 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 的人提供一个提示。

Today I spent my afternoon with analysing a NoClassDefFoundError. After verifying the classpath again and again, it turned out that there was a static member of a class that threw an Exception that was ignored the first time. After that every use of the class throw a NoClassDefFoundError without a meaningful stacktrace:

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

That's all. No more lines.

Reduced to the point, this was the problem:

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();
    }
}

To make it not so easy, all but the last call of A.getId() was hidden somewhere in the initialization code of a very big project.

Question:

Now that I've found this error after hours of trial and error, I'm wondering if there is a straight forward way to find this bug starting from the thrown exception. Any ideas on how to do this?


I hope this question will be a hint for anyone else analysing an inexplicable NoClassDefFoundError.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

╰ゝ天使的微笑 2024-08-27 14:47:33

实际上,您永远不应该捕获错误,但您可以通过以下方法找到可能出现的初始化程序问题。

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


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 都会被打印出来,即使它被捕获。

Really, you shouldn't ever ever catch Error, but here's how you can find initializer problems wherever they might occur.

Here's an agent that will make all ExceptionInInitializerErrors print the stack trace when they are created:


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;
    }
  }
}

It uses javassist to modify the classes. Compile and put it in a jar file with javassist classes and the following MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: InitializerLoggingAgent

Run your app with java -javaagent:agentjar.jar MainClass and every ExceptionInInitializerError will be printed even if it is caught.

岁吢 2024-08-27 14:47:33

我的建议是通过尽可能避免静态初始化器来避免这个问题。由于这些初始化程序是在类加载过程中执行的,因此许多框架不能很好地处理它们,实际上较旧的虚拟机也不能很好地处理它们。

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

此外,大多数类加载器只会一次尝试加载给定的类,如果第一次失败并且处理不当,问题就会得到有效解决,最终会引发通用错误,很少或没有上下文。

My advice would be to avoid this problem by avoiding static initializers as much as you can. Because these initializers get executed during the classloading process, many frameworks don't handle them very well, and in fact older VMs don't handle them very well either.

Most (if not all) static initializers can be refactored into other forms, and in general it makes the problems easier to handle and diagnose. As you've discovered, static initializers are forbidden from throwing checked exceptions, so you've got to either log-and-ignore, or log-and-rethrow-as-unchecked, none of which make the job of diagnosis any easier.

Also, most classloaders make one-and-only-one attempt to load a given class, and if it fails the first time, and isn't handled properly, the problem gets effectively squashed, and you end up with generic Errors being thrown, with little or no context.

酒绊 2024-08-27 14:47:33

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

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

找出谁编写了它并击败他们。我是认真的。试图解雇他们——他们不理解编程的调试部分以任何方式、形式或形式。

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

即使对于临时代码,也不值得将其以某种方式带入生产代码。

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

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

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

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

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

If you ever see code with this pattern:

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

Find out who wrote it and BEAT THE CRAP OUT OF THEM. I'm serious. Try to get them fired--they do not understand the debugging portion of programming in any way, shape or form.

I guess if they are an apprentice programmer you might just beat the crap out of them and then let them have ONE second chance.

Even for temporary code--it's never worth the possibility that it will somehow be brought forward into production code.

This kind of code is caused by checked exceptions, an otherwise reasonable idea made into a huge language pitfall by the fact that at some point we'll all see code like that above.

It can take DAYS if not WEEKS to solve this problem. So you've got to understand that by coding that, you are potentially costing the company tens of thousands of dollars. (There's another good solution, fine them for all the salary spent because of that stupidity--I bet they never do THAT again).

If you do expect (catch) a given error and handle it, make sure that:

  1. You know that the error you handled is the ONLY POSSIBLE source of that exception.
  2. Any other exceptions/causes incidentally caught are either rethrown or logged.
  3. You aren't catching to broad an exception (Exception or Throwable)

If I sound aggressive and angry, it's because I've gotten screwed spending weeks finding hidden bugs like this and, as a consultant, haven't found anyone to take it out on. Sorry.

怪我闹别瞎闹 2024-08-27 14:47:33

该错误给出的唯一提示是类的名称以及该类的初始化期间出现了严重错误。因此,要么在这些静态初始化器、字段初始化之一中,要么在被调用的构造函数中。

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

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

The only hints the error gives are the name of the class and that something went terribly wrong during initialization of that class. So either in one of those static initializers, field initialization or maybe in a called constructor.

The second error has been thrown because the class has not been initialized at the time A.getId() was called. The first initialization was aborted. Catching that error was a nice test for the engineering team ;-)

A promising approach to locate such an error is to initialize the class in a test environment and debug the initialization (single steps) code. Then one should be able to find the cause of the problem.

鹤舞 2024-08-27 14:47:33

今天我花了一个下午的时间来分析 NoClassDefFoundError。经过一次又一次验证类路径后,发现类中有一个静态成员抛出了第一次被忽略的异常

有你的问题!永远不要捕获并忽略错误(或可抛出的错误)。从来没有。

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


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

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

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

Today I spent my afternoon with analysing a NoClassDefFoundError. After verifying the classpath again and again, it turned out that there was a static member of a class that threw an Exception that was ignored the first time.

There is your problem! Don't ever catch and ignore Error (or Throwable). NOT EVER.

And if you've inherited some dodgy code that might do this, use your favourite code search tool / IDE to seek and destroy the offending catch clauses.


Now that I've found this error after hours of trial and error, I'm wondering if there is a straight forward way to find this bug starting from the thrown exception.

No there isn't. There are complicated/heroic ways ... like using doing clever things with a java agent to hack the runtime system on the fly ... but not the sort of thing that a typical Java developer is likely to have in their "toolbox".

Which is why the advice above is so important.

败给现实 2024-08-27 14:47:33

我实在不明白你的推理。您询问“从引发的异常开始查找此错误”,但您捕获了该错误并忽略它......

I really don't understand your reasoning. You ask about "find this bug starting from the thrown exception" and yet you catch that error and ignore it ...

小红帽 2024-08-27 14:47:33

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

If you can reproduce the problem (even occasionally), and it's possible to run the app under debug, then you may be able to set a break point in your debugger for (all 3 constructors of) ExceptionInInitializerError, and see when they git hit.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文