如果 jar 在 JVM 中运行,则可以卸载当前运行的 Jar 并将其从系统中删除。下载新版本并使用与上一个 Jar 相同的名称对其进行重命名,然后初始化新 Jar,从而在 JVM 中创建 Jar 的无缝更新。甚至可以指示 JVM 执行此操作吗?甚至可以在运行时更新 Jar 吗?
如果 jar 在 JVM 中运行,则可以卸载当前运行的 Jar 并将其从系统中删除。下载新版本并使用与上一个 Jar 相同的名称对其进行重命名,然后初始化新 Jar,从而在 JVM 中创建 Jar 的无缝更新。甚至可以指示 JVM 执行此操作吗?甚至可以在运行时更新 Jar 吗?
这是我以前见过很多次(我自己也做过)的事情。我总结了可能出现的问题/解决方案的一些要点。
总而言之,当您想要实现 JAR 重新加载时,您可能会遇到许多问题。幸运的是,有一些方法可以将风险降至最低。最安全的方法是做类似的事情以获得相同的效果。最干净的是自定义类加载器和 JAR 文件版本控制。
通常你不能这样做,因为据我所知,这种行为没有正式定义。
但是,您可以使用官方类路径之外的 jar 文件创建类加载器,然后根据需要从中加载类。通过丢弃类加载器加载的所有类实例,您可以删除当前资源,然后在新 jar 文件上实例化一个新的类加载器,然后加载新类并创建新对象。
这很复杂,所以也许您会将 jar 变成一个 OSGi 模块并通过 OSGi 加载器调用您的程序?
您不能写入正在运行的 jar。没有与 getResourceInputStream 等效的写入方法。我想如果您尝试使用 FileOutputStream 编写,因为 JVM 使用它,您将无法删除它,因为系统会阻止它。
无论如何,仍然可以在不同的 jar 中提供不同模块的更新。因此,您可以想象拥有一个应用程序的主 jar 文件,该文件可以通过一个包含更新程序的小型独立可运行 jar 文件进行更新。
还可以使用 JNLP 来自动无缝更新应用程序。
服务器端应用程序也是向用户隐藏更新的替代方法。
问候, 斯蒂芬
答案在于 Java 类加载器。这些家伙从 JARS 或 .class 文件、byte[]
值或 URL 或其他任何东西加载类。每当您访问一个类时,您都在隐式地使用类加载器来为您提供正确的类实例。
创建您选择的类加载器,并在需要“刷新”类时切换类加载器。看一看Thread.setContextClassLoader方法——这将改变线程的类加载器。
定义您自己的类加载器非常简单——只需对ClassLoader
类进行子类化并覆盖其findClass 方法即可。
请注意汤姆哈伯德的评论。如果某个类尚未在运行时使用(因此未加载),如果您在新 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
您可以使用 HotswapAgent 来实现它。它支持一些广泛使用的框架的插件,也有助于编写新的自定义插件
HotswapAgent - https://github.com/HotswapProjects/HotswapAgent
在我的软件套件中,这是一个复杂的模块化客户端网格,链接到中央服务器进行长期延时摄影,我添加了更新软件的功能。
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
我很想知道这是否可行。
希望这可以帮助!
正如人们所提到的,JVM 进程将打开 jar 文件的文件句柄,直到进程生命周期结束。如果 Linux 是您的目标操作系统,那么您可以执行以下技巧。
unlink(2)
或等效方法删除原始 JAR 文件。该路径将立即从文件系统元数据中删除,但物理文件数据将保留直到最后一个文件句柄关闭,因此不会发生损坏。那么更新类的唯一方法是——即使使用重新定义的类加载器——强制JVM首先卸载类。这里的问题是 JVM 只会在垃圾收集类加载器时这样做。对于标准实现,几乎无法控制该过程。
JAR 文件未“运行”,JVM 正在运行。您的 JAR 文件仅包含使 JVM 执行有用工作的类信息(也称为字节码指令)。在大多数情况下,JVM 实际上不会在您的 JAR 文件上设置系统锁定,因此您可以将该文件替换为您的心内容。
当然,真正的问题是,一旦 JVM 加载了您的 JAR,它就会愉快地继续加载它所加载的内容,并且永远不会再从您的 JAR 文件中读取,无论您覆盖它多少次。这是默认类加载器的行为,无法更改 - 但是正如其他人指出的那样 - 您不必使用默认类加载器。您可以实现自己的,类似于 Web 应用程序服务器使用的,以便从文件系统加载更新的 JARS。不过要注意 - 定义自己的类加载器被认为是“坏主意™”,除非您真的知道自己在做什么。您可以在此处和此处阅读更多内容。