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.