Как проанализировать NoClassDefFoundError, вызванный игнорируемым ExceptionInInitializerError? - PullRequest
9 голосов
/ 06 февраля 2010

Сегодня я провел свой день с анализом 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.

Ответы [ 7 ]

16 голосов
/ 06 февраля 2010

Действительно, вы никогда не должны ловить Error, но вот как вы можете найти проблемы инициализатора, где бы они ни возникали.

Вот агент, который заставит все 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 для изменения классов. Скомпилируйте и поместите его в файл jar с классами javassist и следующим файлом MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: InitializerLoggingAgent

Запустите ваше приложение с java -javaagent:agentjar.jar MainClass, и каждая ошибка ExceptionInInitializerError будет напечатана, даже если она перехвачена.

13 голосов
/ 06 февраля 2010

Мой совет - избегать этой проблемы, избегая статических инициализаторов, насколько это возможно. Поскольку эти инициализаторы выполняются во время процесса загрузки классов, многие фреймворки не очень хорошо справляются с ними, и на самом деле старые виртуальные машины тоже не очень хорошо справляются с ними.

Большинство (если не все) статических инициализаторов могут быть реорганизованы в другие формы, и в целом это облегчает решение и диагностику проблем. Как вы обнаружили, статическим инициализаторам запрещено генерировать проверенные исключения, поэтому вы должны либо регистрировать и игнорировать, либо регистрировать и повторно отбрасывать как не проверенные, ни один из которых не облегчает диагностику.

Кроме того, большинство загрузчиков классов делают одну-единственную попытку загрузить данный класс, и если он не удается в первый раз и не обрабатывается должным образом, проблема эффективно устраняется, и в результате вы получаете общие ошибки быть брошенным, практически без контекста.

5 голосов
/ 06 февраля 2010

Если вы когда-либо видели код с этим шаблоном:

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

Узнайте, кто это написал, и вышибите из них. Я серьезно. Попытайтесь заставить их уволить - они не понимают отладочную часть программирования ни в каком виде, форме или форме.

Полагаю, если они программист-ученик, вы можете просто выбить из них все дерьмо и дать им ОДИН второй шанс.

Даже для временного кода - никогда не стоит вероятность, что он каким-то образом будет перенесен в рабочий код.

Этот тип кода вызван проверенными исключениями, иначе разумная идея превратилась в огромную языковую ловушку из-за того, что в какой-то момент мы все увидим код, подобный этому выше.

Для решения этой проблемы могут потребоваться ДНИ, если не НЕДЕЛИ. Таким образом, вы должны понимать, что кодируя это, вы потенциально стоите компании десятки тысяч долларов. (Есть еще одно хорошее решение, оштрафуйте их на всю зарплату, потраченную из-за этой глупости - держу пари, они никогда ЭТОГО больше не сделают).

Если вы ожидаете (отлавливаете) данную ошибку и обрабатываете ее, убедитесь, что:

  1. Вы знаете, что обработанная ошибка является ЕДИНСТВЕННЫМ ВОЗМОЖНЫМ источником этого исключения.
  2. Любые другие случайно обнаруженные исключения / причины либо сбрасываются, либо регистрируются.
  3. Вы не получаете широкого исключения (Exception или Throwable)

Если я звучу агрессивно и сердито, это потому, что я потратил несколько недель на то, чтобы найти скрытые ошибки, подобные этой, и, как консультант, не нашел никого, кто бы мог их устранить. К сожалению.

1 голос
/ 06 февраля 2010

Сегодня я провел свой день с анализом NoClassDefFoundError. После проверки пути к классам снова и снова оказалось, что существует статический член класса, который выдал исключение, которое игнорировалось в первый раз .

Твоя проблема! Никогда не лови и игнорируй Error (или Throwable). НИКОГДА.

И если вы унаследовали какой-то хитрый код, который может это сделать, используйте ваш любимый инструмент поиска кода / IDE для поиска и уничтожения оскорбительных предложений catch.


Теперь, когда я нашел эту ошибку после нескольких часов проб и ошибок, мне интересно, есть ли прямой способ найти эту ошибку, начиная с выданного исключения.

Нет, нет. Существуют сложные / героические способы ... например, использование умных действий с Java-агентом для взлома системы времени исполнения на лету ... но не то, что типичный Java-разработчик может иметь в своем "наборе инструментов".

Вот почему приведенный выше совет так важен.

1 голос
/ 06 февраля 2010

Единственная подсказка, которую дает ошибка - это имя класса, и что во время инициализации этого класса что-то пошло не так. Так что либо в одном из этих статических инициализаторов, инициализации поля, либо в вызываемом конструкторе.

Вторая ошибка возникла из-за того, что класс не был инициализирован во время вызова A.getId (). Первая инициализация была прервана. Поймать эту ошибку было хорошим тестом для команды инженеров; -)

Многообещающим подходом к обнаружению такой ошибки является инициализация класса в тестовой среде и отладка кода инициализации (за один шаг). Тогда нужно найти причину проблемы.

0 голосов
/ 11 мая 2016

Если вы можете воспроизвести проблему (даже время от времени) и возможно запустить приложение в режиме отладки, то вы сможете установить точку останова в своем отладчике для (всех 3 конструкторов) ExceptionInInitializerError и посмотреть, когда они мерзавец хит.

0 голосов
/ 06 февраля 2010

Я действительно не понимаю твоих рассуждений. Вы спрашиваете «найти эту ошибку, начиная с выданного исключения», и все же вы ловите эту ошибку и игнорируете ее ...

...