Javaインスツルメンテーションガイド

Javaインストルメンテーションのガイド

1. 前書き

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

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

2. セットアップ

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

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

  1. お金を引き出すことができるATMアプリ

  2. そして、投資に費やした時間を測定することでATMのパフォーマンスを測定できるJavaエージェント

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

プロジェクトの構造は次のとおりです。

com.example.instrumentation
base
1.0.0
pom

    agent
    application

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

3. Javaエージェントとは

一般に、Javaエージェントは特別に細工されたjarファイルです。 It utilizes the Instrumentation API that the JVM provides to alter existing byte-code that is loaded in a JVM.

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

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

  • agentmainJava Attach APIを使用してエージェントをJVMに動的にロードします

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

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

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

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

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

次の2種類の負荷があります。

  • static –premainを使用して、-javaagentオプションを使用してエージェントをロードします

  • 動的–agentmainを使用して、Java Attach APIを使用してエージェントをJVMにロードします

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

4.1. 静的負荷

アプリケーションの起動時にJavaエージェントをロードすることを静的ロードと呼びます。 Static load modifies the byte-code at startup time before any code is executed.

静的ロードでは、アプリケーションコードが実行される前に実行される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 classがいつ変換されたかを確認できます。 また、2つのATM引き出しトランザクションログには、各操作の完了にかかった時間が含まれています。

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

4.2. 動的負荷

The procedure of loading a Java agent into an already running JVM is called dynamic load.エージェントはJava Attach APIを使用して接続されます。

より複雑なシナリオは、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エージェントの作成

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

JavaエージェントはJava Instrumentation 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.example.instrumentation.application.MyAtm";
    transformClass(className,inst);
}
public static void agentmain(
  String agentArgs, Instrumentation inst) {

    LOGGER.info("[Agent] In agentmain method");
    String className = "com.example.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. Transformerの定義

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

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エージェントを取得するには、いくつかの属性を持つマニフェストファイルが必要です。

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

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

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

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

6. 結論

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

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

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