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

时间:2010-07-09 21:51:51

标签: java nullpointerexception urlclassloader

我需要为应用程序的某些部分添加插件功能到现有应用程序。我希望能够在运行时添加一个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的下一次迭代。现在这是我不明白的地方。有时更新的类会重新加载而不会出现问题,即下一次迭代运行正常。但有时,我看到一个异常抛出:

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)

我在网上搜索过并摸不着头脑,但是无法得出任何结论,为什么会抛出这种异常,而且有时也是如此 - 有时候,并非总是如此。

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

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

6 个答案:

答案 0 :(得分:14)

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

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

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

URLClassLoader -> URLClassPath ucp -> ArrayList<Loader> 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的{​​{3}}(方法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在类source code中引入了close()方法,可能已解决此问题。我还没有验证过。

答案 1 :(得分:11)

此行为与jvm中的bug有关 记录了2个变通方法here

答案 2 :(得分:2)

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

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

答案 3 :(得分:1)

这是更新 java 7上成功测试。现在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); ... } 对我来说很好用

MyReloader

classLoader.close();

关闭并重新启动ClassReloader

然后更新您的jar并致电

MyReloaderMain.class.getClassLoader()

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

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

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

答案 4 :(得分:0)

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

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

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

答案 5 :(得分:0)

  1. 原则上,不能使用相同的类加载器重新加载已经加载的类。
  2. 对于新的加载,有必要创建一个新的类加载器,从而加载该类。
  3. 使用URLClassLoader有一个问题,那就是jar文件保持打开状态。
  4. 如果您有一个由URLClassLoader的不同实例从一个jar文件加载的多个类,并且在运行时更改了jar文件,通常会收到以下错误:java.util.zip.ZipException: ZipFile invalid LOC header (bad signature)。错误可能有所不同。
  5. 为了避免发生上述错误,必须使用给定的jar文件在所有close上使用URLClassLoader方法。但这实际上是导致整个应用程序重新启动的解决方案。

更好的解决方案是修改URLClassLoader,以便将jar文件的内容加载到RAM缓存中。这不再影响其他从同一jar文件读取数据的URLClassloader。然后可以在应用程序运行时自由更改jar文件。例如,您可以为此目的对URLClassLoader进行以下修改:in-memory URLClassLoader