Die meisten JVMs verfügen über die HotSwap-Funktion von Java und die meisten Entwickler denken, dass es sich nur um ein Debugging-Tool handelt. Mit dieser Funktion ist es möglich, die Implementierung von Java-Methoden zu ändern, ohne den Java-Prozess neu zu starten. Ein typisches Beispiel ist die Codierung mithilfe einer IDE. HotSwap kann diese Funktionalität jedoch in einer Produktionsumgebung implementieren. Auf diese Weise können Sie Ihre Online-Anwendung erweitern oder kleinere Fehler in einem laufenden Projekt beheben, ohne das laufende Programm anzuhalten. In diesem Artikel werde ich die dynamische Bindung demonstrieren, Laufzeitcodeänderungen für die Bindung anwenden, einige Tools API und die Byte Buddy-Bibliothek vorstellen, die einige API-Codeänderungen bequemer bereitstellt.
Angenommen, es gibt eine laufende Anwendung, die eine spezielle Verarbeitung auf dem Server durchführt, indem sie den X-Priority-Header in HTTP-Anfragen validiert. Diese Überprüfung wird mithilfe der folgenden Toolklasse implementiert:
class HeaderUtility { static boolean isPriorityCall(HttpServletRequest request) { return request.getHeader("X-Pirority") != null; } }
Haben Sie den Fehler gefunden? Solche Fehler treten häufig auf, insbesondere wenn konstante Werte zur Wiederverwendung im Testcode in statische Felder zerlegt werden. In einem weniger idealen Szenario würde der Fehler erst bei der Installation des Produkts entdeckt, wenn der Header von einer anderen Anwendung generiert wird und keine Tippfehler enthält.
Es ist nicht schwer, solche Fehler zu beheben. Im Zeitalter der kontinuierlichen Bereitstellung ist die erneute Bereitstellung einer neuen Version nur einen Klick entfernt. Aber in anderen Fällen ist die Änderung möglicherweise nicht so einfach und der Neubereitstellungsprozess kann komplex sein, wenn Ausfallzeiten nicht zulässig sind und eine Ausführung mit Fehlern möglicherweise besser ist. Aber HotSwap bietet uns noch eine weitere Möglichkeit: kleine Änderungen vorzunehmen, ohne die Anwendung neu zu starten.
Um ein laufendes Java-Programm zu ändern, benötigen wir zunächst eine Methode, die sich im laufenden Zustand befinden kann Die Art und Weise, wie die JVM kommuniziert. Da es sich bei der Java-Implementierung der virtuellen Maschine um ein verwaltetes System handelt, verfügt sie über eine Standard-API zum Ausführen dieser Vorgänge. Die an der Frage beteiligte API heißt Anhangs-API und ist Teil der offiziellen Java-Tools. Mithilfe dieser von der laufenden JVM bereitgestellten API kann ein zweiter Java-Prozess mit ihr kommunizieren.
Tatsächlich verwenden wir diese API bereits: Sie wird bereits von Debugging- und Simulationstools wieVisualVM oder Java Mission Control implementiert. Die API zum Anwenden dieser Anhänge ist nicht mit der täglich verwendeten Standard-Java-API gepackt, sondern in einer speziellen Datei namens tools.jar, , die nur das JDK einer virtuellen Maschine enthält. Paket der Release-Version. Was noch schlimmer ist, ist, dass der Speicherort dieser JAR-Datei nicht festgelegt wurde. Er unterscheidet sich in VMs unter Windows, Linux, insbesondere unter Macintosh. Nicht nur der Speicherort der Datei, sondern auch der Dateiname ist unterschiedlich . Bei einigen Distributionen heißt es classes.jar. Schließlich beschloss IBM sogar, die Namen einiger in dieser JAR enthaltenen Klassen zu ändern und alle com.sun-Klassen in den com.ibm Namespace zu verschieben, was für ein weiteres Durcheinander sorgte. In Java 9 wurde das Chaos endlich beseitigt und tools.jar durch Jigsaws Modul jdk.attach ersetzt.
Nachdem Sie das JAR (oder Modul) der API gefunden haben, ist es an der Zeit, es für den Anhangsprozess verfügbar zu machen. Bei OpenJDK heißt die Klasse, die zum Herstellen einer Verbindung zu einer anderen JVM verwendet wird, VirtualMachine. Sie bietet einen Einstiegspunkt für jede VM, die von einem JDK oder einer regulären HtpSpot-JVM auf derselben physischen Maschine ausgeführt wird. Nachdem wir über die Prozess-ID eine Verbindung zu einer anderen virtuellen Maschine hergestellt haben, können wir eine JAR-Datei in einem von der Ziel-VM angegebenen Thread ausführen:// the following strings must be provided by us String processId = processId(); String jarFileName = jarFileName(); VirtualMachine virtualMachine = VirtualMachine.attach(processId); try { virtualMachine.loadAgent(jarFileName, "World!"); } finally { virtualMachine.detach(); }
public class HelloWorldAgent { public static void agentmain(String arg) { System.out.println("Hello, " + arg); } }
到目前来看一切顺利。但是除了成功地同目标 VM 建立起了通信之外,我们还不能够修改目标 VM 上的代码以及 BUG。后续的修改,Java 代理可以定义第二参数来接收一个 Instrumentation 的实例 。稍后要实现的接口提供了向几个底层方法的访问途径,它们中的一个就能够对已经加载的代码进行修改。
为了修正 “X-Pirority” 错字,我们首先来假设为 HeaderUtility 引入了一个修复类,叫做 typo.fix,就在我们下面所开发的 BugFixAgent 后面的代理的 JAR 文件中。此外,我们需要给予代理通过向 manifest 文件添加 Can-Redefine-Classes: true 来替换现有类的能力。有了现在这些东西,我们就可以使用 instrumentation 的 API 来对类进行重新定义,该 API 会接受一对已经加载的类以及用来执行类重定义的字节数组:
public class BugFixAgent { public static void agentmain(String arg, Instrumentation inst) throws Exception { // only if header utility is on the class path; otherwise, // a class can be found within any class loader by iterating // over the return value of Instrumentation::getAllLoadedClasses Class<?> headerUtility = Class.forName("HeaderUtility"); // copy the contents of typo.fix into a byte array ByteArrayOutputStream output = new ByteArrayOutputStream(); try (InputStream input = BugFixAgent.class.getResourceAsStream("/typo.fix")) { byte[] buffer = new byte[1024]; int length; while ((length = input.read(buffer)) != -1) { output.write(buffer, 0, length); } } // Apply the redefinition instrumentation.redefineClasses( new ClassDefinition(headerUtility, output.toByteArray())); } }
运行上述代码后,HeaderUtility 类会被重定义以对应其修补的版本。对 isPrivileged 的任何后续调用现在将读取正确的头信息。作为一个小的附加说明,JVM 可能会在应用类重定义时执行完全的垃圾回收,并且会对受影响的代码进行重新优化。 总之,这会导致应用程序性能的短时下降。然而,在大多数情况下,这是较之完全重启进程更好的方式。
当应用代码更改时,要确保新类定义了与它替换的类完全相同的字段、方法和修饰符。 尝试修改任何此类属性的类重定义行为都会导致 UnsupportedOperationException。现在 HotSpot 团队正试图去掉这个限制。此外,基于 OpenJDK 的动态代码演变虚拟机支持预览此功能。
一个如上述示例的简单的 BUG 修复代理在你熟悉了 instrumentation 的 API 的时候是比较容易实现的。只要更加深入一点,也可以在运行代理的时候,无需手动创建附加的 class 文件,而是通过重写现有的 class 来应用更多通用的代码修改。
编译好的 Java 代码所呈现的是一系列字节码指令。从这个角度来看,一个 Java 方法无非就是一个字节数组,其每一个字节都是在表示一个向运行时发出的指令,或者是最近一个指令的参数。每个字节对应其意义的映射在《Java 虚拟机规范》中进行了定义,例如字节 0xB1 就是在指示 VM 从一个带有 void 返回类型的方法返回。因此,对字节码进行增强就是对一个方法的字节数字进行扩展,将我们想要应用的表示额外的业务逻辑指令包含进去。
当然,逐个字节的操作会特别麻烦,而且容易出错。为了避免手工的处理,许多的库都提供了更高级一点的 API,使用它们不需要我们直接同 Java 字节码打交道。这样的库其中就有一个叫做 Byte Buddy (当然我就是该库的作者)。它的功能之一就是能够定义可以在方法原来的代码之前和之后被执行的模板方法。
Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der Verwendung des dynamischen Mountens zur Implementierung eines Bug-Hotfixes in Java (Bild). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!