Home >Java >javaTutorial >Detailed explanation of using dynamic mounting to implement bug hot fix in Java (picture)
Most JVMs have Java's HotSwap feature, and most developers think it is just a debugging tool. Using this feature, it is possible to change the implementation of Java methods without restarting the Java process. A typical example is coding using an IDE. However HotSwap can implement this functionality in a production environment. In this way, you can extend your online application or fix minor bugs in a running project without stopping the running program. In this article, I will demonstrate dynamic binding, apply runtime code changes for binding, introduce some tools API, and the Byte Buddy library, which provides some API code changes more conveniently.
Suppose there is a running application that performs special processing on the server by validating the X-Priority header in HTTP requests. This verification is implemented using the following tool class:
class HeaderUtility { static boolean isPriorityCall(HttpServletRequest request) { return request.getHeader("X-Pirority") != null; } }
Did you find the error? Such errors are common, especially when constant values are decomposed into static fields for reuse in test code. In a less-than-ideal scenario, the error would only be discovered when the product is installed, where the header is generated by another application and contains no typos.
It is not difficult to fix errors like this. In the age of continuous delivery, redeploying a new version is just a click of a button away. But in other cases, the change may not be that simple, and the redeployment process may be complex, where downtime is not allowed and running with errors may be better. But HotSwap provides us with another option: making small changes without restarting the application.
In order to modify a running Java program, we first need a method that can be in the same running state The way the JVM communicates. Because Java's virtual machine implementation is a managed system, it has a standard API for performing these operations. The API involved in the question is called the attachment API, which is part of the official Java tools. Using this API exposed by the running JVM, a second Java process can communicate with it.
In fact, we have already used this API: it has been applied by debugging and simulation tools such asVisualVM or Java Mission Control . The API for applying these attachments is not packaged with the standard Java API used daily, but is packaged into a special file called tools.jar, which only contains the JDK of a virtual machine Package the release version. What's worse is that the location of this JAR file has not been set. It is different in VMs on Windows, Linux, and especially on Macintosh. Not only the location of the file, but also the file name is different. On some distributions, it is called classes.jar. In the end, IBM even decided to change the names of some classes contained in this JAR and moved all com.sun classes to com.ibm namespace, adding another mess. In Java 9, the mess was finally cleaned up, and tools.jar was replaced by Jigsaw's module jdk.attach.
After locating the JAR (or module) of the API, we should make it available to the attachment process. On OpenJDK, the class used to connect to another JVM is called VirtualMachine, which provides an entry point to any VM run by a JDK or a regular HtpSpot JVM on the same physical machine. After attaching to another virtual machine through the process id, we can run a JAR file in a thread specified by the target VM:// 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(); }After receiving a JAR file, the target virtual machine will view the JAR's program manifest description file (manifest) and locates the class under the Premain-Class attribute. This is very similar to how the VM executes a main method. With a Java agent, the VM and the specified process id can find a method named agentmain, which can be executed by the remote process in the specified thread:
public class HelloWorldAgent { public static void agentmain(String arg) { System.out.println("Hello, " + arg); } }Using this API, as long as we know With the process ID of a JVM, you can run code on it and print out a Hello, World! message. It is even possible to communicate with a JVM that is not familiar with parts of the JDK distribution, as long as the attached VM is a JDK installer that accesses tools.jar.
到目前来看一切顺利。但是除了成功地同目标 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 (当然我就是该库的作者)。它的功能之一就是能够定义可以在方法原来的代码之前和之后被执行的模板方法。
The above is the detailed content of Detailed explanation of using dynamic mounting to implement bug hot fix in Java (picture). For more information, please follow other related articles on the PHP Chinese website!