- 追加された行はこの色です。
- 削除された行はこの色です。
- AspectJ へ行く。
[[アスペクト指向プログラミング]]
* AspectJ [#cd43b2ec]
#contents
** AspectJ の特徴 [#rdd1986e]
AspectJ は,Java で[[アスペクト指向プログラミング]]を実現するための
言語仕様を追加したものです.
Java の部分は基本的にそのままで,
アスペクトを作るための "aspect" というモジュール単位と,
それを記述するための文法が加わっています.
AspectJ の構文情報については,[[./簡易リファレンス]]を作ってみました.
ApsectJ の提供するアスペクトは,ポイントカット+アドバイスを使用した動的な横断(dynamic crosscutting),インタータイプ宣言を用いた静的な横断(static crosscutting)の2種類の横断的関心事の取り扱いができます.
||動的な横断|静的な横断|
|仕組みの名前|ポイントカット+アドバイス|インタータイプ宣言|
|できること|メソッド呼び出し位置をまとめる|メンバーの宣言をまとめる|
|できること|複数箇所でのメソッド呼び出しをまとめる|複数箇所でのメンバー宣言をまとめる|
|処理内容|特定の条件が満たされたらアドバイスを呼び出す|特定条件のクラスにメンバーを追加する|
|処理タイミング|実行時判定(コンパイル時に最適化)|コンパイル時に処理|
*** ポイントカット+アドバイス [#s80b8ea1]
簡単なロギングの例を示します.
aspect Logger {
before() : execution(void Hoge.*()) || execution(void Fuga.*()) {
logger.log(thisJoinPoint.getSignature());
}
}
この中で,before() というのがアドバイスの宣言,":" 以降に指定されているのが
ポイントカットです.
ポイントカットによって指定された条件が成立した(する)とき,
アドバイスで指定されたコードが実行されます.
ポイントカットは,ここでは execution ... という述語を用いています.
AspectJ は,プログラム実行時に発生するイベントを表現するための述語を提供しており,
それらを組み合わせることで,いつアドバイスを実行したいかを記述します.
"{}" でくくられた範囲がアドバイスの本体で,中に実行するべき処理を記述します.
"thisJoinPoint" という特殊オブジェクトが存在することを除けば
アドバイスの中身は Java です.
[[./ポイントカット]]については今のところ記述を分割しています.
*** インタータイプ宣言 [#s1e6f635]
[[インタータイプ宣言]] は,AspectJが提供するもう1つの大きな機能で,
クラス群に新たなメンバーを(外部から)追加することができます.
クラスの定義を後から改変したり,
1つのクラスの定義を,複数のファイルに分割するために使用できます.
詳細については [[インタータイプ宣言]] の項目を読んでください.
* AspectJ のバージョンごとの違い [#cf32d910]
** コンパイラの違い [#r443f770]
AspectJ コンパイラは Java ソースコードとアスペクトのソースコードを読み込み,
最終的に Java バイトコードを出力し,実行することになります.
AspectJ 1.0 と 1.1 の間で,コンパイラのアーキテクチャが変わりました.
AspectJ 1.0 はソースコード上で変換作業を行い,Javaソースコードに展開した後,
通常のJavaコンパイラによってコンパイルされます.
中間状態のソースコードを出力する -preprocessオプションが使用可能です.
どのような結合処理が行われるかを見てみたいという人は,
一度 AspectJ 1.0 を使ってみると良いかもしれません.
AspectJ 1.1 以降では,一度バイトコードに変換した後,
バイトコード上でアスペクトの結合を行います.
そのため,Jar ファイル単位でバイトコードを
扱うための -injars オプション,-outjar オプションがサポートされているほか,
アスペクトだけをコンパイルした中間ファイルを扱えるようにもなっています.
そのかわり,中間結果のソースコードを
出力する -preprocess オプションは廃止されました.
バイトコード上での操作は,たとえば次のようになります.
トレース機能を実装したアスペクトのファイルが tracelib.lst にリストされているとき(1行に1つファイル名が並んだようなファイルを準備したとき),
アスペクトライブラリを次のようにして作成できます.
ajc -argfile tracing/tracelib.lst -outjar tracelib.jar
notrace.lst がトレース機能なしのアプリケーションのファイルリストである場合,
次のように,トレース機能付きアプリケーションを生成できます.
ajc -aspectpath tracelib.jar -argfile tracing/notrace.lst -outjar tracedapp.jar
トレース機能付きアプリケーションを動作させるときは,
次のようにアスペクトライブラリをクラスパスに含めてやります.
java -classpath "$ajrt;tracedapp.jar;tracelib.jar" tracing.ExampleMain
また,クラス側を先にコンパイルしておいた場合は次のようになります.
ajc や javac を使って,アプリケーションをコンパイルします.
-noweave オプションを使って,-injars への入力専用のファイルを生成しています.
ajc -noweave -argfile tracing/notrace.lst -outjar appInput.jar
あるいは
mkdir classes
javac -d classes tracing/*.java
jar cfM app.jar -C classes .
次に,トレース機能を付加します.
app.jar のほうは単体で動作し,appInput.jar は ajc への入力が前提であるという
違いはありますが,使い方としては同じです.
ajc -injars appInput.jar -aspectpath tracelib.jar -outjar tracedapp.jar
ajc -injars app.jar -aspectpath tracelib.jar -outjar tracedapp.jar
プログラムの実行方法は,先ほどと同じです.
java -classpath "$ajrt;tracedapp.jar;tracelib.jar" tracing.ExampleMain
トレースなしのアプリケーションは,通常の javac でコンパイルしたものを使います.
java -classpath "app.jar" tracing.ExampleMain
** AspectJ 1.0 → 1.1 での変更 [#bc499396]
*** ポイントカットの拡張 [#sfeab4be]
AspectJ 1.1 では,アドバイスの実行にアスペクトを貼り付けるための adviceexecution と,オブジェクトの初期化プロセスが始まる前に貼り付けるための preinitialization が加わりました.以下のような使い方が想定されています.
adviceexecution() && within(Aspect); // あるアスペクト内部のアドバイス実行
preinitialization(ConstructorPattern); // あるコンストラクタにマッチする初期化プロセス
*** dominates に代わる precedence [#t300f4ce]
AspectJ 1.1 では,アスペクトが他のアスペクトに制御されることを意味する 宣言として,以前まで用いられている dominates の代わりに, precedence 宣言を使うことになりました. 1.1 では,dominates 宣言はコンパイルエラーとなります.
precedence 宣言は,次のように記述します.
declare precedence ":" TypePatternList ";"
ある join point で,複数のアスペクト A, B が動作するとき, 通常はコンパイラが勝手に順序を決定します. A dominates B と宣言されているとき,A は B より外側(beforeなら先に,afterなら後に) 動作するようになります. 新しく導入された declare precedence は declare dominates を置き換えます. precedence は複数のアスペクト間の優先順序を表現します, たとえば,セキュリティアスペクトが他のアスペクトより優位であること, またロギングアスペクトがセキュリティ以外のアスペクトに対して優位であることを表現するために, 次のように記述することができます.
declare precedence: *..*Security*, Logging+, *;
ここで,ワイルドカードである '*' は他でマッチしなかったようなアスペクトだけが含まれます.
また,次のような記述も可能です.
declare precedence: B, A;
declare precedence: A, B;
この記述は,A と B が共通の join point を持たない限りは有効となるので, この記述を使って,AとBが相互に依存していないことを示すことができます. ただし,依存していないからといって, A と Bがアスペクトの無限ループを生成しないことは保証できないことには注意が必要です.
なお,上記のサンプルコードについては,AspectJ 1.1beta4 Readme から抜粋しました.
** AspectJ 1.1 → 1.2 での変更 [#t9139aa4]
2004年5月25日付けで,AspectJ 1.2 がリリースされました.
AspectJ 1.2 Release Note によると,言語的な変更はなく,
ツールの改善に費やされているようです.
基本はバグ修正なので,1.1 からはあまり悩まずに移行したほうが良いと思います.
ユーザの視点からは,主として,次のような点が影響を受けます.
- コンパイル時間の短縮
- エラーメッセージの改善
- 複数回のウィービングを可能にする(ウィーブ済みクラスにさらに他のアスペクトのウィーブを許可する) -Xreweavable オプションの追加
- JAR 以外にディレクトリを許可する -injar のかわりの -inpath の追加
- Java 1.4 構文への対応オプションが標準設定になった
- アドバイスがどのクラスに影響を及ぼすか,などの情報を JavaDoc として提供するajdoc が再び配布されるようになった(1.1では配布されていませんでした).
- SoftException が Chained Exception で使用される getCause() に対応.
** 1.2 → 1.5M2 [#bf0ae2f7]
最も重要な変化は,annotation,Generics のコードがコンパイルできるようになったことです."-1.5" オプションを指定してコンパイルします.
* AspectJ コンパイラのメモリ使用量と制限 [#z47fe024]
アスペクトを結合する処理は,かなりの量のメモリを消費します.しかし,AspectJ は,実行マシンのメモリを食い尽くさないように, JVM のメモリ制限機能を使って,自分が利用できるメモリを64MB に制限しています.このため,ある程度の規模のプログラムをコンパイルしようとすると,メモリ不足( OutOfMemory )のエラーが出ることがあります.これに対しては,利用可能なメモリを増やしてやることで対応できます.
(AspectJ)/bin/ajc (Windowsではajc.bat) の中身に
"%JAVA_HOME%\bin\java" -classpath "%ASPECTJ_HOME%\lib\aspectjtools.jar;
%JAVA_HOME%\lib\tools.jar;%CLASSPATH%" -Xmx64M org.aspectj.tools.ajc.Main %*
という行があります.この中の -Xmx64M というのがメモリ制限のオプションで,
ここを -Xmx256M のように変更することで,メモリの利用量を設定できます.
処理するアスペクトによっては膨大なメモリを要求することがあるので,
この値は実行マシンに載っている物理メモリにある程度合わせるようにしましょう.
* AspectJ の生成コードの問題 [#hd7b09fd]
** 定数テーブルが大きくなってコンパイルに失敗する [#n48b4a5a]
アスペクトを結合しようとした結果,定数が多すぎる
(定数テーブルが64KBを超える)ということでエラーになる場合があります.
これは,JVM の仕様による制限で,
"4.10 Limitations of the Java Virtual Machine"に記述されています.
通常はコンパイラがこのエラーを検知しますが,
一部のコンパイラではこのエラーチェックを行わず,
JVM がクラスファイルをロードした時点で「不正なクラスファイルである」
と表示される場合もあります.
AspectJ では,join point のソースコード内での位置情報などにアクセス
しようとした場合,そのための情報をクラスの static フィールドに書き出すため,
巨大なクラスの中の多数の join points にアスペクトを結合しようとすると,この制限に抵触する可能性が出てきます.これを避けるためには,なるべく join point の情報にアクセスしないようにする,具体的には thisJoinPoint 変数を多用しすぎないということが必要です.
通常の用途ではまず起こらないエラーですが,コード生成ツールを使って機械的に生成した冗長なコードが大量にある場合には引っかかる可能性があるのでここに紹介しておきます.
註: 以前は「不正なクラスファイルが生成される」というタイトルでしたが,このエラーは本来コンパイラが検知すべきものであるという指摘と共に JVM仕様書へのポインタをいただきましたので,それにあわせて内容を訂正,タイトルも変更しました.情報提供に感謝します.
** パッケージに属さないアスペクトの分割コンパイル問題 [#a6069d88]
aspect を定義するときは,きちんとパッケージに所属させるようにしましょう. aspect を結合するプログラムを,たとえば次のように分割コンパイルする
場合があったとします.
ajc Foo.java HogeAspect.java
ajc bar/Bar.java HogeAspect.java
このとき,HogeAspect.class は2回生成されます.アスペクトがパッケージに所属している場合は絶対パスによってクラスの位置が解決されるため2回のコンパイルで同一のコードが生成されます.しかし,アスペクトがパッケージに所属していない場合は,一緒にコンパイルしたファイルからの相対位置によってクラス名が解決されるため,1回目と2回目のコンパイルで,微妙に異なるコードが生成される場合があります.これが原因で,実行時に「クラスパスが見つからない」エラーが発生する場合があるので,アスペクトはなるべく何らかのパッケージに所属させるようにしたほうが良いようです.
** インタフェースに対するアスペクト結合 (1.0より前のAspectJのバグ) [#n193d1bc]
バージョン 0.8 などの古い AspectJ では, interface に対して execute のような実装を変更するタイプの advice を結合した場合に, interface のメソッド宣言に対してもその advice を実行するような定義が追加されてしまって「interface は実装を持ったらダメだよ」とコンパイラに怒られる場合があります.このような場合は,interface を定義したファイルをアスペクトとは別にコンパイルするか,あるいは pointcut の定義で !within などを使って明示的に interface を除外します.
* アスペクトの結合と,バイナリ改変禁止ライセンスの問題 [#c7d90e33]
ソースコードレベルでのライセンスの議論は [http://www.egroups.co.jp/message/aosd-jp/62 aosd-jp ML]で話題になっていますが,
ここではバイトコード改変としての話題について触れます.
AspectJ 1.1 以降,アスペクトの結合処理は,クラスの Java バイトコードをアスペクト付きバイトコードに変換する作業となりました.これは「バイナリの改変」と考えられるため,クラスロード時の変換処理であっても,バイナリの改変を許可していないライセンスに違反することになります.この影響を受ける代表格が Sun の JDK クラスライブラリです.
バイナリ改変後のコードの再配布が許可されていないライセンスの場合,アスペクトを結合したライブラリを配布するのは NG となりますし,「改変するためのアスペクト」を配布するのもグレーである(AspectJ チームメンバーにはそれが OK だと断言できない)ということから,サードパーティのライブラリにアスペクトを結合する場合は注意するように,というのがAspectJ チームの意見となっています.この旨は,明快に AspectJ の FAQ に記述されています.
FAQ 13. Understanding AspectJ Technology (4) より抜粋:
And just to state the obvious: do not use bytecode weaving,
at load-time or otherwise, to modify .class files protected by license,
without permission from the licensor.
アスペクトの利用者は,アスペクトを含んだライブラリを他のライブラリと同時にコンパイルするとき,ライセンスで保護されたライブラリにまでアスペクトの影響を及ぼしてしまわないよう,注意が必要です.
今のところ,これといった決まりきった方法はありませんが,ライセンス保護ライブラリは結合対象から除くように命令を記述するといった工夫が必要になるかもしれません.
** バイナリ改変ライセンスに抵触しないアスペクト [#j6c52d31]
AspectJ-users メーリングリスト上で,「どのアスペクトがどのコードを改変するのか」という意見に対する Wes Isberg の答えです:
メソッド呼び出し文に連動するアドバイスは呼び出し側だけを改変します.そのため,「ライブラリへの呼び出し」に対するアドバイスはライブラリのコードを改変しません.
ただし,ライセンスで保護されたコード内部にも同様の呼び出し処理が
含まれている可能性がありますから,それらを除去するように
(もしくはユーザが書いたコードだけが対象となるように)
アスペクトを記述する必要があります.