Java ClassLoader 在新线程(可运行)中使用时停止工作
Posted
技术标签:
【中文标题】Java ClassLoader 在新线程(可运行)中使用时停止工作【英文标题】:Java ClassLoader stops working when used in new Thread (Runnable) 【发布时间】:2014-06-17 00:59:30 【问题描述】:我正在开发一个 Eclipse 插件。当插件运行时(例如在 eclipse 的调试实例中),它会从调试工作区中的所有 java-projects 加载某些类。反射用于此目的。为了更好地了解我在这里所做的是一些代码:
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);
以下是我在上面使用的tryLoadClass
的实现,按照 cmets 的要求(一点也不神奇)。 _urlClassLoader 是从 URL
s 的数组(调试工作区中 java-projects 的输出文件夹)创建的,并以当前线程的上下文 ClassLoader 作为父级。
@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;
稍后,这些类被实例化并调用一个函数 (run
):
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();
在插件包中有几个实用程序类,例如MyUtil
,由插件导出,可以被上面加载的类引用。准确地说,MyUtil
在run
函数中被引用。
上面的代码一切正常。但是如果我将test.run()
移动到另一个线程中,我会得到MyUtil
的ClassNotFoundException。又是代码(和上面差不多,只替换了第6行)。 ITest
继承自 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();
我已经尝试了很多东西,比如获取当前线程的ContextClassLoader
并将其传递给新线程,但不是运气。我发现我可以使用当前线程的ContextClassLoader
显式加载MyUtil
,但只有在原始线程中使用它时才可以。如果我将该 ClassLoader 传递给新线程,它就会停止工作。
我认为我的问题是我对 ClassLoaders 的工作原理有一些错误的理解......
以下是有关异常的更多详细信息:
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
因为有一个关于如何加载“de.my.company.MyPlugin”的问题:它只是在包/插件中,这是调试工作区中的 java 项目所需要的,因此我认为它在“ base" 类路径(这里不知道正确的术语......)
/编辑:
我刚刚意识到我没有提到一些可能很重要的事情:test.run()
(以及启动新线程)是在 Eclipse Job
的上下文中完成的。但我想我已经算过了:当我删除 Job
-context 时问题仍然存在(直接从 Eclipse 命令的处理程序中发出代码)。但是,如果ITest
实现被正常实例化(即在eclipse调试实例中;我在一个简单的main()
函数中对其进行了测试),则问题不存在。
我认为问题是由于我的自定义_urlClassLoader
用于加载ITest
。因为 Java 尝试使用一开始就加载容器类的类加载器(afaik)加载任何引用的类,所以我的 _urlClassLoader 将再次使用。但是,单独的 _urlClassLoader 无法加载请求的类(它的“类路径”上只有其他 java 项目的输出文件夹)。相反,必须使用其父级。如果我理解正确,这应该会自动发生,但正是这部分不再适用于新线程......
【问题讨论】:
你能告诉我们更多关于tryLoadClass
的信息吗? de.my.company.MyPlugin
类在哪里存在以及如何将其添加到 URLClassLoader
的类路径中(如异常跟踪中所示)?
你能在线程完成运行之前验证 URLClassloader 没有关闭吗?我问是因为 URLClassloader 应该在使用后关闭,test.run()
将阻塞直到所有工作完成,但thread.start()
运行异步。
关于您的问题的奇怪之处在于您在整个类加载完成的地方创建了Thread
。难怪使用新的Thread
的上下文加载器并不能解决问题,您可以通过将类的 instance 传递给它的构造函数来创建Thread
,所以应该怎么做你用那个Thread
来影响那个类的加载?你应该追查ClassNotFoundException
真正出现的地方,它不可能是新线程。 如果是新线程,则与贴出的代码无关。
@vanOekel:原线程未关闭;我在thread.start()
之后插入了一个无限循环。 @Holger:我可以在“一切正常”和ClassNotFoundException
之间切换,只需将test.run()
替换为构造和启动新线程的两行代码即可。我也认为此时在类加载方面应该没有任何区别。
@Holger:我在问题的最后添加了一些信息;我认为问题与实际执行的代码(= 'ITest.run()' 的实现)的加载方式与 VM 的加载方式不同(使用 _urlClassLoader)有关。至少,如果我正常实例化一个ITest实现(即在eclipse调试实例中),类加载问题就不存在了。
【参考方案1】:
我对 OSGi 加载类的方式并不深入,但我知道标准机制足以说明以下几点:
当使用不同的Thread
时,JVM 解析类的方式(使用引用者的ClassLoader
)不会改变
URLClassLoader
实现的对父加载器的委托是无条件发生的
因此,如果这些部分没有改变,那么改变其行为的是 OSGi 框架提供的父加载器。我发现this blog entry 描述了一种适合观察到的行为的机制:
上下文查找器
上下文查找器是一种由 Equinox 框架安装为默认上下文类加载器的类加载器。调用时,它会在 Java 执行堆栈中搜索除系统类加载器之外的类加载器。
因此,当您启动一个新的Thread
并提供您使用URLClassLoader
动态加载的Runnable
时,该Thread
的堆栈跟踪中没有可用作上下文的包。
一个简单的解决方法是从你的包中添加代码到新 Thread
的堆栈跟踪中,即更改
Thread thread = new Thread(test, "TestThread");
进入
Thread thread = new Thread(new Runnable()
public void run() test.run();
, "TestThread");
当然test
必须改成final
这样,来自 Eclipse 插件的调用者(匿名内部类)位于新的 Thread
堆栈的顶部。由于您的URLClassLoader
的父级已经初始化为 OSGi 类加载器,因此不需要设置上下文类加载器(除非在加载的代码中使用线程上下文加载器进行一些动态加载)。
【讨论】:
完美!我刚试了一下,但是看了你的解释我已经很有信心了。您认为继续下去的最佳方法是什么?您建议的简单解决方法很好,但我更喜欢安装一个 ClassLoader,它可以在我的包中找到类,而不必查看堆栈跟踪。为此目的编写我自己的 ClassLoader 实现感觉就像重新发明***...... 如前所述,我对 OSGi 的事情并不深入,但也许在你的插件类上调用getClass().getClassLoader()
会给你一个绑定到你的插件而不是默认加载器的 ClassLoader
那个搜索算法。如果是这样,使用 ClassLoader
作为 URLClassLoader
的父级可以解决问题。以上是关于Java ClassLoader 在新线程(可运行)中使用时停止工作的主要内容,如果未能解决你的问题,请参考以下文章
java中类加载器ClassLoader,双亲加载机制,启动类加载器,应用类加载器,线程上下文类加载器