1前書き

このチュートリアルでは、https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/Instrumentation.html[Java Instrumentation API]について説明します。既存のコンパイル済みJavaクラスへのバイトコード。

また、Javaエージェントと、それを使用してコードを作成する方法についても説明します。


2セットアップ

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

私たちのアプリケーションは2つのモジュールから構成されます。

  1. 私たちがお金を引き出すことを可能にするATMアプリ

  2. そして私達が私達のパフォーマンスを測定することを可能にするJavaエージェント

時間投資によるお金の測定によるATM

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が提供するhttps://docs.oracle.com/javase/7/docs/api/java/lang/instrument/Instrumentation.html[Instrumentation.html]にロードされている既存のバイトコードの変更JVM。

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


  • premain

    – -javaagentパラメーターを使用してエージェントを静的にロードします

JVM起動時
**

agentmain

– を使用してエージェントをJVMに動的にロードします。

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

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

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


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

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

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

  • static – -javaagentを使用して

    premain

    を使用してエージェントをロードします。

オプション
** dynamic –

agentmain

を使用してエージェントをJVMにロードします。


Java Attach API

を使用してください。

次に、それぞれの種類の負荷を見て、それがどのように機能するかを説明します。


4.1. 静荷重

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

静的ロードは

premain

メソッドを使用することに注意してください。これはアプリケーションコードが実行される前に実行されるので、実行することができます。

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



__ jar


パラメータの前に、常に-


javaagent

__parameterを付ける必要があることに注意してください。

以下は私たちのコマンドのログです。

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. 動的荷重

  • すでに実行中のJVMにJavaエージェントをロードする手順は動的ロードと呼ばれています。/sun/tools/attach/package-summary.html[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エージェントを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エージェントの作成

エージェントの使い方を学んだ後、エージェントを作成する方法を見てみましょう。

バイトコードを変更するためにhttps://www.baeldung.com/javassist[Javassistを使用する方法]を見て、これをいくつかのインストルメンテーションAPIメソッドと組み合わせます。

Javaエージェントはhttps://docs.oracle.com/javase/7/docs/api/java/lang/instrument/Instrumentation.html[Java Instrumentation API]を使用しているため、エージェントの作成に深く入り込む前にこのAPIで最も使用されているメソッドのいくつかとそれらが何をするのかについての簡単な説明を見てみましょう。


  • addTransformer

    – 計装エンジンにトランスフォーマを追加します


  • getAllLoadedClasses

    – 現在すべてのクラスの配列を返す

JVMによってロードされた
**

retransformClasses

– すでにのインストルメンテーションを容易にします

バイトコードを追加してロードされたクラス
**

removeTransformer

– 提供されたトランスの登録を解除します


  • redefineClasses

    – を使用して、提供されたクラスのセットを再定義します。

提供されたクラスファイル。クラスが完全に置き換えられ、

retransformClasses

のように変更されないことを意味します。

====

5.1.

Premain

および

Agentmain

メソッドを作成します

すべてのJavaエージェントには、少なくとも1つの

premain

または

agentmain

メソッドが必要です。後者は動的ロードに使用され、前者は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

__methodを使います。また、計測エンジンにトランスフォーマを追加します。

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

を実装し、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エージェントを取得するために、2つの属性を持つマニフェストファイルが必要です。

したがって、マニフェスト属性の完全なリストはhttps://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html[Instrumentation Package]公式文書にあります。

最後の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. 結論

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

私達はまた私達がゼロから私達の自身のJavaエージェントを作成することをどのようにして行うのかについても調べました。

いつものように、この例の完全な実装はhttps://github.com/eugenp/tutorials/tree/master/core-java[over on Github]にあります。