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 クラスは、GluonstackTrace フィールドの値を 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 クラスに宣言されているフィールド resulttrue をセットします。それ以外の場合は、false 値をセットします。レジデュー (residue) が残っている場合、そのレジデューを表す Java の式が residue フィールドにセットされます。residue は、result と同様、Matcher クラスに宣言されているフィールドです。レジデューはアドバイス・ボディを実行するための実行時条件です。もし residuenull でなければ、その 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 メソッドの中で利用しています。このフィールドは、CallMatcherFieldMatcher クラスの中で宣言されており、現在チェックしているジョインポイントを表しています。

最後の 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.