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 分散チャットルームアプリケーション

推薦する

hostodo、"unspeakable" (x) に最適な非常に安価な VPS

[hostodo]専門的な情報を得るために、海外のウェブサイトにアクセスするための x 機能を実装す...

virmach-VPS は年間 2.55 ドルで、安くて友達もいません!オプションのコンピュータルーム5室

virmach VPS は再び勢いを増し始めています。オプションのデータ センター 5 か所に VP...

実戦:競合他社を利用してウェブサイトのランキングを回復する方法をご覧ください

現代社会では、ネットであろうと現実世界であろうと、争いが起こりやすい場所がある限り、競争は必ず起こり...

検索エンジンの5つの基本的な種類

検索エンジンについて検索エンジンはインターネットユーザーの生活に入り込んでいます。毎日インターネット...

Google 公式「検索エンジン最適化ガイド」 - ウェブサイト管理

重要なヒント:無料のウェブマスター ツールを使用すると、問題を特定し、検索結果でのウェブサイトのパフ...

読み込み速度が再び向上しました:ウェブサイト構築プログラムを詳細に記述します

この冬、人々に最も深い印象を残した2つのウェブサイトは、12306列車のチケット購入ウェブサイトとX...

dedispec-$13/Core2Duo/4g メモリ/250g SSD/100m 無制限

dedispec.comは2009年に設立されました。主に独立サーバーのレンタルとホスティングを行っ...

2020 年に企業はどのクラウド コンピューティング戦略を選択すべきでしょうか?

多くの組織は、クラウド コンピューティングがビジネスの将来において重要な役割を果たすと確信しています...

1か月でトラフィックが2倍に

著者は、共有するための最良の方法は、自分の実践的な経験を共有し、データを提示し、操作方法を説明するこ...

Baidu の最近の変更からわかるユーザー エクスペリエンスの重要性

「山に住めば山の幸を食う。海に住めば海の幸を食う」ということわざがあります。この国では、ウェブマスタ...

毎日の話題: 過去5年間の中国におけるグループ購入: 数千のグループの急激な増加から確立されたパターンへ

A5ウェブマスターネットワーク(www.admin5.com)は6月6日、中国で共同購入が5年にわた...

生き残るための賛辞:タオバオでの買い物旅行の物語

【ポイント】Taobaoのような複雑なシステムを持つウェブサイトでも、ミニマリストスタイルの斬新なウ...

香港データセンターのpqhostingの1Gbps帯域幅無制限トラフィックVPSの簡単なレビュー

香港には大きな帯域幅を持つ VPS はほとんどなく、香港には大きな帯域幅と無制限のトラフィックを備え...

百度の影に生きる草の根ウェブマスター

昨日の午後、QQグループでBaiduのアップデートに関する議論を見ました。Baiduの小さな調整は理...