在运行时更新 JAR

IT小君   2021-12-04T04:05:46

如果 jar 在 JVM 中运行,则可以卸载当前运行的 Jar 并将其从系统中删除。下载新版本并使用与上一个 Jar 相同的名称对其进行重命名,然后初始化新 Jar,从而在 JVM 中创建 Jar 的无缝更新。甚至可以指示 JVM 执行此操作吗?甚至可以在运行时更新 Jar 吗?

评论(9)
IT小君

下载一个新版本并使用与上一个 Jar 相同的名称重命名它,然后初始化新 Jar,在 JVM 中创建 Jar 的无缝更新......甚至可以在运行时更新 Jar 吗?

JAR 文件未“运行”,JVM 正在运行。您的 JAR 文件仅包含使 JVM 执行有用工作的类信息(也称为字节码指令)。在大多数情况下,JVM 实际上不会在您的 JAR 文件上设置系统锁定,因此您可以将该文件替换为您的心内容。

当然,真正的问题是,一旦 JVM 加载了您的 JAR,它就会愉快地继续加载它所加载的内容,并且永远不会再从您的 JAR 文件中读取,无论您覆盖它多少次。这是默认类加载器的行为,无法更改 - 但是正如其他人指出的那样 - 您不必使用默认类加载器。您可以实现自己的,类似于 Web 应用程序服务器使用的,以便从文件系统加载更新的 JARS。不过要注意 - 定义自己的类加载器被认为是“坏主意™”,除非您真的知道自己在做什么。您可以在此处此处阅读更多内容

2021-12-04T04:05:46   回复
IT小君

这是我以前见过很多次(我自己也做过)的事情。我总结了可能出现的问题/解决方案的一些要点。

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

总而言之,当您想要实现 JAR 重新加载时,您可能会遇到许多问题。幸运的是,有一些方法可以将风险降至最低。最安全的方法是做类似的事情以获得相同的效果。最干净的是自定义类加载器和 JAR 文件版本控制。

2021-12-04T04:05:46   回复
IT小君

通常你不能这样做,因为据我所知,这种行为没有正式定义。

但是,您可以使用官方类路径之外的 jar 文件创建类加载器,然后根据需要从中加载类。通过丢弃类加载器加载的所有类实例,您可以删除当前资源,然后在新 jar 文件上实例化一个新的类加载器,然后加载新类并创建新对象。

这很复杂,所以也许您会将 jar 变成一个 OSGi 模块并通过 OSGi 加载器调用您的程序?

2021-12-04T04:05:47   回复
IT小君

您不能写入正在运行的 jar。没有与 getResourceInputStream 等效的写入方法。我想如果您尝试使用 FileOutputStream 编写,因为 JVM 使用它,您将无法删除它,因为系统会阻止它。

无论如何,仍然可以在不同的 jar 中提供不同模块的更新。因此,您可以想象拥有一个应用程序的主 jar 文件,该文件可以通过一个包含更新程序的小型独立可运行 jar 文件进行更新。

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

服务器端应用程序也是向用户隐藏更新的替代方法。

问候, 斯蒂芬

2021-12-04T04:05:47   回复
IT小君

答案在于 Java 类加载器。这些家伙从 JARS 或 .class 文件、byte[]值或 URL 或其他任何东西加载类每当您访问一个类时,您都在隐式地使用类加载器来为您提供正确的类实例。

创建您选择的类加载器,并在需要“刷新”类时切换类加载器。看一看Thread.setContextClassLoader方法——这将改变线程的类加载器。

定义您自己的类加载器非常简单——只需对ClassLoader类进行类化并覆盖其findClass 方法即可

2021-12-04T04:05:47   回复
IT小君

请注意汤姆哈伯德的评论。如果某个类尚未在运行时使用(因此未加载),如果您在新 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
2021-12-04T04:05:48   回复
IT小君

您可以使用 HotswapAgent 来实现它。它支持一些广泛使用的框架的插件,也有助于编写新的自定义插件

HotswapAgent - https://github.com/HotswapProjects/HotswapAgent

2021-12-04T04:05:48   回复
IT小君

在我的软件套件中,这是一个复杂的模块化客户端网格,链接到中央服务器进行长期延时摄影,我添加了更新软件的功能。

byte[] file = recieve();

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

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

我还没有指出可能是也可能不是错误的原因,但是,在某些情况下,我观​​察到了 hs_err_pid.log 的崩溃。

我还有一个变量,final 和 static,称为“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

我很想知道这是否可行。

希望这可以帮助!

2021-12-04T04:05:48   回复
IT小君

如果允许JVM重启

正如人们所提到的,JVM 进程将打开 jar 文件的文件句柄,直到进程生命周期结束。如果 Linux 是您的目标操作系统,那么您可以执行以下技巧。

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

如果不允许JVM 重启

那么更新类的唯一方法是——即使使用重新定义的类加载器——强制JVM首先卸载类。这里的问题是 JVM 只会在垃圾收集类加载器时这样做。对于标准实现,几乎无法控制该过程。

2021-12-04T04:05:48   回复