AspectJで定義するメソッドには、通常引数は付いていません。
引数付きのメソッドを定義するには、target
もしくは args
キーワードを使う必要があります。
以下に例を示します。
通常は、こんな感じになります。
pointcut setter1() : call(void Point.setX(int)); before() : setter1() { System.out.println("before setX()"); }
Point.setX(int) メソッドの呼び出し前に、setter1ロジックが実行されます。
前の例において、Point.setX メソッド呼び出し時の実引数を利用したい場合は
以下のようにします。
pointcut setter2(int x) : args(x) && call(void Point.setX(int)); before(int x) : setter2(x) { System.out.println("before setX(). value = " + x); }
args
キーワードを使うことにより、メソッドに引数xが追加されました。
args部分と call部分の順番は逆でも構いません。
前の例に、さらにもう一つ引数を追加してみます。
pointcut setter3(Point obj, int x) : target(obj) && args(x) && call(void setX(int)); before(Point point, int x) : setter3(point, x) { System.out.println("before setX(). value = " + x + ". instance = " + point); }
今度は、setXを呼び出したときのPointインスタンスがsetter3メソッドの引数に追加されました。
AspectJで定義したメソッド内では、
常に使える変数 thisJoinPoint
が存在します。
この変数を使えば、対象となったJoinPointのメソッド名などが参照できます。
例えば、トレースログを行うには以下のようにします。
aspect Trace { pointcut log() : call(* *(..)) && !within(Trace); before() : log() { System.out.println("enter " + thisJoinPoint); } after() : log() { System.out.println("exit " + thisJoinPoint); } }
thisJoinPoint
は org.aspectj.lang.JoinPoint
クラスのインスタンスなので
詳しくはJavadocを参照して下さい。
表示用の toString() メソッドが実装されているので、
上の例のような簡易手法でも充分実用的です。
上の例で、within
キーワードを付け忘れたらどうなるでしょうか。
こうすると、全てのメソッド呼び出しが捉えられるので
log() ロジックから呼ばれたprintlnメソッドもJoinPointの対象になります。
ここからまた log() が呼ばれるので、結果無限ループに陥ってしまうのです。
これを防ぐために、
!within(Trace)
というキーワードが付加されています。
これにより、Trace内で呼ばれたメソッドはJoinPointの対象外となるので
無限ループに陥ることは無くなります。
ワイルドカードは便利ですが、思わぬ落とし穴もあるので気を付けましょう。
今のところ、無限ループを事前に警告してくれるような機能は無いようですので。
aspectファイルはjavaファイルの拡張なので、
aspect定義内で普通にフィールドを宣言することが出来ます。
aspect Counting { private int count; before() : call(* *(..)) { ++count; } }
さらに、既存のクラスに変数やメソッドを追加することが可能です。Inter-type declarations
と呼ばれる機能です。
aspect PointObserving { private Vector Point.observers = new Vector(); }
このように、既存のPointクラスに observers というフィールドを追加しています。
private宣言されているので、このフィールドは PointObserving のスコープ内からのみ
参照可能です。Pointクラスからは見ることが出来ません。
AspectJを使うと、こんなことまで出来てしまいます。
declare parents: SampleClass implements Comparable; public int SampleClass.compareTo(Object o) { ... }
declare parents
キーワードを使います。
これにより、既存の SampleClass に Comparable インターフェイスを追加します。
当然、この場合は実装メソッドも追加する必要があります。
このとき、AspectJエディタでSampleClass.javaを開いてみると
クラス宣言部分にマーカーが付いています。
これは「aspect A 内で、このクラスにComparableインターフェイスが追加されています」
という印です。
さらに一歩進んで、新規インターフェイスも追加してみましょう。
aspect A { private interface HasName {} declare parents: (Point || Line || Square) implements HasName; private String HasName.name; public String HasName.getName() { return name; } }
これは、Point / Line / Square という3つの既存クラスに
HasNameというインターフェイスを追加しています。
このインターフェイスは、aspect A の中でのみ有効です。
さらに、HasNameにgetName() というメソッドとnameというフィールドを追加しています。
通常、インターフェイスにフィールドを宣言することは出来ませんが
aspect内で定義したインターフェイスにはフィールドを追加することが出来ます。
インターフェイスというよりはクラスの概念に近いですね。
C++などで利用される多重継承のイメージを持つと判りやすいかもしれません。
以上のように、aspectキーワードはJavaのclassキーワードとかなり似ています。
メソッドやフィールドも定義できるし、スコープも存在します。
ただし、いくつかの違いがあります。
当たり前かもしれませんが、pointcut や before などのキーワードは
aspectファイル内からしか利用できません。
上で紹介したように、aspectファイル内では
他クラスへのフィールドを追加することが出来ます。
aspectのインスタンスを生成(new)することは出来ません。
デフォルトでは、全てのaspectはSingletonクラスになっています。
そして全てのロジックは、non-staticで定義されます。
よって、ロジックからは non-static のフィールドやメソッドにアクセスすることが出来ます。
aspect Logging { OutputStream logStream = System.err; before(): move() { logStream.println("about to move"); } }
これらの挙動は、カスタマイズすることが可能です(詳細は不明)。