私はかつてJava programming dynamics(参考文献)というシリーズの中で、クラスワーキングの手法を使うことによって、プログラムの振る舞いにシステム的変更が行えることを示しました。この種の手法は、Java™プラットフォームでのAOPで行われている大部分の作業の基礎となっています。この記事では、同じようなシステム的変更をプログラムの振る舞いに加えるために、クラスワーキングの上に高位レイヤーを構築するAOPフレームワークを使って、使いやすさを向上させる方法を説明します。
Javaプラットフォームでは、数多くのAOPフレームワークを使うことができます。このシリーズの記事では、AspectWerkzフレームワークを使うことにしました。これは、BEA Systemsがスポンサーであるオープンソースのプロジェクトです。AspectWerkzは、Java技術用のAOPとして一番古いわけではありません。一番古いものとしては、IBMがサポートする、Javaプログラミング言語のAspectJエクステンションがあります。しかしAspectWerkzは、標準のJavaコードと組み合わせてAOPを使うための高速で強力、そして柔軟なフレームワークとして、大きな注目を集めているものです。AspectWerkzとAspectJの両チームは最近、合体するという発表を行いました。ですから今後は、両者の最高機能をまとめて1つの製品としたものが見られるようになるでしょう。これについては今後の記事で触れますが、ここではとりあえず、ベースとなるAspectWerkzフレームワークに集中することにします。
注意: AOPの実用的な使い方の紹介として、新しいAOP@Workコラムをぜひ見てください。このコラムは2005の2月に始まり、1年間継続される予定ですが、最先端のAOPエキスパート5人が執筆しており、どの記事にも、皆さんがすぐに応用できるような知識が提供されています。
ロギングは、日々の開発の中でAOP推進者がAOPのアプリケーションをリストアップする時には、必ず取り上げられるようです。ロギングは、典型的な企業アプリケーションのコードの中で、確かに大きな部分にまたがる懸念事項です。また、ロギング・コードはアプリケーションの主目的とは無関係ですが、また同時に、非常にコードの中に侵入しやすいものです。ロギングにはこのような特徴があるため、アスペクトとして構成すべき機能の候補としては理想的です。ここでは、こうした種類の問題領域に対してアスペクトのパラダイムがいかにうまく応用できるかを示すために、ロギングとパフォーマンス・メトリック・アスペクトを組み合わせた実装を見て行きます。
AspectWerkzのWebサイト(参考文献)には、詳細なドキュメンテーションと使用例、そして、AspectWerkzを使った記事へのリングが掲載されています。ここでそれらを再掲することはしませんが、その中で私が最も重要な概念と思うもの、つまりポイントカット(pointcuts)とアスペクト、そしてアドバイスを簡単に紹介しておきましょう。
ポントカットは基本的に、アプリケーション実行の「通常の」(ソースコードで定義された通りの)フローへに割り込み、何か別のことをするための、単なる場所にすぎません。AspectWerkzは、Javaコード構造(例えばメソッド・コールや実行、フィールド取得/設定、例外ハンドラー実行、そして特定な実行パス内のコードを含む組み合わせなど)に結びついた多くの種類のポントカットをサポートしています。
AspectWerkzの用語では、アスペクトはクロスカット・コンサーンのターゲットとして適用される、単なるJavaクラスにすぎません。クラスをアスペクトにするためには、特別なインターフェースの実装は必要ありませんが、アスペクトとしてクラスにアクセスするためには、特定なメソッド・シグニチャーが要求されます。一般的にアスペクト・クラスは、ほとんどの場合AspectWerkzフレームワークでしか使われないため、標準のアプリケーション・クラス階層構造の外に置かれるでしょう。しかし、繰り返しますが、(ランタイムのクラスパスを除き)どこか特別な場所になければならない、ということはありません。
アドバイスはアスペクト・クラスの中のメソッドであり、ポイントカットで行うべき「何か違うこと」として使われます。アドバイスに対するデフォルトのメソッド・シグニチャーは、1つのパラメーターを取ることであり、このパラメーターが、捕捉されるポイントカットに関する情報を提供します。リスト1は、メソッド・コールにおいて前アドバイス、後アドバイスとして使われるべき、1対のメソッドを定義するアスペクト・クラスです。
リスト1. 単純なアドバイス・クラスとアドバイス・メソッド
package com.sosnoski.aspectwerkz;
public class TraceAspect
{
public void beforeMethod(JoinPoint join) {
System.out.println("Entering method");
}
public void afterMethod(JoinPoint join) {
System.out.println("Leaving method");
}
}
|
AspectWerkzは他のメソッド・コール・シグニチャーもサポートしていますが、その設定に関しては少し余分な努力が必要です。そう、設定と言えば、AspectWerkzは次の3つの形式の設定をサポートしていることを最初に言っておくべきでしょう。
- ソースコードに埋め込まれたJava 5.0アノテーション
- ソースコードに埋め込まれたJavadocスタイルのアノテーション
- 外部XMLファイル
私はXML的人間なので(私はdeveloperWorksのXMLゾーンにも定期的に寄稿しています)、この記事ではXMLオプションを使うことにします。リスト2は、リスト1のアドバイスに対するXML設定の例です。
リスト2. XML設定の例
<aspectwerkz>
<system id="AspectWerkzExample">
<package name="com.sosnoski.aspectwerkz">
<aspect class="TraceAspect">
<pointcut name="longMethod"
expression="execution(long ..(..))"/>
<advice name="beforeMethod" type="before"
bind-to="longMethod"/>
<advice name="afterMethod" type="after"
bind-to="longMethod"/>
</aspect>
</package>
</system>
</aspectwerkz>
|
リスト2の設定は、クラス、com.sosnoski.aspectwerkz.TraceAspectの中で実装されている通り、1つのアスペクトを定義しています。このアスペクトに対して、リスト2の設定では、long値(一致対象の表現内の「..」シーケンスはワイルドカードです)を返す任意メソッドの実行を表す1つのポイントカットを定義しており、2つの別々のアドバイス片を、このポイントカットにバインドしています。最初のアドバイスは、そのアドバイス・クラスのbeforeMethod()が実装し、タイプはbefore です(つまり、このアドバイス・メソッドは、ポイントカット・メソッドが実行される前に呼ばれます)。2番目のアドバイスは、afterMethod()が実装し、タイプはafterです(つまり、このアドバイス・メソッドは、ポイントカット・メソッドが実行された後に呼ばれます)。
さて、AspectWerkzの基本的な概念は理解しましたが、こうした概念をロギング機能にどう適用するのでしょう。この前のセクションでのメソッド・コールの例が、出発点として適当なようです。対象とするメソッド・コールの前と後に、AspectWerkzがそれぞれbeforeMethod()とafterMethod()を実行するように設定できれば、これらはアプリケーションの実行トレースとして有効に使えるはずです。
もちろん、実行トレースを有用なものとするためには、最小限の情報しか出力していない、リスト1のメソッドの出力を拡張する必要があります。幸いAspectWerkzには、捕捉されるポイントカットに関する情報に簡単にアクセスできる方法があります。リスト3のコードは、実際のメソッド情報にポイントカットからアクセスする方法を示しています。
リスト3. アドバイス・メソッドが捕捉したメソッドをレポートする
package com.sosnoski.aspectwerkz;
public class TraceAspect
{
public void beforeMethod(JoinPoint join) {
MethodSignature signature =
(MethodSignature)join.getSignature();
System.out.println("Entering method " +
signature.getName());
}
public void afterMethod(JoinPoint join) {
MethodSignature signature =
(MethodSignature)join.getSignature();
System.out.println("Leaving method " +
signature.getName());
}
}
|
この暫定ロギング・コードをテストするために、ちょうど良い再帰的メソッドとして、フィボナッチ数列計算の実装を使います。これをリスト4に示します。
リスト4. フィボナッチ数列の計算
package com.sosnoski.aspectwerkz;
public class FiboTest
{
private long fibo(int value) {
if (value > 2) {
return fibo(value-1) + fibo(value-2);
} else {
return 1;
}
}
public static void main(String[] args) {
FiboTest inst = new FiboTest();
int value = Integer.parseInt(args[0]);
System.out.println("Fibo[" + value + "] is " +
inst.fibo(value));
}
}
|
このコードを直接実行するのは簡単です。
[dms]$ java -cp classes com.sosnoski.aspectwerkz.FiboTest 6 Fibo[6] is 8 |
AspectWerkzを使っての実行は、もう少し複雑です。AspectWerkzを使ってアスペクト処理を実装するには、ビルドタイム・クラス・モディフィケーション(build-time class modification)とランタイム・クラス・モディフィケーション(run-time class modification)という、2つの方法があります。この記事では全体を通して、ランタイム版の方を使います。この方法では、AspectWerkzフレームワークの一部として提供されている、カスタムのクラスローダーが必要です。AspectWerkzインストールのホーム・ディレクトリーに対する環境変数を使い、また(リスト2の)XML設定がaspectwerkz.xmlファイルの中にあるとすると、リスト4のコードをAspectWerkzを使ってLinux™版あるいはUNIX®版用に書くと、次のようになります。
[dms]$ $ASPECTWERKZ_HOME/bin/aspectwerkz -Daspectwerkz.definition.file=aspectwerkz.xml -cp classes com.sosnoski.aspectwerkz.FiboTest 6 |
この結果の出力も、ずっと長くなります。ここでは(そして他の例でも)、AspectWerkzが出力するスタートアップ情報は無視し、私のアドバイスが生成する出力のみを示します。出力は、リスト5のようになります。
リスト5. アドバイスを使った計算結果
Entering method fibo
Entering method fibo
Entering method fibo
Entering method fibo
Entering method fibo
Leaving method fibo
Entering method fibo
Leaving method fibo
Leaving method fibo
Entering method fibo
Leaving method fibo
Leaving method fibo
Entering method fibo
Entering method fibo
Leaving method fibo
Entering method fibo
Leaving method fibo
Leaving method fibo
Leaving method fibo
Entering method fibo
Entering method fibo
Entering method fibo
Leaving method fibo
Entering method fibo
Leaving method fibo
Leaving method fibo
Entering method fibo
Leaving method fibo
Leaving method fibo
Leaving method fibo
Fibo[6] is 8
|
リスト5は、リスト3のアドバイス・メソッドが呼ばれていることを示していますが、実行されているアプリケーションに関しては、大して有効な情報を示していません。この欠点を修正しましょう。メソッド・コールのネストを段付けして示すように、また、やり取りされる値も出力するように、アドバイスを変更します。そのために、アドバイスのタイプも、aroundアドバイス・タイプという別のタイプに変更します。リスト6は、この修正を行ったコードを示しています。
リスト6. 段付けと値の印刷を行うアドバイス・メソッド
private int m_nestingDepth;
private void indent() {
for (int i = 0; i < m_nestingDepth; i++) {
System.out.print(' ');
}
}
private void printCall(JoinPoint call) {
MethodSignature signature =
(MethodSignature)call.getSignature();
System.out.print(signature.getName());
System.out.print('(');
MethodRtti rtti = (MethodRtti)call.getRtti();
System.out.print(rtti.getParameterValues()[0]);
System.out.print(')');
}
public Object traceLong(JoinPoint join) throws Throwable {
// print entering information
indent();
System.out.print("Entering method ");
printCall(join);
// execute the joinpoint method
m_nestingDepth++;
Object result = join.proceed();
m_nestingDepth--;
// print exiting information
indent();
System.out.print("Leaving method ");
MethodSignature signature =
(MethodSignature)join.getSignature();
System.out.print(signature.getName());
System.out.print('<');
System.out.println(result);
// return result from method call
return result;
}
|
名前から想像できるかも知れませんが、aroundアドバイスは、ポイントカット実行の前後を囲みます。このアドバイスは、ポイントカットを実行すべき時に呼ばれますが、ポイントカットを実際にいつ実行するのか、さらには、実際に実行するかどうかさえも決めるだけの柔軟性を持っています。リスト6のコードでは、aroundアドバイスの処理を簡単にしてあります。ここではアドバイス・コード(traceLong()メソッド)はまず、カレントのネスト・レベルまで段付けされた、メソッド名と引数の出力から始めます。次にそのメソッドを呼んで結果を保存した後、カレントのネスト・レベルまで段付けされたメソッド名と戻り値を出力して終了します。
XML設定は、この新しいアドバイスを使うように調整する必要がありますが、これは簡単な変更です。下記は、変更したファイルからのaspect要素です(変更は太字で示してあります)。
<package name="com.sosnoski.aspectwerkz">
<aspect class="TraceAspect">
<pointcut name="longMethod"
expression="execution(long ..(..))"/>
<advicename="traceLong" type="around"
bind-to="longMethod"/>
</aspect>
</package>
|
リスト7は、この新しいアドバイスを使って生成された出力です。
リスト7. 段付けと値の印刷を行う出力
Entering method fibo(6)
Entering method fibo(5)
Entering method fibo(4)
Entering method fibo(3)
Entering method fibo(2)
Leaving method fibo<1
Entering method fibo(1)
Leaving method fibo<1
Leaving method fibo<2
Entering method fibo(2)
Leaving method fibo<1
Leaving method fibo<3
Entering method fibo(3)
Entering method fibo(2)
Leaving method fibo<1
Entering method fibo(1)
Leaving method fibo<1
Leaving method fibo<2
Leaving method fibo<5
Entering method fibo(4)
Entering method fibo(3)
Entering method fibo(2)
Leaving method fibo<1
Entering method fibo(1)
Leaving method fibo<1
Leaving method fibo<2
Entering method fibo(2)
Leaving method fibo<1
Leaving method fibo<3
Leaving method fibo<8
Fibo[6] is 8
|
リスト7の出力であれば、多少使い道がありそうに見えますが、選択性に欠けるのが難です。フィボナッチ数列の指数に、もっと大きな数字(例えば12)を使ってこのコードを実行すると、出力が延々と上にスクロールして行くのを眺める羽目になるでしょう。有用なログ・アスペクト開発の最終ステップとして、出力をうまくフィルターする実装をしてみましょう。
また、実行に一番長く時間がかかるメソッド・コールはどれかが分かるように、ログの実行パスと関連パフォーマンス・メトリックスも得られるようにしたいと思います。ここで使う手法は、私がJava programming dynamics(参考文献)の中でダイレクト・クラスワーキングを使って説明した手法と似ており、java.lang.System.currentTimeMillis()時間値に基づいて行う方法です。このタイマーは高精度なものではありません。Microsoft® Windows®での分解能は約17 ms、私のLinuxシステムでは 1 ms です。ただし、このタイマーは標準Javaプラットフォームであれば必ず使え、デモ用程度でには十分便利なものです。(次回は、タイマーの分解能を上げる方法を議論する予定です。)メソッド・コールにかかった時間で実行トレースをフィルターすれば、面白い結果が出るはずです。
出力の構成を同じに保つため、この、時間ベースのフィルタリングは、少し複雑になります。まず、そのメソッドの要した時間がフィルターを通すのに十分かどうかが分かるまで、各メソッド・コールに対する「入力行」の出力を遅らせる必要があります。これ自体は大したことはありませんが、メソッド情報を本当に出力する段になると、それを囲んでいるメソッド・コールそれぞれに対する入力行を、まず出力しなければなりません。そのためには、まだ出力していないメソッドに対するエントリー情報のリストを保持する必要があり、しかも、カレントのメソッドに対するエントリー情報を出力する前に、これら全てを適切に段付けして吐き出す必要があります。この、未出力メソッドのリストを使うように修正した、新しいアドバイス・メソッドをリスト8に示します。
リスト8. タイミング・フィルターを持つアドバイス・メソッド
private ArrayList m_unprintedCalls = new ArrayList();
private void indent(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print(' ');
}
}
public Object traceTimed(JoinPoint join) throws Throwable {
// add joinpoint to list pending print
m_pendingPrints.add(join);
// time joinpoint execution
long start = System.currentTimeMillis();
m_nestingDepth++;
Object result = join.proceed();
m_nestingDepth--;
// remove joinpoint if still on pending list
// (will always be last in list)
boolean entered = true;
if (m_pendingPrints.size() > 0) {
m_pendingPrints.remove(m_pendingPrints.size()-1);
entered = false;
}
// check if execution time above cutoff
long time = System.currentTimeMillis() - start;
if (time > 1) {
// print all unprinted "entering" lines
int count = m_pendingPrints.size();
for (int i = 0; i < count; i++) {
// print entering information for nesting call
// (nesting depth based on start of pendings)
indent(m_nestingDepth-count+i);
JoinPoint prior =
(JoinPoint)m_pendingPrints.get(i);
System.out.print("Entering ");
printCall(prior);
System.out.println();
}
// clear all pendings now that they're printed
m_pendingPrints.clear();
// print exiting information including time
indent();
System.out.print("Leaving ");
printCall(join);
System.out.print('=');
System.out.print(result);
System.out.print(" took ");
System.out.print(time);
System.out.println(" ms.");
}
// return result from joinpoint execution
return result;
}
|
traceLong()の代わりにtraceTimed()アドバイス・メソッドを使うようにXML設定ファイルを修正した後、同じ引数値「6」を使ってこれを実行すると、トレース情報は全く見えなくなります。これは、全体を実行してもタイマーの最小時間以内であるためです。「6」の代わりに値「15」を使うと、リスト9に示すような出力が得られます。
リスト9. タイミング・フィルターを持つ出力
Entering fibo(15)
Entering fibo(14)
Entering fibo(13)
Entering fibo(12)
Leaving fibo(11)=89 took 2 ms.
Leaving fibo(10)=55 took 2 ms.
Leaving fibo(12)=144 took 11 ms.
Entering fibo(11)
Leaving fibo(10)=55 took 2 ms.
Leaving fibo(9)=34 took 2 ms.
Leaving fibo(11)=89 took 5 ms.
Leaving fibo(13)=233 took 19 ms.
Entering fibo(12)
Leaving fibo(11)=89 took 2 ms.
Entering fibo(10)
Entering fibo(8)
Entering fibo(6)
Leaving fibo(4)=3 took 2 ms.
Leaving fibo(6)=8 took 4 ms.
Leaving fibo(8)=21 took 6 ms.
Leaving fibo(10)=55 took 6 ms.
Leaving fibo(12)=144 took 10 ms.
Leaving fibo(14)=377 took 29 ms.
Entering fibo(13)
Entering fibo(12)
Leaving fibo(11)=89 took 2 ms.
Leaving fibo(10)=55 took 2 ms.
Leaving fibo(12)=144 took 6 ms.
Entering fibo(11)
Leaving fibo(10)=55 took 2 ms.
Leaving fibo(11)=89 took 3 ms.
Leaving fibo(13)=233 took 9 ms.
Leaving fibo(15)=610 took 95 ms.
Fibo[15] is 610
|
リスト9の出力こそ、私が欲しかったものです。では、フィボナッチ数列を卒業し、本物のアプリケーションに移る時です。それを次回に説明しましょう。
このコラムでは、AspectWerkzを使いながら、パフォーマンス・メトリックス機能を持つ基本的な実行トレースの実装方法を見てきました。次回は、この概念をさらに推し進め、トレース・アスペクトを拡張します。そしてトレース・アスペクトに組み込みのロギングを、実世界の複雑なフレームワークに応用し、アスペクト手法がうまく行く場合と、うまく行かない場合とを見て行きます。アスペクトは、ロギング・コードの汚染から私達を守ってくれるのでしょうか。次回の記事にご期待ください。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| ソース・コード | cwt-source.zip | 6.72KB | HTTP |
- この記事の先頭、または最後にあるアイコンCodeをクリックして(またはダウンロード・セクションを見てください)、この記事で使用しているコード例をダウンロードしてください。
- Dennis Sosnoskiによる、クラスワーキング・ツールキット・シリーズ の全記事を読んでください(日本語に翻訳されていない記事もあります)。
- AspectWerkzについて詳しく知るためには、このプロジェクトのWebサイトを見てください。豊富なドキュメンテーションや、様々な記事へのリンクが網羅されています。
- Nicholas Lesieckiによるアスペクト指向プログラミングで、モジュール性を改善する(developerWorks, 2002年1月)を読んで、Java言語用のAspectJアスペクト指向エクステンションについて学んでください。
- Nicholas Lesieckiは、AOPの実際的な使い方に焦点を当てた、新しいAOP@Workシリーズの執筆チームのリーダーとなっています。
- Filippo DiotaleviによるAOPを使いコントラクトを実施する(developerWorks, 2004年7月)を読んで、コンポーネント間のアグリーメントのバインディングにとって、AOPがどれほど強力かを学んでください。
- コンポーネントに柔軟性がなくて困っているのであれば、Andrew GloverによるAOPが密結合の憂うつさを取り除く(developerWorks, 2004年2月)が良いヒントになるかも知れません。
- Gary PolliceによるA look at aspect-oriented programming(developerWorks, 2004年1月)はAOPの詳細以上の話題、つまりソフトウェア開発プロセスにもたらす影響やAOPが直面する困難などについて議論しています。
- 既存のシステムを理解し、維持する上でAOPがどのように役立つかを、Abhijit BelapurkarがレガシーJavaアプリケーションの保守にAOPを使用する(developerWorks, 2004年3月)の中で解説しています。
- この記事の著者のDennis Sosnoskiによる、Javaプログラミング・ダイナミックス・シリーズの全記事を読んで、Javaのクラス構造、リフレクション、クラスワーキングなどを知るツアーに出かけてください(日本語に翻訳されていない記事もあります)。
-
developerWorksのJava technologyゾーンには、Java技術に関する資料が他にも豊富に用意されています。技術的なドキュメンテーションや、ハウツー記事、教育資料、ダウンロード、製品情報など、様々な情報を得ることができます。
-
New to Java technologyには、Javaプログラミングを始めるための助けとなる最新情報が用意されていますので、ご覧ください。
-
Developer Bookstoreには、Java関連の書籍をはじめ、広範な話題を網羅した技術書が豊富に取り揃えられていますので、ぜひご利用ください。

Dennis Sosnoskiはシアトル地域にあるJava技術のコンサルティング会社、Sosnoski Software Solutions, Inc.の創立者で、主席コンサルタントでもあり、またXMLやWebサービスに関するトレーニングやコンサルティングの専門家でもあります。彼のプロとしてのソフトウェア開発経験は30年以上に渡り、ここ数年はサーバー側のXML技術やJava技術に注力しています。Dennisは、全米各地で行われる会議で頻繁に講演を行っています。また、Javaクラスワーキング技術を基に構築された、オープンソースのJiBX XML Data Bindingフレームワークの中心開発者でもあります。