こんにちは、サイオステクノロジー武井です。今回は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に書き換えられたことがわかりますよね。
ということで無事クラスの書き換えができました(•ө•)♡

