大多數 JVM 具備 Java 的 HotSwap 特性,大部分開發者認為它只是一個調試工具。利用此特性,有可能在不重啟 Java 進程條件下,改變 Java 方法的實作。典型的例子是使用 IDE 來編碼。然而 HotSwap 可以在生產環境中實現這項功能。透過這種方式,不用停止運行程序,就可以擴展在線的應用程序,或者在運行的項目上修復小的錯誤。在這篇文章中,我將示範動態綁定、應用運行期程式碼變更進行綁定、介紹一些工具 API 以及 Byte Buddy 函式庫,這個函式庫提供了一些 API 程式碼變更更方便。
假設有一個正在運行的應用程序,透過校驗 HTTP 請求中的 X-Priority 頭部,來執行伺服器的特殊處理。該校驗使用下面的工具類別來實作:
class HeaderUtility { static boolean isPriorityCall(HttpServletRequest request) { return request.getHeader("X-Pirority") != null; } }
你發現錯誤了嗎?這樣的錯誤很常見,尤其是在測試程式碼中常數值分解為靜態欄位重複使用。在不太理想的情況下,這個錯誤只會在產品被安裝的時候才被發現,其中頭透過另一個應用程式產生並沒有拼字錯誤。
修復這樣的錯誤並不難。在持續交付的時代,重新部署一個新的版本只需要點擊一下按鈕。但在其他情況下,變更可能就不是那麼簡單了,重新部署過程可能比較複雜,其中停機是不允許的,帶著錯誤運行可能會比較好。但 HotSwap 為我們提供了另一個選擇:在不重啟應用的前提下進行小幅改動。
為了修改一個運行中的Java 程序,我們首先需要一個可以同處在運行狀態的JVM 進行通訊的方式。因為 Java 的虛擬機器實作是一個被管理的系統,因此擁有進行這些操作的標準 API。提問中涉及到的 API 被稱為 attachment API,它是官方 Java 工具的一部分。使用這個由運作之中的 JVM 所揭露的 API,能讓第二個 Java 進程來同其進行通訊。
事實上,我們已經使用了該 API: 它已經由諸如 VisualVM 或 Java Mission Control 這樣的除錯和模擬工具進行了應用。應用這些配件的API 並沒有同日常使用的標準Java API 打包在一起,而是被打包到了一個特殊的文件之中,叫做 tools.jar,它只包含了一個虛擬機的JDK打包發布版本。更糟的是,這個JAR 檔案的位置並沒有進行設置,它在Windows、Linux,特別是在Macintosh 上的VM 都存在差別,不光檔案的位置,連檔案名稱也各異,有些發行版上就被叫做 classes.jar。最後,IBM 甚至決定對這個 JAR 中包含的一些類別的名稱進行修改,將所有 com.sun 類別挪到 com.ibm 命名空間之中, 又添了一個亂子。在 Java 9 中,亂糟糟的狀態終於得以清理,tools.jar 被 Jigsaw 的模組 jdk.attach 所取代。
在 API 的 JAR (或模組) 進行了定位之後,我們就該讓其對附件程序可用。在 OpenJDK 上,被用來連接到另外一個 JVM 的類別叫做 VirtualMachine,它向任何由位於同一台實體機器上的 JDK 或是一個普通的 HtpSpot JVM 所運行的 VM 提供了一個入口點。在透過進程id 附加到另外一台虛擬機器上之後,我們就能夠在目標VM 指定的一個執行緒中執行一個JAR 檔案:
// 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(); }
在收到一個JAR 檔案之後,目標虛擬機會查看該JAR 的程序清單描述檔(manifest),並定位處在 Premain-Class 屬性之下的類別。這非常類似於 VM 執行一個主方法的方式。有了一個Java 代理,VM 和指定的進程id 就可以查找到一個名為agentmain 的方法,該方法可以由指定執行緒中的遠端進程來執行:
public class HelloWorldAgent { public static void agentmain(String arg) { System.out.println("Hello, " + arg); } }
使用該API,只要我們知道一個JVM 的進程id,就可以來在其上運行程式碼,列印出一條 Hello, World! 訊息。甚至有可能同並不熟 JDK 發行版一部分的 JVM 進行通信,只要附加的 VM 是用來存取 tools.jar 的 JDK 安裝程式。
到目前来看一切顺利。但是除了成功地同目标 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 (当然我就是该库的作者)。它的功能之一就是能够定义可以在方法原来的代码之前和之后被执行的模板方法。
以上是在Java中運用動態掛載實現Bug的熱修復的詳細解(圖)的詳細內容。更多資訊請關注PHP中文網其他相關文章!