Pergunta

I'm developing an eclipse plugin. When the plugin is run (e.g. in a debugging instance of eclipse), it loads certain classes from all java-projects in the debugging workspace. Reflection is used for this purpose. To get a better idea of what I'm doing here is some code:

for (IPath outDir : _outputDirs)
{
    IPath fullTestPackageDir = outDir.append(testPackageDir);
    File dir = fullTestPackageDir.toFile();
    if (dir.exists() && dir.isDirectory())
    {
        for (File classFile : dir.listFiles())
        {
            String binaryClassName = packageName + "." + FileUtils.getNameWithoutExtension(classFile);
            Class<ITest> testClass = tryLoadClass(binaryClassName, ITest.class);
            if (testClass != null)
            {
                testClasses.add(testClass);
            }
        }
    }
}

Following is my implementation of tryLoadClass used above, as requested in the comments (not much magic at all). The _urlClassLoader is created from an array of URLs (output folders of java-projects in the debugging workspace) and has current thread's context ClassLoader as parent.

@SuppressWarnings("unchecked")
private <T> Class<T> tryLoadClass(String binaryClassName, Class<T> baseType)
{
    try
    {
        Class<?> testClass = _urlClassLoader.loadClass(binaryClassName);

        if (!baseType.isAssignableFrom(testClass))
            return null;

        return (Class<T>)testClass;
    }
    catch (ClassNotFoundException e)
    {
        System.err.println("class not found");
        return null;
    }
}

Later, these classes are instantiated and a function (run) is called:

Class<ITest> testClass = testClasses.get(i);

try
{
    ITest test = testClass.getConstructor(IRuntime.class).newInstance(_runtime);
    test.run();
}
catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e)
{
    e.printStackTrace();
}

In the bundle of the plugin there are several utility classes, e.g. MyUtil, which are exported by the plugin, and can be referenced by the classes loaded above. Precisely, MyUtil is referenced in the run function.

Everything works fine with the code above. But if I'm moving test.run() into another Thread, I get a ClassNotFoundException for MyUtil. Here's the code again (it is almost the same as above, only the 6th line is replaced). ITest inherits from Runnable:

Class<ITest> testClass = testClasses.get(i);

try
{
    ITest test = testClass.getConstructor(IRuntime.class).newInstance(_runtime);
    Thread thread = new Thread(test, "TestThread");
    thread.start();
}
catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e)
{
    e.printStackTrace();
}

I already tried a lots of things, like getting the current thread's ContextClassLoader and passing it to the new thread, but not luck. What I found out is that I can use the current thread's ContextClassLoader explicitly to load MyUtil, but only when it is used from within the original thread. If I pass that ClassLoader into the new thread, it stops working.

I think my problem is that I have some wrong understanding of how ClassLoaders work...

Here are some more details about the Exception:

Caused by: java.lang.ClassNotFoundException: de.my.company.MyPlugin
    at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
    ... 2 more

Because there has been a question about how "de.my.company.MyPlugin" is being loaded: it's just in the bundle/plugin, which is required by java projects in the debugging workspace, thus I think it's on the "base" classpath (don't know the right terminology here...)

/edit: I just realized that I did not mention something that might be important: test.run() (as well as starting the new thread) is done in the context of an Eclipse Job. But I think I already counted this out: the problem remains the same when I remove the Job-context (issue the code directly from within the Handler of an Eclipse command). However, the problem does not exist, if the ITest implementation is instantiated normally (i.e. in the eclipse debugging instance; I tested it within a simple main() function).

I think the problem is caused by the fact that my custom _urlClassLoader is used to load ITest. Because Java tries to load any referenced classes using the class-loader that loaded the container class in the very beginning (afaik), my _urlClassLoader would be used again. The _urlClassLoader alone, however, cannot load the requested classes (it only has the output folders of other java projects on its "classpath"). Instead, its parent must be used. If I understood everything right this should happen automatically, but it is exactly this part that doesn't work with the new thread anymore...

Foi útil?

Solução

I’m not deep into the OSGi way of loading classes but I know the standard mechanism good enough to state the following things:

  • The way the JVM resolves classes (using the referrer’s ClassLoader) does not change when using a different Thread
  • The delegation to the parent loader implemented by the URLClassLoader happens unconditionally

So if these parts do not change, it’s the parent loader provided by the OSGi framework which changes its behavior. I found this blog entry describing a mechanism that would fit into the observed behavior:

Context Finder

The context finder is a kind of ClassLoader that is installed by the Equinox Framework as the default context classloader. When invoked, it searches down the Java execution stack for a classloader other than the system classloader.

So when you start a new Thread providing the Runnable you have loaded dynamically using your URLClassLoader, there is no bundle in the stack trace of that Thread that can be used as context.

A simple work-around could be adding code from your bundle to the stack trace of the new Thread, i.e. change

Thread thread = new Thread(test, "TestThread");

into

Thread thread = new Thread(new Runnable() {
  public void run() { test.run(); }
}, "TestThread");

Of course, test must be changed to final

That way your caller (the anonymous inner class) originating from your Eclipse plugin is on the top of the new Thread’s stack. Since your URLClassLoader’s parent is already initialized to the OSGi class loader it should not be necessary to set the context class loader (unless there’s some dynamic loading using the thread context loader in the loaded code).

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top