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