Javassistを触る。
http://www-6.ibm.com/jp/developerworks/java/040206/j_j-dyn0916.html
最近、Dependensy Injectionや、アスペクト指向とかを実現するためにバイトコードをいじってどーにかする系のライブラリが増えている。
気になっていたのでDeveloper Worksで紹介されていたので触ってみることにする。
とりあえずは、
http://www.csg.is.titech.ac.jp/~chiba/javassist/
で 3.0 betaをダウンロード。
操作しているクラスを検索し制御するjavassist.ClassPool クラス 〜 クラス・プールはJavassist APIを通して、ロードしたクラスをデータとして使用可能にします。
つまり、Javassitようのクラスローダーってところか。
こいつにロードさればいろいろ制御させることが出来るようになる。と。
そんで、ClassPoolにロードされたクラスは、javassist.CtClassのインスタンスとして
取り扱えるようになる。
さらに、CtClassからは、CtMethod、CtField、CtConstructorがとれてそいつらを使っていろいろやる。と。
あとは、気になったことを。
挿入したコードの中で使用したいメソッドまたはコンストラクタのパラメーター、メソッドの返り値、 および他のアイテムを表わすために使用される、いくつかの特殊な識別名の追加くらいです。 これらの特殊な識別名はすべて$記号で始まります。
識別名?
追加されているステートメントまたはブロックの外部で宣言されたローカル変数を参照する方法がないということです。
ふむ。ってことは、追加されたステートメントまたはブロックの内部で宣言されたローカル変数は参照可能である。と。
とりあえず、記事内の、メソッドの時間計測のサンプルをもとに、実行対象のメソッドをインターセプトして、
メソッドの実行前後にログを出力するサンプルを作ってみる。
こいつがターゲットになるクラス。
package a.works.study.javassist; public class Main { public static void main(String[] args) { Main m = new Main(); String msg = m.hello(); System.out.println("------------------------------------"); System.out.println("Message is [" + msg + "]"); } public String hello( ) { return "hello javassist"; } }
これが、Javassistでhelloメソッドの実行内容を置き換えるクラス。
内容的には、
- helloメソッドをhello$Implにリネーム
- helloメソッドの内容を開始ログ出力→hello$Imple呼び出し→終了ログ出力を行いメソッドに変更
package a.works.study.javassist; import java.io.IOException; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; import javassist.NotFoundException; public class ModifyHelloMethod { public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException { ClassPool pool = ClassPool.getDefault(); CtClass mainCtClass = pool.get("a.works.study.javassist.Main"); // helloメソッドの取り出し CtMethod orgHelloMethod = mainCtClass.getDeclaredMethod("hello"); // helloメソッドの名前変更 orgHelloMethod.setName("hello$Impl"); // インターセプト用のhelloメソッド作成 CtMethod newHelloMethod = CtNewMethod.copy(orgHelloMethod, "hello", mainCtClass, null); // インターセプト用のメソッドの内容を置き換える。 StringBuffer body = new StringBuffer(); body.append("{\n"); body.append("long startTime = System.currentTimeMillis();\n"); body.append("System.out.println(\"LOG: Method Start.\");\n"); body.append("String ret = hello$Impl();\n"); body.append("long time = System.currentTimeMillis() - startTime;\n"); body.append("System.out.println(\"LOG: Method End. ret:[\" + ret + \"] time:\" + time);\n"); body.append("return ret;\n"); body.append("}\n"); newHelloMethod.setBody(body.toString()); System.out.println("------------------------------------"); System.out.println(body); System.out.println("------------------------------------"); // インターセプト用のクラスを追加する。 mainCtClass.addMethod(newHelloMethod); // 変更した内容をクラスファイルに書き込む // mainCtClass.writeFile(); //実行時のルートにクラスファイルを作成する。 mainCtClass.writeFile("build/classes"); } }
まずは、素のままのMainクラスを実行。
c:\java\javassist>java a.works.study.javassist.Main ------------------------------------ Message is [hello javassist]
次に、ModifyHelloMethodクラスを実行する。
c:\java\javassist>java -classpath .;javassist.jar a.works.study.javassist.ModifyHelloMethod ------------------------------------ { long startTime = System.currentTimeMillis(); System.out.println("LOG: Method Start."); String ret = hello$Impl(); long time = System.currentTimeMillis() - startTime; System.out.println("LOG: Method End. ret:[" + ret + "] time:" + time); return ret; } ------------------------------------
そして、再度Mainクラスを実行すると。
c:\java\javassist>java a.works.study.javassist.Main LOG: Method Start. LOG: Method End. ret:[hello javassist] time:0 ------------------------------------ Message is [hello javassist]
"LOG:〜"の部分が、ModifyHelloMethodクラスによりバイトコードに追加された処理。
Javassistはコンパイル時のコードのチェックを、Java言語仕様によって要求されているよりも、ルーズに実装します。
上記のModifyHelloMethodを2回連続で実行したあと、Mainクラスを実行するとこんな例外がでる。
java.lang.ClassFormatError: a/works/study/javassist/Main (Repetitive method name/signature) at java.lang.ClassLoader.defineClass0(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:537) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:123) at java.net.URLClassLoader.defineClass(URLClassLoader.java:251) at java.net.URLClassLoader.access$100(URLClassLoader.java:55) at java.net.URLClassLoader$1.run(URLClassLoader.java:194) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:187) at java.lang.ClassLoader.loadClass(ClassLoader.java:289) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:274) at java.lang.ClassLoader.loadClass(ClassLoader.java:235) at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:302) Exception in thread "main"
BCELでバイトコードの中身を見てみたら、hello$Implメソッドが2つ作られていた。
なるほど、確かにルーズだ。
矛盾のないクラスを作るには自分が注意しないといけない。Javassistのチェックに頼りすぎてはいけないぞ。と。