使用URLClassLoader重新加载jar时出现问题

我需要为应用程序的某些部分添加插件function到现有应用程序。 我希望能够在运行时添加一个jar,应用程序应该能够从jar加载一个类而无需重新启动应用程序。 到现在为止还挺好。 我使用URLClassLoader在线发现了一些样本,它运行正常。

我还希望能够在jar的更新版本可用时重新加载相同的类。 我再次找到了一些示例和实现这一点的关键,据我所知,我需要为每个新的加载使用一个新的类加载器实例。

我写了一些示例代码但是遇到了NullPointerException。 首先让我告诉你们代码:

package test.misc; import java.io.File; import java.net.URL; import java.net.URLClassLoader; import plugin.misc.IPlugin; public class TestJarLoading { public static void main(String[] args) { IPlugin plugin = null; while(true) { try { File file = new File("C:\\plugins\\test.jar"); String classToLoad = "jartest.TestPlugin"; URL jarUrl = new URL("jar", "","file:" + file.getAbsolutePath()+"!/"); URLClassLoader cl = new URLClassLoader(new URL[] {jarUrl}, TestJarLoading.class.getClassLoader()); Class loadedClass = cl.loadClass(classToLoad); plugin = (IPlugin) loadedClass.newInstance(); plugin.doProc(); } catch (Exception e) { e.printStackTrace(); } finally { try { Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } 

IPlugin是一个简单的接口,只有一个方法doProc:

 public interface IPlugin { void doProc(); } 

和jartest.TestPlugin是这个接口的一个实现,其中doProc只打印出一些语句。

现在,我将jartest.TestPlugin类打包到一个名为test.jar的jar中,并将其放在C:\ plugins下并运行此代码。 第一次迭代运行顺利,类加载没有问题。

当程序执行sleep语句时,我将C:\ plugins \ test.jar替换为包含同一类的更新版本的新jar,并等待while的下一次迭代。 现在这是我不明白的地方。 有时更新的类会重新加载而不会出现问题,即下一次迭代运行正常。 但有时,我看到抛出的exception:

 java.lang.NullPointerException at java.io.FilterInputStream.close(FilterInputStream.java:155) at sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream.close(JarURLConnection.java:90) at sun.misc.Resource.getBytes(Resource.java:137) at java.net.URLClassLoader.defineClass(URLClassLoader.java:256) at java.net.URLClassLoader.access$000(URLClassLoader.java:56) at java.net.URLClassLoader$1.run(URLClassLoader.java:195) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:188) at java.lang.ClassLoader.loadClass(ClassLoader.java:307) at java.lang.ClassLoader.loadClass(ClassLoader.java:252) at test.misc.TestJarLoading.main(TestJarLoading.java:22) 

我已经在网上搜索并且抓了我的脑袋,但是无法得出任何结论,为什么抛出这个exception也是如此 – 只是有时候,并非总是如此。

我需要你的经验和专业知识来理解这一点。 这段代码出了什么问题? 请帮忙!!

如果您需要更多信息,请与我们联系。 谢谢你的期待!

此行为与jvm中的错误有关
这里记录了 2种解决方法

为了每个人的利益,让我总结一下真正的问题和对我有用的解决方案。

正如Ryan所指出的,JVM中存在一个影响Windows平台的错误 。 URLClassLoader在打开它们以加载类之后不会关闭打开的jar文件,从而有效地锁定jar文件。 无法删除或替换jar文件。

解决方案很简单:在读取后打开jar文件。 但是,要获得打开的jar文件的句柄,我们需要使用reflection,因为我们需要遍历的属性不是公共的。 所以我们沿着这条路走下去

 URLClassLoader -> URLClassPath ucp -> ArrayList loaders JarLoader -> JarFile jar -> jar.close() 

关闭打开的jar文件的代码可以添加到扩展URLClassLoader的类中的close()方法中:

 public class MyURLClassLoader extends URLClassLoader { public PluginClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } /** * Closes all open jar files */ public void close() { try { Class clazz = java.net.URLClassLoader.class; Field ucp = clazz.getDeclaredField("ucp"); ucp.setAccessible(true); Object sunMiscURLClassPath = ucp.get(this); Field loaders = sunMiscURLClassPath.getClass().getDeclaredField("loaders"); loaders.setAccessible(true); Object collection = loaders.get(sunMiscURLClassPath); for (Object sunMiscURLClassPathJarLoader : ((Collection) collection).toArray()) { try { Field loader = sunMiscURLClassPathJarLoader.getClass().getDeclaredField("jar"); loader.setAccessible(true); Object jarFile = loader.get(sunMiscURLClassPathJarLoader); ((JarFile) jarFile).close(); } catch (Throwable t) { // if we got this far, this is probably not a JAR loader so skip it } } } catch (Throwable t) { // probably not a SUN VM } return; } } 

(此代码取自Ryan发布的第二个链接。此代码也发布在错误报告页面上。)

但是,有一个问题:为了使这个代码能够工作并且能够获得打开的jar文件的句柄来关闭它们,用于通过URLClassLoader实现从文件加载类的加载器必须是一个JarLoader 。 查看URLClassPath的源代码 (方法getLoader(URL url) ),我注意到只有在用于创建URL的文件字符串不以“/”结尾时才使用JARLoader。 因此,URL必须像这样定义:

 URL jarUrl = new URL("file:" + file.getAbsolutePath()); 

整个类加载代码应如下所示:

 void loadAndInstantiate() { MyURLClassLoader cl = null; try { File file = new File("C:\\jars\\sample.jar"); String classToLoad = "com.abc.ClassToLoad"; URL jarUrl = new URL("file:" + file.getAbsolutePath()); cl = new MyURLClassLoader(new URL[] {jarUrl}, getClass().getClassLoader()); Class loadedClass = cl.loadClass(classToLoad); Object o = loadedClass.getConstructor().newInstance(); } finally { if(cl != null) cl.close(); } } 

更新: JRE 7在URLClassLoader类中引入了一个close()方法,该方法可能已经解决了这个问题。 我还没有validation。

从Java 7开始,你确实在URLClassLoader有一个close()方法,但如果你直接或间接调用ClassLoader#getResource(String)ClassLoader#getResourceAsStream(String)ClassLoader#getResources(String)类型的方法,它还不足以完全释放jar文件。 ClassLoader#getResources(String) 。 实际上,默认情况下, JarFile实例会自动存储到JarFileFactory的缓存中,以防我们直接或间接调用以前的方法之一,即使我们调用java.net.URLClassLoader#close()也不会释放这些实例。

因此,即使使用Java 1.8.0_74,在这种特殊情况下仍然需要hack,这是我的hack https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo我在这里使用的/appma/core/util/Classpath.java#L83 https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/ DefaultApplicationManager.java#L388 。 即使有了这个hack,我仍然必须明确地调用GC来完全释放jar文件,你可以在这里看到https://github.com/essobedo/application-manager/blob/master/src/main/java/com/ github上/ essobedo / appma /核心/ DefaultApplicationManager.java#L419

这是在java 7上成功测试的更新 。 现在URLClassLoader对我来说很好

MyReloader

 class MyReloaderMain { ... //assuming ___BASE_DIRECTORY__/lib for jar and ___BASE_DIRECTORY__/conf for configuration String dirBase = ___BASE_DIRECTORY__; File file = new File(dirBase, "lib"); String[] jars = file.list(); URL[] jarUrls = new URL[jars.length + 1]; int i = 0; for (String jar : jars) { File fileJar = new File(file, jar); jarUrls[i++] = fileJar.toURI().toURL(); System.out.println(fileJar); } jarUrls[i] = new File(dirBase, "conf").toURI().toURL(); URLClassLoader classLoader = new URLClassLoader(jarUrls, MyReloaderMain.class.getClassLoader()); // this is required to load file (such as spring/context.xml) into the jar Thread.currentThread().setContextClassLoader(classLoader); Class classToLoad = Class.forName("my.app.Main", true, classLoader); instance = classToLoad.newInstance(); Method method = classToLoad.getDeclaredMethod("start", args.getClass()); Object result = method.invoke(instance, args); ... } 

关闭并重新启动ClassReloader

然后更新你的jar并打电话

 classLoader.close(); 

然后您可以使用新版本重新启动应用程序。

不要将jar包含在基类加载器中

不要将jar包含在“ MyReloaderMain ”的基类加载器“ MyReloaderMain.class.getClassLoader() ”中,换句话说,开发2个项目,其中2个jar用于“ MyReloaderMain ”,另一个用于实际应用程序而不依赖于这两个,或者你将无法理解我加载了什么。

Windows上的jdk1.8.0_2 5中仍然存在该错误。 虽然@Nicolas的答案很有帮助,但是当我在WildFly上运行它时,我在sun.net.www.protocol.jar.JarFileFactory打了一个ClassNotFound ,并且在调试一些盒子测试时发生了几次vm崩溃……

因此,我最终将处理加载和卸载的代码部分提取到外部jar。 从主代码我只用java -jar....调用它java -jar....所有看起来都很好。

注意:当jvm退出时,Windows会释放已加载的jar文件上的锁,这就是为什么这样做的原因。