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("javassist.gluonj.Gluon") public static class Debug { public static boolean stackTrace = true; } // ここでは、以下を省略します
Gluon
クラスには、boolean
型のフィールド stackTrace
が宣言されています。この stackTrace
の値が true
であると、GluonJ は詳細なエラーメッセージ (正確には、エラーのスタックトレース) を出力します。通常、stackTrace
の値は false
です。しかし、新しいポイントカット指定子を実装しているときには、詳しいエラーメッセージを出力させるために、この値を true
に変更したい場合があります。上記の @Refine
クラスは、Gluon
の stackTrace
フィールドの値を true
に変更するためのものです。もしその stackTrace
の値を false
に戻したければ、この @Refine
クラスの注釈 @Refine
("javassist.gluonj.Gluon") をコメントアウトします。@Refine
によって注釈されていないクラスを、GluonJ は @Refine
クラスとして認識しません。
抽象構文木のノード・クラスを宣言する
まず、annotate
ポイントカット指定子を追加するのに必要な visitor メソッドを呼び出すためのインターフェースを宣言します。
public interface AnnotatePcVisitor { void visit(AnnotatePc pc) throws WeaveException; }
これは MetaTag
クラスに宣言されたネストインターフェースです。
そして、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 { ((AnnotatePcVisitor)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
クラス内の AnnotatePcVisitor
インターフェースに宣言されている visit
メソッドを呼び出します。既存の (元の GluonJ で定義されている) PointcutVisitor
インターフェースには、この visit
メソッドが宣言されていないため、そのメソッドを直接呼び出すことができません。この詳細は、後ほど説明します。
prepare
メソッドと match
メソッドは、annotate
ポイントカットを実装するために使われます。織り込み処理を始めたときに、GluonJ は一度だけ prepare
メソッドを呼び出します。match
メソッドがポイントカットの引数として渡されたパターンを毎回解析する必要のないように、prepare
メソッドがそのパターンを事前に解析しておくのです。もし与えられた注釈がそのパターンにマッチすれば、その match
メソッドは true
を返します。match
メソッドは visitor によって呼び出されます。
Visitor クラスを改良する
今まで、新しいポイントカット指定子のためのノード AnnotatePc
クラスを定義してきました。ここでは、GluonJ がその新しいノードを巡回できるようにするために、PointcutVisitor
インターフェースを改良します。
まず、PointcutVisitor
インターフェースに、上記で定義した AnnotatePcVisitor
インターフェースを継承させるように改良します。この AnnotatePcVisitor
には、引数として AnnotatePc
オブジェクトを受け取る visit
メソッドが宣言されています。
@Refine("javassist.gluonj.pc.PointcutVisitor") interface PcVisitor extends AnnotatePcVisitor {}
そして、PointcutVisitor
インターフェースを実装している全ての visitor クラスを拡張します。
@Refine("javassist.gluonj.Pointcut.PrepareVisitor") public static class Prepare { Parser parser; public void visit(AnnotatePc pc) throws WeaveException { pc.prepare(parser); } }
上記の visit
メソッドでは、AnnotatePc
クラス内で宣言された prepare
メソッドを呼び出します。
@Refine("javassist.gluonj.Gluon.Matcher") public static class Match { protected boolean result; protected Residue residue; 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 である MethodCallMatcher
であり、もう片方はフィールド・アクセスのための visitor である FieldAccessMatcher
です。以下では、それぞれの visit
メソッドを編集します。
@Refine("javassist.gluonj.weave.CallMatcher") public static class CallMatch { private MethodCall joinPoint; protected boolean result; 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("javassist.gluonj.weave.FieldMatcher") public static class FieldMatch { boolean result; private FieldAccess joinPoint; 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); } } }
CallMatch
クラスと FieldMatch
クラスは、共に joinPoint
という名前のフィールドを宣言しています。このフィールドは、現在チェックしているジョインポイントを表しています。
最後の visitor は、CflowCollector
です。織り込み時に GluonJ は、そのジョインポイントと cflow
ポイントカットとして指定されたジョインポイントがマッチしているかどうかをチェックするために、この visitor が使われます。cflow
ポイントカットのために、GluonJ はそのポイントカットとして指定されたメソッドにプログラムの制御が移ったかどうかを、実行時に監視していなければなりません。それゆえ、GluonJ は、織り込み時に cflow
ポイントカットで指定されたメソッドの入り口と出口で、現在のスレッドを監視できるように、そのバイトコードを編集します。今回、annotate
ポイントカットは、cflow
ポイントカットと関係がないため、CflowCollector
クラスの visit
メソッドを改良する必要はありません。
@Refine("javassist.gluonj.weave.CflowCollector") public static class Cflow { 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
クラスが以下です。
public interface AnnotateAvailable { Pointcut annotate(String tag); } @Refine("javassist.gluonj.Pointcut") static abstract class Pcut implements AnnotateAvailable { abstract void setTree(PointcutNode pcn); public Pointcut annotate(String tag) { setTree(new AnnotatePc(tag)); return (Pointcut)Gluon.$refine(this); } }
新しく追加された Pointcut
クラスの annotate
メソッドにアクセスするため、AnnotateAvailable
インターフェースを宣言します。
まず、annotate
メソッドは、すでに宣言している AnnotatePc
クラスのオブジェクトを作成します。そして、その AnnotatePc
オブジェクトを Pointcut
オブジェクトの葉のノードとして登録します。setTree
は、元の Pointcut
クラスに宣言されているメソッドです。
上記の annotate
メソッドの最後の 1 文:
return (Pointcut)Gluon.$refine(this);
は、この Pointcut
オブジェクトへの参照を返しています。Gluon.$refine
は、Pcut
型から Pointcut
型への型変換を行うための特殊なメソッドです。Pcut
クラス内で定義された annotate
メソッド中で使われる this
は、Pcut
オブジェクトを参照します。織り込み後には、Pcut
クラスは Pointcut
クラスにマージされますが、Pcut
をコンパイルする段階では、いまだ Pointcut
クラスにマージされていません。それゆえ、以下のイディオムを使用し、明示的に型変換を行わなければならないのです。
( 型の名前 )Gluon.$refine( 値 )
Pcd
クラスにも、annotate
メソッドを追加しなければなりません。
@Refine("javassist.gluonj.Pcd") static class Pcd2 { @Abstract private static Pointcut make() { return null; } public static Pointcut annotate(String tag) { AnnotateAvailable pc = (AnnotateAvailable)make(); return pc.annotate(tag); } }
Pointcut
クラスのコンストラクタの修飾子は、public
ではないため、代わりに Pcd
クラスにすでに宣言されている make
メソッドを利用します。この make
は、Pointcut
オブジェクトを生成するためのメソッドです。変数 pc
の型は、上記で宣言した AnnotateAvailable
です。この型でなければ、Pointcut
クラスに新しく追加した annotate
メソッドを呼び出すことができないのです。
上述した (@Refine
クラス) Pcd
内で、make
メソッドを呼び出すために、GluonJ の利用者は以下の宣言をその Pcd
クラスに追加しなければなりません。
@Abstract private static Pointcut make() { return null; }
これにより、織り込み後には、Pcd
クラス内の make
メソッドの呼び出しは、Pointcut
クラスに宣言された make
メソッドの呼び出しに変換されます。
@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.