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

我需要为应用程序的某些部分向现有应用程序添加插件功能。我希望能够在运行时添加一个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();
}

和罐头。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)

我在网上搜索并挠了挠头,但无法真正得出任何结论,为什么会抛出这个例外,而且也是如此 - 只是有时,并非总是如此。

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

如果您需要更多信息,请告诉我。感谢您的光临!


答案 1

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

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

解决方案很简单:在读取打开的 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发布的第二个链接。此代码也发布在 bug 报告页上。

但是,有一个问题:为了使此代码正常工作并能够获取打开的jar文件的句柄以关闭它们,用于通过URLClassLoader实现从文件中加载类的加载程序必须是。查看(方法)的源代码,我注意到只有当用于创建URL的文件字符串不以“/”结尾时,它才使用JARLoader。因此,必须按如下方式定义 URL:JarLoaderURLClassPathgetLoader(URL 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()


答案 2

此行为与此处记录的 jvm
2 解决方法中的 bug 有关


推荐