minimize

事業拡大のため、新しい仲間を募集しています。
→詳しくはこちら

target と args

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メソッドの引数に追加されました。

thisJoinPoint

AspectJで定義したメソッド内では、
常に使える変数 thisJoinPoint が存在します。
この変数を使えば、対象となったJoinPointのメソッド名などが参照できます。
例えば、トレースログを行うには以下のようにします。

aspect Trace {
  pointcut log() : call(* *(..)) && !within(Trace);
  before() : log() {
      System.out.println("enter " + thisJoinPoint);
  }
  after() : log() {
      System.out.println("exit  " + thisJoinPoint);
  }
}

thisJoinPointorg.aspectj.lang.JoinPoint クラスのインスタンスなので
詳しくはJavadocを参照して下さい。
表示用の toString() メソッドが実装されているので、
上の例のような簡易手法でも充分実用的です。

無限ループに注意

上の例で、within キーワードを付け忘れたらどうなるでしょうか。
こうすると、全てのメソッド呼び出しが捉えられるので
log() ロジックから呼ばれたprintlnメソッドもJoinPointの対象になります。
ここからまた log() が呼ばれるので、結果無限ループに陥ってしまうのです。

これを防ぐために、

!within(Trace)

というキーワードが付加されています。
これにより、Trace内で呼ばれたメソッドはJoinPointの対象外となるので
無限ループに陥ることは無くなります。

ワイルドカードは便利ですが、思わぬ落とし穴もあるので気を付けましょう。
今のところ、無限ループを事前に警告してくれるような機能は無いようですので。

aspectファイル内での変数定義

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キーワードについて

以上のように、aspectキーワードはJavaのclassキーワードとかなり似ています。
メソッドやフィールドも定義できるし、スコープも存在します。
ただし、いくつかの違いがあります。

aspect式を利用できる

当たり前かもしれませんが、pointcut や before などのキーワードは
aspectファイル内からしか利用できません。

Inter-type declarations が使える

上で紹介したように、aspectファイル内では
他クラスへのフィールドを追加することが出来ます。

Singletonクラスである

aspectのインスタンスを生成(new)することは出来ません。
デフォルトでは、全てのaspectはSingletonクラスになっています。
そして全てのロジックは、non-staticで定義されます。
よって、ロジックからは non-static のフィールドやメソッドにアクセスすることが出来ます。

aspect Logging {
    OutputStream logStream = System.err;
    before(): move() {
        logStream.println("about to move");
    }
}

これらの挙動は、カスタマイズすることが可能です(詳細は不明)。