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 {
    protected 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 static class Account extends Account {
        protected 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 static class Afternoon extends Hello {
        public String header() {
            return "Good afternoon, ";
        }
    }
}

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

package test;

public class Hello {
    public 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 クラスを利用する織り込み

実行前の織り込みとロード時の織り込みのどちらの場合でも、指定できる @Glue クラスは 1 つです。そのような例はすでに上で述べました。もし複数の @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 static class Person extends test.Person {
        public String message = "Hello";
        public void greet() {
            System.out.println(message);
        }
    }
}

この glue クラス (@Glue クラス) は、Person という名前の static な入れ子クラスを含んでいます。この Person@Refine により注釈されています。このように @Refine によって注釈されたクラスを @Refine クラスと呼びます。なお static な入れ子クラスだけが @Refine クラスになれます。インナークラス (static でない入れ子クラス) は @Refine クラスになれません。

この Diff という名前の入れ子になった @Refine クラスは、test.Person の普通のサブクラスであるように見えます。しかしながら、@Refine で注釈されているため、このクラスは Person クラスの定義を直接修正します。まず、Person クラスへ新しいフィールド message を追加します。さらに、元のクラスで宣言されている greet メソッドを、@Refine クラスの greet メソッドに置き換えます (つまり、元のクラスで宣言された greet メソッドは上書きされます)。修正の対象となるクラス (元クラス) は、@Refine クラスの extends で指定されます。 @Refine クラスの名前付けに規則はありません。Person2Expand などといった他の名前でも大丈夫です。

サブクラスの定義は、親クラスを拡張した新しいクラスを作り出しますが、作り出されたクラスと元のクラスは共存します。一方、@Refine は親クラスを拡張したクラスを作り出すものの、作り出したクラスで元の親クラスを置き換えてしまいます。作り出されたクラスが元の親クラスを上書きしてしまうのです。

上記の @Glue クラス SayHello を織り込むと、Person クラスの定義は、@Refine クラスによって以下のように変更されます。

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

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

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

@Glue class SayHello {
    @Refine Diff refines;
}

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

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

新しいメソッドの追加

@Refine クラスは元クラスにフィールドと同様、新しいメソッドを追加することができます。

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

以下の @Refine クラスは test.Person クラスに sayHi メソッドを追加します。

package test;

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

    public static class Test {
        public static void main(String[] args) {
            Person p = new Person();
            ((Diff)p).sayHi("Paul");
        }
    }
}

Person オブジェクトの sayHi メソッドを呼び出すためには、そのオブジェクトの型を Person から Diff へキャストしなければなりません。test.Cheerful.Test クラスの main メソッドに注目してください。@Glue クラス Cheerful をコンパイルし Person クラスへ織り込むまで、sayHi メソッドは Person クラスに追加されません。ソースコード・レベルでは、sayHiDiff クラスに宣言されているメソッドなのです。@Glue クラスが織り込まれた後、@Refine クラスの名前は元クラスと同じ名前として扱われます。

@Refine クラスのメソッド内で、元クラス (つまり親クラス) のメソッドを呼び出すことができます。

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

        public void greet() {
            super.greet();
            System.out.println("How are you?");
        }
    }
}

上記の greet メソッド内の super.greet() は、Person クラスにあらかじめ宣言されている greet メソッドを呼び出します。一方、sayHi メソッド内の greet() は、Diff クラスで宣言された greet メソッドを呼び出します。

同一クラスを改良する @Refine クラスが複数あった場合、それぞれの @Refine クラスは上述した優先順序で織り込まれ、その対象クラスを修正します。super の呼び出しはこのセマンティクスを反映します。例えば、

package test;

@Glue class Greeting1 {
    @Refine static class Hi extends Person {
        public void greet() {
            System.out.println("Hi!");
            super.greet();
        }
    }
}

@Glue class Greeting2 {
    @Refine static class Wow extends Person {
        public void greet() {
            System.out.println("Wow!");
            super.greet();
        }
    }
}

@Glue class Greeting {
    @Include Greeting1 glue1;
    @Include Greeting2 glue2;
}

上記の Greeting が織り込まれると、2 つの @Refine クラス HiWow は、この順に Person クラスへ織り込まれ、Person を改良します。まず、Hi が元の Person クラスの greet メソッドを修正します。そして次に、WowHi によって変更された greet メソッドを修正します。それゆえ、Wow 内の super.greet() は、Hi が拡張した greet メソッドを呼び出します。結果的に Person クラスの greet メソッドの振る舞いは、以下のメソッド宣言と同じようになります。

public void greet() {
    System.out.println("Wow!");
    System.out.println("Hi!");
    System.out.println("I'm " + name);
}

static メソッドの上書き

Java の正規のクラスと異なり、@Refine クラスは親クラス (元クラス) に宣言されている static メソッドを上書きすることができます。例えば、

package test;
public class Recursive {
    public static int factorial(int n) {
      if (n == 1)
          return 1;
      else
          return n * factrial(n - 1);
    }
}

以下の @Refine クラス Iterative は、上記の Recursive クラスに元々宣言されている factorial メソッドを上書きします。

package test;
@Glue class Iterative {
    @Refine static class Diff extends Recursive {
        public static int factorial(int n) {
            int f = 1;
            while (n > 1)
                f *= n--;
            return f;
        }
    }
}

この @Glue クラスが Recursive クラスに織り込まれると、Recursivefactorial メソッドの中身は、@Refine クラス Diff 内で記述されている factorial の実装に上書きされます。Recursive.factorial メソッドの呼び出しは、新しい実装を使って与えられた数字の階乗を計算します。

@Refine クラスに宣言されている static メソッド内では、その static メソッドによって上書きされるメソッドを呼び出すことができます。

package test;
@Glue class Terminator {
    @Refine static class Diff2 extends Recursive {
        public static int factorial(int n) {
            if (n >= 1)
                return Recursive.factorial(n);    // like a call on super.
            else
                return 0;
        }
    }
}

上記の @Refine クラス Diff2 内の Recursive.factorial メソッドの呼び出しは、factorial の元の実装を呼び出します。呼び出し元が Diff2 ではなければ、Recursive.factorial メソッドの呼び出しは、元クラスに定義された実装ではなく、@Refine クラスの実装を呼び出すことに注意してください。このセマンティクスは、super を使ったメソッド呼び出しのセマンティクスから取り入れられたものです。super を使うと、親クラスで宣言されている上書きされたメソッドを呼び出せるのと、同じことです。

インターフェースとフィールド

@Refine クラスは、元クラスにフィールドやインターフェースを追加することができます。

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

下記の @Refine クラスは test.Person に 3 つの新しいメンバーを追加しています。who メソッドと Nameable インターフェース、そして counter フィールドです。

package test;

interface Nameable {
    void who();
}

@Glue class WhoAreYou {
    @Refine static class Diff extends Person implements Nameable {
        public int counter = 0;
        public String name = "Anonym";
        public void who() {
            counter++;
            greet();
        }
    }
}

@Refine クラス test.WhoAreYoutest.Person へ織り込まれると、Person のインターフェースのリストに test.Nameable インターフェースが追加されます。Nameable インターフェースが持っている who メソッドも、この @Refine クラスによって Person クラスに追加されます。

counter フィールドもまた、Person クラスに追加されます。このフィールドの初期値は 0 です。一方、name フィールドは Person には追加されません。これは Person クラスがそのフィールドと同じ名前のフィールドをすでに持っているからです。@Refine クラス WhoAreYou は、元の name フィールドを上書きします。上述した @Glue クラスを織り込むと、Person クラスの name フィールドの初期値には "Anonym" がセットされます。現在の GluonJ の実装では、Person のコンストラクタの実行の直後に、そのフィールドの新しい初期値が代入されます。コンストラクタの実行中、フィールドには元の初期値が入っています。this() を使って他のコンストラクタを呼び出しているコンストラクタがあったとき、呼び出された側のコンストラクタ実行の直後にのみ、新しい初期値がフィールドに代入されます。

元クラス (親クラス) で定義されたフィールドを上書きしている @Refine クラスは、その初期値だけではなく注釈も元フィールドに追加することができます。例えば、下記の @Refine クラスは、test.Person クラスに定義されている name フィールドに注釈 @Setter を追加します。

package test;

public @inerface Setter {}

@Glue class Annotater {
    @Refine static class Diff extends Person {
        @Setter public String name;
    }
}

GluonJ は、クラスを改良する機能 (@Refine クラス) だけではなく、型を改良する機能も提供しています。@Refine インターフェースは、元インターフェース (親インターフェース) に新しいメソッドや親インターフェースを追加することができます。例えば、以下の @Refine インターフェースは新しい親インターフェースを追加します。

package test;

@Glue class Annotater {
    @Refine static interface Diff extends Nameable, Cloneable {
    }
}

この @Refine インターフェースは Nameable インターフェースを変更します。Diff インターフェースの第 2 親インターフェースである Cloneable は、Nameable に親インターフェースとして追加されます。@Refine インタフェースの場合、extends に続く最初のインタフェースが修正対象となります。したがって第 2、第 3 インタフェースが親インタフェースとして第 1 インタフェースに追加されます。

@Refine クラスのコンストラクタ

@Refine クラスは Java の正規のクラスではないので、@Refine クラスの定義には以下のような制約があります。

もし修正の対象クラスが引数のないデフォルト・コンストラクタを持たなければ、その @Refine クラスのコンストラクタ内で、対象クラス (つまり親クラス) のデフォルトではないコンストラクタを呼ばなければなりません。しかし、その @Refine クラスを織り込んだとき、このコンストラクタ呼び出しは無視されます。例えば、

package test;

public class Counter {
    private int counter
    public Counter(int c) { counter = c; }
    public void decrement() {
        if (--counter <= 0)
            throw new RuntimeException("Bang!");
    }
}

@Glue class Increment {
    @Refine static Diff extends Counter {
        private int delta;
        public Diff() {
            super(0);
            delta = 1;
        }
        public void increment() {
            counter += delta;
        }
    }
}

上記の @Refine クラス Diff を織り込むと、Diff が持つコンストラクタの中身が、Counter のコンストラクタにコピーされます。しかし、その親クラスのコンストラクタへの呼び出しはコピーされません。例えば、Diff のコンストラクタ内の super(0) は、織り込み後 Counter のコンストラクタにコピーされません。@Refine クラスによって拡張された Counter のコンストラクタは、以下のようになります。

public Counter(int c) {
    counter = c;
    delta = 1;          // copied from the @Refine class
}

元クラスの private メンバーへのアクセス

@Privileged で注釈されているクラスは、元クラスの private メソッドや private フィールドにアクセスできます。この機能は、主にロギングやトレースを行うために提供されているものであり、それ以外の利用用途では、可能な限りこの機能を使わないようにするべきです。この機能は元クラスのカプセル化を破壊してしまうおそれがあるからです。

元クラスに宣言されている private フィールドと同じ名前の private フィールドを @Refine クラスがもつとき、@Refine クラスのそのフィールドは、元クラスに宣言されているフィールドとみなされます。そのフィールドへのアクセスは、元クラスのフィールドへのアクセスになります。

package test;

public class Person {
    private String name;
    public void setName(String newName) { name = newName; }
    private String getName() { return name; }
    public String whoAreYou() { return "I'm " + getName(); }
}
package test;

@Glue class PersonExtender {
    @Refine @Privileged
    static class Diff extends Person {
        private String name;
        public String whoAreYou() { return "My name is " + name; }
    }
}

上記の例では、Diff クラスの name フィールドは、元クラス Person に宣言されている name フィールドとみなされます。それゆえ、whoAreYou メソッド内の name フィールドへのアクセスは、Personname フィールドへのアクセスになります。例えば、上記の @Glue クラスと Person クラスを織り込むと、

Person p = new Person();
p.setName("Bill");
String s = p.whoAreYou();

変数 s の値は、"My name is Bill" となります。"I'm Bill""My name is null" ではありません。

また以下のようにして @Refine クラスは、元クラスに宣言されている private フィールドの初期値を変更できます。

package test;

@Glue class PersonExtender2 {
    @Refine @Privileged
    static class Diff2 extends Person {
        private String name = "Unknown";
    }
}

@Refine クラス Diff2 は、Person で宣言されている name フィールドの初期値を変更します。変更された値は、"Unknown" です。

@Privileged で注釈された @Refine クラスは、元クラスの private メソッドを上書きすることもできます。

package test;

@Glue class PersonExtender3 {
    @Refine @Privileged
    static class Diff3 extends Person {
        private String name;
        private String getName() {
            return name.toUpperCase();
        }
    }
}

上記の @Glue クラスと Person を織り込むと、 PersonwhoAreYou メソッドは、@Refine クラス Diff3 で実装された getName メソッドを呼び出します。

@Refine クラスが @Privileged で注釈されている場合、その @Refine クラスで宣言されたメソッドの中から、元クラスの private メソッドを呼び出すこともできます。

package test;

@Glue class PersonExtender4 {
    @Refine @Privileged
    static abstract class Diff4 extends Person {
        abstract String super_getName();
        public String whoAreYou() {
            return "My name is " + super_getName();
        }
    }
}

Person クラスの private メソッド getName を呼び出すためには、その @Refine クラス内で super_getName という名前の抽象 (abstract) メソッドを宣言する必要があります。宣言するメソッドは、super_ から始まる名前でなければなりません。super_ の後には、元クラスの private メソッドの名前が続きます。元クラス Person@Glue クラスを織り込む際に、super_getName メソッドの呼び出しは、Person であらかじめ宣言されていた getName メソッドの呼び出しに変換されます。

また、以下のようにして @Refine クラスのメソッドの中から、そのメソッドによって上書きされる元クラスの private メソッドを呼び出せます。

package test;

@Glue class PersonExtender5 {
    @Refine @Privileged
    static abstract class Diff5 extends Person {
        abstract String super_getName();
        private String getName() {
            return super_getName().toUpperCase();
        }
    }
}

@Refine クラスの getName メソッドは、元クラス Person の private メソッド getName を上書きし、内部で super_getName メソッドを呼び出しています。super_getName は同じ @Refine クラスで宣言されているメソッドです。 Person クラスに上記の @Glue クラスを織り込むと、この super_getName メソッドの呼び出しは、Person クラスに実装された元の getName メソッドへの呼び出しに変換されます。

@Refine のまとめ

@Refine クラスを使った元クラスの改良 (refinement) のセマンティクスは、サブクラス手法を使ったクラスの拡張のセマンティクスとほとんど同じです。以下に、例外を示します。

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.