大多数 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 给我们提供了另外一种选择:在不重启应用的前提下进行小幅改动。 Attach API:使用动态附件来渗透另外一个 JVM为了修改一个运行中的 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 usString 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 安装程序。 Instrumentation API:修改目标 VM 的程序到目前来看一切顺利。但是除了成功地同目标 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 |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|