18

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

我已经在网上搜索并挠了挠头,但无法真正得出关于为什么会引发此异常的任何结论 - 只是有时,并非总是如此。

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

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

4

6 回答 6

16

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

正如 Ryan 所指出的,JVM 中存在一个影响 Windows 平台的bugURLClassLoader在打开它们以加载类后不会关闭打开的 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. 查看(method ) 的源代码,我注意到它仅在用于创建 URL 的文件字符串不以“/”结尾时才使用 JARLoader。因此,必须像这样定义 URL:URLClassPathgetLoader(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 7close()在类中引入了一个方法URLClassLoader,可能已经解决了这个问题。我还没有验证。

于 2010-12-10T07:57:00.490 回答
11

此行为与jvm 2 变通办法中的错误有关,此处记录

于 2010-07-10T04:28:42.080 回答
3

从Java 7开始,你确实有一个close()方法,URLClassLoader但是如果你直接或间接调用类型的方法 ClassLoader#getResource(String)ClassLoader#getResourceAsStream(String)或者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/core/DefaultApplicationManager.java#L419

于 2016-03-11T17:28:47.340 回答
1

这是在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.class.getClassLoader()”中MyReloaderMain,换句话说,使用 2 个 jar 开发 2 个项目,一个用于“ MyReloaderMain”,另一个用于您的实际应用程序,两者之间没有依赖关系,否则您将无法了解我加载了什么。

于 2015-07-25T07:32:35.187 回答
0

错误仍然存​​在于jdk1.8.0_25 上Windows。虽然@Nicolas 的回答有帮助,但我在 WildFly 上运行它时遇到了一个ClassNotFound问题sun.net.www.protocol.jar.JarFileFactory,并且在调试一些盒子测试时有几个 vm 崩溃了......

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

注意:当 jvm 退出时,Windows 确实会释放加载的 jar 文件上的锁,这就是它起作用的原因。

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

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

于 2020-07-08T19:01:43.403 回答