I am trying to use the Google Closure Compiler from Java code, but want it to be an optional dependency (present at build time, but may not be around when deployed).
The problem I'm having is that I'm getting a NoClassDefFoundError
when the library is not on the classpath at runtime.
This would of course be expected, were I to execute code referencing library symbols. But all such code is never executed (behind a flag). In fact, the exception occurs when the class containing that code is first accessed.
I have been banging my head against this for a bit now and I think I may be missing some insights into the semantics of class loading in the JVM.
I have reduced the problem down to the following:
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());
}
}
The only code accessing the library is in a method that is never called.
Compile with (jar downloaded from maven):
javac -cp closure-compiler-v20240317.jar Main.java
And when running, I get a NoClassDefFoundError
:
$ java Main
Error: Unable to initialize main class Main
Caused by: java.lang.NoClassDefFoundError: com/google/javascript/jscomp/WarningsGuard
I would have expected the program to run successfully. There is no reason why the JVM should attempt to load any of the library classes.
Why does this happen? I have been looking at the JVM Spec and don't see how the loading of Main
would cause the JVM to attempt to load symbols referenced in thisMethodIsNotCalled
.
More curiously, replacing the contents of thisMethodIsNotCalled
with the following, does not cause this error, so this seems to be caused by a very specific interaction.
new StrictWarningsGuard();
new CompilerOptions().addWarningsGuard(null);
This happens with OpenJDK 21 on 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)
But I believe this not specific to that version, I have also reproduced this with various other versions.
According to this answer, verification of a class can happen
on the loading of the containing class or its first use
Please also note that the method signature of the called method is
addWarningsGuard:(Lcom/google/javascript/jscomp/WarningsGuard;)V
And WarningsGuard
is an abstract class
(see here). Hence, the JVM wants to "verify" when loading the class Main
that StrictWarningsGuard
is actually an implementation of WarningsGuard
. Therefore it first tries to load WarningsGuard
itself and fails with NoClassDefFoundError
for WarningsGuard
.
In your second implementation you just pass null
to addWarningsGuard()
. The JVM does not need to verify at this stage that StrictWarningsGuard
is actually an implementation of WarningsGuard
.
To make the dependency optional, you should place all code that depends on the optional library inside another class and load this class via reflection.