1

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();
        }
    }

[...]

}
RenWal
  • 181
  • 2
  • 7
  • Have a look at this: http://stackoverflow.com/questions/1771679/difference-between-threads-context-class-loader-and-normal-classloader – GerritCap Mar 14 '16 at 19:06
  • @GerritCap I came across this question, but I couldn't really figure out how the context class loader affects my class, as the plugin class' classloader does have the `Builder` class on its classpath. – RenWal Mar 14 '16 at 19:19
  • 1
    `NoClassDefFoundException`? You probably mean `java.lang.NoClassDefFoundError`. What is its cause? – aventurin Mar 14 '16 at 19:38
  • @aventurin Thanks, corrected that! – RenWal Mar 14 '16 at 19:59
  • Did you check if the cause (`getCause()`) of the `java.lang.NoClassDefFoundError` gives you a hint what the cause of the error might be? – aventurin Mar 14 '16 at 20:02
  • The cause is that Builder is not found on the class path. (Here lies my problem, because it _should_ be in my understanding) – RenWal Mar 14 '16 at 20:04
  • Need to supply some source code. – Christopher Schneider Mar 14 '16 at 20:06
  • Makes sure that your plugin is compiled and in your classpath. I'm assuming the plugin exists in a different jar from your main application that is trying to instantiate it? – ManoDestra Mar 14 '16 at 20:19
  • @ManoDestra It is because Plugin loads just fine, Builder does not. Both reside in `plugin.jar`, which is indeed a different jar than where the main application resides. – RenWal Mar 14 '16 at 20:31
  • 1
    Double check the classpath. And check that testapp/testplugin/Builder exists in the plugin.jar. – ManoDestra Mar 14 '16 at 21:09
  • For me this works. So the simplest explanation is that testapp.testplugin.Builder is not in the testplugin.jar. – aventurin Mar 14 '16 at 21:25
  • Well, I made some progress debugging and discovered something strange: When I load the real plugin with the SSCCE main application, the plugin indeed loads and runs perfectly fine (with makes it a really bad example - sorry). But as soon as I load it (exact same `plugin.jar`, just copy-pasted into the app's directory) in the complete application that this example code is derived from, it breaks. – RenWal Mar 15 '16 at 17:08
  • Okay. Solved it. I was being incredibly stupid and 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 '16 at 20:49
  • @RenWal Thank you very much, life saver! I have landed into a project where the classloader is being explicitly and unnecessarily passed around among different threads. We applied some code changes to comply with SONAR warnings and made the URLClassloader autoclosable. And voila, several side effects where some of them don't even generate an exception. So thanks again, big relief for us. – selman Jul 21 '23 at 14:08

1 Answers1

2

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.