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.