ほとんどの JVM には Java の HotSwap 機能があり、ほとんどの開発者はそれが単なる デバッグ ツールであると考えています。この機能を使用すると、Java プロセスを再起動せずに Java メソッドの実装を変更できます。典型的な例は、IDE を使用したコーディングです。ただし、HotSwap は実稼働環境でこの機能を実装できます。このようにして、実行中のプログラムを停止することなく、オンライン アプリケーションを拡張したり、実行中のプロジェクトの軽微なバグを修正したりできます。この記事では、動的バインディングのデモを行い、バインディング用のランタイム コード変更を適用し、いくつかのツール API と、いくつかの API コード変更をより便利に提供する Byte Buddy ライブラリを紹介します。
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 を使用して、2 番目の Java プロセスは JVM と通信できます。
実際、私たちはすでにこの API を使用しています。これは、VisualVM や Java Mission Control などのデバッグおよびシミュレーション ツールによってすでに実装されています。これらの添付ファイルを適用するための API は、日常的に使用される標準 Java API にはパッケージ化されていませんが、tools.jar、 という特殊なファイルにパッケージ化されています。このファイルには、仮想マシンの JDK パッケージ リリース バージョンのみが含まれています。さらに悪いことに、この JAR ファイルの場所が設定されていないのです。Windows、Linux、特に Macintosh では、ファイルの場所だけでなく、ファイル名も異なります。一部のディストリビューションでは、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 記述ファイル (マニフェスト) を開き、Premain-Class 属性の下にあるクラスを見つけます。これは、VM が main メソッドを実行する方法と非常に似ています。 Java エージェントを使用すると、VM と指定されたプロセス ID は、agentmain という名前のメソッドを見つけることができます。このメソッドは、指定されたスレッドのリモート プロセスによって実行できます:
public class HelloWorldAgent { public static void agentmain(String arg) { System.out.println("Hello, " + arg); } }
JVM ID のプロセスがわかっている限り、この API を使用します。でコードを実行し、Hello, World! メッセージを出力できます。接続された VM が tools.jar にアクセスする JDK インストーラーである限り、JDK ディストリビューションの一部に慣れていない JVM と通信することも可能です。
到目前来看一切顺利。但是除了成功地同目标 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 にバグホットフィックスを実装する方法の詳細な説明 (図)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。