AspectJ 1.6で追加されたポイントカット

4/23に、AspectJ 1.6がリリースされました。
1.6では、多くはありませんが、いくつかポイントカットの指定に関して拡張されています。
でも、使ってみたら全然使い物にならないことが発覚!
私の使い方が悪いことに期待したい!!

Parameter annotation matching

これは、メソッドやコンストラクタの引数に付与されるアノテーションに対するポイントカットの拡張です。

%ASPECTJ_INSTALL_HOME%\doc\README-160.html
では、

execution(* *(@A *)); //(1)

と言う、表記もありますが、これはAspectJ 1.5からも指定できた表記法です。


1.6から増えた表記は、

execution(* *(@A (*)));    //(2)
execution(* *(@A (@B *)))  //(3)

の2つの表記方法です。

(1)と、(2)の違いは非常に微妙です。
英語が得意なら一発なのでしょうが、私は苦手なので悩んでしまいました。
言い訳すれば、(1)は、1.5からもある旨書かれていればピンと来たんですけどね。

(1)は、引数のクラス自身に付与されているアノテーションに対してポイントカットを指定する方法で、(2)は、引数そのものに付与するアノテーションに対してポイントカットを指定する方法になっています。

例えば、以下のようなアノテーションがあって、

ParamAnno.java
package aspect16sample;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER})
public @interface ParamAnno {
}

以下のようなクラスがあって、

Sample1.java
package aspect16sample;

public class Sample1 {
    public static void main(String[] args) {
        foo1(null);
        foo2(null);
    }

    static void foo1(InnerClass obj) { System.out.println("foo1"); }
    static void foo2(@ParamAnno InnerClass obj) { System.out.println("foo2"); }

    @ParamAnno
    public static class InnerClass { }
}

さらに、以下のようなアスペクトがあったとします。

Sample1Aspect.aj
package aspect16sample;

public aspect SampleAspect1 {
    before() : execution(* *(@ParamAnno *)) {    //(1)
        System.out.println("Hook1");
    }
    before() : execution(* *(@ParamAnno (*))) {  //(2)
        System.out.println("Hook2");
    }
}

で、Sample1を実行すると結果は以下の通りです。

Hook1
foo1
Hook1
Hook2
foo2

(1)のアスペクトは、引数のクラスにアノテートされたParamAnnoに対して反応(?)するので、InnerClassオブジェクトを第1引数に取るfoo1、foo2両方に対してアドバイスが編みこまれています。
(2)のアスペクトは、パラメタにアノテートされたParamAnnoに対して反応するので、foo2のみがアドバイスの編みこみ対象となっているのが分かると思います。


この拡張は、パラメータの事前条件検証をアスペクトで編み込むケースでは非常に有用です。
例えば従来であれば、

@ContractCheck
public void foo(@NotNull String name, @NumberRange(min=0, max=100) int point) {
:
}

のように、必ずメソッド自体にマーカーアノテーションを付与する必要がありましたが、1.6からは不要になります。
また、ポイントカットを分けて定義できるため、NotNullのチェックを行うアドバイスと、NumberRangeを検証するアドバイスを静的に分離可能に出来るのもアドバイスコードがシンプルになりそうです。(性能的には微妙かもしれません。)


た・だ・し
パラメタ検証にこの表記を利用するには1点非常に困った問題があるのです。
どのパラメタにあってもよいという表記が出来ないのです。
NotNullアノテーションなんて、何番目のパラメタに現れようがチェックしやがれって感じなのですが出来ないのです。


例えば、最初に思いつくのは、

before() : execution(* *(.., @NotNull(*), ..)) { }

と言う表記なのですが、この書き方をすると「... has not been applied [Xlint:adviceDidNotMatch]」と怒られます。そんなパターンにマッチする箇所はねぇと言うことらしいです。
じゃぁと言うことでこうも書いてみました。

before() : execution(* *(@NotNull(..))) { }

すると今度は「Syntax error on token ".", "name pattern" expected」と怒り心頭(コンパイルエラー)になってしまいます。

残念ながら、、、使えません。


ついでに言うと、パラメータのアノテーションを拾うのは若干面倒なのです。
例えば、Sample1のfoo2メソッドにアノテートされたアノテーションを取得するには以下のようなコードをアドバイス内で記述する必要があります。

    MethodSignature ms = (MethodSignature)thisJoinPointStaticPart.getSignature();
    Method m = ms.getMethod();
    Annotation[][] ans = m.getParameterAnnotations();
    ParamAnno ano = (ParamAnno)ans[0][0];

このケースでは、第1引数に1個しかアノテーションが付与されていないので、面倒と感じないかもしれませんが、任意のパラメタに任意個のアノテーションが付与されるとするとそりゃぁもぅ面倒で面倒で。。。
まぁ、これはAspectJの問題ではなく、Javaの問題なんですけどね。


Annotation Value Matching

これは、アノテーションに指定されている値でポイントカットを絞り込むための拡張です。
この拡張は手放しで嬉しい。

例えば、よくあるトランザクション制御とかをアスペクトで編みこもうとするなら、

Sample2.java
package aspect16sample;

public class Sample2 {
    public static void main(String[] args) throws Exception {
        foo1();
        foo2();
    }
    @Transaction(type=TransactionType.REQUIRED)
    static void foo1() {
        System.out.println("foo1");
    }
    @Transaction(type=TransactionType.REQUIRED_NEW)
    static void foo2() {
        System.out.println("foo2");
    }
}
SampleAspect2.aj
package aspect16sample;

public aspect SampleAspect2 {
    before() : execution (@Transaction(type=TransactionType.REQUIRED_NEW) * *(..)) {
        System.out.println("REQUIRED NEW Transaction.");
    }
    before() : execution (@Transaction(type=TransactionType.REQUIRED) * *(..)) {
        System.out.println("REQUIRED Transaction.");
    }
}

と、書けばよくなります。
1.5までの表記法では、アドバイス内でTransactionTypeを判断して処理分岐をする必要があります。
今回の拡張では、アノテーションの値にアドバイスを静的に分離でるようになるので、性能向上と可読性の向上が可能となっています。


早速実行してみましょう。

foo1
REQUIRED NEW Transaction.
REQUIRED Transaction.
foo2

あれっ?
foo2にしか、編みこまれていません。
REQUIRED_NEWのアドバイスをコメントして再実行してみます。

REQUIRED Transaction.
foo1
foo2

今度はfoo1にのみ、編みこまれました。動作はこれでOKなので、再度コメントアウトして実行。

foo1
REQUIRED NEW Transaction.
REQUIRED Transaction.
foo2

???元に戻りました。
ちなみに、changesでは、

before(): execution(@Trace !@Trace(TraceLevel.NONE) * *(..))

見たいに書けと書いてある。単なる例だと思いつつ、とりあえず、

before() : execution (@Transaction !@Transaction(type=TransactionType.REQUIRED_NEW) * *(..))

結果はまぁ、想像通り逆なっただけでした。

REQUIRED Transaction.
REQUIRED NEW Transaction.
foo1
foo2

ちなみに以下のように書いちゃうと、AJDTが狂うのでEclipseの強制終了と、別テキストエディターでの修正が必要になるみたいなので要注意です。

before() : execution (@Transaction @Transaction(type!=TransactionType.REQUIRED_NEW) * *(..))


うーん。。。全く使い物になりません。
こちらを見る限り、6/30にバージョンアップを予定しているので、そのときに直っていることを期待しましょう。

1.5が出たときもそうですが、そうじゃなくてもAspectJは敷居が高いのに、こうも品質が悪いと誰も使わなくなっちゃうぞ!