Reputation: 181
I have a java project that uses a URLClassLoader to load classes from another jar file at runtime, like a plugin system.
Let me give you a simplified version of the problem: Let's say that in my main method I would create the ClassLoader, pass it getClass().getClassLoader()
as the parent class loader and load my plugin class from the jar.
In the main method, I create an instance inst
of the class and then pass it to a new thread. This new thread calls inst.getObject()
, which is a method I defined.
Now, getObject()
creates an instance of another class Builder
in the jar via new
- assuming the URLClassLoader would now load this class as well as it is the defining classloader of the current class. Here, a NoClassDefFoundError
is thrown for Builder
if the method is invoked from the thread, but not when invoked from the main method:
Exception in thread "Thread-0" java.lang.NoClassDefFoundError: testapp/testplugin/Builder
at testapp.testplugin.Plugin.getObject(Plugin.java:88)
at testapp.mainapp.TestInit$1.run(TestInit.java:90)
Caused by: java.lang.ClassNotFoundException: testapp.testplugin.Builder
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.net.FactoryURLClassLoader.loadClass(URLClassLoader.java:814)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 7 more
When I put System.out.println(getClass().getClassLoader().toString())
inside getObject()
, the output is exactly the same whether I call the method from main or from the thread.
Any ideas as to why this happens? Here is some sample code:
Plugin (in plugin.jar):
package testapp.testplugin;
// Pluggable defines the getObject() method, common interface for all plugins
public class Plugin implements Pluggable{
Builder build;
public Plugin() {
// set some fields
}
@Override
public Object getObject()
{
// lazy initialisation for "build"
if (build == null)
build = new Builder(); ///// !NoClassDefFoundError! /////
// make Builder assemble an object and return it
return build.buildObject();
}
}
Main application (in runnable app.jar):
package testapp.mainapp;
public class TestInit {
public static void main(String[] args) throws Exception {
// create URLClassLoader
URLClassLoader clazzLoader = URLClassLoader.newInstance(new URL[]{new URL("testplugin.jar"},
getClass().getClassLoader());
// load plugin class
Class<?> clazz = Class.forName("testapp.testplugin.Plugin", true, clazzLoader);
Class<? extends Pluggable> subClazz = clazz.asSubclass(Pluggable.class);
// instantiate plugin class using constructor (to avoid Class.newInstance())
Constructor<? extends Pluggable> constr = subClazz.getConstructor();
final Pluggable plugin = constr.newInstance();
// create new thread and run getObject()
Thread t = new Thread(){
@Override
public void run() {
// something more sophisticated in the real application, but this is enough to reproduce the error
System.out.println(plugin.getObject());
}
};
t.start();
}
}
My current workaround is to force-load the Builder
class as soon as the plugin class is loaded:
public class Plugin {
static
{
try
{
Class.forName("testapp.testplugin.Builder");
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
[...]
}
Upvotes: 1
Views: 1426
Reputation: 21
There's already the answer in your own comment (many thanks for that, btw), but it's bit hard to find it, so I'm quoting it here:
Okay. Solved it. I [...] closed the ClassLoader in the main application because I put it in an AutoClosable wrapper class in a try statement. Changed this and now it works. – RenWal Mar 15 at 20:49
Also I can suggest using java.lang.ThreadGroup if you decide to close your class loader after execution of your loaded class' method and all derived threads is over:
...
Class mainClass = customClassLoader.findClass(name);
ThreadGroup threadGroup = new ThreadGroup("Custom thread group");
Thread thread = new Thread(threadGroup, new Runnable() {
@Override
public void run() {
try {
mainClass.getMethod("main", ...).invoke(...);
} catch (Throwable e) {
// exception handling
}
}
});
thread.start();
while (threadGroup.activeCount() > 0) {
Thread.sleep(100);
}
customClassLoader.close();
All threads and thread groups created in the context of the thread will belong to the threadGroup directly or indirectly. Thus we can just wait until the active thread count will become zero.
UPD. Of course that will not save if e.g. ExecutorService is called, and its tasks require class loading, or a listener is registered and thus code goes out of the thread group. So in general case closing a class loader is only safe on JVM exit.
Upvotes: 2