WEBワークショップ

HOME > WEBワークショップ > Javassistチュートリアル



Javassistチュートリアル


4.イントロスペクションとカスタマイズ

CtClass はイントロスペクションのためのメソッドを提供しています。Javassist の提供するイントロスペクションは Java のリフレクション API が提供するものと互換性のあるものです。CtClass は getName()、getSuperclass()、getMethods() といったメソッドを提供します。CtClass はまた、クラスの記述を変更するためのメソッドも提供しています。これらを利用することで 新たなフィールド、コンストラクタ、メソッドを追加することができます。メソッドのボディを変更することさえ可能です。

CtMethod はメソッドを表すオブジェクトです。CtMethod はメソッドの記述を変更するためのいくつかのメソッドを提供しています。あるメソッドがスーパークラスから継承したものである場合、そのメソッドを表す CtMethod オブジェクトはスーパークラスで宣言されているメソッドを表すものと同じものであるということに気をつけてください。1つの CtMethod オブジェクトが全てのメソッド宣言に相当するのです。

たとえば、Point クラスにおいて move() メソッドが宣言されているとき、サブクラス ColorPoint では move() メソッドをオーバーライドすることができません。Point クラスとその継承クラスである ColorPoint で宣言された2つの move() メソッドは、全く同じ CtMethod オブジェクトで表されているのです。この CtMethod オブジェクトが表すメソッドの記述を変更した場合、その変更は両方のメソッドに反映されるのです。ColorPoint クラスのメソッドだけを変更したい場合、まず最初に Point クラスの move() メソッドを表す CtMethod オブジェクトのコピーを ColorPoint クラスへ追加しなくてはなりません。CtMethod オブジェクトのコピーは CtNewMethod.copy() によって取得することができます。

Javassistでメソッドやフィールドを削除することはできません。しかし、名前を変更することはできます。従って、あるメソッドが必要なくなった場合は、CtMethod クラスの setName()、setModifiers() メソッドを使用することでメソッドの名前を変更してプライベート属性に変更します。

Javassist はまた、既存のメソッドに対して新たな引数を追加することもできません。その代わり、従来の引数に加えて新たな引数を追加した新しいメソッドを同じクラスに追加してください。新しい int 型のパラメータ newZ を以下のメソッドに追加する場合、

 void move(int newX, int newY) { x = newX; y = newY; }


Point クラスに以下のようなメソッドを追加します。

 void move(int newX, int newY, int newZ) {
     // newZ に対して行いたい処理を記述する
     move(newX, newY);
 }


Javassist はクラスファイルを直接編集するための低レベルAPIも提供しています。たとえば、CtClass の getClassFile() メソッドはクラスファイルに含まれる method_info 構造体を表す MethodInfo オブジェクトを返します。低レベルAPIはJava仮想マシン仕様で規定されている用語に基づいているため、これらを使用するにはクラスファイルとバイトコードに関する知識が必要となります。詳しくは、javassist.bytecode を参照してください。

Javassist によって変更されたクラスファイルは、$で始まる特殊な識別子が使用された場合、ランタイムサポートを受けるために javassist.runtimeパッケージを必要とします。これらの特別な識別子については、あとで説明します。特殊な識別子を使用せずに変更されたクラスについては、動作する上で javassist.runtime パッケージをはじめとする Javassist のパッケージを必要としません。詳しくは javassist.runtime パッケージの API リファレンスを参照してください。

●4.1 メソッドの先頭/最後へのソースコード挿入方法

CtMethod と CtConstructor は insertBefore()、insertAfter()、addCatch() の3つのメソッドを提供しています。これらのメソッドは、既に存在しているメソッドの本体にコード断片を挿入するために使用します。これらを利用するときは、Javaで書かれたコード断片のテキストとして渡します。Javassist ソースコードをコンパイルするためのシンプルな Java コンパイラを持っています。このコンパイラは Java で書かれたソースコードのテキストを受け取り、バイトコードへコンパイルしてメソッド本体に挿入します。

挿入先の行番号を指定してコード断片を挿入することもできます(クラスファイルに行番号テーブルが含まれている場合)。CtMethod や CtConstructor クラスの insertAt() メソッドは、ソーステキストとオリジナルクラスにおける挿入先行番号を引数に取ります。ソーステキストはコンパイルされ指定された行へ挿入されます。

insertBefore()、insertAfter()、addCatch()、insertAt() メソッドはステートメントまたはブロックを表す String オブジェクトを引数に取ります。ステートメントは if 文、while 文 またはセミコロンで終わる文です。ブロックは { } で囲まれた複数のステートメントのことです。以下の例での各行は、正しい形式のステートメントやブロックです。

 System.out.println("Hello");
 { System.out.println("Hello"); }
 if (i < 0) { i = -i; }


ステートメントやブロックは、フィールドやメソッドを参照することができます。また、メソッドが -g オプション(ローカル変数に関する情報をクラスファイルに含めるためのオプション)付きでコンパイルされてる場合、メソッドに追加されたパラメータへアクセスすることもできます。一方、メソッドパラメータに対しては $0、$1、$2・・・といった特殊な変数を通してアクセスしなければなりません。ブロック内で新たなローカル変数を宣言することはできますが、メソッド内で元々宣言されているローカル変数に対してアクセスすることはできません。しかし、insertAt() では、対象メソッドが -g オプション付きでコンパイルされ、変数が行番号で特定できる場合はステートメントやブロックからローカル変数にアクセスすることができます。

insertBefore()、insertAfter()、addCatch()、insertAt() メソッドに渡される String オブジェクトは、Javassist の内蔵コンパイラによってコンパイルされます。このコンパイラは $ で始まるいくつかの特別な意味を持つ識別子によって、言語を拡張しています。

$0, $1, $2, ...
実際の引数です。
$args
引数の配列。$args の型は Object[] です。
$$
全ての引数を示します。たとえば、m($$) は m($1,$2,...) に相当します。
$cflow(...)
cflow 変数です。
$r
戻り値の型です。これはキャスト時に使用されます。
$w
ラッパの型です。これはキャスト時に使用されます。
$_
戻り値です。
$sig
パラメータの型を表す java.lang.Class オブジェクトの配列です。
$type
戻り値の型を表す java.lang.Class のオブジェクトです。
$class
現在編集しているクラスを表す java.lang.Class オブジェクトです。

●$0, $1, $2, ...

ターゲットメソッドに渡された引数は、元のパラメータ名称を使用する代わりに $0、$1、$2・・・といった識別子でアクセスできます。$1 は最初の引数、$2 は2番目・・・というように順番に引数を表します。これらの変数の型は、引数の型と同じです。$0 は this に相当します。ただし static メソッドの場合、$0 は利用できません。

これらの変数の利用方法を説明します。以下のような Point というクラスがあるとしましょう。

class Point {
	int x, y;
	void move(int dx, int dy) { x += dx; y += dy; }
}


move() メソッドが呼ばれたときに変数 dx、dy を表示するには以下のようなプログラムを実行します。

 ClassPool pool = ClassPool.getDefault();
 CtClass cc = pool.get("Point");
 CtMethod m = cc.getDeclaredMethod("move");
 m.insertBefore("{ System.out.println($1); System.out.println($2); }");
 cc.writeFile();


insertBefore() メソッドに渡すソーステキストはブレース {} で囲まれていなければなりません。insertBefore() は単一のステートメントかブレースで囲まれたブロックのみを受け付けます。

変更後の Point クラス以下のようになります。

 class Point {
 	int x, y;
 	void move(int dx, int dy) {
		{ System.out.println(dx); System.out.println(dy); }
 		x += dx; y += dy;
 	}
 }


$1、$2 はそれぞれ dx、dy に置きかえられます。

$1、$2、$3・・・は更新することもできます。これらの変数に新しい値が代入されると、変数に対応する引数の値も更新されます。

●$args

変数$argsは全ての引数を表すObject型の配列です。引数の型が int 型のようなプリミティブなものである場合、引数の型は java.lang.Integer のようなラッパオブジェクトに変換されて $args に格納されます。従って、最初の引数がプリミティブ型でない限り、$args[0] と $1 は等しくなります。$args[0] は $0 とは異なる点に気をつけてください($0 は this を表します)。

$args に Object 配列が格納された場合、配列の各要素がそれぞれの引数に格納されます。パラメータの型がプリミティブ型であった場合、対応する要素の型はラッパ型でなければなりません。値は引数へ格納される前にラッパ型からプリミティブ型に変換されます。

●$$

変数 $$ は、コンマで区切られた全パラメータの省略型です。たとえば、move() メソッドの引数の数が3つあった場合、

 move($$)


は、以下の表現と等価です。

 move($1, $2, $3)


move() が引数を取らない場合、move($$) は move() と等価になります。

$$ は別のメソッドでも利用することができます。たとえば、以下のように記述したとしましょう。

 exMove($$, context)


これは以下の表現と等価になります。

 exMove($1, $2, $3, context)


$$ は引数の数に関係のない、一般的な表現でのメソッドコールを可能とします。これは、後で説明する $proceed とともに利用されます。

●$cflow

$cflow は「制御の流れ」を意味します。この読み込み専用変数は、特定のメソッドに対して再帰呼び出しの深さを返します。

以下のメソッドが CtMethod のオブジェクト cm で表されているとします。

 int fact(int n) {
     if (n <= 1)
         return n;
     else
         return n * fact(n - 1);
 }


$cflow を使用するには、まず fact() メソッドの呼び出しを関しするために $cflow を使用することを宣言します。

 CtMethod cm = ...;
 cm.useCflow("fact");


useCflow() に対する引数は、宣言された $cflow の識別子です。ここでは、Java で識別子として使用可能な文字列がすべて利用可能です。識別子は . (ピリオド) を含むこともできます。たとえば、"my.Test.fact" は有効な識別子となります。

そして、$cflow(fact) は cm で示されるメソッドの再帰呼び出しの深さを示します。メソッドが最初にコールされたとき、$cflow(fact) の値は 0(ゼロ) となります。また、再帰呼び出しにより次に実行されたときは 1 となります。

例えば、以下の例では fact() メソッドを引数を表示するように変更します。

 cm.insertBefore("if ($cflow(fact) == 0)"
  + "    System.out.println(\"fact \" + $1);");


$cflow(fact) の値がチェックされるため、再帰呼び出しによる実行の際は、引数を表示することはありません。

$cflow 変数の値は、現在のスレッドにおける cm の示すメソッドに関連づけられたスタックフレームの数となります。$cflow は cm の表すメソッドとは別のメソッドの中からでもアクセスすることができます。

●$r

$r はメソッドの戻り値型を表します。この変数は、キャスト表現の中でキャスト型として使用します。例えば、以下のような使い方が典型的な例です。

 Object result = ... ;
 $_ = ($r)result;


戻り値型がプリミティブ型での場合、($r) は特別な意味を持ちます。まず、キャスト表現のオペランド側がプリミティブ型であった場合、($r) は戻り値型への通常のキャスト演算子として働きます。一方、オペランドがラッパ型であった場合、($r) は java.lang.Integer から int に変換されます。

戻り値が void の場合、($r) は型変換を行いません。ただし、オペランドが void メソッドへの呼び出しになっている場合、($r) の結果は null となります。例えばメソッドの戻り値型が void で foo() が void メソッドの場合、以下の文は正しいものになります。

 $_ = ($r)foo();


キャスト演算子 ($r) は、return 文の中でも利用することができます。戻り値型が void であっても、以下の文は有効です。

 return ($r)result;


ここで result がローカル変数の場合、($r) が指定されているため、戻り値は破棄されてしまいます。この場合、この return 文は以下のような戻り値なしの return 文と同じであると見なされます。

 return;

●$w

$w はラッパ型を表します。これはキャスト表現の中でキャスト型として使用されます。($w) はプリミティブ型をそれに相当するラッパ型に変換します。その例を以下に示しましょう。

 Integer i = ($w)5;

どのラッパ型に変換されるかは、($w) に続く式の型によって変わります。式の型が double 型であった場合、ラッパ型は java.lang.Double となります。

($w) に続く式の型がプリミティブ型でなければ、($w) は何もしません。

●$_


CtMethod や CtConstructor の insertAfter() メソッドは、メソッドの最後にコンパイルされたコードを挿入します。insertAfter() に渡された文の中では、ここまでに挙げた $0、$1、・・・といった変数だけでなく、$_ も利用することができます。

変数 $_ は、メソッドの戻り値を表します。変数の型は、メソッドの戻り値の型と等しくなります。戻り値の型が void の場合、$_ の型は Object 型となり、値が null になります。

insertAfter() によって挿入されるコンパイルされたコードは、通常はメソッドの処理が正常に終了した後に実行されますが、メソッドから例外がスローされた場合にも実行することができます。例外がスローされたときに処理を実行するには、insertAfter() メソッドの2番目のパラメータである asFinally を true に設定します。

例外がスローされると、insertAfter() によって挿入されたコードは finally 節として実行されます。$_ の値は 0 または null となります。挿入されたコードの実行後は、元々スローされた例外が呼び出し元へ再スローされます。このとき、 $_ の値は呼び出し元に返されません。この値は既に破棄されているのです。

●$sig

$sig はメソッド引数の型を表す java.lang.Class の配列で、宣言された順に格納されています。

●$type

$type は、メソッドの戻り値型を表す java.lang.Class オブジェクトです。この変数は CtMethod や CtConstructor クラスの insertAfter() で挿入されたコードの中でのみ有効です。

●$class

変数 $class は、現在編集しているクラスの型を表している java.lang.Class オブジェクトです。これは、$0(this) の型と同じです。

●addCatch()

addCatch() は、メソッド本体の中で例外がスローされ、呼び出し元に制御が移る際に実行されるコードを挿入することができます。挿入するコードを表すテキストの中では、$e という特別な変数を通してスローされた例外を参照することができます。

例を示しましょう。

 CtMethod m = ...;
 CtClass etype = ClassPool.getDefault().get("java.io.IOException");
 m.addCatch("{ System.out.println($e); throw $e; }", etype);


このプログラムでは m で表されるメソッドを次のように変換します。

 try {
     元のメソッドの処理
 }
 catch (java.io.IOException e) {
     System.out.println(e);
     throw e;
 }


挿入されたコードは throw または return 文で終了しなければならないことに注意してください。

●4.2 メソッド本体の変更

CtMethod や CtConstructor ではメソッド全体を置きかえるために、 setBody() メソッドを提供しています。このメソッドは、与えられたソースコードを Java バイトコードへコンパイルし、元のメソッド本体と置きかえます。与えられたソースコードが null だった場合は、置きかえられたメソッドは戻り値型が null でないかぎり、0 または null を返す return 文のみを含むものに置きかえられます。

setBody() メソッドに渡すソースコードのなかでは、$ で始まる識別子は特殊な意味を持ちます。

$0, $1, $2, ...
実際の引数です。
$args
引数の配列です。$args の型は Object[] です。
$$
全ての引数を示します。
$cflow(...)
メソッドが再帰呼び出しされた場合の深さを表します。
$r
戻り値の型です。これはキャスト時に使用されます。
$w
ラッパ型を表します。これはキャスト時に使用されます。
$sig
パラメータの型を表す java.lang.Class オブジェクトの配列です。
$type
戻り値の型を表す java.lang.Class のオブジェクトです。
$class
現在編集しているクラスを表す java.lang.Class オブジェクトです($0 の型を示します)。

注意:$_ は利用できません。

●メソッド中の式の置き換え

Javassist はメソッド中の1つの式だけを変更する方法を提供しています。javassist.expr.ExprEditor は、メソッド中の1つの式を変更するためのクラスです。このクラスの利用者は ExprEditor のサブクラスを作成し、どのようにして式を変更するかを決めることができます。

ExprEditor オブジェクトを利用するには、CtMethod または CtClass の instrument() メソッドを使用します。

 CtMethod cm = ... ;
 cm.instrument(
     new ExprEditor() {
         public void edit(MethodCall m)
                       throws CannotCompileException
         {
             if (m.getClassName().equals("Point")
                           && m.getMethodName().equals("move"))
                 m.replace("{ $1 = 0; $_ = $proceed($$); }");
         }
     });


上の例では、cm で表されるメソッドから Point クラスの move() メソッドに対する呼び出しを以下のようなブロックに置きかえています。

 { $1 = 0; $_ = $proceed($$); }


結果、move() メソッドに対する最初の引数は 0 となります。置きかえられたコードは、式ではなく文やブロックである点に注目してください。

instrument() メソッドはメソッドの中を検索し、メソッドコール、フィールドアクセス、オブジェクト生成といった式を見つけた場合は与えられた ExprEditor オブジェクトの edit() メソッドを呼び出します。edit() メソッドに対する引数は、見つかった式を表すオブジェクトになります。edit() メソッドでは、このオブジェクトを通して式を調べたり置きかえたりすることができます。

edit() メソッドの引数として渡されたオブジェクトの replace() メソッドを呼び出すことで、その式を文やブロックで置きかえることができます。引数で与えられたブロックが空のブロックであった場合−つまり replace("{}") が実行された場合−式はそのメソッドから取り除かれます。式の手前や後に文やブロックを追加した場合、replace() メソッドへ以下のようなブロックを渡します。

 { 前に実行する文;
   $_ = $proceed($$);
   後に実行する文; }


対象となる式がメソッドコール、フィールドアクセス、オブジェクト生成など、どのような式であっても、2番目の文は以下のようになります。

 $_ = $proceed();


式が読み込みアクセスの場合は上記のような文、

 $proceed($$);


そして書き込みアクセスの場合はこのような文になります。

instrument() メソッドで検索の対象となるメソッドが -g オプション(クラスファイルにローカル変数に関する属性を含めるようにするためのものです)付きでコンパイルされている場合、対象の式内で有効なローカル変数は replace() メソッドで渡すソースコードの中でも有効です。

●javassist.expr.MethodCall

MethodCall は、メソッド呼び出しを表すオブジェクトです。MethodCall クラスの replace() はメソッド呼び出しを文やブロックに置き換えます。置き換える文やブロックを表すソースコードを引数として渡しますが、insertBefore() メソッドと同じように $ で始まる識別子は特別な意味を持ちます。

$0
メソッド呼び出しの対象となるオブジェクトを表します。呼び出し側のオブジェクトを表す this ではありませんので注意してください。呼び出し先が static メソッドの場合、$0 は null になります。
$1, $2, ...
メソッド呼び出しの引数を表します。
$_
メソッド呼び出しの戻り値を表します。
$r
呼び出しの戻り値の型を表します。
$class
そのメソッドを宣言しているクラスを表す java.lang.Class オブジェクトを表します。
$sig
引数の型を表す java.lang.Class オブジェクトの配列です。
$type
戻り値の型を表す java.lang.Class オブジェクトです。
$proceed
式の中で元々呼び出されていたメソッドの名称を表します。

ここでいう「メソッド呼び出し」とは、MethodCall オブジェクトが表すものという意味です。

$w、$args、 $$ といった他の識別子についても利用することができます。

メソッド呼び出しの戻り値型が void である場合を除き、ソースコードの中で $_ に戻り値を設定しなければなりません。$_ の型は戻り値の型と同じになります。戻り値型が void の場合、$_ は Object 型となり、$_ に代入された値は無視されます。

$proceed は文字列ではなく、特殊な構文です。$proceed のあとには、カッコ ( ) でくくられた引数のリストを記述する必要があります。

●javassist.expr.FieldAccess

FieldAccess オブジェクトはフィールドへのアクセスを表すオブジェクトです。ExprEditor の edit() メソッドはフィールドへのアクセスを検出した場合に FieldAccess オブジェクトを受け取ります。FieldAccess の replace() メソッドはフィールドアクセスを置き換える文やブロックを表すソースコードを引数として取ります。

ソースコードの中で、$で始まる識別子は特別な意味を持ちます。

$0
その式によってフィールドにアクセスされる対象となるオブジェクトを表します。これは this を表すわけではありません。this はその式を実行するメソッドを含むオブジェクトを表しています。static フィールドに対するアクセスの場合、$0 は null となります。

$1
式が書き込みアクセスの場合、そのフィールドに格納される値を示します。読み込みアクセスの場合、$1 は利用できません。

$_
式が読み込みアクセスの場合、フィールドから取得した値を示します。書き込みアクセスの場合、$_ に格納された値は破棄されます。

$r
式が読み込みアクセスの場合、フィールドの型を表します。書き込みアクセスの場合、$r は void となります。

$class
アクセス対象フィールドを宣言したクラスを表す java.lang.Class オブジェクトです。

$type
アクセス対象フィールドの型を表す java.lang.Class オブジェクトです。

$proceed
元のフィールドアクセスを実行するための仮想的なメソッドの名前です。

$w、$args、$$ といった他の識別子についても利用することができます。

式が読み込みアクセスの場合、置換するソースコードの中で読み取り結果は $_ に代入されていなければなりません。$_ の型は、アクセス対象フィールドの型となります。

●javassist.expr.NewExpr

NewExpr オブジェクトは、new 演算子による新たなオブジェクトの生成を表します(配列の生成は除きます)。ExprEditor の edit() メソッドはオブジェクトが生成されたときに NewExpr オブジェクトを受け取ります。NewExpr の replace() メソッドはオブジェクト生成を置換する文やブロックを表すソースコードを引数としてとります。

ソースコードの中では、$ で始まる識別子は特別な意味を持ちます。

$0
null です。
$1, $2, ...
コンストラクタに対する引数を表します。
$_
オブジェクト生成の結果を表します。生成されたオブジェクトはこの変数に格納されていなければなりません。
$r
生成されたオブジェクトの型を表します。
$sig
引数の型を表す java.lang.Class オブジェクトの配列です。
$type
生成されたオブジェクトの型を表す java.lang.Class オブジェクトです。
$proceed
元のオブジェクト生成を実行する仮想メソッドの名称です。

$w、$args、$$ といった他の識別子も利用することができます。

●javassist.expr.NewArray

NewArray オブジェクトは new 演算子による配列の生成を表します。ExprEditor の edit() メソッドは配列の生成を検知するとこのオブジェクトを受け取ります。NewArray の replace() メソッドは、配列生成を置換する文やブロックを表すソースコードを引数としてとります。

ソースコードの中で、$ で始まる識別子は特別な意味を持ちます。

$0
nullです。
$1, $2, ...
各要素のサイズを表します。
$_
生成された配列を表す変数です。新たに生成された配列は、この変数に格納されなければなりません。
$r
生成された配列の型を表します。
$type
生成された配列の型を表す java.lang.Class オブジェクトです。
$proceed
元の配列生成を実行する仮想メソッドの名称です。


$w、$args、$$ といった他の識別子についても利用することができます。

たとえば、以下のような配列生成が行われたとします。

 String[][] s = new String[3][4];


$1、$2 の値はそれぞれ 3、4 となります。$3 には何も格納されません。

また、以下のような配列生成では、$1 が 3 となり、$2 には何も格納されません。

 String[][] s = new String[3][];

●javassist.expr.Instanceof

Instanceof オブジェクトは instanceof 演算子による式を表します。ExprEditor の edit() メソッドは、instanceof 演算子を使用した式が見つかったときにこのオブジェクトを受け取ります。Instanceof の replace() メソッドは、この式を置き換えるための文やブロックを引数として取ります。

ソースコードの中で、$ で始まる識別子は特別な意味を持ちます。

In the source text, the identifiers starting with $ have special meaning:

$0
null です。
$1
instanceof 演算子の左辺の値を表します。
$_
式の結果となる値を表します。$_ の型は boolean 型です。
$r
instanceof 演算子の右辺の値を表します。
$type
instanceof 演算子の右辺のオブジェクトの型を表す java.lang.class オブジェクトです。
$proceed
元の instanceof 式を実行する仮想メソッドの名称です。このメソッドは java.lang.Object 型の引数を1つ取ります。この引数が元の instanceof 式の右辺オブジェクトの型と一致していれば true、そうでなければ false を返します。

$w、$args、$$ といった他の変数についても利用可能です。

●javassist.expr.Cast

Cast オブジェクトは、明示的な型キャストを表します。ExprEditor の edit() メソッドは、明示的な型キャストが見つかった時にこのオブジェクトを受け取ります。Cast クラスの receive() メソッドは、キャスト式を置き換えるための文やブロックを引数としてとります。

ソースコードの中では、$ で始まる識別子は特別な意味を持ちます。

$0
null です。
$1
キャストされる値の型を表します。
$_
式の結果を表す値です。$_ の型は、キャスト後の型(つまり、括弧の中に記述された型)となります。
$r
キャスト後の型、または括弧の中に記述された型を表します。
$type
$r の型を表す java.lang.Class オブジェクトです。
$proceed
元のキャスト式を実行する仮想メソッドの名称です。このメソッドは java.lang.Object 型の引数を1つとり、元の式で表されるキャストを実行した結果を返します。

$w、$args、$$ といった他の識別子についても利用可能です。

●javassist.expr.Handler

Handler オブジェクトは try-catch 文の catch 節を表します。ExprEditor の edit() メソッドは、catch 節が見つかったときにこのオブジェクトを受け取ります。Handler クラスの insertBefore() メソッドは引数として受け取ったソースコードをコンパイルし、catch 節の先頭に挿入します。

ソースコードの中では、$ で始まる識別子は特別な意味を持ちます。

$1
catch 節によって捕捉された例外オブジェクトを表します。
$r
catch 節によって捕捉された例外オブジェクトの型を表します。これはキャスト式の中で使用します。
$w
ラッパ型を表します。これはキャスト式の中で使用します。
$type
catch 節によって捕捉された例外の型を表す java.lang.Class オブジェクトです。

新しい例外オブジェクトが $1 に代入されると、元のキャッチ節には代入されたオブジェクトが例外オブジェクトとして渡されます。

●4.3 新しいメソッド・フィールドの追加

●メソッドの追加

Javassist では、新しいメソッドやコンストラクタをゼロから作ることができます。CtNewMethod や CtNewConstructor クラスは、CtMethod や CtConstructor オブジェクトを生成することのできる static なファクトリメソッドをいくつか提供しています。とりわけ make() メソッドでは与えられたソースコードから CtMethod や CtConstructor オブジェクトを生成することができます。

 CtClass point = ClassPool.getDefault().get("Point");
 CtMethod m = CtNewMethod.make(
                  "public int xmove(int dx) { x += dx; }",
                  point);
 point.addMethod(m);


例えば、上のプログラムでは Point クラスに xmove() という public メソッドを追加しています。この例では、x は Point クラスの int 型フィールドです。

make() メソッドに渡されるソースコードの中では、setBody() メソッドと同じように $ で始まる識別子を利用することができます(ただし $_ は除きます)。make() メソッドの引数にターゲットとなるオブジェクトとメソッドの名称が指定されていれば、$proceed も利用できます。

 CtClass point = ClassPool.getDefault().get("Point");
 CtMethod m = CtNewMethod.make(
                  "public int ymove(int dy) { $proceed(0, dy); }",
                  point, "this", "move");


例えば、上のプログラムでは以下のような ymove() メソッドを生成します。

 public int ymove(int dy) { this.move(0, dy); }


$proceed が this.move に置き換えられていることに注目してください。

Javassist では、新しいメソッドを追加する別の方法も提供します。以下の例では最初に抽象メソッドを作成し、そのメソッドに対して実際の処理を与えています。

 CtClass cc = ... ;
 CtMethod m = new CtMethod(CtClass.intType, "move",
                           new CtClass[] { CtClass.intType }, cc);
 cc.addMethod(m);
 m.setBody("{ x += $1; }");
 cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);


Javassist は、抽象メソッドが追加されるとクラスを抽象クラスにします。そのため、setBody() メソッドを呼び出した後は明示的にクラスを非抽象クラスに変更しなければなりません。

相互循環呼び出し

Javassist では、まだクラスに追加されていないメソッドに対する呼び出しをコンパイルすることができません(そのメソッド自身に対する再帰呼び出しはコンパイルすることができます)。このような相互循環呼び出しをメソッドに追加するには、以下のようなトリックが必要となります。ここでは、メソッド m() と n() を cc で表されるクラスに追加したいとします。

 CtClass cc = ... ;
 CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc);
 CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc);
 cc.addMethod(m);
 cc.addMethod(n);
 m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
 n.setBody("{ return m($1); }");
 cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);


まず最初に、2つの抽象メソッドを作成してクラスに追加します。このようにすることで、それぞれのメソッドの中でお互いに対するメソッド呼び出しがあっても、それぞれのメソッド本体を追加することができます。最後に、クラスを非抽象クラスに変更します。これは、抽象メソッドを追加したときに addMethod() が自動的にクラスを抽象クラスに変更しているためです。

●フィールドの追加

Javassist では、新しいフィールドを生成することもできます。

 CtClass point = ClassPool.getDefault().get("Point");
 CtField f = new CtField(CtClass.intType, "z", point);
 point.addField(f);


このプログラムでは、Point クラスに z というフィールドを追加します。

追加するフィールドに初期値を設定する必要がある場合は、上のプログラムを以下のように変更します。

 CtClass point = ClassPool.getDefault().get("Point");
 CtField f = new CtField(CtClass.intType, "z", point);
 point.addField(f, "0");    // initial value is 0.


ここでは、addField() メソッドに2つめの引数を追加しています。この引数はフィールドに初期値を与えるための式を表すソースコードです。このソースコードでは、式の値の型がフィールドの型に一致さえしていれば、どのような Java 表現も可能です。ただし、式の最後にセミコロン(;)をつける必要がない点に注意してください。

●4.4 実行時サポートクラス

多くの場合、Javassist によって変更されたクラスは実行時に Javassist のライブラリを必要としません。しかし、Javassist によって生成されたある種のバイトコードは、実行時に javassist.runtime パッケージのクラスを必要とします(詳しくは、このパッケージの APIリファレンスを参照してください)。javassist.runtime パッケージは Javassist によって変更されたクラスが実行時に必要とする唯一のパッケージです。Javassist に含まれる他のパッケージは変更されたクラスの実行時には一切使用されません。

●4.5 制限

現在の実装では、Javassist のコンパイラは受理できる言語について、いくつかの制限を抱えています。

  • 全てのクラス名は、パッケージを含む名称でなければなりません。これは、コンパイラが import 宣言をサポートしていないためです。ただし、java.lang パッケージは例外です。たとえば、コンパイラは Object を java.lang.Object と同じように受理します。
  • ブレース { } で囲まれたカンマ区切りの式を含む配列の初期化はサポートされていません。
  • インナークラスや無名クラスはサポートされていません。
  • synchronized 文はまだサポートされていません。
  • ラベル付きの continue や break 文はサポートされていません。
  • try-catch 節に続く finally 節はサポートされていません。
  • コンパイラは Java のメソッドディスパッチアルゴリズムを正しく実装しているわけではありません。1つのクラスの中で、同じ名前で引数リストの異なるメソッドが宣言されている場合、コンパイラは混乱してしまいます。
  • クラス名と static メソッドまたはフィールドとのセパレータとして # を使用することを推奨します。例えば、通常の Java における以下のようなプログラムを見てください。

 javassist.CtClass.intType.getName()


これは、javassist.CtClass クラスの static フィールドである intType が示すオブジェクトの getName() メソッドを呼び出しています。Javassist では、これを以下のように表現することを推奨します。

 javassist.CtClass#intType.getName()


このように記述することで、コンパイラは高速に式を評価することができます。


1 2 3 4 5

ページトップへ
Java? は、米国Sun Microsystems,Inc. の登録商標です。
原著:Copyright c 2000-2005 O’Reilly Media, Inc. All rights reserved.
邦訳:Copyright c 2005 Acroquest, Co.,Ltd. All rights reserved.