Неожиданная `NoClassDefFoundError`, когда дополнительная библиотека не находится в пути к классам, но связанный код не выполняется

Я пытаюсь использовать Google Closure Compiler из кода Java, но хочу, чтобы он был дополнительной зависимостью (присутствовал во время сборки, но может отсутствовать при развертывании). Проблема, с которой я столкнулся, заключается в том, что я получаю NoClassDefFoundError, когда библиотека не находится в пути к классам во время выполнения. Этого, конечно, следовало бы ожидать, если бы я выполнял код, ссылающийся на библиотечные символы. Но весь такой код никогда не выполняется (за флагом). Фактически, исключение возникает при первом доступе к классу, содержащему этот код.

Я уже немного ломаю голову над этим и думаю, что, возможно, мне не хватает некоторого понимания семантики загрузки классов в JVM.

Я свел проблему к следующему:

import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.StrictWarningsGuard;

public class Main {
    public static void main(String[] args) {
        System.out.println("HelloWorld");
    }

    private static void thisMethodIsNotCalled() {
        new CompilerOptions().addWarningsGuard(new StrictWarningsGuard());
    }
}

Единственный код, обращающийся к библиотеке, находится в методе, который никогда не вызывается.

Скомпилируйте с помощью (jar, скачанного с maven):

javac -cp closure-compiler-v20240317.jar Main.java

И при беге я получаю NoClassDefFoundError:

$ java Main
Error: Unable to initialize main class Main
Caused by: java.lang.NoClassDefFoundError: com/google/javascript/jscomp/WarningsGuard

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

Почему это происходит? Я просматривал JVM Spec и не понимаю, как загрузка Main приведет к тому, что JVM попытается загрузить символы, указанные в thisMethodIsNotCalled.

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

new StrictWarningsGuard();
new CompilerOptions().addWarningsGuard(null);

Дополнительный контекст

Это происходит с OpenJDK 21 в Ubuntu 24.04:

$ java -version
openjdk version "21.0.3" 2024-04-16
OpenJDK Runtime Environment (build 21.0.3+9-Ubuntu-1ubuntu1)
OpenJDK 64-Bit Server VM (build 21.0.3+9-Ubuntu-1ubuntu1, mixed mode, sharing)

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

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

Jim Garrison 29.05.2024 20:10

Это все еще не объясняет, почему это происходит только при определенных обстоятельствах. См. альтернативное содержимое метода, которое не вызывает ошибку.

Patrick Ziegler 29.05.2024 20:17

Он воспроизводится в macOS с помощью OpenJDK 21.0.3.

aled 29.05.2024 21:29

Если вы запустите javap -v Main.class для обоих вариантов и сравните результаты, только постоянные записи пула с номерами от 21 до 27 будут находиться в другом порядке. Поэтому это должно быть очень специфично для реализации. См. также stackoverflow.com/questions/34259275/…

siom 29.05.2024 21:50
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
4
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Согласно этому ответу может произойти проверка класса

при загрузке содержащего класса или его первом использовании

Также обратите внимание, что сигнатура метода вызываемого метода

addWarningsGuard:(Lcom/google/javascript/jscomp/WarningsGuard;)V

А WarningsGuard — это abstract class (см. здесь). Следовательно, JVM хочет «проверить» при загрузке класса Main, что StrictWarningsGuard на самом деле является реализацией WarningsGuard. Поэтому он сначала пытается загрузить WarningsGuard сам, но терпит неудачу с NoClassDefFoundError для WarningsGuard.

Во второй реализации вы просто передаете null на addWarningsGuard(). На этом этапе JVM не нужно проверять, что StrictWarningsGuard на самом деле является реализацией WarningsGuard.

Чтобы сделать зависимость необязательной, вам следует поместить весь код, зависящий от необязательной библиотеки, внутри другого класса и загрузить этот класс посредством отражения.

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

Patrick Ziegler 30.05.2024 12:24

Неважно, согласно спецификации должно соблюдаться следующее: «Класс или интерфейс полностью проверены и подготовлены перед инициализацией». Таким образом, такая ошибка может возникнуть самое позднее при инициализации класса.

Patrick Ziegler 30.05.2024 12:38

Другие вопросы по теме