在运行时更新JAR文件

35

如果一个JAR文件在JVM内运行,能否卸载当前正在运行的JAR并将其从系统中删除。下载一个新版本并使用与上一个JAR相同的名称重命名它,然后初始化新的JAR,在JVM中创建无缝更新的JAR。是否可以指示JVM执行此操作?甚至在JAR文件运行时更新它是否可能?

9个回答

30
下载一个新的版本并将其重命名为与上一个JAR文件相同的名称,然后初始化新的JAR文件,在JVM中创建无缝更新的JAR文件...即使在JAR文件运行期间更新它也是可能的吗?
JAR文件本身并不是“正在运行”,而是JVM正在运行。你的JAR文件只包含使JVM执行有用工作的类信息(也称为字节码指令)。在大多数情况下,JVM实际上不会对您的JAR文件进行系统锁定,因此您可以随心所欲地替换该文件。
当然,真正的问题在于一旦JVM加载了您的JAR文件,它将快乐地进行加载,并永远不会再次从您的JAR文件中读取,无论您覆盖它多少次。这是默认类加载器的行为,不能更改。但是,正如其他人指出的那样,您不必使用默认类加载器。您可以实现自己的类加载器,类似于Web应用程序服务器使用的方式,以从文件系统加载更新的JARS。需要注意的是,除非您确切知道自己在做什么,否则定义自己的类加载器被认为是“坏主意™”。您可以在这里这里阅读更多信息。

6
我们曾遇到过这样的问题,即当我们在类被加载后更新了JAR包时,出现了NoClassDefined错误。我猜测请求的类尚未被加载,当JVM试图加载它时,JAR包已经发生了足够大的变化以至于被“破坏”。 - Tom Hubbard
4
了解,Windows 7 会锁定 JAR 文件。 - etherous
1
默认的类加载器是否在开始时加载每个类?你能引用一下来源吗? - Cybermonk
1
在Ubuntu和Oracle的JDK上,我遇到了NoClassDefined异常,我认为最有可能的解释是在先前版本运行时覆盖了JAR文件。 - H2ONaCl

30

这是我之前看到过很多次的事情(也做过自己的)我总结了一些问题/解决方案的要点。

  • 如果您重写JVM稍后将使用的JAR文件,JVM将崩溃并转储。
    • 稍后指类加载非常懒惰,并且某些类可能仅在程序生命周期的后期加载
    • JVM为JAR文件打开句柄,lib将因JAR和指针变得错误而失败
    • 可以通过从JAR文件中预加载所有类和资源来降低概率
    • 如果您有自定义的类加载器,则可以自行关闭处理器句柄。
  • 你需要了解类加载是如何完成的。最好是被控制。
    • 一个自定义类加载器,将为每个JAR创建类加载器并管理版本
    • 了解应用程序如何使用类加载器以及如何作用于新的JAR文件(例如检查Tomcat在重写WAR档案时会发生什么)
  • 在Windows上,您的JAR文件将被锁定,并且无法覆盖它们。如果您有控制权,则可以在使用后解锁它们(关闭它们)。对于第三方系统,您必须找到相应的标志。例如,您可以在Tomcat上下文配置中检查antiJARLocking。
  • 最好避免重写相同的文件,而是进行一些版本控制

总之,当您想要实现JAR重新加载时,可能会遇到许多问题。幸运的是,有几种方法可以将风险降至最低。最安全的方式是采用类似方法获得相同的效果。最清洁的是自定义类加载器和JAR文件版本控制。


如果你覆盖了JAR文件并且后来需要使用需要懒加载的方法,对我来说它不会崩溃。我的(多线程)程序只是在100%的CPU上挂起,花费了我很多时间才发现重建JAR可能是原因。 - peschü

2
一般来说,据我所知,你不能这样做,因为这种行为并没有被官方定义。但是,你可以创建一个类加载器,使用一个在正式类路径之外的 jar 文件,并在需要时从中加载类。通过丢弃类加载器加载的所有类的实例,你可以删除当前资源,然后在新的 jar 文件上实例化一个新的类加载器,加载新的类并创建新的对象。这相当复杂,因此也许你可以将 jar 文件作为 OSGi 模块,并通过 OSGi 加载器调用你的程序?

1

你无法写入正在运行的 jar 文件。没有相应的 getResourceInputStream 函数可用于编写。我猜想,如果您尝试使用 FileOutputStream 进行写操作,由于 JVM 使用它,您将无法删除该文件,因为系统会阻止它。

总之,仍然可以通过不同 jar 包中的不同模块来提供更新。因此,您可以想象一个包含更新程序的小型独立运行 jar 文件,用于更新应用程序的主 jar 文件。

还可以使用 JNLP 自动和无缝地更新应用程序。

服务器端应用程序也是隐藏用户更新的另一种选择。

祝好, Stéphane


1
答案在于Java类加载器。这些家伙从JAR文件、.class文件、byte[]值或URL等任何地方加载类。每当您访问一个类时,都会隐式使用类加载器来为您提供正确的类实例。
选择一个类加载器并创建它,只需在需要“刷新”类时切换类加载器即可。查看Thread.setContextClassLoader方法--这将更改线程的类加载器。
定义自己的类加载器非常简单--只需子类化ClassLoader类并覆盖其findClass方法

1
请注意Tom Hubbard的评论。如果一个类在运行时尚未被使用(因此未加载),如果您在新的JAR中删除该类,则会出现一些问题。
例如,如果您执行以下代码:
    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        while (true) {
            long now = System.currentTimeMillis();
            long delta = (now - startTime);
            System.out.println("Running... delta is " + delta);
            if (TimeUnit.MILLISECONDS.toSeconds(delta) > 30) {
                Me1 m1 = new M1();
                me1.method1(); // ###
            }
            Thread.sleep(1000);
        }
    }

如果你把上述内容放入一个Jar文件并执行,在30秒之前,你将修改Jar文件,使得类"Me1"不再包含"method1"方法(当然你需要删除标记为"###"的那一行),在执行30秒后,你会收到一个异常。
    Exception in thread "main" java.lang.NoClassDefFoundError: Me1
    at Me.main(Me.java:16)
    Caused by: java.lang.ClassNotFoundException: Me1
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

0
在我的软件套件中,这是一个复杂的模块化客户端网状结构,连接到一个长期延时摄影的中央服务器,我已经添加了更新软件的功能。
byte[] file = recieve();

FileOutputStream fos = new FileOutputStream("software.jar");
fos.flush(); fos.write(file); fos.close();

完成此过程后,客户端在重新启动之前需要执行一系列步骤。其中包括长时间的线程休眠、文件读写和网络交互等过程。

我还没有确定可能存在的错误,但在某些情况下,我观察到会出现hs_err_pid.log崩溃。

我还有一个名为“SOFTWARE_VERSION”的变量,它是最终且静态的。我确认了这个变量在替换后可以更新(从服务器界面观察),而无需重新启动软件。

经过仔细考虑,我决定在软件更新后立即重新启动机器(该程序将在启动时执行)。由于更新的完整性不可靠,我认为最好给它一个全新的开始。可以运行像这样的操作(未经测试):

Runtime.getRuntime().exec("sudo java -jar software.jar");
System.exit(0);

然而,我不知道那会有多可靠。你也可以尝试运行类似以下代码:

Runtime.getRuntime().exec("run_software.sh")
System.exit(0);

然后在 run_software.sh 文件中:

sleep 1000
sudo java -jar software.jar

我很想知道那是否可行。

希望这有所帮助!


0

0

如果允许JVM重启

正如人们所提到的,JVM进程将保持jar文件的文件句柄直到进程结束。如果Linux是您的目标操作系统,则可以使用以下技巧。

  1. 使用unlink(2)或等效方法删除原始JAR文件。路径将立即从文件系统元数据中删除,但物理文件数据将保留,直到关闭最后一个文件句柄,因此不会发生损坏。
  2. 将新文件写入相同的路径。即使路径相同,实际数据也将进入另一个块,因此再次 - 没有数据损坏,一切都是100%合法的。
  3. 使用与启动原始进程时使用的相同命令行重启JVM进程。

如果不允许JVM重启

那么更新类的唯一方法是-即使重新定义了类加载器-也要强制JVM首先卸载该类。问题在于,当垃圾收集类加载器时,JVM才会执行此操作。对于标准实现,几乎无法控制该过程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接