简体   繁体   English

使用 URLClassLoader 重新加载 jar 的问题

[英]Problem reloading a jar using URLClassLoader

I need to add plugin functionality to an existing application for certain parts of the application.我需要为应用程序的某些部分向现有应用程序添加插件功能。 I want to be able to add a jar at runtime and the application should be able to load a class from the jar without restarting the app.我希望能够在运行时添加一个 jar,并且应用程序应该能够在不重新启动应用程序的情况下从 jar 加载一个类。 So far so good.到现在为止还挺好。 I found some samples online using URLClassLoader and it works fine.我使用 URLClassLoader 在网上找到了一些示例,它工作正常。

I also wanted the ability to reload the same class when an updated version of the jar is available.我还希望能够在 jar 的更新版本可用时重新加载相同的类。 I again found some samples and the key to achieving this as I understand is that I need to use a new classloader instance for each new load.我再次找到了一些示例,据我所知,实现这一目标的关键是我需要为每个新加载使用一个新的类加载器实例。

I wrote some sample code but hit a NullPointerException.我写了一些示例代码,但遇到了 NullPointerException。 First let me show you guys the code:先给大家看看代码:

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 is a simple interface with just one method doProc: IPlugin 是一个简单的接口,只有一个方法 doProc:

public interface IPlugin {
    void doProc();
}

and jartest.TestPlugin is an implementation of this interface where doProc just prints out some statements.而 jartest.TestPlugin 是这个接口的一个实现,其中 doProc 只是打印出一些语句。

Now, I package the jartest.TestPlugin class into a jar called test.jar and place it under C:\\plugins and run this code.现在,我将 jartest.TestPlugin 类打包到一个名为 test.jar 的 jar 中,并将其放在 C:\\plugins 下并运行此代码。 The first iteration runs smoothly and the class loads without issues.第一次迭代运行顺利,类加载没有问题。

When the program is executing the sleep statement, I replace C:\\plugins\\test.jar with a new jar containing an updated version of the same class and wait for the next iteration of while.当程序执行 sleep 语句时,我将 C:\\plugins\\test.jar 替换为包含同一类的更新版本的新 jar,并等待下一次迭代 while。 Now here's what I don't understand.现在这是我不明白的地方。 Sometimes the updated class gets reloaded without issues ie the next iteration runs fine.有时更新的类被重新加载而没有问题,即下一次迭代运行良好。 But sometimes, I see an exception thrown:但有时,我会看到抛出异常:

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)

I have searched on the net and scratched my head but can't really arrive at any conclusion as to why this exception is thrown and that too - only sometimes, not always.我在网上搜索过,摸不着头脑,但无法真正得出关于为什么会抛出这个异常以及为什么会抛出任何结论 - 只是有时,并非总是如此。

I need your experience and expertise to understand this.我需要你的经验和专业知识来理解这一点。 What's wrong with this code?这段代码有什么问题? Please help!!请帮忙!!

Let me know if you need any more info.如果您需要更多信息,请告诉我。 Thanks for looking!感谢您的关注!

For everyone's benefit, let me summarize the real problem and the solution that worked for me.为了大家的利益,让我总结一下真正的问题和对我有用的解决方案。

As Ryan pointed out, there is a bug in JVM, which affects Windows Platform.正如 Ryan 所指出的,JVM 中存在一个bug ,它会影响 Windows 平台。 URLClassLoader does not close the open jar files after it opens them for loading classes, effectively locking the jar files. URLClassLoader在打开用于加载类的 jar 文件后不会关闭它们,从而有效地锁定了 jar 文件。 The jar files can't be deleted or replaced.无法删除或替换 jar 文件。

The solution is simple: close the open jar files after they've been read.解决方案很简单:在读取后关闭打开的 jar 文件。 However, to get a handle to the open jar files, we need to use reflection since the properties we need to traverse down are not public.但是,要获得打开的 jar 文件的句柄,我们需要使用反射,因为我们需要向下遍历的属性不是公开的。 So we traverse down this path所以我们沿着这条路走下去

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

The code to close the open jar files can be added to a close() method in a class extending URLClassLoader:可以将关闭打开的 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;
    }
}

(This code was taken from the second link that Ryan posted. This code is also posted on the bug report page.) (此代码取自 Ryan 发布的第二个链接。此代码也发布在错误报告页面上。)

However, there's a catch: For this code to work and be able to get a handle to the open jar files to close them, the loader used to load the classes from the file by URLClassLoader implementation has to be a JarLoader .但是,有一个问题:要使此代码工作并能够获取打开的 jar 文件的句柄以关闭它们,用于通过 URLClassLoader 实现从文件加载类的加载器必须是JarLoader Looking at the source code of URLClassPath (method getLoader(URL url) ), I noticed that it uses a JARLoader only if the file string used to create the URL does not end in "/".查看URLClassPath源代码(方法getLoader(URL url) ),我注意到它仅在用于创建 URL 的文件字符串不以“/”结尾时才使用 JARLoader。 So, the URL must be defined like this:因此,必须像这样定义 URL:

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

The overall class loading code should look something like this:整个类加载代码应如下所示:

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();
    } 
}

Update: JRE 7 has introduced a close() method in the class URLClassLoader which may have solved this issue.更新: JRE 7 在URLClassLoader类中引入了close()方法,它可能已经解决了这个问题。 I haven't verified it.我没有验证过。

This behaviour is related to a bug in the jvm此行为与 jvm 中的错误有关
2 workarounds are documented here 此处记录2 个解决方法

Starting from Java 7, you indeed have a close() method in URLClassLoader but it is not enough to release completely the jar files if you call directly or indirectly methods of type ClassLoader#getResource(String) , ClassLoader#getResourceAsStream(String) or ClassLoader#getResources(String) .从 Java 7 开始,您确实在URLClassLoader有一个close()方法,但是如果您直接或间接调用ClassLoader#getResource(String)ClassLoader#getResourceAsStream(String)ClassLoader#getResources(String)类型的方法,则不足以完全释放 jar 文件ClassLoader#getResources(String) Indeed by default, the JarFile instances are automatically stored into the cache of JarFileFactory in case we call directly or indirectly one of the previous methods and those instances are not released even if we call java.net.URLClassLoader#close() .事实上,默认情况下, JarFile实例会自动存储到JarFileFactory的缓存中,以防我们直接或间接调用前面的方法之一,即使我们调用java.net.URLClassLoader#close()也不会释放这些实例。

So a hack is still needed in this particular case even with Java 1.8.0_74, here is my hack https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/util/Classpath.java#L83 that I use here https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/DefaultApplicationManager.java#L388 .因此,即使使用 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 Even with this hack, I still had to call the GC explicitly to fully release the jar files as you can see here https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/DefaultApplicationManager.java#L419即使有了这个 hack,我仍然必须明确调用 GC 以完全释放 jar 文件,如您在此处所见https://github.com/essobedo/application-manager/blob/master/src/main/java/com/ github/essobedo/appma/core/DefaultApplicationManager.java#L419

This is an update tested on java 7 with success .这是在java 7 上测试成功的更新 Now the URLClassLoader works fine for me现在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);

...
}

Close and Restart the ClassReloader关闭并重新启动 ClassReloader

then update your jar and call然后更新你的罐子并打电话

classLoader.close();

then you can restart the app with the new version.然后您可以使用新版本重新启动应用程序。

Do not include your jar into your base class loader不要将您的 jar 包含在您的基类加载器中

Do not include your jar into your base class loader " MyReloaderMain.class.getClassLoader() " of the " MyReloaderMain ", in other words develop 2 project with 2 jars one for " MyReloaderMain " and the other one for your real application without dependency between the two, or you will not able to understand who i loading what.不包括你的jar放到你的基类加载器“ MyReloaderMain.class.getClassLoader()了”的“ MyReloaderMain ”,换句话说,开发二期工程,2瓶为一个“ MyReloaderMain ”,另一种为您的实际应用没有之间的依赖关系两者,否则你将无法理解我加载了什么。

The error is still present in jdk1.8.0_2 5 on Windows .该错误仍然存​​在于Windows上的jdk1.8.0_2 5 中。 Although @Nicolas' answer helps, I hit a ClassNotFound for sun.net.www.protocol.jar.JarFileFactory when running it on WildFly, and several vm crashes while debugging some box tests...尽管@Nicolas 的回答有所帮助,但在 WildFly 上运行sun.net.www.protocol.jar.JarFileFactory时,我遇到了ClassNotFound ,并且在调试一些框测试时有几个虚拟机崩溃了......

Therefore I ended up extracting the part of the code which deals with loading and unloading, to an external jar.因此,我最终将处理加载和卸载的代码部分提取到外部 jar 中。 From the main code I just call this with java -jar.... all looks fine for now.在主代码中,我只是用java -jar....调用它。现在一切看起来都很好。

NOTE: Windows does release the locks on the loaded jar files when the jvm exits, that is why this works.注意:当 jvm 退出时,Windows 确实会释放对加载的 jar 文件的锁,这就是为什么这样做有效。

  1. In principle, a class that has already been loaded cannot be reloaded with the same classloader.原则上,已经加载的类不能用同一个类加载器重新加载。
  2. For a new load, it is necessary to create a new classloader and thus load the class.对于新的加载,需要创建一个新的类加载器,从而加载该类。
  3. Using URLClassLoader has one problem and that is that the jar file remains open.使用URLClassLoader有一个问题,那就是 jar 文件保持打开状态。
  4. If you have multiple classes loaded from one jar file by different instances of URLClassLoader and you change the jar file at runtime, you will usually get this error: java.util.zip.ZipException: ZipFile invalid LOC header (bad signature) .如果您通过URLClassLoader的不同实例从一个 jar 文件加载了多个类,并且您在运行时更改了 jar 文件,您通常会收到以下错误: java.util.zip.ZipException: ZipFile invalid LOC header (bad signature) The error may be different.错误可能不同。
  5. In order for the above errors not to occur, it is necessary to use the close method on all URLClassLoader s using the given jar file.为了不发生上述错误,必须使用给定的 jar 文件对所有URLClassLoader使用close方法。 But this is a solution that actually leads to a restart of the entire application.但这是一个实际上会导致整个应用程序重新启动的解决方案。

A better solution is to modify the URLClassLoader so that the contents of the jar file are loaded into the RAM cache.更好的解决方案是修改URLClassLoader以便将 jar 文件的内容加载到 RAM 缓存中。 This no longer affects other URLClassloader s that read data from the same jar file.这不再影响从同一个 jar 文件读取数据的其他URLClassloader The jar file can then be freely changed while the application is running.然后可以在应用程序运行时自由更改 jar 文件。 For example, you can use this modification of URLClassLoader for this purpose: in-memory URLClassLoader例如,您可以为此目的使用URLClassLoader这种修改: in-memory URLClassLoader

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM