javaagentでクラスの書き換え

こんにちは、サイオステクノロジー武井です。今回はjavaagentでクラスの書き換えをしてみたいと思います。

javaagentとは?

簡単に言いますとクラスのローディングをフックする、つまりクラスの読み込み前に何らかの処理を行ったり、クラスの書き換えをできたりします。

具体的な使いみちとしては、最近流行りのAPM(Application Performance Management)を実現する際に、通信ライブラリなどをフックして、処理時間の計測を行ったりとかに使ったりします。

早速やってみよう!!

では、早速実践です。ここではcom.example.api.Api1というクラスをcom.example.api.Api2に書き換えてみます。

javaagentを呼び出す方のクラス

まずはjavaagentを呼び出す方のクラスを作ります。

書き換え対象のcom.example.api.Api1です。単純に標準出力にhogeしているだけです。

package com.example.api;

public class Api1 {

    public static void test() {
        System.out.println("hoge");
    }
    
}

 

書き換え対象のcom.example.api.Api1を呼び出すメインクラスです。

package com.example.api;

public class App {
    public static void main(String[] args) {

        // Api1というクラスのStaticなメソッドを呼んでいるだけ。
        Api1.test();
    }
}

 

上記のソースをコンパイルしてできたjarをapi.jarとします。

javaagent本体のクラス

javaagent本体のクラスを作成します。

com.example.api.Api1の書き換え後のクラスcom.example.api.Api2を作成します。

package com.example.api;

public class Api2 {

    public static void test() {
        System.out.println("fuga");
    }
    
}

 

javaagentのメインのクラスを作成します。

package com.example.agent;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import com.example.api.Api2;
import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;

public class App {
    // javaagentにはpremainというメソッドが必要になります。
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(new ClassFileTransformer() {
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                    ProtectionDomain protectionDomain, byte[] classfileBuffer) {

                // 書き換え対象のクラスのバイト配列を返すために、javaasistというライブラリを使っています。
                ClassPool pool = ClassPool.getDefault();

                // このpremainメソッドによって、クラスが順次ロードされるので、書き換え対象のクラスのみ
                // 書き換えるようにif文で定義します。
                if (className.equals("com/example/api/Api1")) {
                    try {
                        // 書き換え対象のクラスパスを取得して、先ほど定義したClassPoolに追加します。
                        ClassClassPath ccpath = new ClassClassPath(Api2.class);
                        pool.insertClassPath(ccpath);
                        CtClass replacedClass = pool.get("com.example.api.Api2");

                        // 書き換え対象のクラスのクラス名を書き換えます。実際に呼ばれるのは、com.example.api.Api1なので
                        // com.example.api.Api2からcom.example.api.Api1に名前変更します
                        replacedClass.replaceClassName("com.example.api.Api2", "com.example.api.Api1");
 
                        // 書き換え後のクラスのバイト配列を返します。ここでreturnしたバイト配列のクラスに
                        // 書き換えられることとなります。
                        return replacedClass.toBytecode();

                    } catch (NotFoundException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (CannotCompileException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }

                }

                return null;
            }
        });

    }
}

 

これをコンパイルするわけですが、その際、マニフェストファイルに以下を加えてください。

Premain-Class: com.example.agent.App

 

上記のソースをコンパイルしてできたjarをagent.jarとします。

呼び出してみよう!!

以下のコマンドを実行してください。

$ java -javaagent:agent.jar -jar api.jar
fuga

 

hogeと表示されるcom.example.api.Api1が、fugaと表示されるcom.example.api.Api2に書き換えられたことがわかりますよね。

 

ということで無事クラスの書き換えができました(•ө•)♡

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

2人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です