AspectJ 環境を構築しよう

Last Updated : 2004/7/24 (2002/9/1 正式公開)
asato <asato@ncfreak.com>


注意 - その 1: AspectJ のバージョン 1.1 に向けて移行中です。1.0.6 でのコードと 1.1rc1 でのコードが混ざっています。基本的には 1.0.6 でテストされています。"1.1rc1 OK" という註釈があれば 1.1rc1 でテストされています (その他のバージョンについても同様)。何もなければ 1.0.6 では動作するはずですがそれ以降のバージョンではそのままではうまく動作しないかもしれません (異なる動作をするかもしれません)。コードだけでなく、文章についても同様です。

注意 - その 2: ところどころにコードに色が付いていたり付いていなかったりするのには、特に深い意味はありません。ただ、できるかぎり読みやすくしようと思って色をつけようとしていますが、一部には、まだ付けいてないだけです。

注意 - その 3: できるかぎり用語を適切に使うようには努力していますが、一部には間違っている部分もあると思います。プログラミングするだけでは、用語の正しい使い方については、気にする必要はないと思いますが、文章 (この記事のような) を書く場合には、正しい用語の使い方は、望ましいものです。とはいえ、多少億劫です。この記事で使われている用語を鵜呑みにしていないでください

記事の概要

この記事では AspectJ を使って AOP (Aspect-Oriented Programming : アスペクト指向プログラミング) を学ぼうと思っている方むけに以下のことを紹介します: advice、Pointcut Designators、Inter-type declarations、そしてアスペクト関連の各項目について、豊富な サンプルコードを提供しています。

テスト環境

この記事を読む前に

著者は、AspectJ + AOP のエキスパートでは ありません ので、この記事を読む前に、より経験者から見た AspectJ + AOP のプログラミングの雰囲気を知り、また、誤読(AspectJ + AOP の過小評価や過大評価)を避けるためにも、developerWorks が提供している以下の記事を先に読む方が良いかもしれません。 また AspectJ のサイト で見ることができる (ダウンロード もできます)The AspectJ Programming GuideFAQ のドキュメントも参考になりますので、見逃さないようにしてください。

AspectJ によるアスペクト指向プログラミング実例

この章では、アスペクト指向プログラミングが効力を発揮する場面として、ロギングへの適用を考えます。

読者の方には、この章を読む代わりに、同様のテーマを扱った文章である、Nicholas Lesiecki 氏の書いた「アスペクト指向プログラミングで、モジュール性を改善する - Java言語の世界に AOP をもたらした AspectJ」を読むことを勧めます(この章を書くに当たって、大いに参考にしました)。筆力と経験の差は歴然です!

ロギング

アスペクト指向プログラミングが適した状況になるような実例として、ロギングの処理がよくあげられます。その理由の 1 つとしては、従来の Java + OOP という技術だけを用いるのでは、クリーンな解決策を得ることができないためです。AspectJ を用いることで、従来の方法では、ゴチャゴチャなコードになってしまうようなプログラム上の要求をうまく扱うことができます。

この節では、アプリケーションの実行中に実行されるすべてのメソッドの呼び出しをログすることが要求されるような場面を考えます。このような要求に対しての、AspcetJ を使った解決策を紹介します。また、AspcetJ を使うにあたって重要な概念である、join point、pointcut、advice についてを簡単に述べます (これらの概念と使い方についてのサンプルコードなどは、後の章で詳しく紹介しています)。

たとえば、すべてのクラスのすべてのメソッドについて、その 実行 を記録したいという要望があるとすると、従来の Java でのアプローチのコードはどのようなものになるでしょうか。まずは、次のような Hello クラスと Bye クラスがあると考えてください。両クラスは、それぞれ Hello クラスは "Hello!" を出力する hello() メソッドを、Bye クラスは "Bye!" を出力する bye() メソッドを実装しています:

public class Hello {
	public void hello() {
		System.out.println("Hello!");
	}
}
public class Bye {
	public void bye() {
		System.out.println("Bye!");
	}
}
今、要求されている、すべてのクラス (この例では Hello クラスと Bye クラス) のすべてのメソッド (Hello.hello() メソッドと Bye.bye() メソッド) について、それらのメソッドを実行した、ということを記録したいとは、hello() メソッドと bye() メソッドの 実行 に、そのシグニチャ("hello()" か "bye()" )を出力することを意味しているとします。

たとえば、そのためには、次のようにしてその要望を満たすコードを (AspectJ が使えないとすると) 書くことができます:

public class Hello {
	public void hello() {
		System.out.println("hello()");
		System.out.println("Hello!");
	}
}
public class Bye {
	public void bye() {
		System.out.println("bye()");
		System.out.println("Bye!");
	}
}
この解決策が十分でないのは、この例では、たった二つのクラス (Hello と Bye) のみを考えていますが、実際にはより多くのクラスに対しても同様のことをしなければいけないと考えられるためです。さらには、このような要望は一時的なものであり、正式リリースの時には、ログのために追加された内容はすべて除かれていることが望まれるかもしれません。

AspectJ を用いることで、これら複数のクラス(あるいはオブジェクト)にまたがるような動作(crosscutting concerns, 横断的関心 と呼ばれます)を1つの地点(アスペクト、と呼ばれます)にまとめて記述することができます。

この例でいえば、アスペクトを導入することにより、各クラス (Hello と Bye) の各メソッド内に、前述のようなログ用のコードを埋め込む必要がなくなり、その代わりに、そのようなログを目的としたコードは、特定のアスペクト (この例では、Log アスペクトとします) 内の一箇所に記述されます。

例えば、次の Log アスペクトは、ログ要求へのコードを満たします:

public aspect Log {

	pointcut method():
		execution( * *.*(..) ); // すべてのクラスのメソッドの実行

	before(): method() {
		System.out.println( thisJoinPoint.getSignature() ); // 実行中のメソッドのシグニチャを
		                                                      // 取得し、出力する
	}
}
Log アスペクトのコードからは、いくつか、馴染みのないキーワード (aspect, pointcut, execution, before) が見られます。これらのキーワードと意味については後述しますが、まずは Log アスペクトを実行したときの動作を見てみることにします。

では、この Log アスペクトを使ってみた場合のプログラムの動作を見てみましょう。以下のコードでは、プログラムを実行できるようにするために Hello クラスには main(String[] args) メソッドを追加しています。この main メソッドの中では Hello クラスをインスタンス化し hello() メソッドを呼び出しています。同様に Bye クラスをインスタンス化し bye() メソッドを呼び出しています:

public class Hello {
	public static void main(String[] args) {
		new Hello().hello();
		new Bye().bye();
	}
	public void hello() {
		System.out.println("Hello!");
	}
}
public class Bye {
	public void bye() {
		System.out.println("Bye!");
	}
}
プログラムの実行結果は以下のようになります。
java> java Hello
void aoptest.Hello.main(String[])
void aoptest.Hello.hello()
Hello!
void aoptest.Bye.bye()
Bye!
この出力結果では Hello クラスに main(String[]) メソッドを追加したため、このメソッドの 実行 も出力されています(もし必要でなければ、つまり、このメソッドのみを除きたいなら、そのようなアスペクトを書くことができます)。

Log アスペクトを見てみる

次に、先ほど紹介した Log アスペクトを見てみましょう。
public aspect Log {

	pointcut method():
		execution( * *.*(..) ); // すべてのクラスのメソッドの実行

	before(): method() {
		System.out.println( thisJoinPoint.getSignature() );
	}
}
このコードからは、普段使っているような言語 (Java, C++ など) とは、大きく異なるような特徴をもったキーワードを見ることができます。まず最初に気付くのは、一行目の aspect 宣言です。アスペクトの宣言はクラスの場合と似ています (実際、アスペクト内には、クラスの場合と同じように、特定のフィールドやメソッドを書くこともできます)。この aspect 宣言に加えて、この Log アスペクトには、普段あまり目にしたことのないような特徴をもったキーワードである pointcutadvice も含まれています。pointcut と advice (before の部分です) はお互いに関連しあっています。つまり、一方が他方を必要とします。

pointcut と join point

pointcut や advice の考え方は多くの人にとって初めてのものだと思います。pointcut とは何か、を知る前に、さらに新しい用語である join point を知っておく必要があります。join point とは、プログラムの実行局面の中で、適切に定義された (well-defined) ポイントを表します。AspectJ における 典型的な join point には、メソッド呼び出し、メソッドの実行、フィールドへのアクセスなどがあります。

AspectJ プログラマが何をするかというと、特定の join point (例えばメソッドの実行時) を基にして、そこで何をしたいのかを表したコード (例えば、メソッドの実行の 前に、ログを出力する) を書くことです。

pointcut は join point を選び出します。また、上記のコードから見られるように pointcut には名前が付けられています (上記の例では method() と名付けています。また、名前のない pointcut も書けます)。pointcut method() は、プリミティブな pointcut (Java での int などと同じ雰囲気です) である execution を使ってシステム内のすべてのメソッドの実行を選び出すことを表しています。

プリミティブな pointcut (Pointcut Designators) の役割は、プログラム内での、特定の join point を選び出すことです。AspectJ で定義されている (つまり、プログラム実行中でのどんな地点でもが join point と見なせるわけではありません) join point には、前述のように、メソッドの呼び出しやフィールドへのアクセスなどがあります (他にもあります。詳しくは「Pointcut Designators」の章を見てください)。各 Pointcut Designator は、それぞれの用途 (どの特定の join point を選び出したいのかどうか) にあうように、いくつかの種類があります。また、Pointcut Designator を組み合わせることで、特定の join point を選び出せるような細かい指定もできます。たとえば、すべてのクラスのインスタンスのメソッドの実行をログするのではなく、特定のクラスのインスタンスのメソッドの実行だけを選び出すような pointcut を記述することができます。

Log アスペクト内では、Pointcut Designators の 1 つであり、メソッドの実行の join point を選択することを目的とした execution pointcut を使用していました。そのほかの Pointcut Designators としては、メソッドの 呼び出し を表す call pointcut やフィールドへのアクセスをあらわす set pointcut, get pointcut、これらの他には、例外がスローされたときのポイントを表す handler などがあります(他にもあります)。

advice

join point とは、プログラム実行内での、特定のポイント (例えば、メソッドの実行の地点) でした。AspectJ では、Pointcut Designators を使うことによって、どの join point を選び出したいのかを指定することができます。

pointcut を指定するだけでは、特に嬉しいことはありません。AspectJ を使ってできることは、これら pointcut を中心にして、なんらかのコードを実行できることです。Log アスペクトの例では、各メソッドの実行の join point を選び出していました。そして、そのポイントを軸にして、シグニチャを出力することを目的としたログ用のコードを書いていました。

advice とはそれら join point (メソッドの実行、フィールドへのアクセスなど) に対して、その join point に達する 前に (before advice) 何らかの処理を行ったり、join point の 後に (after advice) 何らかの処理を行ったり、join point の 前後 (あるいは、代わりに) (around advice) で何らかの処理を行ったりするコードです。

advice の種類としては、今 join point に対して、前、後、前後 (あるいは、代わりに) というように区別したように、大きく分けて 3 つの種類があります (細かく分けると、after advice には 3 つの種類があります): before advice, after advice, around advice の 3 つです (これら 各 advice についての膨大なサンプルコードは「Advice」の章で紹介しています)。

このロギングの例では before advice を使うことにより、各メソッドの実行 (join point になります) の に それら各 join point でのシグニチャを出力するような処理を行っていました。

もし、各メソッドの実行の 後にも シグニチャを出力することが要求されるなら、たとえば、以下のように Log アスペクトを修正すれば、可能です:

public aspect Log {

	pointcut method():
		execution( * *.*(..) ); // すべてのクラスのメソッドの実行

	before(): method() {
		System.out.println(thisJoinPoint.getSignature() );
	}
	after(): method() {
		System.out.println(thisJoinPoint.getSignature() );
	}
}

より正確な用語の意味

join point や pointcut、そして advice といった用語についてのより正確な意味については AspectJ FAQ にも記述があるので参考にしてください:

(2003/4/2 追加)

Ant 環境と Ajc Ant Task

テスト環境

この記事で紹介しているサンプルコードは、以下のようなディレクトリ構成でテストされました。
+ java
    +-- build.xml
    |
    +-- src
    |    +-- aoptest
    |           +---- Hello.java // Java ファイル
    |           +---- AspectHello.java // アスペクトファイル
    |           +---- xxx.java // その他のファイル
    +-- dest
         +-- aoptest
                +---- Hello.class
基本的には、すべてのファイル(Java ファイルとアスペクトファイル)を同一の src/aoptest ディレクトリに置きました。パッケージに関しては、aoptest がルートのパッケージとなります(たとえば package aoptest; public class Hello { ... } )。

また、コードの見た目をシンプルにするために、特別な場合がないかぎり、パッケージ名 (package aoptest;) を省略していることもあります。

Ant 環境

アスペクトファイルをコンパイルするには、Ant のコアタスクの 1 つである Javac タスクの代わりに、aspectj 専用の Ajc Ant タスクを使用します。Ajc タスクの使用方法は、javac とほとんど同じです。この記事では、主に次のようなビルドファイルを使用します。
<?xml version="1.0" encoding="UTF-8" ?>
<project default="main" basedir=".">

	<target name="main">

		<taskdef name="ajc" classname="org.aspectj.tools.ant.taskdefs.Ajc"/>

		<mkdir dir="dest" />

		<ajc destdir="dest" srcdir="src"/>
	</target>
</project>
問題なく動作するかを確認するために、以下の Hello クラスを上記のビルドファイルでコンパイルできるかどうか試してみてください。
package aoptest;

public class Hello {
	public static void main(String[] args) {
		new Hello().hello();
	}

	public void hello() {
		System.out.println("Hello!");
	}
}
以下のようになれば成功です。
java> java -cp dest aoptest.Hello
Hello!

java>

ajc

(AspectJ 1.2.0 OK)

ここでは、AspectJ のコマンドラインのコンパイラである ajc の使用方法を簡単な例を挙げながら紹介します。ここで紹介している以外にどのようなコマンドラインオプションがあるのかについては、AspectJ の「The AspectJ Development Environment Guide」を参照してください。

-sourceroots DirPaths

コンパイル前のディレクトリ構成:
+ 作業ディレクトリ
    +-- src
        +-- aj // パッケージ名
            +---- MyClass.java // Java ファイル
            +---- MyClassAspect.java // アスペクトファイル
            +---- Main.java
ソースファイル:
public class MyClass {

	public void method() {
		System.out.println("MyClass.method()");
	}
}
public aspect MyClassAspect {

	before() : call( void MyClass.method() ) {
		System.out.println("before");
	}
}
public class Main {

	public static void main(String[] args) {

		new MyClass().method();
	}
}
コンパイル例:
>ajc -classpath C:\aspectj1.2\lib\aspectjrt.jar -sourceroots src
コンパイル後のディレクトリ構成:
+ 作業ディレクトリ
    +-- src
        +-- aj // パッケージ名
            +---- MyClass.java // Java ファイル
            +---- MyClassAspect.java // アスペクトファイル
            +---- Main.java
            +---- MyClass.class
            +---- MyClassAspect.class
            +---- Main.class
実行例:
>java -cp C:\aspectj1.2\lib\aspectjrt.jar;src aj.Main
before
MyClass.method()

-outjar output.jar

コンパイル前のディレクトリ構成(前の節と同じ):
+ 作業ディレクトリ
    +-- src
        +-- aj // パッケージ名
            +---- MyClass.java // Java ファイル
            +---- MyClassAspect.java // アスペクトファイル
            +---- Main.java
ソースファイル:(前の節と同じ)
public class MyClass {

	public void method() {
		System.out.println("MyClass.method()");
	}
}
public aspect MyClassAspect {

	before() : call( void MyClass.method() ) {
		System.out.println("before");
	}
}
public class Main {

	public static void main(String[] args) {

		new MyClass().method();
	}
}
コンパイル例:
>ajc -classpath C:\aspectj1.2\lib\aspectjrt.jar -sourceroots src -outjar my.jar
コンパイル後のディレクトリ構成:
+ 作業ディレクトリ
    +-- my.jar
    +-- src
        +-- aj // パッケージ名
            +---- MyClass.java
            +---- MyClassAspect.java
            +---- Main.java
実行例:
>java -cp C:\aspectj1.2\lib\aspectjrt.jar;my.jar aj.Main
before
MyClass.method()

-Xreweavable[:compress]

コンパイル前のディレクトリ構成:
+ 作業ディレクトリ
    +-- src
    |    +-- aj // パッケージ名
    |        +---- MyClass.java // Java ファイル
    |        +---- MyClassAspect.java // アスペクトファイル
    |        +---- Main.java
    +-- src2
         +-- aj
             +---- MyClassAspect2.java
src ディレクトリのソースファイル:(前の節と同じ)
public class MyClass {

	public void method() {
		System.out.println("MyClass.method()");
	}
}
public aspect MyClassAspect {

	before() : call( void MyClass.method() ) {
		System.out.println("before");
	}
}
public class Main {

	public static void main(String[] args) {

		new MyClass().method();
	}
}
src2 ディレクトリのソースファイル:
public aspect MyClassAspect2 {

	before() : call( void MyClass.method() ) {
		System.out.println("MyClassAspect2 - before");
	}
}
src ディレクトリのファイルのコンパイル例:
>ajc -classpath C:\aspectj1.2\lib\aspectjrt.jar -sourceroots src -outjar my.jar -Xreweavable
src ディレクトリのファイルのコンパイル後のディレクトリ構成:
+ 作業ディレクトリ
    +-- my.jar
    +-- src
    |    +-- aj // パッケージ名
    |        +---- MyClass.java // Java ファイル
    |        +---- MyClassAspect.java // アスペクトファイル
    |        +---- Main.java
    +-- src2
         +-- aj
             +---- MyClassAspect2.java
src2 ディレクトリのファイルのコンパイル例:
>ajc -classpath C:\aspectj1.2\lib\aspectjrt.jar; -injars my.jar -sourceroots src2 -outjar my2.jar
src2 ディレクトリのファイルのコンパイル後のディレクトリ構成:
+ 作業ディレクトリ
    +-- my.jar
    +-- my2.jar
    +-- src
    |    +-- aj // パッケージ名
    |        +---- MyClass.java // Java ファイル
    |        +---- MyClassAspect.java // アスペクトファイル
    |        +---- Main.java
    +-- src2
         +-- aj
             +---- MyClassAspect2.java
実行例:
>java -cp C:\aspectj1.2\lib\aspectjrt.jar;my2.jar aj.Main
MyClassAspect2 - before
before
MyClass.method()

Advice

別ページです。こちら からどうぞ。

Pointcut Designators

別ページです。こちら からどうぞ。

inter-type declarations (Introduction)

別ページです。こちら からどうぞ。

アスペクト関連

別ページです。こちら からどうぞ。

リソース&参考にしたもの

ここでは、この記事を書くにあたって参考にしたものや、AspectJ と AOP に関連する情報を入手できる(僕が実際に眺めたことのある)論文や本を紹介しています。なお、カテゴリの分け方は独断ですので注意してください。

更新履歴

todo