1. 序章

このチュートリアルでは、 Java Instrumentation APIについて説明します。これは、既存のコンパイル済みJavaクラスにバイトコードを追加する機能を提供します。

また、javaエージェントと、それらを使用してコードをインストルメント化する方法についても説明します。

2. 設定

記事全体を通して、インストルメンテーションを使用してアプリを構築します。

このアプリケーションは、次の2つのモジュールで構成されます。

  1. お金を引き出すことができるATMアプリ
  2. そして、お金を費やして費やした時間を測定することによって、ATMのパフォーマンスを測定できるようにするJavaエージェント

JavaエージェントはATMバイトコードを変更し、ATMアプリを変更せずに引き出し時間を測定できるようにします。

私たちのプロジェクトは次のような構造になります。

<groupId>com.baeldung.instrumentation</groupId>
<artifactId>base</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
    <module>agent</module>
    <module>application</module>
</modules>

インストルメンテーションの詳細に入る前に、javaエージェントとは何かを見てみましょう。

3. Javaエージェントとは

一般に、Javaエージェントは特別に細工されたjarファイルです。 JVMが提供するInstrumentationAPIを利用して、JVMにロードされている既存のバイトコードを変更します。

エージェントが機能するには、次の2つのメソッドを定義する必要があります。

  • premain –JVMの起動時に-javaagentパラメーターを使用してエージェントを静的にロードします
  • agentmain Java AttachAPIを使用してエージェントをJVMに動的にロードします

覚えておくべき興味深い概念は、Oracle、OpenJDKなどのJVM実装は、エージェントを動的に開始するメカニズムを提供できるということですが、これは必須ではありません。

まず、既存のJavaエージェントをどのように使用するかを見てみましょう。

その後、バイトコードに必要な機能を追加するために最初から作成する方法を見ていきます。

4. Javaエージェントのロード

Javaエージェントを使用できるようにするには、最初にJavaエージェントをロードする必要があります。

負荷には2つのタイプがあります。

  • static – premain を使用して、-javaagentオプションを使用してエージェントをロードします
  • 動的– agentmain を使用して、 Java AttachAPIを使用してエージェントをJVMにロードします。

次に、各タイプの負荷を見て、それがどのように機能するかを説明します。

4.1. 静的負荷

アプリケーションの起動時にJavaエージェントをロードすることを、静的ロードと呼びます。 静的ロードは、コードが実行される前の起動時にバイトコードを変更します。

静的ロードでは、 premain メソッドが使用されます。このメソッドは、アプリケーションコードが実行される前に実行され、実行できるようになります。

java -javaagent:agent.jar -jar application.jar

常に– javaagent パラメーターを– jarパラメーターの前に置く必要があることに注意することが重要です。

以下は、コマンドのログです。

22:24:39.296 [main] INFO - [Agent] In premain method
22:24:39.300 [main] INFO - [Agent] Transforming class MyAtm
22:24:39.407 [main] INFO - [Application] Starting ATM application
22:24:41.409 [main] INFO - [Application] Successful Withdrawal of [7] units!
22:24:41.410 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
22:24:53.411 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:24:53.411 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

premain メソッドが実行されたとき、およびMyAtmクラスが変換されたときを確認できます。 また、各操作の完了にかかった時間を含む2つのATM引き出しトランザクションログも表示されます。

元のアプリケーションでは、今回のトランザクションの完了時間はなく、Javaエージェントによって追加されたことを思い出してください。

4.2. 動的負荷

すでに実行中のJVMにJavaエージェントをロードする手順は動的ロードと呼ばれます。エージェントはJavaAttachAPIを使用して接続されます。

より複雑なシナリオは、ATMアプリケーションがすでに実稼働環境で実行されており、アプリケーションのダウンタイムなしでトランザクションの合計時間を動的に追加したい場合です。

それを行うための小さなコードを書いてみましょう。このクラスを呼び出します。 AgentLoader。 簡単にするために、このクラスをアプリケーションのjarファイルに入れます。 したがって、アプリケーションjarファイルは、アプリケーションを起動し、エージェントをATMアプリケーションにアタッチすることができます。

VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();

AgentLoader ができたので、アプリケーションを開始し、トランザクション間の10秒間の一時停止で、AgentLoaderを使用してJavaエージェントを動的にアタッチします。

また、アプリケーションを開始するか、エージェントをロードするための接着剤を追加しましょう。

このクラスをLauncherと呼び、メインのjarファイルクラスになります。

public class Launcher {
    public static void main(String[] args) throws Exception {
        if(args[0].equals("StartMyAtmApplication")) {
            new MyAtmApplication().run(args);
        } else if(args[0].equals("LoadAgent")) {
            new AgentLoader().run(args);
        }
    }
}

アプリケーションの開始

java -jar application.jar StartMyAtmApplication
22:44:21.154 [main] INFO - [Application] Starting ATM application
22:44:23.157 [main] INFO - [Application] Successful Withdrawal of [7] units!

Javaエージェントの接続

最初の操作の後、JavaエージェントをJVMに接続します。

java -jar application.jar LoadAgent
22:44:27.022 [main] INFO - Attaching to target JVM with PID: 6575
22:44:27.306 [main] INFO - Attached to target JVM and loaded Java agent successfully

アプリケーションログを確認する

エージェントをJVMに接続したので、2回目のATM引き出し操作の合計完了時間がわかります。

これは、アプリケーションの実行中にその場で機能を追加したことを意味します。

22:44:27.229 [Attach Listener] INFO - [Agent] In agentmain method
22:44:27.230 [Attach Listener] INFO - [Agent] Transforming class MyAtm
22:44:33.157 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:44:33.157 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

5. Javaエージェントの作成

エージェントの使用方法を学んだ後、エージェントを作成する方法を見てみましょう。 Javassist を使用してバイトコードを変更する方法を見て、これをいくつかのインストルメンテーションAPIメソッドと組み合わせます。

javaエージェントはJavaInstrumentation API を使用するため、エージェントの作成に深く踏み込む前に、このAPIで最もよく使用されるメソッドのいくつかとそれらの機能の簡単な説明を見てみましょう。 :

  • addTransformer –計装エンジンに変圧器を追加します
  • getAllLoadedClasses –JVMによって現在ロードされているすべてのクラスの配列を返します
  • retransformClasses –バイトコードを追加することにより、すでにロードされているクラスのインストルメンテーションを容易にします
  • removeTransformer –付属の変圧器の登録を解除します
  • redefineClasses –提供されたクラスファイルを使用して、提供されたクラスのセットを再定義します。つまり、クラスは retransformClasses のように変更されるのではなく、完全に置き換えられます。

5.1. PremainおよびAgentmainメソッドを作成します

すべてのJavaエージェントには、premainまたはagentmainメソッドの少なくとも1つが必要であることがわかっています。 後者は動的ロードに使用され、前者はJavaエージェントをJVMに静的にロードするために使用されます。

このエージェントを静的および動的の両方でロードできるように、エージェントで両方を定義しましょう。

public static void premain(
  String agentArgs, Instrumentation inst) {
 
    LOGGER.info("[Agent] In premain method");
    String className = "com.baeldung.instrumentation.application.MyAtm";
    transformClass(className,inst);
}
public static void agentmain(
  String agentArgs, Instrumentation inst) {
 
    LOGGER.info("[Agent] In agentmain method");
    String className = "com.baeldung.instrumentation.application.MyAtm";
    transformClass(className,inst);
}

各メソッドで、変更するクラスを宣言し、transformClassメソッドを使用してそのクラスを変換するために掘り下げます。

以下は、MyAtmクラスの変換を支援するために定義したtransformClassメソッドのコードです。

このメソッドでは、変換するクラスを見つけ、transformメソッドを使用します。 また、インストルメンテーションエンジンにトランスを追加します。

private static void transformClass(
  String className, Instrumentation instrumentation) {
    Class<?> targetCls = null;
    ClassLoader targetClassLoader = null;
    // see if we can get the class using forName
    try {
        targetCls = Class.forName(className);
        targetClassLoader = targetCls.getClassLoader();
        transform(targetCls, targetClassLoader, instrumentation);
        return;
    } catch (Exception ex) {
        LOGGER.error("Class [{}] not found with Class.forName");
    }
    // otherwise iterate all loaded classes and find what we want
    for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
        if(clazz.getName().equals(className)) {
            targetCls = clazz;
            targetClassLoader = targetCls.getClassLoader();
            transform(targetCls, targetClassLoader, instrumentation);
            return;
        }
    }
    throw new RuntimeException(
      "Failed to find class [" + className + "]");
}

private static void transform(
  Class<?> clazz, 
  ClassLoader classLoader,
  Instrumentation instrumentation) {
    AtmTransformer dt = new AtmTransformer(
      clazz.getName(), classLoader);
    instrumentation.addTransformer(dt, true);
    try {
        instrumentation.retransformClasses(clazz);
    } catch (Exception ex) {
        throw new RuntimeException(
          "Transform failed for: [" + clazz.getName() + "]", ex);
    }
}

これが邪魔にならないように、MyAtmクラスのトランスフォーマーを定義しましょう。

5.2. トランスフォーマーの定義

クラストランスフォーマーは、 ClassFileTransformer を実装し、transformメソッドを実装する必要があります。

Javassist を使用して、 MyAtm クラスにバイトコードを追加し、ATW引き出しトランザクションの合計時間のログを追加します。

public class AtmTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
      ClassLoader loader, 
      String className, 
      Class<?> classBeingRedefined, 
      ProtectionDomain protectionDomain, 
      byte[] classfileBuffer) {
        byte[] byteCode = classfileBuffer;
        String finalTargetClassName = this.targetClassName
          .replaceAll("\\.", "/"); 
        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName) 
              && loader.equals(targetClassLoader)) {
 
            LOGGER.info("[Agent] Transforming class MyAtm");
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(
                  WITHDRAW_MONEY_METHOD);
                m.addLocalVariable(
                  "startTime", CtClass.longType);
                m.insertBefore(
                  "startTime = System.currentTimeMillis();");

                StringBuilder endBlock = new StringBuilder();

                m.addLocalVariable("endTime", CtClass.longType);
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append(
                  "endTime = System.currentTimeMillis();");
                endBlock.append(
                  "opTime = (endTime-startTime)/1000;");

                endBlock.append(
                  "LOGGER.info(\"[Application] Withdrawal operation completed in:" +
                                "\" + opTime + \" seconds!\");");

                m.insertAfter(endBlock.toString());

                byteCode = cc.toBytecode();
                cc.detach();
            } catch (NotFoundException | CannotCompileException | IOException e) {
                LOGGER.error("Exception", e);
            }
        }
        return byteCode;
    }
}

5.3. エージェントマニフェストファイルの作成

最後に、動作するJavaエージェントを取得するには、いくつかの属性を持つマニフェストファイルが必要です。

したがって、マニフェスト属性の完全なリストは、 InstrumentationPackageの公式ドキュメントにあります。

最終的なJavaエージェントjarファイルで、マニフェストファイルに次の行を追加します。

Agent-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent

これで、Javaインストルメンテーションエージェントが完成しました。 実行するには、この記事のJavaエージェントのロードセクションを参照してください。

6. 結論

この記事では、JavaInstrumentationAPIについて説明しました。 Javaエージェントを静的および動的の両方でJVMにロードする方法を確認しました。

また、独自のJavaエージェントを最初から作成する方法についても検討しました。

いつものように、例の完全な実装はGithubにあります。