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.Logging
と test.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
クラスの名前付けに規則はありません。P
や Expand
などといった他の名前でも大丈夫です。
このネストした @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 はそのメソッドの中身を無視するので、どんなステートメントを書いても構いません。普通は、そこに null
や 0
を返すような 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
クラス内のそれぞれのメンバは、以下の規則で、元のクラスにコピーされます。
- GluonJ は
@Refine
クラス内で宣言されるフィールドを元クラスに追加する。
(もしそのフィールドの名前と同じ名前のフィールドが、元のクラスまたはそのスーパークラス内に宣言されていない場合)元のクラス内のそのフィールドとして扱う。
(もし同じ名前のフィールドが、元のクラス内に宣言されている場合)そのフィールドに対応する元のクラス内のフィールドの初期値を上書きします。
(もしそのフィールドの初期値が、@Refine
クラス内で宣言されている場合)
(なお、新しいフィールドの初期値はコンストラクタ終了直前に代入されるので、コンストラクタ実行中は古い初期値の値が使われるかもしれません)そのフィールドのアノテーションは、対応する元のクラス内のフィールドに追加されます。
- GluonJ は
@Refine
クラス内で宣言されるメソッドを元のクラスに追加する。
(もしそのメソッドの名前と同じ名前のメソッドが、元のクラス内に宣言されていない場合)インターフェースを経由して、そのメソッドを呼び出すことができます。そのインターフェースも
@Refine
クラスによって追加しなければなりません。もしメソッドの宣言が
abstract
であり、その名前がorig_XXX
のようにorig_
から始まるものであるならば、元のクラスのXXX
という名前のメソッドを表現します。
元クラス内で定義されたメソッドとして扱います。
(もしそれらが同じ名前、同じシグネチャであった場合)元のクラスのメソッドが
abstract
でなければ、@Refine
クラスのメソッドは、元のクラス内に宣言されている同名のメソッドを上書きします。それ以外、つまり
@Refine
クラスのメソッドがabstract
であった場合、それは元のクラスのメソッドになります。
もし元のクラスに新しいメソッドを追加する場合、その追加するメソッドへアクセスするためのインターフェースも、元のクラスは実装しなければなりません。@Refine
クラスが実装しているインターフェースは、元のクラスに追加されます。
- GluonJ は
@Refine
クラスが実装しているインタフェースを(もしまだ未実装なら) 元のクラスにも実装させる。なお
@Refine
が継承しているスーパークラスは、元のクラスのスーパークラスにはならない。
@Refine
はインターフェースの拡張にも使えます。もしターゲットがインターフェースであれば、@Refine
クラスもまたインターフェース型でなければなりません。メソッド追加の規則は以下の通りです。
-
GluonJ は
@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.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
は、メソッドが呼び出しのターゲットオブジェクトを表現する変数です。元の計算の呼び出し元を表現する特殊変数 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.