Introduction to GluonJ
千葉 滋 (訳: 西澤 無我)
Table of Contents
1. なぜ GluonJ が必要か?
2. Glue クラスを定義する
3. クラスの改良 (Refinement)
4. ポイントカット (Pointcut) とアドバイス (Advice)
5. Glue クラスの拡張
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.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 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
クラスの名前付けに規則はありません。Person2
や Expand
などといった他の名前でも大丈夫です。
サブクラスの定義は、親クラスを拡張した新しいクラスを作り出しますが、作り出されたクラスと元のクラスは共存します。一方、@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
クラスに追加されません。ソースコード・レベルでは、sayHi
は Diff
クラスに宣言されているメソッドなのです。@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
クラス Hi
と Wow
は、この順に Person
クラスへ織り込まれ、Person
を改良します。まず、Hi
が元の Person
クラスの greet
メソッドを修正します。そして次に、Wow
が Hi
によって変更された 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
クラスに織り込まれると、Recursive
の factorial
メソッドの中身は、@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; @Super public String name = "Anonym"; public void who() { counter++; greet(); } } }
@Refine
クラス test.WhoAreYou
が test.Person
へ織り込まれると、Person
のインターフェースのリストに test.Nameable
インターフェースが追加されます。Nameable
インターフェースが持っている who
メソッドも、この @Refine
クラスによって Person
クラスに追加されます。
counter
フィールドもまた、Person
クラスに追加されます。このフィールドの初期値は 0
です。一方、name
フィールドは Person
には追加されません。これは Person
クラスがそのフィールドと同じ名前のフィールドをすでに持っているからです。@Refine
クラス WhoAreYou
は、元の name
フィールドを上書きします。@Super
は、フィールドが元のものを上書きすることを意味します。
Note:
@Super
は省略可能です。しかし、もし@Super
と注釈されているフィールドが、誤字などの理由で元のフィールドを上書きしていない場合は、エラーが表示されます。
上述した @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; } }
ここで name フィールドに @Super
が注釈されていないことに注意してください。もし @Super
が注釈されていると、@Super
も test.Person
クラスの 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
クラスのインスタンスを生成できない。@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 { @Privileged @Refine static class Diff extends Person { @Super private String name; public String whoAreYou() { return "My name is " + name; } } }
上記の例では、Diff
クラスの name
フィールドは、元クラス Person
に宣言されている name
フィールドとみなされます。それゆえ、whoAreYou
メソッド内の name
フィールドへのアクセスは、Person
の name
フィールドへのアクセスになります。例えば、上記の @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 { @Privileged @Refine static class Diff2 extends Person { @Super private String name = "Unknown"; } }
@Refine
クラス Diff2
は、Person
で宣言されている name
フィールドの初期値を変更します。変更された値は、"Unknown"
です。
@Privileged
で注釈された @Refine
クラスは、元クラスの private メソッドを上書きすることもできます。
package test; @Glue class PersonExtender3 { @Privileged @Refine static class Diff3 extends Person { private String name; private String getName() { return name.toUpperCase(); } } }
上記の @Glue
クラスと Person
を織り込むと、
Person
の whoAreYou
メソッドは、@Refine
クラス Diff3
で実装された getName
メソッドを呼び出します。
@Refine
クラスが @Privileged
で注釈されている場合、その @Refine
クラスで宣言されたメソッドの中から、元クラスの private メソッドを呼び出すこともできます。
package test; @Glue class PersonExtender4 { @Privileged @Refine static abstract class Diff4 extends Person { @Super("getName") abstract String super_getName(); public String whoAreYou() { return "My name is " + super_getName(); } } }
Person
クラスの private メソッド getName
を呼び出すためには、その @Refine
クラス内で @Super
で注釈されている抽象 (abstract) メソッドを宣言する必要があります。
例では super_getName
となっているこの抽象メソッドは、getName
と同じ型の引数を取らなければなりません。
@Super
の引数は、呼び出したい元クラスの private メソッドの名前です。
この super_getName
メソッドの呼び出しは、元クラス Person
と @Glue
クラスを織り込む際に、Person
であらかじめ宣言されていた getName
メソッドの呼び出しに変換されます。
また、以下のようにして @Refine
クラスのメソッドの中から、そのメソッドによって上書きされる元クラスの private メソッドを呼び出せます。
package test; @Glue class PersonExtender5 { @Privileged @Refine static abstract class Diff5 extends Person { @Super("getName") 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
メソッドへの呼び出しに変換されます。
final クラスの上書き
修正したいクラスが final
クラスであると、サブクラスを定義することができません。そのような場合は、上書きしたいクラスの名前を @Refine
に引数として与えます。例えば、
package test; public final class Person { public String name = "Ellen"; public void greet() { System.out.println(name + "!"); } }
この Person
クラスは final
です。したがって、これを修正する @Refine
クラスは、次のようになります。
@Glue class Cheerful { @Privileged @Refine("test.Person") public static class Diff { @Super public String name; public void greet() { System.out.println("Hi " + name); } } }
@Refine
への引数はクラスの完全修飾名です。上の Diff
クラスは、test.Person
クラスの greet
メソッドを上書きします。元クラスの greet
が final
メソッドであっても上書き可能です。test.Person
の name
フィールドにアクセスするためには、同名のフィールドを @Super
という注釈つきで @Refine
クラスに宣言します。これは我々が元クラスの private
フィールドにアクセスするために使ったテクニックと同じです。
@Refine
のまとめ
@Refine
クラスを使った元クラスの改良 (refinement) のセマンティクスは、サブクラス手法を使ったクラスの拡張のセマンティクスとほとんど同じです。以下に、例外を示します。
static
メソッドを上書きできる。フィールドの初期値を上書きできる。
フィールドの注釈を追加できる。
@Refine
クラスのコンストラクタはデフォルト・コンストラクタでなければならない。@Privileged
が注釈されている@Refine
クラスは、元クラスの private メンバーにアクセスしたり上書きしたりすることが可能。
@Refine
は、クラスだけでなくインタフェースの改良にも使えます。その場合は
@Refine
インタフェースを使います。これは extends
キーワードに続く最初のインタフェースの定義を改良します。
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
は、メソッドが呼び出しのターゲットオブジェクトを表現する変数です。したがって $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)); } }
5. 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.