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

标签 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 是一个简单的接口(interface),只有一个方法 doProc:

public interface IPlugin {
    void doProc();
}

jartest.TestPlugin 是此接口(interface)的一个实现,其中 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)

我在网上搜索并摸不着头脑,但无法真正得出关于为什么会抛出此异常以及抛出那个异常的任何结论 - 只是有时,并非总是如此。

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

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

最佳答案

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

正如 Ryan 所指出的,有一个 bug在影响 Windows 平台的 JVM 中。 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。看着 source code 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() 方法。这可能已经解决了这个问题。我还没有验证过。

关于java - 使用 URLClassLoader 重新加载 jar 时出现问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3216780/

相关文章:

java - Monitor USB IO "traffic"(任何语言)

Java 锦标赛日程递归

java - Apache Tomcat - 读取生成的旧文件

java - JPA,查询提示是否特定于供应商?

java - Java Android 应用 MediaPlayer 中的 NullPointerException

java - URLClassLoader 在同一文件夹中找到 X 但找不到 Y

java - ClassLoader类依赖(插件)

android - 方向更改后,菜单中的 TextView 始终为 null

java - Spring依赖注入(inject)在调用其类方法时得到NullPointEException

java - 如何动态加载目录中的所有jar?