Я пытаюсь использовать 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)
Но я считаю, что это не относится только к этой версии, я также воспроизводил это с другими версиями.
Это все еще не объясняет, почему это происходит только при определенных обстоятельствах. См. альтернативное содержимое метода, которое не вызывает ошибку.
Он воспроизводится в macOS с помощью OpenJDK 21.0.3.
Если вы запустите javap -v Main.class для обоих вариантов и сравните результаты, только постоянные записи пула с номерами от 21 до 27 будут находиться в другом порядке. Поэтому это должно быть очень специфично для реализации. См. также stackoverflow.com/questions/34259275/…




Согласно этому ответу может произойти проверка класса
при загрузке содержащего класса или его первом использовании
Также обратите внимание, что сигнатура метода вызываемого метода
addWarningsGuard:(Lcom/google/javascript/jscomp/WarningsGuard;)V
А WarningsGuard — это abstract class (см. здесь). Следовательно, JVM хочет «проверить» при загрузке класса Main, что StrictWarningsGuard на самом деле является реализацией WarningsGuard. Поэтому он сначала пытается загрузить WarningsGuard сам, но терпит неудачу с NoClassDefFoundError для WarningsGuard.
Во второй реализации вы просто передаете null на addWarningsGuard(). На этом этапе JVM не нужно проверять, что StrictWarningsGuard на самом деле является реализацией WarningsGuard.
Чтобы сделать зависимость необязательной, вам следует поместить весь код, зависящий от необязательной библиотеки, внутри другого класса и загрузить этот класс посредством отражения.
О, вполне логично, что именно проверка вызывает эту ошибку. Также объясняется, почему это происходит только с этими конкретными вызовами (где необходимо ввести проверку параметров вызова). Я предполагаю, что утверждение в ответе, на который вы ссылаетесь, о том, что проверка происходит при первом использовании метода, здесь не применимо, вместо этого, похоже, это происходит при загрузке класса (что также соответствует моему опыту в более крупной базе кода)
Неважно, согласно спецификации должно соблюдаться следующее: «Класс или интерфейс полностью проверены и подготовлены перед инициализацией». Таким образом, такая ошибка может возникнуть самое позднее при инициализации класса.
Тот факт, что ваш метод не выполняется во время выполнения, не означает, что библиотека не является зависимостью, загружаемой во время инициализации.