RenWal
RenWal

Reputation: 181

Why does my URLClassLoader not work from another thread?

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

Answers (1)

Cheshire Rat
Cheshire Rat

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

Related Questions