Introduction to GluonJ
千葉 滋 (訳: 西澤 無我)
Table of Contents
1. なぜ GluonJ が必要か?
2. Glue クラスを定義する
3. クラスの改良 (Refinement) その 1
4. クラスの改良 (Refinement) その 2
5. ポイントカット (Pointcut) とアドバイス (Advice)
6. Glue クラスの拡張
5. ポイントカット (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.Hello の say メソッドが呼び出されるときに、以下のブロック:
{ 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 クラス内に宣言されているファクトリ・メソッドを紹介します。
Pointcut call(String methodPattern)
methodPatternによって指定されるメソッド、もしくはコンストラクタが呼ばれるとき。Pointcut get(String fieldPattern)
fieldPatternによって指定されるフィールドの値を読み込むとき。Pointcut set(String fieldPattern)
fieldPatternによって指定されるフィールドの値を書き込むとき。Pointcut within(String classPattern)
classPatternによって指定されるクラス内で、宣言されているメソッドが実行されている間。Pointcut within(String methodPattern)
methodPatternによって指定されるメソッドが実行されている間。Pointcut annotate(String annotationPattern)
annotationPatternによって指定されるアノテーションつきのメソッドまたはフィールドがアクセスされている間。Pointcut when(String javaExpression)
javaExpressionがtrueであるとき。これは AspectJ のifポイントカット指定子に相当します。Pointcut cflow(String methodPattern)
methodPatternによって指定されるメソッドが実行されている間。
call、get、および set
以外のポイントカットは、通常、それら 3 つのポイントカットの 1 つと一緒に用いられます。
ポイントカットを組み合わせる
.and もしくは .or を利用して、これらのポイントカットを組み合わせ、より細やかなポイントカットを定義することができます。例えば、
Pointcut pc = Pcd.call("test.Hello#say(..)").and.within("test.Main");
上記の Pointcut オブジェクトは、test.Hello の say メソッドが、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 を提供しています。.not
は Pcd または .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 文の直後で実行します。
@Before(adviceBody)
この@Beforeで注釈されたポイントカット・フィールドによって指定されたタイミングの直前で、GluonJ はアドバイス・ボディを実行します。@After(adviceBody)
この@Afterで注釈されたポイントカット・フィールドによって指定されたタイミングの直後で、GluonJ はアドバイス・ボディを実行します。@Around(adviceBody)
この@Aroundで注釈されたポイントカット・フィールドによって指定された計算の代わりに、GluonJ はアドバイス・ボディを実行します。
@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.Hello の say メソッドを呼び出します。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 は、メソッドが呼び出しのターゲットオブジェクトを表現する変数です。したがって $0.getClass()
はターゲットオブジェクトの型を返します。呼ばれたメソッドの名前は $name
で得られます。
また、元の計算の呼び出し元を表現する特殊変数 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));
}
}
6. Glue クラスの拡張
GluonJ では @Glue のサブクラスを作ることも可能です。
織り込まれた @Glue クラスがサブクラスである場合、そのスーパークラスも一緒に織り込まれます。
織り込みの際には、まずスーパークラスが最初に織り込まれ、その次にサブクラスに
@Include で含まれている
@Glue クラスが順に織り込まれます。
サブクラスである @Glue クラスが織り込まれるのは最後です。
それぞれの @Glue クラスは 1 回しか織り込まれません。
例えばもしスーパークラスである @Glue クラスが、@Incude
でサブクラスに含まれている @Glue クラスでもある場合、@Incude による織り込みは実行されません。
@Incude は無視されます。
Glue クラスのサブクラスの中では、スーパークラス中で宣言されている
@Refine クラスのサブクラスを宣言することができます。
例えば、今、次のようなクラスがあるとします。
package test;
public class Person {
public String name;
public void greet() {
System.out.println("I'm " + name);
}
}
package test;
import javassist.gluonj.*;
@Glue class AbstractSayHello {
@Refine static abstract class Diff extends Person {
protected abstract String getMessage();
public void greet() {
System.out.print(getMessage());
super.greet();
}
}
}
すると、次のような AbstractSayHello のサブクラスを定義することができます。
package test;
import javassist.gluonj.*;
@Glue class SayHello extends AbstractSayHello {
@Refine static class Diff2 extends Diff {
protected String getMessage() {
return "Hi, ";
}
}
}
@Refine クラス Diff2 は、Diff のサブクラスです。
Diff は、AbstractSayHello クラスの中で宣言された
@Refine クラスです。
@Refine クラス Diff2 の元クラスは、Person
となります。
もし @Refine クラスのスーパークラスが @Refine クラスである場合、サブクラスである @Refine クラスの元クラスは、スーパークラスである @Refine クラスの元クラスと同じになります。
スーパークラスである @Refine クラスは、サブクラスより先に元クラスに対して適用されます。
上の例でも、Diff クラスは
Diff2 より先に Person に適用されるので、まず
abstract メソッドである getMessage
が Person クラスに追加されます。
次に Diff2 で宣言されている getMessage
メソッドが上書きして追加されます。
したがって、織り込み後の Person クラスで宣言されている
getMessage メソッドは、呼ばれると "Hi, " を返します。
もし @Refine クラスのスーパークラスが @Refine
クラスであり、さらにそのスーパークラスが @Super
で注釈されたメソッドを宣言している場合、サブクラスである @Refine
クラスの中からも、その @Super つきのメソッドを呼ぶことができます。
package test;
import javassist.gluonj.*;
public class Person {
private String getName() { return "Steve"; }
public void speak() {
System.out.println("I'm " + getName());
}
}
package test;
import javassist.gluonj.*;
@Glue class LastName {
@Privileged @Refine static abstract class Diff extends Person {
@Super("getName") abstract String super_getName();
private String getName() { return super_getName() + " Jobs"; }
}
}
package test;
@Glue class Mr extends LastName {
@Privileged @Refine static abstract class Diff2 extends Diff {
private String getName() { return "Mr. " + super_getName() }
}
}
Diff2 クラスの getName メソッドは、そのスーパークラスである Diff クラスで宣言されている
super_getName メソッドを呼んでいます。
super_getName メソッドは、@Super で注釈されているので、実際には元クラスの getName をさします。
ここで super_getName がさす getName
メソッドの本体は、Diff クラスで宣言されている
getName メソッドの本体です。
したがって全ての @Refine クラスが織り込まれた後に
Person クラスの speak メソッドを呼ぶと、seak メソッドは "I'm Mr. Steve Jobs" を返します。
"I'm Mr. Steve" を返すことはありません。
@Super で注釈されたメソッドは、元クラスのメソッドをさしますが、そのメソッドの本体は、スーパクラスである @Refine クラスをすべて適用した後の結果のメソッド本体となります。
上の例でも、super_getName は、Diff2
から呼ばれた場合、Diff2 クラスのスーパークラスである
Diff 適用後の
Person クラスの getName メソッドをさすことになります。
Copyright (C) 2006-2007 by Shigeru Chiba and Muga Nishizawa. All rights reserved.