JavaAgent を使用して JVM を騙す

JavaAgent を使用して JVM を騙す

[[435136]]

Spring に精通している友人は、AOP をよりよく理解できるはずです。アスペクト指向プログラミングを使用すると、ターゲット メソッドの前後に実行するロジックを組み込むことができます。本日ご紹介する Java エージェント テクノロジーは、概念的には AOP に似ており、Java エージェントまたは Java プローブ テクノロジーとも言い換えることができます。

Java エージェントは JDK1.5 以降に登場しました。これにより、プログラマーはエージェント テクノロジを使用して、アプリケーションから独立したエージェント プログラムを構築できるようになります。幅広い用途があり、他の JVM 上のプログラムの監視、実行、さらには置き換えにも役立ちます。どのようなシナリオで使用されるかを確認するには、次の図を見てみましょう。

これを見ると、どのような魔法の技術がこれほど多くのシナリオに適用できるのか、興味が湧いてくるのではないでしょうか。さて、今日はそれを詳しく調べて、魔法の Java エージェントが最下層でどのように動作し、非常に多くの優れたアプリケーションを静かにサポートしているかを見ていきます。

記事の冒頭の例えに戻って、AOP との比較を使用して Java エージェントの概要を理解しましょう。

  • アクションレベル: AOPはアプリケーション内のメソッドレベルで実行され、エージェントは仮想マシンレベルで動作できます。
  • コンポーネント: AOP の実装にはターゲット メソッドとロジック拡張メソッドが必要ですが、Java エージェントを有効にするには 2 つのプロジェクトが必要です。1 つはエージェントで、もう 1 つはプロキシされるメイン プログラムです。
  • 実行機会: Ao​​p はスライスの前、後、またはその前後などで実行できますが、Java エージェントには 2 つの実行モードしかありません。 JDK 1.5 で提供される preMain モードはメイン プログラムの実行前に実行され、JDK 1.6 で提供される agentMain モードはメイン プログラムの実行後に実行されます。

次に、両方のモードでエージェント プログラムを実装する方法について説明します。

プリメインモード

Premain モードでは、メイン プログラムが実行される前にエージェントを実行できます。実装は非常に簡単です。以下では、 2 つのコンポーネントを個別に実装します。

エージェント

メイン プログラムが実行される前に文を出力し、プロキシに渡されたパラメータを出力する簡単な関数を記述してみましょう。

  1. パブリッククラスMyPreMainAgent {
  2. 公共 静的void premain(文字列agentArgs、インストルメンテーションinst) {
  3. システム。出力.println( "premain start" );
  4. システム。 out .println( "args:" +agentArgs);
  5. }
  6. }

エージェント ロジックを記述した後、それを jar ファイルにパッケージ化する必要があります。ここでは、Maven プラグインのパッケージ化方法を直接使用し、パッケージ化する前にいくつかの構成を実行します。

  1. <ビルド>
  2. <プラグイン>
  3. <プラグイン>
  4. <groupId>org.apache.maven.plugins</groupId>
  5. <artifactId>maven-jar-plugin</artifactId>
  6. <バージョン>3.1.0</バージョン>
  7. <構成>
  8. <アーカイブ>
  9. <マニフェスト>
  10. <addClasspath>は true </addClasspath>
  11. </マニフェスト>
  12. <マニフェストエントリ>
  13. <Premain クラス>com.cn.agent.MyPreMainAgent</Premain クラス>
  14. <Can-Redefine-Classes>は true </Can-Redefine-Classes>
  15. <Can-Retransform-Classes>は true </Can-Retransform-Classes>
  16. <Can-Set-Native-Method-Prefix>は true </Can-Set-Native-Method-Prefix>
  17. </マニフェストエントリ>
  18. </アーカイブ>
  19. </構成>
  20. </プラグイン>
  21. </プラグイン>
  22. </ビルド>

構成されたパッケージ パラメータで、manifestEntries を通じて MANIFEST.MF ファイルに属性を追加します。以下にいくつかのパラメータを示します。

  • Premain-Class: premainメソッドを含むクラス。クラスのフルパスとして設定する必要があります。
  • Can-Redefine-Classes: trueの場合、クラスは再定義できることを意味します
  • Can-Retransform-Classes: trueの場合、バイトコード置換を実現するためにクラスを再変換できることを意味します。
  • Can-Set-Native-Method-Prefix: trueの場合、ネイティブメソッドのプレフィックスを設定できることを意味します。

Premain-Class は必須の構成であり、残りのオプションはオプションです。これらはすべてデフォルトで false ですが、通常は推奨されます。これらの機能については後ほど詳しく紹介します。設定が完了したら、mvn コマンドを使用してパッケージ化します。

  1. mvn クリーンパッケージ

パッケージ化が完了すると、myAgent-1.0.jar ファイルが生成されます。 jar ファイルを解凍して、生成された MANIFEST.MF ファイルを確認します。

追加された属性がファイルに追加されたことがわかります。この時点でエージェント部分は完成です。エージェントは直接実行できず、他のプログラムにアタッチする必要があるため、メイン プログラムを実装するための新しいプロジェクトが作成されます。

メインプログラム

メイン プログラム プロジェクトでは、実行可能なメイン メソッド エントリのみが必要です。

  1. パブリッククラスAgentTest {
  2. 公共 静的void main(String[] args) {
  3. システム。 out .println( "メインプロジェクトの開始" );
  4. }
  5. }

メインプログラムが完成した後に検討する必要があるのは、メインプログラムとエージェントプロジェクトをどのように接続するかです。ここで、-javaagent パラメータを使用して実行中のエージェントを指定できます。コマンドの形式は次のとおりです。

  1. java -javaagent:myAgent.jar -jar AgentTest.jar

指定できるエージェントの数に制限はありません。各エージェントは指定された順序で実行されます。 2 つのエージェントを同時に実行する場合は、次のコマンドを実行します。

  1. java -javaagent:myAgent1.jar -javaagent:myAgent2.jar -jar AgentTest.jar

idea でプログラムを実行する例を取り、VM オプションに起動パラメータを追加します。

  1. -javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=ハイドラ
  2.  
  3. -javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=トランク

メイン メソッドを実行し、出力結果を表示します。

実行結果の print ステートメントによると、メイン プログラムを実行する前にエージェントが 2 回連続して実行されたことがわかります。実行エージェントとメインプログラムの実行順序は次の図で表すことができます。

欠陥

利便性を提供する一方で、プリメインモードにはいくつかの欠点もあります。たとえば、エージェントの動作中に例外が発生した場合、メインプログラムの起動は失敗します。上記の例のエージェント コードを変更し、手動で例外をスローします。

  1. 公共 静的void premain(文字列agentArgs、インストルメンテーションinst) {
  2. システム。出力.println( "premain start" );
  3. システム。 out .println( "args:" +agentArgs);
  4. 新しい RuntimeException( "error" ) をスローします。
  5. }

メインプログラムを再度実行します。

エージェントが例外をスローした後、メイン プログラムが開始されないことがわかります。 premain モードのいくつかの欠陥に対処するために、JDK 1.6 以降では agentmain モードが導入されました。

エージェントメインモード

エージェントメインモードは、プレメインのアップグレード版とも言えます。これにより、まずターゲット メイン プログラムの JVM を起動し、次にアタッチ メカニズムを通じて 2 つの JVM を接続できるようになります。 3つの部分に分けて実装します。

エージェント

エージェント部分は上記と同じで、シンプルな印刷機能を実現します。

  1. パブリッククラスMyAgentMain {
  2. 公共 静的void agentmain(String agentArgs, Instrumentation インストルメンテーション) {
  3. システム。出力.println( "エージェントメイン開始" );
  4. システム。 out .println( "args:" +agentArgs);
  5. }
  6. }

Maven プラグインの設定を変更し、Agent-Class を指定します。

  1. <プラグイン>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-jar-plugin</artifactId>
  4. <バージョン>3.1.0</バージョン>
  5. <構成>
  6. <アーカイブ>
  7. <マニフェスト>
  8. <addClasspath>は true </addClasspath>
  9. </マニフェスト>
  10. <マニフェストエントリ>
  11. <エージェント クラス>com.cn.agent.MyAgentMain</エージェント クラス>
  12. <Can-Redefine-Classes>は true </Can-Redefine-Classes>
  13. <Can-Retransform-Classes>は true </Can-Retransform-Classes>
  14. </マニフェストエントリ>
  15. </アーカイブ>
  16. </構成>
  17. </プラグイン>

メインプログラム

ここでは、メイン プログラムを直接起動し、エージェントがロードされるのを待ちます。メイン プログラムでは、System.in を使用してブロックし、メイン プロセスが途中で終了するのを防ぎます。

  1. パブリッククラスAgentmainTest {
  2.  
  3. 公共 静的void main(String[] args)はIOExceptionをスローします{
  4.  
  5. システム.in.read ( ) ;
  6.  
  7. }
  8.  
  9. }

アタッチ機構

プレメイン モードとは異なり、起動パラメータを追加してエージェントとメイン プログラムを接続することはできなくなります。ここでは、com.sun.tools.attach パッケージの VirtualMachine ツール クラスを使用する必要があります。このクラスは JVM 標準仕様ではなく、Sun 自身によって実装されていることに注意してください。使用する前に、依存関係を導入する必要があります。

  1. <依存関係>
  2. <groupId>com.sun</groupId>
  3. <artifactId>ツール</artifactId>
  4. <バージョン>1.8</バージョン>
  5. <scope>システム</scope>
  6. <システムパス>${JAVA_HOME}\lib\tools.jar</システムパス>
  7. </依存関係>

VirtualMachine は、接続される Java 仮想マシン、つまりプログラムで監視する必要があるターゲット仮想マシンを表します。外部プロセスは、VirtualMachine のインスタンスを使用して、エージェントをターゲットの仮想マシンにロードできます。まず、静的メソッドのattachを見てみましょう。

  1. 公共 静的仮想マシンをアタッチします(String var0);

jvm オブジェクト インスタンスは、attach メソッドを通じて取得できます。ここで渡されるパラメータは、実行中のターゲット仮想マシンのプロセス ID pid です。つまり、attach を使用する前に、起動したメイン プログラムの pid を取得し、jps コマンドを使用してスレッド pid を表示する必要があります。

  1. 11140
  2. 16372 リモートMavenServer36
  3. 16392 エージェントメインテスト
  4. 20204 日本
  5. 2460 ランチャー

メイン プログラム AgentmainTest のランタイム pid は 16392 で、仮想マシンの接続に適用されます。

  1. パブリッククラスAttachTest{
  2. 公共 静的void main(String[] args) {
  3. 試す {
  4. 仮想マシン vm = 仮想マシン.アタッチ( "16392" );
  5. vm.loadAgent( "F:\\Workspace\\MyAgent\\target\\myAgent-1.0.jar" , "param" );
  6. } キャッチ (例外 e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. }

VirtualMachine インスタンスを取得したら、loadAgent メソッドを使用してエージェント クラスを挿入できます。メソッドの最初のパラメーターはエージェントのローカル パスであり、2 番目のパラメーターはエージェントに渡されるパラメーターです。 AttachTest を実行し、メイン プログラム AgentmainTest のコンソールに戻ります。エージェント内のコードが実行されたことがわかります。

このようにして、単純なエージェントメイン モード エージェントが実装されます。 3 つのモジュール間の関係は、次の図で整理できます。

応用

ここまで、2 つのモードを実装する方法について簡単に説明しました。しかし、質の高いプログラマーとして、エージェントを使用して単にステートメントを印刷するだけでは満足してはなりません。次に、Java Agent を使用して実用的な操作を行う方法を見てみましょう。

上記の 2 つのモードでは、エージェント部分のロジックはそれぞれ premain メソッドと agentmain メソッドに実装されます。さらに、これら 2 つのメソッドには、シグネチャ内のパラメータに関する厳格な要件があります。 premain メソッドでは、次の 2 つの方法で定義できます。

  1. 公共 静的void premain(String agentArgs)
  2.  
  3. 公共 静的void premain(文字列 agentArgs、インストルメンテーション inst)

agentmain メソッドは、次の 2 つの方法で定義できます。

  1. 公共 静的void agentmain(文字列 agentArgs)
  2.  
  3. 公共 静的void agentmain(文字列 agentArgs, インストルメンテーション inst)

エージェント内に同じシグネチャを持つ 2 つのメソッドがある場合、Instrumentation パラメータを持つメソッドの方が優先順位が高く、JVM によって最初にロードされます。そのインスタンス inst は JVM によって自動的に挿入されます。 Instrumentation を通じてどのような機能が実現できるかを見てみましょう。

計装

まず、Instrumentation インターフェースの概要を説明します。この中のメソッドを使用すると、実行時に Java プログラムを操作して、バイトコードの変更、jar パッケージの追加、クラスの置き換えなどの機能を実現できます。これらの機能により、Java はより強力な動的制御および解釈機能を備えることができます。エージェントを作成するプロセスでは、Instrumentation の次の 3 つの方法がより重要であり、よく使用されます。詳しく見てみましょう。

トランスフォーマーを追加

addTransformer メソッドを使用すると、クラスがロードされる前にクラスを再定義できます。メソッドの定義を見てみましょう。

  1. void addTransformer(ClassFileTransformer トランスフォーマー);

ClassFileTransformer は、変換メソッドを 1 つだけ持つインターフェースです。メイン プログラムのメイン メソッドが実行される前に、ロードされた各クラスを transform を通じて 1 回実行する必要があります。コンバーターとも言えます。このメソッドを実装してクラスを再定義できます。使い方の例を見てみましょう。

まず、メイン プログラム プロジェクトに Fruit クラスを作成します。

  1. パブリッククラスFruit {
  2.  
  3. パブリックvoid getFruit(){
  4.  
  5. システム。 .println ( "バナナ" );
  6.  
  7. }
  8.  
  9. }

コンパイルが完了したら、クラス ファイルをコピーして Fruit2.class に名前を変更し、Fruit 内のメソッドを次のように変更します。

  1. パブリックvoid getFruit(){
  2.  
  3. システム。 .println( "apple" );を出力します
  4.  
  5. }

メイン プログラムを作成し、メイン プログラム内に Fruit オブジェクトを作成して、その getFruit メソッドを呼び出します。

  1. パブリッククラスTransformMain {
  2.  
  3. 公共 静的void main(String[] args) {
  4.  
  5. 新しい Fruit().getFruit();
  6.  
  7. }
  8.  
  9. }

この時点で実行結果として apple が出力され、その後、premain プロキシ部分の実装が開始されます。

エージェントの premain メソッドで、Instrumentation の addTransformer メソッドを使用してクラスの読み込みをインターセプトします。

  1. パブリッククラスTransformAgent {
  2.  
  3. 公共 静的void premain(文字列agentArgs、インストルメンテーションinst) {
  4.  
  5. inst.addTransformer(new FruitTransformer());
  6.  
  7. }
  8.  
  9. }

FruitTransformer クラスは ClassFileTransformer インターフェースを実装します。クラス部分を変換するロジックは、transform メソッドにあります。

  1. パブリッククラス FruitTransformer は ClassFileTransformer を実装します {
  2. @オーバーライド
  3. パブリックbyte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  4. ProtectionDomain protectionDomain、byte[] classfileBuffer){
  5. if (!className.equals( "com/cn/hydra/test/Fruit" ))
  6. classfileBufferを返します
  7.  
  8. 文字列ファイル名 = "F:\\Workspace\\agent-test\\target\\classes\\com\\cn\\hydra\\test\\Fruit2.class" ;
  9. getClassBytes(fileName) を返します
  10. }
  11.  
  12. 公共 静的byte[] getClassBytes(String fileName){
  13. ファイル file = new File(fileName);
  14. InputStream をnew FileInputStream(file) に置き換えてみます
  15. バイト配列出力ストリーム bs = 新しいバイト配列出力ストリーム()){
  16. 長い長さ = file.length();
  17. byte[] バイト = 新しい byte[( int ) 長さ];
  18.  
  19. 整数n;
  20. while ((n = is . read (bytes)) != -1) {
  21. bs.write(バイト, 0, n);
  22. }
  23. バイトを返します
  24. }catch (例外 e) {
  25. e.printStackTrace();
  26. 戻る ヌル;
  27. }
  28. }
  29. }

transform メソッドでは、主に次の 2 つの処理が行われます。

  • addTransformer メソッドは変換するクラスを指定できないため、現在ロードされているクラスがインターセプトするターゲット クラスであるかどうかを判断するには、className を使用する必要があります。非ターゲットクラスの場合は、元のバイト配列を直接返します。 className の形式に注意してください。を交換する必要があります。完全修飾クラス名に / を付けて
  • 先ほどコピーしたクラスファイルを読み取り、バイナリ文字ストリームを読み取り、元のclassfileBufferバイト配列を置き換えて返します。これにより、クラス定義の置き換えが完了します。

エージェント部分をパッケージ化した後、メイン プログラムに起動パラメータを追加します。

  1. -javaagent:F:\Workspace\MyAgent\target\transformAgent-1.0.jar

メインプログラムを再度実行し、結果を出力します。

  1. バナナ

この方法では、メイン メソッドが実行される前にクラスが置き換えられます。

クラスを再定義する

メソッドの名前からその機能を直感的に理解できます。クラスを再定義するということは、簡単に言えば、指定されたクラスを置き換えることを意味します。メソッドの定義は次のとおりです。

  1. void redefineClasses(ClassDefinition... definitions) は ClassNotFoundException、UnmodifiableClassException をスローします。

そのパラメータは可変長の ClassDefinition 配列です。 ClassDefinition のコンストラクタを見てみましょう。

  1. パブリックClassDefinition(Class<?> theClass,byte[] theClassFile) {...}

ClassDefinition で指定された Class オブジェクトと変更されたバイトコード配列は、簡単に言えば、元のクラスを指定されたクラス ファイル バイトに置き換えます。さらに、redefineClasses メソッドの再定義プロセス中に、ClassDefinition の配列が渡され、クラス間の相互依存関係がある場合の変更を満たすために、この配列の順序でロードされます。

プレメイン プロキシ部分を例にして、その有効性プロセスを見てみましょう。

  1. パブリッククラスRedefineAgent {
  2. 公共 静的void premain(文字列 agentArgs、インストルメンテーション inst)
  3. UnmodifiableClassException、ClassNotFoundException をスローします {
  4. 文字列ファイル名 = "F:\\Workspace\\agent-test\\target\\classes\\com\\cn\\hydra\\test\\Fruit2.class" ;
  5. ClassDefinition def=新しいClassDefinition(Fruit.class,
  6. FruitTransformer.getClassBytes(ファイル名));
  7. inst.redefineClasses(新しいClassDefinition[]{def});
  8. }
  9. }

メインプログラムは上記を直接再利用し、実行後に印刷することができます。

  1. バナナ

元のクラスが指定したクラス ファイルのバイトに置き換えられ、指定したクラスの置き換えが実現されていることがわかります。

再変換クラス

retransformClasses は agentmain モードに適用され、クラスのロード後にクラスを再定義、つまりクラスの再ロードをトリガーできます。まず、このメソッドの定義を見てみましょう。

  1. void retransformClasses(Class... classes) は UnmodifiableClassException をスローします。

パラメータ classes は変換する必要のあるクラスの配列であり、可変長パラメータは redefineClasses メソッドと同様にクラス定義を一括変換できることも示しています。

次に、例を通して retransformClasses メソッドの使用方法を見ていきます。エージェントのコードは次のとおりです。

  1. パブリッククラスRetransformAgent {
  2. 公共 静的void agentmain(文字列 agentArgs, インストルメンテーション inst)
  3. UnmodifiableClassException をスローします {
  4. inst.addTransformer(new FruitTransformer(), true );
  5. inst.retransformClasses(Fruit.class);
  6. システム。 out .println( "再変換成功" );
  7. }
  8. }

ここで呼び出される addTransformer メソッドの定義を見てみましょう。これは上記とは少し異なります。

  1. void addTransformer(ClassFileTransformer トランスフォーマー、boolean canRetransform);

ClassFileTransformer コンバーターは、上記の FruitTransformer を引き続き再利用します。新しく追加された 2 番目のパラメータに注目してください。 canRetransform が true の場合、クラスの再定義が許可されることを意味します。このとき、コンバーター ClassFileTransformer の transform メソッドを呼び出すのと同じになり、変換されたクラスのバイトが新しいクラス定義としてロードされます。

メイン プログラム コードでは、クラスが変更されたかどうかを監視するために、無限ループで print ステートメントを継続的に実行します。

  1. パブリッククラスRetransformMain {
  2. 公共 静的void main(String[] args)はInterruptedExceptionをスローします{
  3. while( true ){
  4. 新しい Fruit().getFruit();
  5. TimeUnit.SECONDS.sleep(5);
  6. }
  7. }
  8. }

最後に、アタッチ API を使用してエージェントをメイン プログラムに挿入します。

  1. パブリッククラスAttachRetransform{
  2. 公共 静的void main(String[] args)は例外をスローします{
  3. 仮想マシン vm = 仮想マシン.attach( "6380" );
  4. vm.loadAgent( "F:\\Workspace\\MyAgent\\target\\retransformAgent-1.0.jar" );
  5. }
  6. }

実行結果を表示するには、メイン プログラム コンソールに戻ります。

プロキシを挿入した後、print ステートメントが変更され、クラス定義が変更されて再ロードされたことがわかります。

他の

これらの主な方法に加えて、Instrumentation には他の方法もいくつかあります。ここでは、一般的なメソッドの機能を簡単にリストします。

  • removeTransformer: ClassFileTransformer クラスコンバーターを削除します
  • getAllLoadedClasses: 現在ロードされているクラスを取得します
  • getInitiatedClasses: 指定されたClassLoaderによってロードされたクラスを取得します
  • getObjectSize: オブジェクトが占めるスペースのサイズを取得します
  • appendToBootstrapClassLoaderSearch: 起動クラスローダーに jar パッケージを追加します
  • appendToSystemClassLoaderSearch: システムクラスローダーにjarパッケージを追加します
  • isNativeMethodPrefixSupported: ネイティブメソッドにプレフィックスを追加できるかどうか、つまりネイティブメソッドをインターセプトできるかどうかを決定します。
  • setNativeMethodPrefix: ネイティブメソッドのプレフィックスを設定する

ジャバシスト

上記の例では、クラス ファイル内のバイトを直接読み取り、クラスを再定義または変換します。しかし、実際の作業環境では、クラスファイルのバイトコードを動的に変更することが必要になる場合があります。この場合、javassist を使用すると、バイトコード ファイルをより簡単に変更できます。

簡単に言えば、javassist は Java バイトコードを分析、編集、作成するためのクラス ライブラリです。これを使用すると、提供される API を直接呼び出して、クラスの構造をコーディングの形で動的に変更したり生成したりすることができます。基礎となる仮想マシン命令の理解を必要とする ASM などの他のバイトコード フレームワークと比較すると、javassist は非常にシンプルで高速です。

次に、簡単な例を使って、Java エージェントと Javassist を一緒に使用する方法を説明します。まず、javassist 依存関係を導入します。

  1. <依存関係>
  2. <groupId>org.javassist</groupId>
  3. <artifactId>javassist</artifactId>
  4. <バージョン>3.20.0-GA</バージョン>
  5. </依存関係>

実現したい機能は、プロキシを介してメソッドの実行時間を計算することです。プレメインプロキシ部分は基本的に以前と同じです。まずコンバーターを追加します:

  1. パブリッククラスエージェント{
  2. 公共 静的void premain(文字列agentArgs、インストルメンテーションinst) {
  3. inst.addTransformer(新しい LogTransformer());
  4. }
  5.  
  6. 静的クラス LogTransformer は ClassFileTransformer を実装します {
  7. @オーバーライド
  8. パブリックbyte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  9. ProtectionDomain protectionDomain、byte[] classfileBuffer)
  10. IllegalClassFormatException をスローします {
  11. if (!className.equals( "com/cn/hydra/test/Fruit" ))
  12. 戻る ヌル;
  13.  
  14. 試す {
  15. 計算()を返します
  16. } キャッチ (例外 e) {
  17. e.printStackTrace();
  18. 戻る ヌル;
  19. }
  20. }
  21. }
  22. }

calculate メソッドでは、javassist を使用してメソッド定義を動的に変更します。

  1. 静的byte[] calculate()は例外をスローします{
  2. クラスプール プール = ClassPool.getDefault();
  3. CtClass ctClass = pool.get( "com.cn.hydra.test.Fruit" );
  4. CtMethod ctMethod = ctClass.getDeclaredMethod( "getFruit" );
  5. CtMethod copyMethod = CtNewMethod.copy(ctMethod, ctClass, 新しい ClassMap());
  6. ctMethod.setName( "getFruit$agent" );
  7.  
  8. StringBuffer本体 = 新しいStringBuffer( "{\n" )
  9. .append( "long begin = System.nanoTime();\n" )
  10. .append( "getFruit$agent($$);\n" )
  11. .append( "System.out.println(\"use \"+(System.nanoTime() - begin) +\" ns\");\n" )
  12. .append( "}" );
  13. コピーメソッド.setBody(body.toString());
  14. ctClass.addMethod(コピーメソッド)
  15. ctClass.toBytecode()を返します
  16. }

上記のコードでは、主に以下の機能が実装されています。

  • 完全修飾名を使用してクラスCtClassを取得します
  • メソッド名に従ってCtMethodメソッドを取得し、CtNewMethod.copyメソッドを通じて新しいメソッドをコピーします。
  • 古いメソッド名をgetFruit$agentに変更します
  • コピーされたメソッドの内容はsetBodyメソッドを通じて変更され、新しいメソッドでロジックが強化され、古いメソッドが呼び出され、最後に新しいメソッドがクラスに追加されます。

メイン プログラムは以前のコードを引き続き再利用し、結果を実行して表示し、プロキシで実行時間統計関数を完了します。

この時点で、もう一度反省を通して見てみましょう。

  1. for (メソッドメソッド: Fruit.class.getDeclaredMethods()) {
  2.  
  3. システム。出力.println(method.getName());
  4.  
  5. メソッドを呼び出します(新しい Fruit());
  6.  
  7. システム。出力.println( "-------" );
  8.  
  9. }

結果を見ると、クラスに新しいメソッドが実際に追加されたことがわかります。

さらに、javassist には、新しいクラスの作成、親クラスの設定、バイトコードの読み書きなど、他の多くの機能があります。特定のシナリオでの使用法を学ぶことができます。

要約する

日常業務では、Java Agent を直接使用する場面は多くないかもしれませんが、ホットデプロイメント、監視、パフォーマンス分析などのツールの中で業務システムの片隅に隠れ、静かに大きな役割を果たしている可能性があります。

この記事では、Java エージェントの 2 つのモードから始めて、手動で実装し、そのワークフローを簡単に分析します。ここではいくつかの単純な機能のみが完成していますが、Java エージェントの出現により、プログラムが従来の方法で実行されなくなり、コードに無限の可能性がもたらされたと言わざるを得ません。

<<:  Kubernetesを導入することで、企業は時間とコストを節約できる

>>:  HarmonyOS 分散チャットルームアプリケーション

推薦する

分散ストレージシステム(問題、概念、ドメイン言語)面接で必ず知っておくべきポイント

意味分散ストレージシステムは、インターネットを介して接続された多数の一般的な PC サーバーであり、...

ユーザーを理解して適切なVDIユースケースを選択する

1993年の大ヒット映画『ジュラシック・パーク』で、イアン・マルコム博士は恐竜を復活させるという考え...

ハイブリッドおよびマルチクラウドの管理: エージェントかエージェントレスか?

エージェント監視とエージェントレス監視は、IT サービス管理業界で常に熱く議論されているトピックです...

高級品ウェブサイトがパンデミックに終止符を打つ:オンライン購入におけるサプライチェーンの混乱

中国の高級品オンラインショッピングの巨大な市場需要は、少しの混乱で簡単に変わることはないだろう。現在...

servarica-$7/Xen/2.5g メモリ/25SSD/1T トラフィック/G ポート/カナダ

Servarica は 2010 年に設立され、カナダに登録されています。データ センターもカナダに...

ウェブサイトの乾癬が再浮上、ブラックリンクとの戦いが加速

最近、一部のネットユーザーがWeiboで、Dedecmsで構築された多くのウェブサイトがハッキングさ...

1 つの記事で詳細に説明しています: 可用性の高い分散アーキテクチャを設計するにはどうすればよいでしょうか?

この記事の著者は、現在主流となっている分散アーキテクチャ、分散アーキテクチャにおける一般的な理論、お...

licloud 香港 VPS の簡単なレビュー - 香港合理化ネットワーク 100M 帯域幅、その仕組みを説明します

数日前、licloud の香港 VPS がリリースされました。これは、100Mbps の帯域幅を年間...

間違いをしないでください! Weiboマーケティングに関する5つの誤解を挙げる

ショートビデオ、セルフメディア、インフルエンサーのためのワンストップサービス企業であれ個人であれ、W...

ユーザーエクスペリエンスはバランスである

バランスポイントを見つけるのは非常に難しく、起業家はユーザーの根本的なニーズを把握する必要があります...

この記事を読んだ後でも、JVM を理解していると言えるでしょうか?

導入[[256737]]物理メモリが 8G あり、主に Java サービスを実行している一部のサーバ...

ウェブサイトのパフォーマンスを向上させるために必要な17のこと: 行動喚起を追加する

北京時間4月19日、海外メディアの報道によると、あなたのウェブサイトはあなたの「顔」です。実店舗をお...

Qingyun: 香港 VPS、動的 IP、1Gbps 帯域幅、無制限のトラフィック

LightNetwork Computing Limited は最近、香港 HKT データ センター...

Cert Manager を使用して Kubernetes Gateway 証明書を自動的に管理する

背景Kubernetes ゲートウェイおそらく、より正確にはKubernetes Gateway A...