AspectJ

AspectJ におけるポイントカット記述

AspectJ におけるアスペクト

アスペクトは,ポイントカット(フックを仕掛ける Join Point の集合)と ポイントカットに仕掛ける処理(アドバイス)の集合によって記述されます.

主な利用方法は AspectJ Programming Guide の Appendix A, クイックリファレンスに記述されています.

AspectJ の構文情報については,AspectJ/簡易リファレンスを作ってみました.

Call(呼び出し) と Execution(実行)の違い

AspectJ における代表的なポイントカットの1つが, メソッド呼び出しを表現する call と,メソッド実行に対応する execution です. いずれも,引数としてメソッドのシグネチャを取り,次のように書きます.

// Foo.foo() への呼び出し文(引数はどんなものでも)
pointcut Foo_foo_call(): call(void Foo.foo(..));   

// Foo クラスのすべてのメソッド呼び出し
pointcut Foo_any_call(): call(void Foo.*(..));     

// すべてのクラスの foo() というメソッド呼び出し
pointcut any_foo_call() : call(void *.foo(..));    

// int を引数に取る Foo.foo(int) 呼び出し
pointcut Foo_foo_call_with_int_arg(int x): 
    call(void Foo.foo(x)) && args(x);    
// Foo.foo() のメソッド実行
pointcut execution(void Foo.foo());

call はメソッド呼び出し文に作用し, execution は呼び出されるメソッド本体に作用します.

call はメソッド文に対してのみ作用するので, 呼び出し対象クラスの改変が禁止されている場合, 呼び出し側クラスの条件によって処理を変更する場合などに有効です. そのかわり,super を用いたスーパークラスのメソッド実行など, 一部の特殊な呼び出し文には反応しません.

execution は,メソッド本体に対して作用するので, reflection による呼び出しなど,通常では反応しにくい場合にも有効です. ただし,呼び出し側についての情報にはアクセスできなくなるので, 使えない場合もあります.

以下では,コード変換上での違いを,小規模なコード例で示します.

まず,元コードとして,次のような単純なメソッド呼び出しを想定します.

// 元コード
Foo f = new Foo();
f.foo();

ここで,call(void Foo.foo()) に対して before, after アドバイスが 与えられると,AspectJ は先のコードは次のようなコードに変換されます.

Foo f = new Foo();
/* call(Foo.foo()) に対する before アドバイスを実行  */
try {
  f.foo(); // 本来の呼び出し
} finally {
  /* call(Foo.foo()) に対する after アドバイスを実行  */
}

around アドバイスの場合は,本来存在している f.foo() メソッド呼び出し文を 取り除いて,代わりに around アドバイスの中身が展開されます.

execution を使った場合は,次のようになります.

// execution に対する before, after がある場合
class Foo {
  void foo() {
    /* execution(void Foo.foo()) に対する before アドバイスの実行 */
    try {
      /* 元の Foo.foo() の定義がここに入る */
    } finally {
      /* execution(void Foo.foo()) に対する after アドバイスの実行 */
    }
  }
  :
}

コード変換の正確な結果は, AspectJ 1.0 では -preprocess オプションを用いて, また AspectJ 1.1 以降では decompiler などを用いて確認することができますので, 興味のある方は参考にしてみてください.

複数のアドバイスが同じポイントカットに設定されたときの結果は?

複数のアドバイスが共通のタイミングで動作する場合, アドバイスは "適当な" 順序で直列化されます. 順序は基本的に「外側」「内側」という概念で, 一番内側にポイントカットが関連付けられている join point が存在します. before アドバイスは外側から先に実行して内側へ向かっていき, 処理が join point まで到達すると(around アドバイスが join point を スキップすることはありますが), after アドバイスを内側から外側へと順番に実行していくことになります.

注: 「適当な順序」というのはコンパイラの実装に依存しますが,
    たとえばアルファベット順などです.

アスペクト間の優先度は,AspectJ 1.0 では dominates 宣言によって, AspectJ 1.1 では precedence 宣言によって制御します. 優先度の高いものほど外側で実行されます.

詳しくは,プログラミングガイドを参照してください.

制御フローへのアクセス: cflow, cflowbelow

cflow, cflowbelow は,

cflow(call(* Foo.do(..)))

のように他のポイントカットを引数に取り, 「あるポイントカットに属する制御フロー」を意味します. たとえば上記の例は,「Foo クラスの do メソッドに対する呼び出し」に 属する制御フロー,という意味になります. これは,たとえば以下のように使われます.

cflow(call(* Foo.do(..))) && call(* Bar.*(..));

このポイントカットは「Foo.do メソッドの中,あるいは Foo.do から呼ばれた メソッドの中での Bar クラスに対するメソッド呼び出し」を表現します.

このようなポイントカットを実現するために, AspectJ は,Bar に対するメソッド呼び出しが Foo.do メソッドの中から出てきたものかどうかをチェックするためのコードを 生成します.

cflow はパフォーマンスを劣化させる?

cflow についての現在の実装では, 「Call Stack」というStack オブジェクトが準備され, 関数呼び出しごとにその呼び出し情報を積み上げます.

そして,cflow を使ったポイントカットごとに, 先の例であれば Bar に対するメソッド呼び出しごとに, Stack の中身を調べて「現在実行しているのは Foo.do の 呼び出しから来た制御フローか?」と調べます.

メソッド呼び出しが頻繁なプログラムでは, これが膨大なオーバーヘッドを発生させることがあります.

以前,再帰的に処理を行うタイプの構文解析プログラム (メソッド呼び出し回数にして50万回程度)に cflowbelow を仕掛けたところ, 通常時で10秒くらいだった処理に3分程度消費するようになったことがあります. ほとんどのプログラムでは関係ないことかもしれませんが, プログラムの性質をきちんと考えて pointcut を 設計しないと,実行時間の増大につながる恐れがあります.

this と within を使い分ける

ある特定のメソッドの実行だけをアスペクトで扱いたい場合, ポイントカットの指定としてはオブジェクトの型で限定する this と コードの位置で限定する within が利用できます.

クラス階層として3つのクラス A, B, C (A が先祖,C が子孫)を想定します. このとき,継承されオーバーライドされたメソッド A.foo(), B.foo(), C.foo() があり,B.foo() の実行を監視したいとします.

このとき,メソッドの実行を表現する execution は メソッドのシグネチャをパラメータに取りますから,

execution(void B.foo())

という記述で簡単に実現することができそうに見えます. しかし,実際には execution(void B.foo()) は B.foo() と C.foo() の両方にマッチしてしまいます.

これに対して,

within(B) && execution(void B.foo())

を使うことで,B.foo() に対してだけアドバイスを適用することができます. ただし,B の内部クラスとして class D extends B { ... } というように B のサブクラスであるようなクラス D が定義されている場合, D.bar() に対してもマッチします.

このような場合は,クラス階層を使う this を使って

this(B) && !this(C) && !this(B.D) && execution(void B.foo());

のように記述することで,より厳密な指定が可能です.

継承階層についての注意として,private メソッドがあります. private メソッドは,クラスの外部からは不可視であるため, オーバーライドの対象とはなりませんが,偶然同名のメソッドが存在する場合があります. 単純に execution(void A.bar()) とだけ書いてしまうと, 同名の B.bar() に対しても偶然マッチしてしまいますので, 忘れずに this あるいは within を付加しておく必要があります.

this と within では,サブクラス化されているものにも適用したい場合には thisを多用するほうが「より安全である」と考えられます. ただ,特定クラス内でのみアスペクトを有効にしたい場合には withinを使うほうが簡便です. 内部クラスを使っていない場合には影響もありません.

ポイントカットがマッチしていることを確認するには

特定の pointcut がマッチしているか/していないかが気になる場合は, declare error/warning を使って,その pointcut 定義が生成されているかを 調べることが有効です.

Eclipse を使っている場合は,AJDT プラグインが,特定メソッドに対して 連動しているアドバイスの一覧をマーカーとして置いてくれるので, 適当な空のアドバイスを接続して,マーカーが表示されるかどうかで 確認することもできます.

注: AJDTの場合,pointcut がマッチしているだけで, アドバイスが連動していない状態では,マーカーは表示されません.

コンストラクタ呼び出しに対する join point の使い分け

コンストラクタには,super 呼び出しを先頭に書かなければならない (他の処理をする前にオブジェクトの親クラス部分を先に構築する必要がある), サブクラスでオーバーライドされるようなメソッドを呼び出してはならない (子クラスの部分がまだ初期化されていないのにメソッドを呼ぶ可能性がある), などといったいくつかの注意すべき制約があります. アスペクトを貼り付ける場合にも,いくつかの注意点があります.

1. もしオブジェクトの外側にアスペクトを結合するなら, 基本的に次のように記述しましょう.

after() returning (Foo f): call(Foo.new(..)) { ... }

新しいオブジェクトを取得するために this や target を 使いたくなるかもしれませんが,オブジェクト生成の「外側」にいるのなら, いつからオブジェクトが存在しているかを完全に捉えることはできないので, オブジェクトの生成と初期化が完了して return してきたタイミングで 処理を実行するようにしましょう.

2. もし,特定のコンストラクタの内部で処理を実行したいのなら, 次のように記述しましょう.

after(Foo f) returning: this(f) && execution(Foo.new(..)) { ... }

before を使うと,親オブジェクトが初期化されているものの コンストラクタ本体はまだ実行されておらず, オブジェクトが初期化されていないことに注意しましょう.

3. もしコンストラクタが他のコンストラクタを this(...) 形式で呼び出していて, どんな初期化経路であれ,そのオブジェクトのただ一度だけの初期化を 捉えたい場合は,次のように記述します.

after(Foo f) returning: this(f) && initialization(Foo.new(..)) { ... }

上記の説明は, Erik Hilsdale が AspectJ-users メーリングリストに 投稿してくれた内容の日本語訳です. 丁寧な説明をまとめてくれた Hilsdale に感謝します.

アスペクト作成時の注意点

呼び出し元と呼び出し先でスレッドが違うことがある

プログラム監視用のアスペクトなどを設計するとき, スレッドごとに別のログファイルを生成する, といった処理を書きたくなる場合があります.

具体的コードとしては,次のようなものです.

before() : call(* Foo.*(..)) {
  Logger logger = (Logger)hash_threads(Thread.currentThread());
  logger.log(CALL, thisJoinPoint);
}
before() : execution(* Foo.*(..)) {
  Logger logger = (Logger)hash_threads(Thread.currentThread());
  logger.log(EXEC, thisJoinPoint);
}

このようなコードを書くときの仮定として, 「メソッド呼び出し元と呼び出し先のスレッドは同じである」というものがあります. しかし,Thread.start 呼び出しや,javax.swing クラスの コンストラクタについては,それが成立しません.

Thread.start については当然だと思われるでしょうが, javax.swing クラスのコンストラクタはなぜでしょうか. Windows 版の Sun の JVM (JDK1.3.1付属)で実験した結果ですが, Java の GUI オブジェクトのコンストラクタは, Look & Feel Class Loder と呼ばれる特殊なスレッド上で実行されることがあります. このとき,呼び出し元のスレッドはロードが終わるまでロックされているようです.

javax.swing のコンストラクタ呼び出しはそれほど頻繁に行われないため 気にならないかもしれませんが,先に示したようなコードを書いて プログラムを解析しようとした場合, はまる原因になことがあるるので注意が必要です.

アスペクトでの無限ループ発生

AspectJ で不用意にアスペクトを貼り付けると,無限ループが 発生してしまうことがあります.

// ループが発生するアスペクト
Hashtable hash;
before(Object o): execution(int o.hashCode()) {
  hash.put(thisJoinPoint.getSourceLocation(), o);
}

上記のコードの場合,hashCode の呼び出しにフックを仕掛けていますが, ハッシュテーブルの操作中(hash.put 時)に hashCode が呼び出されるため, 自分で作成した hashCode メソッドに対して上記のアドバイスが起動し, 無限ループに陥ります.

これは,ロギングアスペクトを作る際に誤って toString() にも アドバイスを結合してしまった場合などにも発生します. この無限ループの発生で注意すべき点は, アスペクトの結合対象として与えた(通常はユーザが作った) hashCode や toString にしかアスペクトが結合されないことです. そのため,上に示したような潜在的に問題を含むアスペクトでも, hashCode を自分で定義していなければ,問題なく動作してしまいます.

リフレクション機能と相性の悪いポイントカット

AspectJ では call と execution の2つの join point がありますが, コードの展開先が呼び出し側と呼び出される側という点で異なっています. リフレクションを用いたメソッド呼び出しに対しては call が引っかからないため,コードが展開されず,潜在的な問題となる場合が あります.リフレクションをプログラム内で使用していて, もし呼び出し先のソースコードがある場合は,execution を使うほうが安全です. また,次善策として,次のような書き方もありでしょう.

before(Method m): call (m.invoke(..)) && if(m.getName().equals("foo"))

リフレクション自体はプログラム中であまり利用されないこと, 元々のコストが高いことから,このようなアドバイスを付加しても それほどプログラムのオーバーヘッドは増えないと予想されます. もちろん,最終的にはプロファイリングなどで 実測して確認してみないと何ともいえないところではありますが….

コンストラクタへの before advice の特殊な実行順序

コンストラクタは,先頭にスーパークラスの初期化が来ないといけないという ルールがあるため,before execution(Hoge.new) といったアドバイスを書こうとすると 動作に失敗する場合があります. なるべくなら call(Hoge.new) で引っ掛けましょう.

class L, class M extends L があると仮定し, before/after execution (L+.new()) をくっつけた場合, L, M は次のような形式で展開されます.

public L() {
 super(); 
 // L のインスタンス変数初期化 (int x = 0; のようなフィールド初期化の展開)
 try {
   before execution の処理
   元のコンストラクタの中身
 } finally {
   after execution の処理
 }
}

実行順序は before(L) - after(L) - before(M) - after(M) となります. before(M) - before(L) とはならないので注意が必要です.

pointcut のクラスは標準ではサブクラスを含まない

ポイントカットとして記述するクラス名は,そのクラスのみを指し, サブクラスを含みません.サブクラスを含ませたい場合は,次のように記述します.

Foo のサブクラスすべての foo 呼び出しを捕らえる場合:
 ×:    pointcut FooCall(): call(* Foo.foo() );  
 ○:    pointcut FooCall(): call(* Foo+.foo() );
 ○:    pointcut FooCall(): target(Foo) && call(* foo());
注: call( Foo.foo() ) は,Bar のインスタンスが Foo 型変数に代入されている場合の
    呼び出し文には反応しますが,Bar 型変数に代入されている場合に反応しません.

別にこれ自体は珍しいことでも何でもないのですが,Foo のサブクラスが 後から追加されたときに,どの書き方をしているかによって影響が出てきます. クラスが継承される可能性を考えて,アスペクトはそのサブクラスを 含むつもりかどうかによって,2種類の記述を使い分けることが必要です. アスペクトがサブクラスを含んではならない理由がある場合は, その旨をコメントで残しておくなどしたほうが安全かもしれません.

例外ハンドラに対しては before アドバイスしか使用できない

この制約は,AspectJ 1.1 から発生し, Jim Hugunin がメーリングリスト上で指摘したものです.

AspectJ 1.1 では,一度 Java ソースコードをコンパイルしてから, バイトコード上で Weaving を行います. このため,例外 catch 節に対応するポイントカット記述である handler に対しては, after, around によってアドバイスを定義することはできません.

なぜかというと,例外 catch 節の終端がどこにあるかを, バイトコードから計算することが不可能であるためです. Hugunin は,以下のバイトコードの例を挙げています.

 Method void bar()
    0 invokestatic #2 <Method Test.foo()V>
    3 return
    4 astore_0
    5 invokestatic #2 <Method Test.foo()V>
    8 return
 Exception table:
    from   to  target type
      0     4     4   <Class java.lang.NullPointerException>

このバイトコードは,次の二通りのソースコードのどちらからでも生成されます.

 A:  public static void bar() {
       try {
         foo();
         return;
       } catch (NullPointerException ex) {
         foo();
       }
     }
 B:  public static void bar() {
       try {
         foo();
         return;
       } catch (NullPointerException ex) {
       }
       foo();
     }

この問題のため,AspectJ 1.1 では,handler に対しては before アドバイスのみを利用可能とする,ということになりました. この点は,AspectJ 1.0 とは非互換な部分です.

ポイントカットは思ったところにマッチしないときは?

アドバイスが正しく動かない!というときは,いくつか問題が考えられます.

  • アドバイスの本体の実装に間違いがある.
  • ポイントカット定義が間違っており,正しい実パラメータが得られていない.
  • ポイントカット定義が間違っており,正しいタイミングでアドバイスが動いていない.

ポイントカット定義の間違いをどう調べるかというと, あまりたいした方法ではありませんが, 思いつく限りの変種を色々並べる(いわゆるprintfデバッグの変形)というのが 1つの方法です.

静的に決定可能なポイントカット(cflow や if などを含まないもの)なら declare warning によって,プログラムを実行しなくても マッチする場所を調べることができます.

ポイントカット定義時に間違いやすいポイントなど,以下の記事で(英語ですが)指摘されています.

http://www.aspectcookbook.net/moin.cgi/DebugPointcutRecipe


トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2007-01-04 (木) 09:28:09 (4334d)