Javassistを使って実行時のクラス拡張をやってみた。
Javassistを使うと、実行時のクラス拡張(コンパイル済みのクラスをJavaプログラム実行時に改変し、メソッドに処理を追加したり、フィールドを追加したりすること。)ができます。以下は簡単なサンプルとはまったところについてのメモです。
簡単なサンプル
"com.example.Kitten"を"名前を話すcom.example.Kitten"に拡張してみます。基本的な手順は次の通り。Javassistは3.4を使用しています。
- ClassPoolを作る。
- CtClassを作る。
- CtClassのAPIを呼び出し、クラスを拡張する。
- CtClass#getDeclaredMethod(String)で修正するメソッドを取得。
- CtMethod#setBody(String)でメソッドの本体を修正。
- CtClass#toClass(ClassLoader, ProtectionDomain)でClassLoaderに拡張したクラスを読み込ませる。
// ClassPoolを得る。 ClassPool cp = new ClassPool(); cp.appendSystemPath(); // 拡張するクラスを指定してCtClassを取得する。 CtClass cc = cp.get("com.example.Kitten"); // 拡張するメソッドを取得し、修正。 CtMethod m = cc.getDeclaredMethod("meow"); m.setBody("System.out.println( this.name + \" : meow!\" );"); // ClassLoaderに拡張したクラスを読み込ませる。 Class c = cc.toClass( ClassLoader.getSystemClassLoader(), Sample.class.getProtectionDomain()); // インスタンスを生成。メソッドを呼び出してみる。 Kitten mii = (Kitten) c.newInstance(); mii.setName( "mii" ); mii.meow(); // "mii : meow!" が表示される。
拡張前のKittenクラスは以下。
package com.example; /** * 猫。 * * @version $Revision:$ * @author $Author:$ */ public class Kitten { /** * 名前 */ private String name = ""; /** * 年齢 */ private int age = 0; /** * コンストラクタ */ public Kitten (){} /** * コンストラクタ * * @param name * 名前 * @param age * 年齢 */ public Kitten ( String name, int age ) { this.name = name; this.age = age; } /** * 名前を取得する。 * @return 名前 */ public String getName () { return name; } /** * 名前を設定する。 * @param name 名前 */ public void setName ( String name ) { this.name = name; } /** * 年齢を取得する。 * @return 年齢 */ public int getAge () { return age; } /** * 年齢を設定する。 * @param age 年齢 */ public void setAge ( int age ) { this.age = age; } /** * 鳴く */ public void meow( ) { System.out.println( "meow!" ); } }
実行結果です。
mii : meow!
クラスの置き換え
CtClass#toClass(ClassLoader, ProtectionDomain)を呼び出すと、指定したClassLoaderに拡張したクラスを読み込ませます。これ以降、引数で指定したクラスローダーを使って改変前のクラスをnewすると改変されたクラスが生成されます。なお、CtClass#toClass(ClassLoader, ProtectionDomain)を呼び出す前に、改変前のクラスがロードされているとエラーになります。ご注意。
ClassPool cp = new ClassPool(); cp.appendSystemPath(); CtClass cc = cp.get("com.example.Kitten"); CtMethod m = cc.getDeclaredMethod("meow"); m.setBody("System.out.println( this.name + \" : meow!\" );"); Class c = cc.toClass( ClassLoader.getSystemClassLoader(), Sample.class.getProtectionDomain()); // インスタンスを生成。メソッドを呼び出してみる。 Kitten mii = (Kitten) c.newInstance(); mii.setName( "mii" ); mii.meow(); // "mii : meow!" が表示される。 // このクラスは、toClass()の引数で指定したSystemClassLoaderでロードされているので、 // new した場合も拡張したクラスが使われる。 Kitten tora = new Kitten("tora", 0); tora.meow(); // "tora : meow!" が表示される。
setBody()で指定した処理の中で、メソッドに渡された引数を参照したい。
「$1」など、Javassist内部のコンパイラで処理される記号を使用します。チュートリアルの中に情報があります Javassistチュートリアル - 4.イントロスペクションとカスタマイズを参照。
ClassPool cp = new ClassPool(); cp.appendSystemPath(); CtClass cc = cp.get("com.example.Kitten"); CtMethod m = cc.getDeclaredMethod("setName", new CtClass[] { cp.get( "java.lang.String" ) }); // insertAfter()で指定する処理内でメソッドに渡された引数を使う。 // $1で最初の引数を取得。1から始まるので注意。 m.insertAfter("System.out.println( \"set name : \" + $1 );");
プリミティブ型のCtClassを得る
プリミティブ型のCtClassはCtClassの定数で定義されています。
CtClass boolCc = CtClass.booleanType;
配列型のCtClassを得る
配列型のCtClassは、ClassPool.get(String)で取得できます。プリミティブ型の配列も同様の手順で作成できます。
CtClass kittenArrayCc = cp.get( "com.example.Kitten[]" ); CtClass byteArrayCc = cp.get( "byte[]" );
参考: