Introduction to GluonJ

千葉 滋 (訳: 西澤 無我)

Table of Contents

1. なぜ GluonJ が必要か?
2. Glue クラスを定義する
3. クラスの改良 (Refinement)
4. ポイントカット (Pointcut) とアドバイス (Advice)


1. なぜ GluonJ が必要か?

GluonJ (グルーオン・ジェー) は Java 用のシンプルな AOP (Aspect-Oriented Programming) システムです。Java の文法で AOP の機構を提供しているので、アスペクトを書くために独自の文法を覚える必要はありません。

AOP の応用はすでにいくつも知られています。その中でも特に、ソフトウェアのテストプログラムを書くのに AOP は役立つと言われています。以下にそのテストプログラムの例を示します。

package demo;

import java.math.BigDecimal;

public class Bank {
    public BigDecimal transfer(Account src, Account dest,
                               BigDecimal amount) {
        dest.deposit(amount);
        return src.withdraw(amount);
    }
}
package demo;

import java.math.BigDecimal;

public class Account {
    private BigDecimal balance;

    public BigDecimal deposit(BigDecimal amount) {
        return balance.add(amount);
    }

    public BigDecimal withdraw(BigDecimal amount) {
        return balance;         // incomplete!
    }
}

上記の Account クラスの定義は、完成していません。フィールド balance の値は初期化されていませんし、withdraw メソッドは何も行いません。ところが、時々開発者は、こういった完成していないクラスを含んだプログラムを、テストしなければならないこともあります。以下は Bank クラスの transfer メソッドの実装をテストするためのテストプログラムです。

package demo;

import junit.framework.TestCase;
import java.math.BigDecimal;

public class BankTest extends TestCase {
    public void testTransfer() {
        Account mine = new Account();
        Account yours = new Account();
        BigDecimal pay = new BigDecimal(20000);
        BigDecimal balance = new BigDecimal(10000);
        Bank bank = new Bank();
        assertEquals(balance, bank.transfer(yours, mine, pay));
    }
}

もしこのテストプログラムをそのまま実行したら、当然テストは失敗してしまうでしょう。テストメソッドの最後の行で呼ばれている transfer メソッドは、Account クラスの withdraw メソッドを呼び出します。ところが withdraw メソッドは完全には実装されていないのです。

このテストの失敗と transder メソッドの実装とは無関係です。テストの対象である transfer メソッドの実装自体にバグがなければ、このテストプログラムは期待通りに終了されるべきです。我々はこのテストの問題点を解決し、テストを成功させるべきです。

GluonJ を利用すれば、この問題を簡単に解決することができます。以下のような glue クラスを書けばよいのです。

package demo;

import javassist.gluonj.*;
import java.math.BigDecimal;

@Glue public class Mock {
    @Refine("demo.Account")
    static class Account {
        private BigDecimal balance = new BigDecimal(30000);

        public BigDecimal withdraw(BigDecimal amount) {
            return balance.subtract(amount);
        }
    }
}

もし上記の全てのプログラム (Bank, Account,BankTest, そして Mock) をいっしょに実行すれば、テストプログラム BankTest は期待した通りに終了し、テストは成功します。依存性の注入 (dependency injection) フレームワークのように、glue クラスは balance フィールドへ初期値 30000 を代入します。また、glue クラスは、元の Account クラスの withdraw メソッド実装を仮の実装に置き換えます (完全な実装では、amount の値が大きすぎたときに、例外を投げるべきです)。なお詳しい説明は後でおこないます。

Account を完全に実装し終えた後でも、上述した glue クラスを、有効に利用することができます。単体テストでは、そのテスト結果は、テスト対象プログラムが呼び出している他のコンポーネント、モジュール、やクラスに依存しているべきではありません。Bank クラスの単体テストを行っている間、上記の glue クラスは Bank クラスから Account クラスの実装を分離します。すなわち、Banck クラスから Account クラスの実装への依存度を下げているのです。withdraw メソッドの実装が完成した後も、glue クラスは単体テストの間、withdraw の実装をテスト用の仮の実装に戻してしまいます。

上記のプログラムは簡単に実行することができます。JDK 1.5 もしくはそれ以降の JDK を利用しているのであれば、コマンドラインから java コマンドを実行するときに、以下のオプションを追加します。

-javaagent:gluonj.jar=demo.Mock

ここでは、gluonj.jar をカレントディレクトリに置いているものとしています。もし Java 用の統合開発環境 eclipse を使用しているのであれば、上記のオプションを VM 引数として JVM に渡します。VM 引数は、Launch Configurations ダイアログ (構成および実行ダイアログ) によって指定できます。

2. Glue クラスを定義する

Glue は、既存のアプリケーション・クラスに対する様々な拡張を集めたものです。Glue は、@Glue によって注釈されたクラスによって表現されます (このクラスを以下、@Glue クラスと呼びます)。@Glue クラス内で行える拡張は、ポイントカットとアドバイス (pointcut-advice)、もしくはクラスの改良 (refinement) です。これらは好きな数だけ @Glue クラスの中に含めることができます。ポイントカットとアドバイスは、@Glue クラス内で宣言された Pointcut 型のフィールドです。このフィールドは @Before, @After, もしくは @Around によって注釈を付けられていなければなりません。Refinement は、@Glue クラス内に含まれる static なネストクラスです。この refinement を @Refine クラスと呼びます。Refinement は AspectJ のインタータイプ宣言に相当します。

以下に、@Glue クラスの例を示します。

package test;

import javassist.gluonj.*;

@Glue class Logging {
    @Before("{ System.out.println(`call!`); }")
    Pointcut pc = Pcd.call("test.Hello#say(..)");

    @Refine("test.Hello")
    static class Afternoon {
        private String header() {
            return "Good afternoon, ";
        }
    }
}

この @Glue クラスは以下の test.Hello クラスを拡張しています。

package test;

public class Hello {
    private String header() {
        return "Good morning, ";
    }

    public void say(String toWhom) {
        System.out.println(header() + toWhom);
    }

    public static void main(String[] args) {
        new Hello().say("Bill");
    }
}

もし test.Hello クラスを @Glue クラスである Logging 抜きに実行すると、その出力結果は以下となるでしょう。

Good morning, Bill

一方、もし @Glue クラスといっしょに test.Hello クラスを実行した場合、出力結果は下記のように変更されます。

call!
Good afternoon, Bill

@Glue クラス Logging 内のポイントカットとアドバイス (@Before で注釈されたフィールド) は、say メソッドが呼ばれる直前で、"call!" を出力します。また、@Refine クラスは、header メソッドが "Good afternoon, " を返すように test.Hello クラスを改良します。詳しい説明は後ろの章を見てください。

織り込み

他のクラスに @Glue クラスを適用するためには、織り込み (weaving) を実行しなければなりません。この織り込みとはコンパイル後のプログラム変換です。GluonJ では、2 つのタイプの織り込み (実行前の織り込みとロード時の織り込み) をサポートしています。

実行前の織り込みを支援する Ant タスク

まず実行前の織り込みについて説明します。GluonJ は、宣言した @Glue クラスに従い、コンパイルされたクラスファイルを変換する Ant タスクを提供しています。以下に、その Ant タスクを定義し、呼び出す build.xml ファイルの例を示します。

<?xml version="1.0"?>
<project name="hello" basedir=".">
    <taskdef name="weave" classname="javassist.gluonj.ant.taskdefs.Weave">
        <classpath>
            <pathelement location="./gluonj.jar"/>
        </classpath>
    </taskdef>

    <target name="weave">
        <weave glue="test.Logging" destdir="./out" debug="false" >
            <classpath>
                <pathelement path="./classes"/>
            </classpath>
            <fileset dir="./classes" includes="test/**/*" />
        </weave>
    </target>
</project>

ここでは、先ほどと同様に gluonj.jar をカレントディレクトリに置いているものとして、build.xml ファイルを記述します。また、Java コンパイラによって生成されたクラスファイルを ./classes ディレクトリに置いているものとします。さらに、クラスファイルが変換された後、それらがセーブされる場所は ./out だとします。@Glue クラスは test.Logging です。その @Glue クラスは、fileset 要素によって指定されているクラスファイルを変換します。上記の例では、./classes/test ディレクトリ以下にあるすべてのクラスファイルが変換の対象となります (test で始まるパッケージ名のクラスのクラスファイルが対象となります)。

weave タスクは debug 属性を持ちます。この属性の値が true なら、GluonJ は詳細なログメッセージを出力します。この属性の値を明示的に指定しない場合、値は false となります。

コマンドラインによる実行前の織り込み

織り込みはコマンドラインからおこなうこともできます。

java -jar gluonj.jar test.Logging test/Hello.class

上の java コマンドを実行する前には、 ./classes ディレクトリに移動していなければなりません。クラスファイルはカレントディレクトリになければなりません。パス名はそれぞれ、./test/Logging.class./test/Hello.class となります。 2 番目の引数 test.Logging@Glue クラスの完全修飾名です。もし複数のクラスファイルを変換しなければならないときは、test.Logging に続く 3 番目、4 番目、... の引数として与えます。それらの引数はクラスファイルのパス名です。クラス名でないことに注意してください。

もしなにも引数を与えないと、gluonj.jar は GluonJ のバージョン番号とコピーライトを表示します。

java -jar gluonj.jar

ロード時の織り込み

もう片方のロード時の織り込みについて説明します。Java virtual machine (JVM) が各クラスファイルをロードするタイミングで、GluonJ はそれらのクラスを変換することができます。この織り込みを行うためには、JVM にコマンドラインのオプション -javaagent を利用します。例えば、

java -javaagent:gluonj.jar=test.Logging -cp "./classes" test.Hello

再度、gluonj.jar をカレントディレクトリに置いているものとし、./classes ディレクトリにクラスファイルを置いているものとします。@Glue クラスは、test.Logging です。ここで、-javaagent:... は VM 引数です。もし Ant で上記のコマンドを実行するのなら、以下のように build.xml ファイルに書きます。

<java fork="true" classname="test.Hello">
    <classpath>
        <pathelement path="./classes"/>
    </classpath>
    <jvmarg value="-javaagent:./gluonj.jar=test.Logging"/>
</java>

もし詳細なログメッセージを出力させたい場合は、debug オプションを指定します。例えば、

java -javaagent:gluonj.jar=test.Logging,debug -cp "./classes" test.Hello

のようにします。なおカンマと debug の間に空白を入れてはいけません。

複数の @Glue クラスを利用する織り込み

実行前の織り込み、もしくはロード時の織り込みの場合、1 つの @Glue クラスを指定することができます。その例を上述しました。もし複数の @Glue クラスを他のクラスに適用したいときには、1 つの @Glue クラスの定義に、他の @Glue クラスを含めます。

例えば、以下の @Glue クラスの定義は、他の 2 つの @Glue クラスを含んでいます。

package test;

import javassist.gluonj.*;

@Glue class AllGlues {
    @Include Logging glue0;
    @Include Tracing glue1;
}

@Glue クラス test.Loggingtest.Tracing は、子供の @Glue クラスとして @Glue クラス AllGlues に含まれています。AllGlues は親と呼びます。

@Include は、他の @Glue クラスを指定するために使われます。もしフィールド宣言が @Include によって注釈されていれば、GluonJ はそのフィールドの型が @Glue クラスだと解釈します。そのクラスは、元の @Glue クラスの子供の @Glue クラスになります。GluonJ は、@Include によって注釈されたフィールド (すなわち @Glue クラス) を、フィールドの名前の辞書順でソートします。そして、そのソートした順番通りに @Glue クラスを他のクラスに適用します。親の @Glue クラス (@Include で注釈されたフィールドをもっているクラス) は最後に適用されます。

3. クラスの改良 (Refinement)

@Refine で既存のクラスをどのように 改良 (refinement) するかを指示することができます。言い換えると、@Refine は元のクラスの定義を拡張します。しかし、継承や mixin 機構などと異なり、@Refine は元のクラス定義を直接修正・変更します。@Refine は AspectJ のインタータイプ宣言に相当します。

以下に、@Refine を用いた簡単な例を示します。

package test;
public class Person {
    public String name;
    public void greet() {
        System.out.println("I'm " + name);
    }
}

test.Person のクラス定義を @Refine で拡張します。

@Glue class SayHello {
    @Refine("test.Person")
    static class Person {
        public String message = "Hello";
        public void greet() {
            System.out.println(message);
        }
    }
}

この glue クラス (@Glue クラス) は、Person という名前の static なネストクラスを含んでいます。この Person@Refine により注釈されています。このように @Refine によって注釈されたクラスを @Refine クラスと呼びます。@Refine クラスの名前付けに規則はありません。PExpand などといった他の名前でも大丈夫です。

このネストした @Refine クラスは、test.Person クラスへ、新しいフィールド message を追加します。さらに、元のクラスで宣言されている greet メソッドを、ネストクラスの greet メソッドに置き換えます (つまり、元のクラスで宣言された greet メソッドは上書きされます)。GluonJ の利用者は、フィールドやメソッドが追加されるターゲットのクラスの名前を、@Refine のパラメータで指定します。ターゲットクラスの名前をパラメータに書くときには、完全修飾名でなければなりません。

上記の @Refine クラスで改良された元クラス test.Person は、以下のようになります。

package test;
public class Person {
    public String name;
    private String message = "Hello";
    public void greet() {
        System.out.println(message);
    }
}

@Glue クラス SayHello@Refine クラスを 1 つしか含んでいませんが、@Glue クラスはそのメンバとして複数の @Refine クラスを含めることもできます。

@Refine クラスをネストクラスとしてだけではなく、トップレベルのクラスとしても宣言することができます。以下のプログラムは、上述した @Glue クラス SayHello と同じ意味をもちます。

@Glue class SayHello {
    @Refine("test.Person") Diff refines;
}

public class Diff {
    public String message = "Hello";
    public void greet() {
        System.out.println(message);
    }
}

@Refine によって注釈されたフィールドは、クラスの改良 (refinement) を表しています。宣言されたフィールドの型は @Refine クラスでなければなりません。この場合、@Refine クラス Diff の定義に @Refine を注釈する必要はありません。この @Refine クラスは、@Refine の引数によって指定されたクラスを拡張するのに使われます。上記の場合、Diff クラス (@Refine クラス) は、test.Person クラス (元クラス) を拡張します。もちろん、フィールドの名前付けに規則はありません。どんな名前でも構いません。

新しいメソッドの追加

@Refine クラスは元のクラスに新しいメソッドを追加することができます。

@Glue class Cheerful {
    @Refine("test.Person")
    static class Person implements CheerfulPerson {
        public void sayHi(String toWhom) {
            System.out.println("Hi " + toWhom);
        }
    }
}

public interface CheerfulPerson {
    void sayHi(String toWhom);
}

この @Refine クラスは、test.Person クラスに sayHi メソッドを追加します。@Refine クラス Person は、CheerfulPerson インターフェースを実装しています。そのため、@Refine クラスは、CheerfulPerson インターフェースを実装するように、元の test.Person クラスを拡張します。

CheerfulPerson を実装することは、追加された sayHi メソッドを呼び出すために必要です。

Person p = new Person();
((CheerfulPerson)p).sayHi("Stieve");

test.Person の元のクラス定義には、sayHi は含まれていないため、CheerfulPerson への型キャストをしなければ、上記のコード断片はコンパイルできません。 コンパイルの終了後に glue が適用されるまで test.Person クラス内に、sayHi メソッドは追加されません。

もし追加されたメソッドが static である場合、上で説明した方法は使えません。異なる方法を用いる必要があります。

@Glue class Cheerful2 {
    @Refine("test.Person")
    static class NewPerson {
        public static void sayHi(String toWhom) {
            System.out.println("Hi " + toWhom);
        }
    }

    @Refine("test.Skit")
    static class NewSkit {
        public static void main(String[] args) {
            NewPerson.sayHi("Paul");
        }
    }
}

NewSkit クラスの main メソッドは、NewPerson クラスの static メソッドを呼んでいます。 NewPerson は元のクラスではなく、@Refine クラスです。 しかしながら、この sayHi の呼び出しは、test.Person クラスの sayHi の呼び出しと解釈されます。

@Glue クラスを織り込むために、GluonJ は @Refine クラス内のメソッドを元のクラスにコピーします。 このとき GluonJ は、メソッド定義の中に現れる @Refine クラスの名前を対応する元のクラスの名前で全て置き換えてしまいます。 上の例では、 main メソッドを NewSkit から Skit クラスへコピーするときに、NewPerson が置き換えられて全て test.Person になってしまいます。 なお置き換えの対象となるクラス名は、同一の @Glue クラスに含まれる全ての @Refine クラスの名前です。

元のクラス内で宣言されているフィールドへのアクセス

@Refine クラス内に宣言されるメソッドの中で、元のクラス内のフィールドにアクセスすることができます。例えば、

@Glue class Setter {
    @Refine("test.Person")
    static class Person implements PersonSetter {
        public String name;
        public void setName(String newName) {
            name = newName;
        }
    }
}

public interface PersonSetter {
    void setName(String newName);
}

上記の @Refine クラスは、元のクラスに setName メソッドを追加しています。ここで、@Refine クラス内に宣言された name フィールドは、元のクラスで宣言された name フィールドを表しています。もし @Refine クラスと元のクラスとが同じ名前 (正確には、同じシグネチャ) のフィールドを含んでいれば、GluonJ はそれらのフィールドを同一のものとして扱います。上記の場合、@Refine クラスに従って拡張された test.Person クラスの定義は、以下のようになります。

package test;
public class Person implements PersonSetter {
    public String name;
    public void greet() {
        System.out.println("I'm " + name);
    }
    public void setName(String newName) {
        name = newName;
    }
}

上記の @Refine クラスは、PersonSetter インターフェースを実装しているため、改良された元のクラスもそのインターフェースを実装します。

もし @Refine クラス内で宣言されているフィールドに初期値を宣言した場合、GluonJ は、そのフィールドに対応している元のクラスのフィールドの初期値を上書します。例えば、

@Glue class Init {
    @Refine("test.Person")
    static class Person {
        public String name = "Joe";
    }
}

この glue が適用された後、test.Person クラスの name フィールドの初期値は "Joe" となります。

元のクラス内で宣言されているメソッドへのアクセス

@Refine クラスのメソッドの中で、元のクラスに宣言されているメソッドを呼び出すこともできます。もし @Refine クラス内に、元のクラスのメソッドと同じ名前 (正確には、同じシグネチャ) をもつ abstract メソッドを宣言すれば、その abstract メソッドは元のクラス内のそれに対応するメソッドを表現します。

@Glue class Sociable {
    @Refine("test.Person")
    static abstract class Person implements CheerfulPerson {
        public abstract void greet();
        public void sayHi(String toWhom) {
            System.out.println("Hi " + whom);
            greet();
        }
    }
}

上記の glue で、sayHi メソッドは、元のクラス内で宣言された greet メソッドを呼び出しています。ただし、@Refine クラス内に、greet という名前の abstract メソッドを宣言します。また、@Refine クラス自体も abstract クラスにします。Java では abstract クラスしか、abstract メソッドをもつことができないためです。

もし @Refine クラス内の abstract メソッドを、orig_ で始まる名前で宣言すると、そのメソッドの宣言は特別な意味をもちます。

@Glue class Greeting {
    @Refine("test.Person")
    static abstract class Person {
        public abstract void orig_greet();
        public void greet() {
            System.out.println("Hello");
            orig_greet();
        }
    }
}

この @Refine クラスは、元のクラスの greet メソッドを上書きします。上書きされた greet メソッドは、上書きした greet メソッドによって、orig_greet で呼び出されることができます。abstract メソッド orig_greet は、元のクラスで宣言されている元の (上書きされる前の) greet メソッドを表しているのです。

abstract orig_... メソッドを宣言することで、上書きされた元のメソッドを呼び出すことができました。しかし、上書きされた元の static メソッドを呼び出しには、このテクニックを使うことができません。Java では、abstract メソッドとして static メソッドを宣言できないからです。

static メソッドを上書きするために、GluonJ では異なるテクニックを使う必要があります。例えば、

package test;
public class Greeting {
    public static String say(Person p) {
        return "Hi, " + p.name;
    }
}

この test.Greeting クラスの static メソッド say を、以下で上書きします。

@Glue class Verbose {
    @Refine("test.Greeting")
    static class Diff {
        @Abstract public static String orig_say(Person p) {
            return null;
        }
        public static String say(Person p) {
            return orig_say(p) + ". How are you?";
        }
    }
}

上記の例で、上書きされた元の say メソッドを呼び出すには、orig_say メソッドを呼び出します。p.name"Bill" であるならば、上書きされたメソッドは "Hi, Bill. How are you?" を返します。

static orig_... メソッドの宣言を、@Abstract によって注釈しなければなりません。GluonJ はそのメソッドの中身を無視するので、どんなステートメントを書いても構いません。普通は、そこに null0 を返すような return 文を書きます。もし返り値の型が void であれば、そのときはメソッドの中身を空にすることともできます。

this の利用

@Refine クラスの中で、特殊変数 this を利用したい場合は、その this の型に注意しなければなりません。

import test.Person;
import static javassist.gluonj.Gluon.$refine;

@Glue class UseThis {
    @Refine("test.Person")
    static class Diff {
        public Person self() {
            return (Person)$refine(this);
        }
    }
}

上記の Diff 内で使われる this の型は、test.Person ではありません。そのため、self メソッドは、型キャストをしなければなりません。この目的のために、GluonJ は $refine という名前の static メソッドを提供しています。this をキャストするには、以下の形式で行うべきです。

( <target type> )$refine(this)

@Refine のまとめ

@Refine クラス内のそれぞれのメンバは、以下の規則で、元のクラスにコピーされます。

もし元のクラスに新しいメソッドを追加する場合、その追加するメソッドへアクセスするためのインターフェースも、元のクラスは実装しなければなりません。@Refine クラスが実装しているインターフェースは、元のクラスに追加されます。

@Refine はインターフェースの拡張にも使えます。もしターゲットがインターフェースであれば、@Refine クラスもまたインターフェース型でなければなりません。メソッド追加の規則は以下の通りです。

GluonJ は、@Refine インターフェースが継承しているインターフェースを、元のインターフェースのスーパー・インターフェースのリストに追加します。

4. ポイントカット (Pointcut) とアドバイス (Advice)

@Glue クラスの他のメンバは、ポイントカットとアドバイスです。これは @Before, @After, もしくは @Around によって注釈したフィールドで定義します。AspectJ とは異なり、ポイントカットとアドバイスを分離して記述しません。@Glue クラスには、複数のポイントカット-アドバイスのペアをフィールドとして宣言することができます。

ポイントカットとアドバイスを表しているフィールド (以下、ポイントカット・フィールド と呼びます) は、Pointcut 型でなければなりません。@Glue クラスの中で、 Pointcut 型の複数のフィールドを宣言でき、それらのうちのいくつかを @Before らで注釈することができます。もし Pointcut 型のフィールドを @Before らで注釈しなければ、GluonJ はそれらを pointcut フィールドとして扱いません。

典型的な pointcut フィールドは以下のようになります。

@Before("{ System.out.println(`call!`); }")
Pointcut pc = Pcd.call("test.Hello#say(..)");

ただし、GluonJ は @Before の引数の中で使われているバック・クウォート (`) を、エスケープされたダブル・クウォート (\") として処理します。

上記の宣言は、test.Hellosay メソッドが呼び出されるときに、以下のブロック:

{ System.out.println("call!"); }

が実行されることを指示しています。フィールドの注釈が @Before であるため、say メソッドへ渡されるすべての実引数が評価され、そのメソッドの中身が実行される直前で、ブロックが実行されます。これからは、上記のようなブロックのことを アドバイス・ボディ (advice body) と呼びます。

Pointcut オブジェクトは、アドバイス・ボディが実行されるタイミングを指定します。Pcd クラス (ポイントカット指定子は以下で説明します) は、Pointcut オブジェクトを生成するための、ファクトリ・メソッドの集合を提供しています。call メソッドは、メソッドが呼び出されるタイミングを指定するための Pointcut オブジェクトを返します。callメソッドへ渡している String 型の引数:

test.Hello#say(..)

は、test.Hello 内で宣言された say メソッドの呼び出しを指定しています。クラス名とメソッド名の間は、# で分けます。指定したメソッドの引数に (..) と書くことで、say メソッドの引数の型を明記する必要がなくなります。上記の call の引数により、say(),say(int), say(String,int) などのメソッドの呼び出しを指定することができます。

Note: 上記のポイントカットとアドバイスの例の中で、アドバイス・ボディが @Before の引数として与えられていることに混乱している方もいると思います。実際に、他の AOP システムなどでは、利用者は 以下のように @Before の引数として、ポイントカット式を書きます。

@Before("call(test.Hello#say(..))")
public void advice() {
    System.out.println(`call!`);
}

GluonJ が、このような設計になっているのには理由があります。我々は、アドバイス・ボディは可能な限り短く (他のオブジェクトを呼び出すための 1 文程度に短く) 書くべきだと考えています。@Glue クラスは、横断的関心事 (crosscutting concern) を実装するためのコンポーネントではありません。@Glue クラスは、そのようなコンポーネント (いわゆるアスペクト) と他のコンポーネントとを結ぶ glue (のり、もしくは接着剤) なのです。それゆえ、アドバイス・ボディは、それらのコンポーネントを結びつけるための糊付けに徹するべきです。我々は、このような GluonJ の設計によって、アスペクトの暗黙的なインスタンス生成のための複雑な規則などの困難さから、アプリケーション開発者が解放されるだろうと考えています。

ポイントカット指定子

GluonJ では様々なポイントカットを扱うことができます。以下に、Pcd および Pointcut クラス内に宣言されているファクトリ・メソッドを紹介します。

callget、および set 以外のポイントカットは、通常、それら 3 つのポイントカットの 1 つと一緒に用いられます。

ポイントカットを組み合わせる

.and もしくは .or を利用して、これらのポイントカットを組み合わせ、より細やかなポイントカットを定義することができます。例えば、

Pointcut pc = Pcd.call("test.Hello#say(..)").and.within("test.Main");

上記の Pointcut オブジェクトは、test.Hellosay メソッドが、test.Main 内で宣言されているメソッドから呼び出されるタイミングを指定します。複数の .and.or を、1 つの式の中で使うこともできます。例えば、

Pointcut pc = Pcd.call("test.Hello#say(..)").and.within("test.Main")
                 .or.call("test.Hello#greet());

これは、test.Main クラス内で宣言されているメソッドの中で、say メソッドが呼ばれているタイミング、もしくはすべてのクラス内で greet メソッドが呼ばれているタイミングを指定しています。

.and.or よりも高い優先度をもっています。つまり、GluonJ は .and の計算を、.or の計算よりも先に行います。もしこの優先度の規則を変更したければ、Pcd あるいは Pointcut クラスの expr メソッドを利用する必要があります。このメソッドは括弧の役割を果たします。

Pointcut pc = Pcd.expr(Pcd.call("test.Hello#say(..)").or.call("test.Hello#greet()"))
                 .and.within("test.Main")

このポイントカットは、test.Main クラス内で、say もしくは greet のどちらかが呼ばれるタイミングを指定しています。2 つの call ポイントカットは expr メソッドによって 1 つのグループにまとめられます。

expr メソッドは式の途中で使うこともできます。

Pointcut pc = Pcd.within("test.Main")
                 .and.expr(Pcd.call("test.Hello#say(..)")
                           .or.call("test.Hello#greet()"))

上記のポイントカット宣言 (Pointcut オブジェクト) を 2 つ宣言に分割することも可能です。

Pointcut pc0 = Pcd.call("test.Hello#say(..)").or.call("test.Hello#greet());

@Before("{ System.out.println(`call!`); }")
Pointcut pc = Pcd.expr(pc0).and.within("test.Main");

この方法でも、先と同じ意味をもつ Pointcut オブジェクトを生成することができます。pc0 の宣言には注釈が付けられていないため、GluonJ は変数 pc0 をポイントカット・フィールドとして扱わず、一時的な変数とみなします。

GluonJ は否定を表現するために .not を提供しています。.notPcd または .and.or の後ろに書けます。また .not はもっとも高い優先度を持ちます。 例えば

Pointcut pc = Pcd.call("test.Hello#say(..)").and.not.within("test.Main");
Pointcut pc2 = Pcd.not.within("test.Main").and.call("test.Hello#say(..)");

これら 2 つのポイントカットは共に test.Main 以外のクラスで宣言されているメソッドから say メソッドが呼び出されるタイミングを指定します。

パターン

call メソッドは、そのメソッドの引数としてメソッド・パターン (methodPattern) を受け取ります。メソッド・パターンは、クラス名、メソッド名、そして引数の型を連結した文字列です。クラス名とメソッド名は # によって分離されます。例えば、

test.Point#move(int, int)

は、test.Point クラス内で宣言された move メソッドを表しています。さらに、その move メソッドは、2 つの int 型の引数をとることを表しています。クラスの名前は、パッケージ名を含めた完全修飾名でなければなりません。クラス名、メソッド名には、ワイルドカード * が利用できます。例えば、test.* は、test パッケージの任意のクラスを意味します。引数の型のリストには、ワイルドカード * を含めることはできません。すべての型を表すために、(..) を使うことができます。

メソッド・パターン中に書かれるメソッド名が new であれば、それはコンストラクタを表現します。例えば、

test.Point#new(int, int)

このパターンは、2 つの int 型の引数をとる test.Point コンストラクタを表しています。

Pcd クラスの get メソッドと set メソッドは、その引数としてフィールド・パターン (fieldPattern) を受け取ります。このフィールド・パターンはメソッド・パターンによく似ており、クラス名とフィールド名を連結した文字列です。例えば、

test.Point#xpos

は、test.Point クラス内で宣言された xpos フィールドを表現しています。クラス名とメソッド名ともに、ワイルドカード * を利用することができます。

最後に within メソッドは、クラス・パターン (classPattern) を引数にとります。これはクラスの完全修飾名です。このパターンでもワイルドカード * を利用することができます。また annotate メソッドは、アノテーション・パターン (annotationPattern) を引数にとります。これは例えば @test.Change のような @ で始まる (始まらなくてもかまいません) アノテーション名の完全修飾名です。ワイルドカード * を利用することもできます。

アドバイス・ボディ

GluonJ の利用者は、ポイントカット・フィールドの宣言を、@Before, @After, もしくは @Around のどれかによって注釈しなければなりません。もし @Before を利用すれば、ポイントカット・フィールドによって指定されたタイミングの直前で、GluonJ はアドバイス・ボディを実行します。もし @After であれば、ポイントカット・フィールドによって指定されたタイミングの直後に、アドバイス・ボディを実行します。例えば、ポイントカット・フィールドがメソッドを呼び出すタイミングを指定していたとすると、GluonJ はアドバイス・ボディを、その呼ばれたメソッドのボディの return 文の直後で実行します。

@Around は特別な注釈です。もし @Around が使われると、GluonJ は、その @Around によって注釈されたポイントカット・フィールドが指定したコードの断片の代わりに、与えられたアドバイス・ボディを実行します。例えば、

@Around("{ System.out.println(`call!`); }")
Pointcut pc = Pcd.call("test.Hello#say(..)")
                 .and.within("test.Main");

もし test.Main クラス内のメソッドが say メソッドを呼び出すとき、GluonJ はそのメソッドの呼び出しをインターセプトします。そして、say メソッドのボディを実行せず、代わりに @Around で与えられたアドバイス・ボディを実行します。言い換えると、test.Main クラスのメソッド内では、say メソッドの呼び出しの代わりに、アドバイス・ボディを実行します。

実行時コンテキスト

アドバイス・ボディは {} で囲まれたブロック、もしくはセミコロン ; で終了する単一のステートメントであり、利用者はそれを Java の文法で記述できます。しかし、アドバイス・ボディ内では、いくつかの特殊変数を利用することができます。

$proceed

@Around の引数で与えられるアドバイス・ボディ内では、特殊な式 $proceed(...) を利用することができます。GluonJ は @Around アドバイス・ボディを、ポイントカット・フィールドによって指定された元の計算の代わりに実行するものですが、この $proceed(...) で元の指定された計算を呼び出すことができます。

@Around("{ System.out.println(`call!`); $_ = $proceed($$); }")
Pointcut pc = Pcd.call("test.Hello#say(..)");

まず、このアドバイス・ボディは、メッセージを出力します。そして、ポイントカット・フィールドとして指定された test.Hellosay メソッドを呼び出します。GlonJ は $proceed をメソッドの名前として使います。もし $proceed メソッドが呼ばれると、GluonJ は元の指定された計算である say メソッドを呼び出します。$$ は元の計算である say に渡される実引数を表しています。

$_ はアドバイス・ボディの計算結果として格納される値を表す特殊変数です。@Around はポイントカット・フィールドによって指定された元の計算を置き換えます。そのため、アドバイス・ボディの計算が終了する前に、利用者は充当する値を $_ へセットしなければなりません。もしポイントカット・フィールドによって指定された元の計算の返り値の型が、void であるならば、GluonJ は $_ に格納された値を無視します。それ以外の場合、$_ に格納された値を、アドバイス・ボディの結果として利用します。

例えば、say メソッドは String 型の値を返すとします。

String s = hello.say();

もしそのポイントカット・フィールド:

@Around("{ $_ = "OK"; }")
Pointcut pc = Pcd.call("test.Hello#say(..)");

が、say メソッドの呼び出しを指定しているのであれば、GluonJ はその $_ に格納された値 (すなわち "OK") を、変数 s に代入します。これは、say メソッドの呼び出しの代わりに、アドバイス・ボディを実行しているためです。

$proceed, $$, そして $_ のもつ意味は、元の指定される計算の種類 (メソッド呼び出しやフィールド・アクセス) によって変化します。しかし、以下のステートメント:

$_ = $proceed($$);

は、元の計算が何であれ、元の計算を表します。

もし元の計算がメソッド呼び出しであれば、$proceed はそのメソッド呼び出しを実行します。$proceed は元のメソッド呼び出しと同じ引数を受け取り、同じ型の値を返します。$$ は元の実引数を表します。また $_ の型は、元のメソッドの返り値の型です。さらに、GluonJ は $_ に格納された値を、アドバイス・ボディの結果として利用します。

もし元の計算がフィールドの読み込みであれば、$proceed はその命令を実行します。フィールドの読み込みには引数を必要としないため、$$ は空のリストです。また、$proceed はそのフィールドの値を返します。それゆえ、$_ の型はフィールドの型と同じです。そして、$_ に格納された値は、アクセスされたフィールド値に代わり、フィールド・アクセスの結果として使われます。

もし元の計算がフィールドへの書き込みであれば、$proceed はそのフィールドの値を変更します。フィールドへの書き込みは、引数として書き込む値が必要ですので、$$ は元のフィールドへの書き込み計算が代入しようとしている値を表します。また、フィールドへの書き込みの計算は、値を返しません (つまり void です)。それゆえ、変数 $_ は意味をもちません。正確には、$_ を利用することはできますが、GluonJ は $_ に格納された値を無視します。

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

$$ は元の計算を行う際の、実引数のリストを表現していますが、実引数の個々の値もまた特殊変数を用いてアクセスすることができます。もし元の計算がメソッド呼び出しであれば、$1 は 1 番目の実引数の値を、$2 は 2 番目の実引数の値を、$n は n 番目の実引数の値を表す変数になります。$0 は、メソッドが呼び出しのターゲットオブジェクトを表現する変数です。元の計算の呼び出し元を表現する特殊変数 this も使うことができます。

GluonJ は $1, $2, ... に代入された値を、$$ に反映します。例えば、

@Around("{ $1 = "Joe"; $_ = $proceed($$); }")
Pointcut pc = Pcd.call("test.Hello#sayHi(String)");

このアドバイス・ボディが実行された後、特殊変数 $_ には、第 1 引数の値が "Joe" で呼び出された sayHi メソッドの返り値が格納されます。それゆえ、上記のアドバイス・ボディは、以下のブロックと同じ意味をもちます。

@Around("{ $_ = $proceed("Joe"); }")
Pointcut pc = Pcd.call("test.Hello#sayHi(String)");

$0, $1, $2, ... は、@Before もしくは @After によって与えられたアドバイス・ボディ内でも有効な特殊変数です。

エイリアス

特殊変数のエイリアスを定義することができます。例えば、

@Before("{ System.out.println(msg + ` ` + callee); }")
Pointcut pc = Pcd.define("msg", "$1").define("callee", "$0")
                 .call("test.Hello#sayHi(String)");

define メソッドは、エイリアスを定義しています。define メソッドは、Pcd. の後、もしくは他の define メソッドの呼び出しの後で、呼び出す必要があります。上記のケースでは、2 つのエイリアスを定義しています。1 つ目は、$1 のエイリアスである msg です。2 つ目は、$0 のエイリアスである callee です。このように定義された 2 つのエイリアスは、@Before のアドバイス・ボディ内で利用することができます。エイリアスを利用することで、アドバイス・ボディをより直感的に定義することができるようになります。

ロギング・アスペクト

ロギング・アスペクトは、AOP 言語におけるハローワールドです。 本節では、ロギング・アスペクトの例を紹介します。

ロギング・アスペクトを書くのは、他の AOP 言語で書くのと同じくらい簡単です。例えば下記の @Glue クラスを使うと、 demo.BankTest クラスの testTransfer メソッドが demo.Bank クラスの transfer メソッドを呼ぶ直前にログ・メッセージが出力されます。demo.BankTest クラスと demo.Bank クラスの定義は既に 1 章で示しました。

package demo;

import javassist.gluonj.*;

@Glue public class Logging {
    @Before("{ System.out.println(`transfer: ` + $3 + ` ` + balance); }")
    Pointcut pc = Pcd.call("demo.Bank#transfer(..)")
                     .and.within("demo.BankTest#testTransfer(..)");
}

call("demo.Bank#transfer(..)")transfer メソッドの呼び出しを指定します。 一方、within("demo.BankTest#testTransfer(..)") は、 call によって選ばれたメソッド呼び出しのうち、testTransfer メソッドから呼ばれた場合だけを残します。したがって、ログ・メッセージは transfer メソッドが testTransfer メソッドの中から呼ばれる直前にだけ出力されます。

上のアドバイス・ボディは $3 の値と balance の値を出力します。$3 は、呼ばれた transfer メソッドの第 3 引数を表します。balance は、呼ぶ側のメソッドである testTransfer メソッドの局所変数 です。 アドバイス・ボディは、呼ぶ側のメソッドの文脈で実行されるので、呼ぶ側のメソッド (すなわち within で指定された testTransfer メソッド) 内で有効な局所変数やフィールドの値にアクセスすることができます。 上の @Glue クラスが織り込まれた BankTest クラスは、以下の普通の Java プログラムとほぼ等価です。

public class BankTest extends TestCase {
    public void testTransfer() {
        Account mine = new Account();
        Account yours = new Account();
        BigDecimal pay = new BigDecimal(20000);
        BigDecimal balance = new BigDecimal(10000);
        Bank bank = new Bank();
        System.out.println("transfer: " + pay + " " + balance);
        assertEquals(balance, bank.transfer(yours, mine, pay));
    }
}


Copyright (C) 2006 by Shigeru Chiba and Muga Nishizawa. All rights reserved.