How to extend GluonJ
千葉 滋、西澤 無我 (訳:西澤 無我)
我々は GluonJ を拡張し、新しいポイントカット指定子を簡単に追加することができます。これには GluonJ の機能を使って、GluonJ 自体を拡張します。実際に、GluonJ の annotate
ポイントカット指定子は、@Glue
クラスで定義されています (javassist.gluonj.plugin
パッケージに置かれています)。ここでは、GluonJ を拡張していくその実装方法を紹介していきます。
GluonJ のアーキテクチャ
GluonJ は、起動時に利用者から渡された @Glue
クラスを読み込みます。このとき、GluonJ の内部では、読み込んだ @Glue
クラスを、Gluon
オブジェクトとして保持します。Gluon
オブジェクトには、@Refine
クラスのリストと、ポイントカットとアドバイスのペアとが格納されています。
GluonJ は、@Glue
クラスを読み込むと、まず @Glue
クラスのオブジェクトを 1 つ生成します。その際に、Pointcut
型のフィールドの初期値をチェックし、ポイントカットの抽象構文木を作ります。抽象構文木のノードの型は、javassist.gluonj.pc.PointcutNode
です。Gluon
オブジェクトは、ポイントカットとして、この抽象構文木を保持します。
織り込み時に、GluonJ は織り込み対象のクラスのメソッド・ボディを 1 つずつ読み込みます。そのメソッド・ボディ内に、メソッド呼び出しやフィールド・アクセスなどのジョインポイント (join points) を見つけると、GluonJ はそのジョインポイントが Gluon
オブジェクトに保持されるポイントカットにマッチしているかどうかをチェックします。このチェックは visitor パターンに従って行われます。もしそのジョインポイントがポイントカットにマッチしていれば、GluonJ はそのジョインポイントの箇所にアドバイス・ボディを挿入します。
それゆえ、以下のステップで、我々は新しいポイントカット指定子を GluonJ に追加することができます。
-
PointcutNode
のサブクラスであるノード・クラスを宣言します。新しいポイントカット指定子を表すために、このノードを使います。 -
GluonJ で使われている全ての visitor クラスを改良します。これらの visitor クラスは、
javassist.gluonj.pc.PointcutVisitor
のサブクラスです。 -
Pcd
クラスとPointcut
クラスを改良します。 -
最後に、新しいポイントカット指定子を実装した
@Glue
クラスをインストールします。javassist.gluonj.plugin.Installed
クラスを編集することで、この作業を行うことができます。
annotate
ポイントカット指定子を実装する
これから、annotate
ポイントカット指定子を、javassist.gluonj.plugin.MetaTag
クラスで実装していきます。この MetaTag
は、@Glue
クラスです。以下に、この @Glue
クラスのソースコードを説明していきます。
アクセスされたメソッドやフィールドに付けられる注釈をジョインポイントとして選択するために、GluonJ の利用者は annotate
ポイントカット指定子を使うことができます。例えば
@Before("{ System.out.println(`call!`); }") Pointcut pc = Pcd.call("*(..)").and.annotate("@test.Print");
このポイントカットにより、@test.Print
で注釈されたメソッドの呼び出し箇所を全て指定することができます。
デバッグトレースのオン/オフ
javassist.gluonj.plugin
パッケージの下にある MetaTag
クラスのソースコードは、以下の static
なネストクラスの宣言から始まっています。
package javassist.gluonj.plugin; import javassist.gluonj.*; // 以下、省略します @Glue public class MetaTag { @Refine static class Debug extends Gluon { public Debug() { super(null); } public static boolean stackTrace = true; } // ここでは、以下を省略します
Gluon
クラスには、boolean
型のフィールド stackTrace
が宣言されています。この stackTrace
の値が true
であると、GluonJ は詳細なエラーメッセージ (正確には、エラーのスタックトレース) を出力します。通常、stackTrace
の値は false
です。しかし、新しいポイントカット指定子を実装しているときには、詳しいエラーメッセージを出力させるために、この値を true
に変更したい場合があります。上記の @Refine
クラスは、Gluon
の stackTrace
フィールドの値を true
に変更するためのものです。もしその stackTrace
の値を false
に戻したければ、この @Refine
クラスの注釈をコメントアウトします。@Refine
によって注釈されていないクラスを、GluonJ は @Refine
クラスとして認識しません。
抽象構文木のノード・クラスを宣言する
まず、annotate
ポイントカット指定子用の抽象構文木のノードを表現するクラスを新しく宣言します。
public static class AnnotatePc extends PointcutNode { private String arg; private PcPattern pattern; public AnnotatePc(String pat) { this.arg = pat; } public String toString() { return "annotate(" + arg + ")"; } public void prepare(Parser p) throws WeaveException { pattern = p.parseClass(removeAt(arg)); if (!pattern.isClassName()) throw new WeaveException("bad pattern: " + toString()); } private static String removeAt(String name) { if (name != null && name.length() > 1 && name.charAt(0) == '@') return name.substring(1); else return name; } public void accept(PointcutVisitor v) throws WeaveException { ((PcVisitor2)v).visit(this); } /* 与えられたジョインポイントがパターンにマッチしているかどうかを * チェックしています。Visitor によって呼ばれます。 */ public boolean match(AnnotationsAttribute attr) throws WeaveException { if (attr == null) return false; Annotation[] anno = attr.getAnnotations(); int n = anno.length; for (int i = 0; i < n; i++) if (pattern.matchClass(anno[i].getTypeName())) return true; return false; } }
このクラスに、visitor を動作させるために必要な accept
メソッドを宣言しなければなりません。この accept
メソッドは、MetaTag
クラス内の PcVisitor2
インターフェースに宣言されている visit
メソッドを呼び出します。既存の (元の GluonJ で定義されている) PointcutVisitor
インターフェースには、この visit
メソッドが宣言されていないため、そのメソッドを直接呼び出すことができません。PcVisitor2
インタフェースは後で定義します。
prepare
メソッドと match
メソッドは、annotate
ポイントカットを実装するために使われます。織り込み処理を始めたときに、GluonJ は一度だけ prepare
メソッドを呼び出します。match
メソッドがポイントカットの引数として渡されたパターンを毎回解析する必要のないように、prepare
メソッドがそのパターンを事前に解析しておくのです。もし与えられた注釈がそのパターンにマッチすれば、その match
メソッドは true
を返します。match
メソッドは visitor によって呼び出されます。
Visitor クラスを改良する
ここまでで、新しいポイントカット指定子のためのノード・クラス AnnotatePc
を定義しました。次に、その新しいノードが正しく巡回されるように PointcutVisitor
インターフェースを修正します。
まず、PointcutVisitor
インターフェースを拡張して新しい visit
メソッドを追加します。このメソッドは、引数として AnnotatePc
オブジェクトを受け取ります。
@Refine interface PcVisitor2 extends PointcutVisitor { void visit(AnnotatePc pc) throws WeaveException; }
なお、型 PcVisitor2
は先に示した AnnotatePc
クラスの accept
メソッドの中で使われていました。
次に、PointcutVisitor
インターフェースを実装している全ての visitor クラスを拡張します。
@Refine static class Prepare2 extends PrepareVisitor { public Prepare2() { super(null); } public void visit(AnnotatePc pc) throws WeaveException { pc.prepare(parser); } }
PrepareVisitor
に追加される visit
メソッドは、AnnotatePc
クラス内で宣言された prepare
メソッドを呼び出すだけです。
@Refine static abstract class Match2 extends Matcher { public void visit(AnnotatePc pc) throws WeaveException { result = false; } }
Matcher
クラスは、与えられたジョインポイントがポイントカットにマッチしているかどうかをチェックする機能をもった visitor クラスのスーパークラスです。織り込み時に、GluonJ は織り込み対象クラスのすべてのメソッドの中身を読み込みます。そして、そのメソッド内にメソッド呼び出しなどのジョインポイントを発見すると、そのジョインポイントにマッチするポイントカットがあるかどうかを visitor を利用して検索します。GluonJ の visitor は、保持しているすべてのポイントカットの抽象構文木を巡回し、現在のジョインポイントがそのポイントカットにマッチしているかどうかをチェックします。もしマッチしているジョインポイントが見つかれば、そのジョインポイントの箇所で指定されたポイントカットの対になっているアドバイス・ボディを実行するよう、そのメソッドのバイトコードを編集します。
Matcher
のサブクラスの visit
メソッドは、その引数として渡されたノードオブジェクトがジョインポイントにマッチしているかどうかをチェックします。もしマッチしていれば、Matcher
クラスに宣言されているフィールド result
に true
をセットします。それ以外の場合は、false
値をセットします。レジデュー (residue) が残っている場合、そのレジデューを表す Java の式が residue
フィールドにセットされます。residue
は、result
と同様、Matcher
クラスに宣言されているフィールドです。レジデューはアドバイス・ボディを実行するための実行時条件です。もし residue
が null
でなければ、その residue
に格納されている Java の式の実行時の値が true
である場合のみ、GluonJ はアドバイス・ボディを実行するようにバイトコードを編集します。
編集されるバイトコードは、以下のようになります。
if (residue
フィールドに格納されている Java の式) アドバイス・ボディを実行 ;
織り込み時に、GluonJ は発見したジョインポイントの種類によって、異なる visitor を使います。annotate
ポイントカットを使用するために、我々は Matcher
の 2 つのサブクラスを編集する必要があります。1 つは、メソッド呼び出しというジョインポイントのための visitor である CallMatch2
であり、もう片方はフィールド・アクセスのための visitor である FieldMatch2
です。以下では、それぞれの visit
メソッドを編集します。
@Refine static class CallMatch2 extends CallMatcher { public CallMatch2() { super(null); } public void visit(AnnotatePc pc) throws WeaveException { try { MethodInfo minfo = joinPoint.getMethod().getMethodInfo2(); AnnotationsAttribute aa1 = (AnnotationsAttribute) minfo.getAttribute(AnnotationsAttribute.invisibleTag); AnnotationsAttribute aa2 = (AnnotationsAttribute) minfo.getAttribute(AnnotationsAttribute.visibleTag); result = pc.match(aa1) || pc.match(aa2); } catch (NotFoundException e) { throw new WeaveException(e); } } } @Refine static class FieldMatch2 extends FieldMatcher { public FieldMatch2() { super(null); } public void visit(AnnotatePc pc) throws WeaveException { try { FieldInfo finfo = joinPoint.getField().getFieldInfo2(); AnnotationsAttribute aa1 = (AnnotationsAttribute) finfo.getAttribute(AnnotationsAttribute.invisibleTag); AnnotationsAttribute aa2 = (AnnotationsAttribute) finfo.getAttribute(AnnotationsAttribute.visibleTag); result = pc.match(aa1) || pc.match(aa2); } catch (NotFoundException e) { throw new WeaveException(e); } } }
CallMatch2
クラスと FieldMatch2
クラスは、共に joinPoint
という名前のフィールドを visit
メソッドの中で利用しています。このフィールドは、CallMatcher
と FieldMatcher
クラスの中で宣言されており、現在チェックしているジョインポイントを表しています。
最後の visitor は、CflowCollector
です。GluonJ は織り込み時に、そのジョインポイントと cflow
ポイントカットとして指定されたジョインポイントがマッチしているかどうかをチェックするために、この visitor を使います。cflow
ポイントカットのために、GluonJ はそのポイントカットとして指定されたメソッドにプログラムの制御が移ったかどうかを、実行時に監視していなければなりません。それゆえ、GluonJ は、織り込み時に cflow
ポイントカットで指定されたメソッドの入り口と出口で、現在のスレッドを監視できるように、そのバイトコードを編集します。今回、annotate
ポイントカットは、cflow
ポイントカットと関係がないため、CflowCollector
クラスの visit
メソッドを改良する必要はありません。
@Refine static class Cflow2 extends CflowCollector { public Cflow2() { super(null); } public void visit(AnnotatePc pc) throws WeaveException { // 何もしません } }
Pcd
クラスと Pointcut
クラスを改良する
ここからは、GluonJ の利用者へ annotate
ポイントカットを提供するために、annotate
メソッドを Pcd
クラスと Pointcut
クラスに追加します。以下のように annotate
ポイントカットを利用できるようにしていきます。
@Before("{ System.out.println(`call!`); }") Pointcut pc = Pcd.call("*(..)").and.annotate("@test.Print");
Pointcut
クラスに annotate
メソッドを追加する @Refine
クラスが以下です。
@Refine static class Pcut2 extends Pointcut { Pcut2() { super(null); } public Pointcut annotate(String tag) { setTree(new AnnotatePc(tag)); return this; } }
annotate
メソッドは、まず、すでに示した AnnotatePc
クラスのオブジェクトを作成します。そして、その AnnotatePc
オブジェクトを Pointcut
オブジェクトの葉のノードとして登録します。setTree
は、元の Pointcut
クラスに宣言されているメソッドです。
次に Pcd
クラスにも、annotate
メソッドを追加しなければなりません。
@Refine static class Pcd2 extends Pcd { public static Pointcut annotate(String tag) { Pcut2 pc = (Pcut2)make(); return pc.annotate(tag); } } }
Pointcut
クラスのコンストラクタの修飾子は、public
ではないため、代わりに Pcd
クラスにすでに宣言されている make
メソッドを利用します。この make
は、Pointcut
オブジェクトを生成するためのメソッドです。変数 pc
の型は、上記で宣言した Pcut2
です。この型でなければ、Pointcut
クラスに新しく追加した annotate
メソッドを呼び出すことができないのです。
@Glue
クラスをインストールする
最後に、これまで示してきた @Glue
クラスをインストールします。これを行うためには、javassist.gluonj.plugin.Installed
クラスに、以下の宣言を含めなければなりません。
@Include MetaTag glue0;
この Installed
もまた @Glue
クラスです。GluonJ のビルドスクリプト (build.xml
) は、ビルドの最後に、Installed
を GluonJ に織り込みます。結果として、Installed
クラスは以下のような宣言になります。
@Glue public class Installed { @Include MetaTag glue0; }
もし @Glue
クラス内に、@Include
によって注釈されたフィールドが宣言されていれば、そのフィールドの型である @Glue
クラスも編み込まれます。この glue0
という名前には特殊な意味はありません。どんな名前でもかまいません。
修正した Installed
クラスを使って GluonJ
を再構築するには、ant
コマンドを 2 回実行しなければなりません。
ant ant -buildfile build-plugin.xml
最初の実行では、build.xml
をビルドファイルに使って、プラグインを含まない標準の gluonj.jar
を生成します。その後の 2
回目の実行で Installed
クラスに記述されたプラグインを織り込みます。これによって完全な gluonj.jar
が得られます。
Copyright (C) 2006 by Shigeru Chiba and Muga Nishizawa. All rights reserved.