Introduction to GluonJ
千葉 滋 (訳: 西澤 無我)
Table of Contents
1. なぜ GluonJ が必要か?
2. Glue クラスを定義する
3. クラスの改良 (Refinement) その 1
4. クラスの改良 (Refinement) その 2
5. ポイントカット (Pointcut) とアドバイス (Advice)
6. Glue クラスの拡張
3. クラスの改良 (Refinement) その 1
@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
クラスの名前は元クラスの別名として扱われます。したがって
Person
のサブクラスから Diff
へのキャストも可能です。
Note:
@Refine
クラスの名前が元クラスの名前の別名として扱われるのは、@Glue
クラス内に含まれる (メンバー) クラスの内部だけです。
Note: コンパイラによっては、
Person
からDiff
への型変換を許さないかもしれません。その場合は、javassist.gluonj.GluonJ
クラスの$refine
メソッドを使います:
((Diff)GluonJ.$refine(p)).sayHi("Paul");
$refine
メソッドは、単にjava.lang.Object
へ型変換をおこなうだけです。
@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
クラスを使った元クラスの改良 (refinement) のセマンティクスは、サブクラス手法を使ったクラスの拡張のセマンティクスとほとんど同じです。以下に、例外を示します。
static
メソッドを上書きできる。フィールドの初期値を上書きできる。
フィールドの注釈を追加できる。
@Refine
クラスのコンストラクタはデフォルト・コンストラクタでなければならない。@Privileged
が注釈されている@Refine
クラスは、元クラスの private メンバーにアクセスしたり上書きしたりすることが可能。
@Refine
は、クラスだけでなくインタフェースの改良にも使えます。その場合は
@Refine
インタフェースを使います。これは extends
キーワードに続く最初のインタフェースの定義を改良します。
4. クラスの改良 (Refinement) その 2
@Refine
クラスはワイルドカードで指定された複数の元クラス
に適用することができます。
また@Refine
クラスは元クラスのメソッドを条件的に上書きすることもできます。
以下では、これらの機能の詳細を説明します。
複数の元クラス
@Refine
の引数で元クラスを指定できることは既に説明しました。
もし @Refine
が引数をとるなら、@Refine
クラスの親クラスは java.lang.Object
でなければなりません。
もちろん元クラスとは見なされません。
@Refine
の引数は、final
クラスを元クラスにしたいときだけでなく、複数のクラスを元クラスにしたいときにも使えます。
@Refine
の引数にワイルドカードが含まれているときは、@Refine
クラスは引数として与えたパターンに名前が一致する全てのクラスに適用されます。
例えば
package test; @Glue class Cheerful { @Refine("test.P*") public static class Diff implements Human { public void greet() { System.out.println("Hello!"); } } }
package test; public interface Human { void greet(); }
@Refine
クラス Diff
の元クラスは、与えられたパターン "test.P*"
に一致する全てのクラスです。
例えば test.Person
や test.People
が元クラスになります。
greet
メソッドはそれら全ての元クラスに追加されます。
もし @Refine
クラスが 1 個以上の元クラスをもつとき、その
@Refine
クラスの名前は、元クラスの名前の別名とは解釈されません。
したがって @Refine
クラスによって追加されたメソッドを呼ぶときに、型キャストを使うことができなくなります。
public void test() { Person p = new Person(); ((Diff)p).greet(); // error }
Person
から Diff
への型キャストは、同じ
Glue
クラスの中であっても不正となります。
greet
メソッドを呼ぶためには、まず Person
クラスに Human
インタフェースを追加して、変数 p
の型を Person
から Human
に変換しなければなりません。
public void test() { Person p = new Person(); ((Human)p).greet(); // OK }
@Refine
クラス Diff
は
Human
インタフェースを元クラスに追加していることに注意してください。
もし @Refine
クラスの元クラスが複数である場合、元クラスの各々にインタフェースを追加して、@Refine
クラスで追加したメソッドが、追加したインタフェースを通してアクセス可能になるようにしなければなりません。
GluonJ では現在 *
と |
、?
をワイルドカードとして @Refine
の引数の中へ含めることができます。
使えます。
|
は「または (OR)」を意味し、複数のクラスやインタフェースの名前を列挙するのに使います。
?
はインタフェース名の末尾につけます。
もしパターンが ?
で終わるインタフェース名を含んでいる場合、そのパターンはそのインタフェースを直接実装しているクラス全てと一致します。
例えば
@Refine("test.Person|test.People")
はtest.Person
またはtest.People
と一致します。@Refine("test.P*|test.Q*")
はtest.P
またはtest.Q
から始まるクラスやインタフェースの名前と一致します。@Refine("test.Human?")
test.Human
インタフェースを直接実装しているクラスの名前と一致します。 今test.Person
クラスがtest.Human
インタフェースを実装しているとします (test.Person
クラスのimplements
節にtest.Human
が明示的に書かれている)。 すると、このパターンはtest.Person
とは一致しますが、その子クラスとは一致しません。 もしtest.Human
インタフェースが子インタフェース (つまりtest.Human
を extends するインタフェース) をもつと、このパターンはその子インタフェースを直接実装しているクラスの名前にも一致します。
複数メソッドの上書き
@Refine
クラスのメソッドは、元クラスで宣言された複数のクラスを修正することができます。
package test; @Glue class Cheerful { @Refine public static class Diff extends Person { @Overwrite("say*") public void newSay() { System.out.println("Hello!"); } } }
この @Refine
クラスは元クラス Person
を改良します。
newSay
メソッドは @Overwrite
で注釈されているので、Person
クラスの newSay
は上書きしません。
引数に与えられたパターン "say*"
に一致する名前をもち、newSay
と同じ型の引数列をもつメソッド全てを上書きします。
上書きされるのは Person
クラスのメソッドだけです。
Person
の親クラスのメソッドは (パターンと一致しても)
上書きされません。
newSay
メソッドと同じ名前・引数列のメソッドが
Person
クラスに宣言されていてはなりません。
@Overwrite
メソッドには、他で使われていない名前を選ばなければなりません。
注意: 注釈は
@Override
ではなく@Overwrite
です。@Override
はjava.lang
パッケージに含まれている注釈です。
@Overwrite
つきのメソッドにより上書きされた元のメソッドを呼ぶときには、@SuperOf
を使って転送メソッドを宣言しなければなりません。
super.newSay()
と書いて元のメソッドを呼ぶことはできません。
Person
クラスが newSay
メソッドを宣言しているわけではないからです。
package test; @Glue class Cheerful { @Refine public static class Diff extends Person { @SuperOf("newSay") abstract void super_newSay(); @Overwrite("say*") public void newSay() { System.out.println("Hello!"); super_newSay(); } } }
super_newSay
メソッドは newSay
用の転送メソッドとなります。
super_newSay
が @SuperOf
(これは @Super
とは異なります)
で注釈された抽象メソッドであるからです。
@SuperOf
への引数は、対応する
@Overwrite
メソッドの名前です。
また転送メソッドの引数列の型は、対応する @Overwrite
メソッドと同じでなければなりません。
転送メソッドの名前について特に命名規則はありません。
好きな名前を選ぶことができます。
newSay
メソッドの中から転送メソッドを呼ぶと、元のメソッドが実行されます。
もし Person
クラスに sayHello
と
sayHi
メソッドが宣言されていたとすると、上の
@Overwrite
メソッドはそれら 2
つのメソッドを修正し、sayHello
や sayHi
を呼ぶと、まず
"Hello!"
を表示し、その後それぞれの元のメソッドの本体が実行されるようにします。
限定的な上書き
@Refine
クラスは元クラスのメソッドを条件付きで上書きすることができます。
そのような上書きを限定的な上書き (quantified overriding) と呼びます。
例えば
package test; public class Person { public String name = "Paul"; public void greet() { System.out.println("I'm " + name); } }
package test; @Glue class SayHiToMain { @Refine static class Diff extends test.Person { @Within("test.Main") public void greet() { System.out.println("Hi"); super.greet(); } } }
Diff
クラスは、Person
クラスの
greet
メソッドを上書きします。
しかしながら、この上書きは greet
メソッドが
test.Main
クラスの中から呼ばれたときだけ有効になります。
例えば
package test; class Main { public void test(Person p) { p.greet(); // "Hi" is printed } } class Test { public void start(Person p) { p.greet(); // "Hi" is not printed } }
このとき、
test
メソッドから greet
を呼び出すと、@Refine
クラスである Diff
で宣言されている greet
メソッドを実行することになります。
一方、start
メソッドからの呼び出しでは、Diff
の greet
メソッドは実行されません。
@Within
@Within
の引数は、クラスの完全修飾名だけでなく、メソッド名にすることもできます。
もしメソッド名である場合、クラスの完全修飾名とメソッド名は
#
で区切ります。例えば
@Within("test.Point#move(int, int)") @Within("test.Point#move(..)")
これら 2 つの @Within
への引数は、どちらも
test.Point
クラスの
move
メソッドを表します。
上の @Within
には 2 つの int
型の引数をとる
move
メソッドだけが一致します。
一方で下の @Within
には、任意の引数をとる
move
メソッドが一致します。
詳しくは
メソッド・パターン
を見てください。
@If
限定的な上書きを実現する注釈には @If
もあります。
この注釈がついたメソッドは、@If
の引数として与えられた Java
の式が true
であるときだけ、元クラスのメソッドを上書きします。
package test; @Glue class SayHiIf { @Refine static class Diff extends test.Person { @If("$0.age > 20") public void greet() { System.out.println("Hi"); } } }
@Refine
クラスの greet
メソッドが実行されるのは、Person
クラスの
greet
メソッドが呼び出され、呼ばれたオブジェクトのフィールド
age
が 20 より大きいときです。
特殊変数 $0
はメソッドが呼ばれたオブジェクトを表します。
@If
の引数の式は、メソッドの呼び出し側の文脈 (スコープ)
で評価されます。
Call ポイントカット対限定的な上書き
限定的な上書きを利用すると、@Refine
クラスを AspectJ
の call
ポイントカットの代わりに使うことができます。
例えば、次に示す AspectJ のアドバイスが "Hi"
を表示するのは、Person
クラスの greet
メソッドを呼んだのが test.Main
クラスのメソッドであったときだけです。
void around(): call(void Person.greet()) && within(test.Main) { System.out.println("Hi"); proceed(); }
このアドバイスはおおよそ次の @Refine
クラスに相当します。
@Refine class Diff extends test.Person { @Within("test.Main") public void greet() { System.out.println("Hi"); super.greet(); } }
どちらも greet
メソッドの振る舞いを変更します。
しかし、メソッドを呼ばれたオブジェクトの静的な型が
Person
クラスの親クラスである場合、AspectJ
と GluonJ の間に違いが生まれます。
今、Person
が Mammal
クラスの子クラスでるとします。
Mammal
クラスも greet
メソッドを宣言していると、
Person p = new Person(); Mammal m = p; m.greet(); // "Hi" は表示されるのか?
m
に対する greet
メソッドを呼び出しても、上に示した AspectJ のアドバイスは実行されません。
変数 m
の静的な型が Person
でないからです。
一方、この m
に対するメソッド呼び出しによって、@Refine
クラスが宣言した greet
の方は実行されます。
"Hi"
が表示されるのです。
@Client
AspectJ では、call
と this
ポイントカットの組み合わせにより、メソッドを呼んでいる
(クライアント) オブジェクトをアドバイスに渡すことができます。
void around(test.Main from): call(void Person.greet()) && within(test.Main) && this(from) { System.out.println("Hi " + from.toString()); proceed(); }
アドバイス本体の中では、greet
メソッドを呼んだ
test.Main
オブジェクトを from
として参照できます。
GluonJ では、上のアドバイスは次のように書けます。
package test; @Glue class SayHi { @Refine static class Diff extends test.Person { public void greet(@Client test.Main from) { System.out.println("Hi " + from.toString()); super.greet(); } } }
第 1 引数 greet
は @Client
で注釈されます。
この引数は Person
クラスの greet
メソッドを呼んでいるクライアント・オブジェクトを表します。
この引数は、上書きされたメソッドを決めるときのメソッド・シグネチャからは除外されます。
つまり、@Refine
クラスの greet
メソッドは、Person
クラスが宣言していて引数をとらない
greet
メソッドを上書きします。
test.Main
オブジェクトを引数にとる
greet
メソッドを上書きすることはありません。
@Client
は限定的な上書きを実現する注釈の 1 つもあります。
上の例では、@Refine
クラスの greet
メソッドによる上書きは、メソッドを呼び出したオブジェクトが
test.Main
クラスのオブジェクトであるときだけです。
test.Main
以外のクラスのメソッドから
greet
メソッドが呼ばれているときは、
@Refine
クラスによる上書きは無効となります。
@Get と @Set
@Refine
クラスはフィールド・アクセスの動作を変えることもできます。
package test; public class Person { public String name = "Paul"; public void greet() { System.out.println("I'm " + name); } }
package test; @Glue class Accessor { @Refine static class Diff extends test.Person { @Get("name") public String get_name() { return "Mr. " + name; } @Set("name") public void set_name(String s) { System.out.println("name = " + s); name = s; } } }
この @Refine
クラスは、Person
クラスの
name
フィールドのゲッター・メソッドとセッター・メソッドを定義します。
@Get
や @Set
の引数は、元クラスのフィールドの名前でなければなりません。
ゲッター・メソッドとセッター・メソッドの名前については特別な規則はありません。
好きな名前をつけることができます。
ゲッター・メソッドとセッター・メソッドは、フィールドへの直接アクセスを自動的にそれらのメソッドの呼び出しに置き換えます
(アクセスが @Glue
クラスの中でおこなわれている場合を除く)。
例えば
package test; public class PersonTest { public void test() { Person p = new Person(); p.name = "Black"; System.out.println(p.name); } }
test
メソッドを実行すると、@Refine
クラスの set_name
と get_name
メソッドが呼ばれます。
p.name
への代入は、直接的なフィールド・アクセスとはなりません。
代わりに set_name
メソッドが呼ばれ、そのメソッドが
name
フィールドに "Mr. Black"
を代入します。
p.name
の読み出しも同様に
get_name
メソッドを呼び出します。
name
フィールドを直接読むことはできません。
ゲッター・メソッドによってアクセスされます。
なお、このようなフィールド・アクセスの置き換えは、
@Get
や @Set
で注釈されているメソッドの本体の中にはおよびません。
したがって無限退行がおこることはありません。
Copyright (C) 2006-2007 by Shigeru Chiba and Muga Nishizawa. All rights reserved.