Reputation: 203
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 URL
s (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...
Upvotes: 4
Views: 2233
Reputation: 298429
I’m not deep into the OSGi way of loading classes but I know the standard mechanism good enough to state the following things:
ClassLoader
) does not change when using a different Thread
URLClassLoader
happens unconditionallySo 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).
Upvotes: 1