I'm trying to dynamically load libraries instead of shading them into my JAR to reduce file size. It's a standalone application with a Bootstrap class and a Main class. The main class is responsible for loading libraries and calling the bootstrap class' execute(...) method.
The downloading of the artifacts and loading of the library classes using Class.forName(class, true, libraryLoader) works, but when I try to load the Bootstrap class it can't find the class definitions for the library classes.
Here is how I'm currently trying to achieve this:
//////// Main.java /////////
/* ... public static void main(String[] args) { */
// download libraries
// these URLs reference the local JAR files downloaded
// i'm not going to get into detail on how i'm doing this right now, as this works
URL[] libraryUrls = ...;
// create parent loader for libraries
final ClassLoader rootLoader = Main.class.getClassLoader(); // app class loader
ClassLoader libraryLoader = new DelegatingClassLoader(new URLClassLoader(
libraryUrls,
rootLoader
), rootLoader);
/* this passes, so the libraries are successfully loaded onto the loaders ucp */
Class.forName("org.eclipse.jgit.transport.CredentialsProvider", true, libraryLoader);
// find URL for JAR of bootstrap class
ProtectionDomain domain = Main.class.getProtectionDomain();
CodeSource source = domain.getCodeSource();
URL file = source.getLocation();
// create child loader with our JAR file
ClassLoader appLoader = new DelegatingClassLoader(new URLClassLoader(
new URL[] { file }, libraryLoader), libraryLoader);
// load bootstrap class and execute
Class<?> bootstrap = Class.forName( // the error occurs here
"my.package.Bootstrap",
/* initialize */ true,
appLoader
);
bootstrap.getMethod("execute", String[].class)
.invoke(null, (Object) args);
/* } ... */
The DelegatingClassLoader first tries to load a class through the delegate and if that fails delegates to the parent. Here is the exact source.
The specific error I get:
Exception in thread "main" java.lang.NoClassDefFoundError: org/eclipse/jgit/transport/CredentialsProvider
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:467)
at my.package.Main.main(Main.java:108)
// line 108 is the line where i call Class.forName("my.package.Bootstrap, ...)
I was expecting the references to library classes to be resolved correctly because the class loader of the Bootstrap class should delegate to the library class loader, which I confirmed to have successfully 'loaded' the libraries.
A
NoClassDefFoundErrorindicates that a class, who was successfully loaded from someClassLoader, is statically referring to another class, whose definition thatClassLoadercan not find.That is, if a class A refers to a class B, the runtime does
Suppose you have the following ClassLoader hierarchy:
where
Arefers toB, andBrefers toC, and we dochild.loadClass("A"). Then, the following happens:childis asked to loadA, finds its definition, defines the class, and then proceecds to resolve its references:childis asked to loadB, and can not find its definition. Therefore,parentis asked to loadB, finds its definition, defines the class, and then proceeds to resolve its referencesparentis asked to loadC, can not find its definition, and throws a ClassNotFoundExceptionsince the reference to C could not be resolved, a NoClassDefFoundError for
Cis thrownThat is, loading
Afromchildfails with aNoClassDefFoundErrorforC, even thoughchildhas a definition forC.As we've just seen, that doesn't follow. Each class resolves its dependencies using its defining class loader, not the class loader that originally triggered the loading attempt.
So, how could this have happened? You have set up the following class loader hierarchy:
That is, your Bootstrap classes are on the classpath twice, in two different
ClassLoaders. You're probably hoping that the extra classes inrootLoaderdo no harm, because theappLoaderprefers its own definition over that of its ancestors. However, thelibraryLoaderdoesn't see the definitions in theappLoader, and will delegate to therootLoader.Suppose that
BootstrapusesX, who usesY, who usesCredentialsProvider. Then, the following happens:That is, the extra class definitions obscure the origin of dependency problems. If a library class incorrectly depends on an application class, you don't get a NoClassDefFoundError for that application class, but a NoClassDefFoundError for one of its library dependencies. And if there are no library dependencies, the class will be loaded a second time, and you will have separate classes with the same name in the system, which can cause really hard to diagnose issues (for instance: why is that static field having different values depending on who uses it?).
For the sake of your sanity, and those who come after you, I therefore recommend that you do not put your application classes into two different class loaders in the hierarchy.
PS: I can't tell you the identity of X and Y, but you should be able to find them as follows:
rootLoaderloadClass()implementation that triggers if the argument ends with"CredentialsProvider"(note that conditional breakpoints in library code can be a bit brittle. For instance, when I just tested this in a toy example, I had to briefly halt execution with a normal breakpoint, to give the conditional breakpoint enough time to hook into the ClassLoader code)
Alternatively, you could write your own class for
libraryLoader, which throws, logs, or halt in a breakpoint whenever it is asked for classes that should come fromappLoader.