この記事は、富士通システムズウェブテクノロジー Advent Calendarの19日目の記事です。
(お約束)本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。
はじめに
この記事は、JavaでAOPを実現するOSSであるAspectJが、どんなコードを生成しているのかを目視確認してみよう!というものです。
AOP自体は昔から使われている技術ですが、個人的にはあまりプロジェクトで使われている印象がありませんでした。が、最近になって自分の周りでAOPを使っているプロジェクトをちらほら見かけるようになり、今後使う人が増えるかも?と思い記事を書いてみました。
AOPは使ってみると便利なのですが、正直裏で何が起こっているのかよくわからない部分があります。
こういうところの理解を深めるために、本記事ではAOPでウィービングした際のコードを覗いてみます。
前提知識
AOPってなんぞや!
みたいな話は既にいろんな記事でまとめられているので割愛します。
以下の記事はすごいまとまってるのでおすすめ。
時代が来るかも?アスペクト指向プログラミング, AspectJ言語とは何か
まずは、本記事を読むにあたって必要な用語だけ簡単に説明します。
ウィービング
簡単に言うと、コンパイルして生成したクラスファイルを後から書き換えて、任意の処理を埋め込むこと。
例えば、全くログが実装されてないプログラムに、元のソースを書き換えずにデバッグログを埋め込むような芸当ができます。
そう、AOPならね。
ちなみに、ウィービングには以下の2種類があります。今回は静的ウィービングを使います。
- 静的ウィービング:Javaのクラスファイルに直接処理を織り込む方法
- 動的ウィービング:JVMにクラスがロードされた際、動的に処理を織り込む方法
ポイントカット
ウィービングを実施する際、処理を埋め込むメソッドなどをポイントカットとして定義します。
ポイントカットには様々な種類があり、それぞれがプログラム実行上の様々なタイミングを表しています。
代表的なもの:
- execution:特定のメソッドが実行されたとき
- call:特定のメソッドを呼び出したとき
- handle:特定の例外がハンドルされたとき
アドバイス
ポイントカットで特定したメソッドのどこに処理を埋め込むかを定義するのがアドバイスです。
こちらも埋め込む場所に応じていくつかの種類が用意されています。
代表的なもの:
- before:前処理
- after:後処理
- after returning:後処理(正常時のみ)
- after throwing:後処理(異常時のみ)
ウィービング結果を覗いてみる
用語説明はこれくらいにして、ウィービングによって処理がどう書き換えられるのかを見てみます。
今回はアドバイスはbefore, afterとし、よく使うポイントカット3種でウィービングしました。
使ったもの
- Eclipse
- AdoptOpenJDK 11.0.5.10 hotspot
- aspectj-1.9.5:AspectJのライブラリやコンパイラなど
- jd-gui-windows-1.6.5:Javaのデコンパイラ
サンプルコード
ウィービング対象のプログラム
public class AspectjPointcutSampleMain {
public static void main(String[] args) {
new AspectjPointcutSampleMain().doProcess();
}
private void doProcess() {
Sample sample = new Sample();
sample.outputMessage();
sample.getString();
try {
sample.throwException();
} catch (Exception e) {
e.printStackTrace();
}
}
public static class Sample {
private String sampleStr;
public void outputMessage() {
System.out.println("this is sample.");
}
public String getString() {
return this.sampleStr;
}
public void throwException() throws Exception {
throw new Exception();
}
}
}
ウィービングの定義
@Aspect
public class SampleAspect {
@Before("execution(public * *..*.Sample.*(..))")
public void beforeExecutionPointcut() throws Throwable {
System.out.println("execution before process.");
}
@After("execution(public * *..*.Sample.*(..))")
public void afterExecutionPointcut() throws Throwable {
System.out.println("execution after process.");
}
}
SampleAspect.javaにウィービングのための定義を記述しました。
各メソッドのアノテーションでアドバイス、ポイントカットを指定します。
今回はSampleクラスのpublicメソッド全てに処理を埋め込むよう定義しています。
ウィービング結果
execution
ウィービング定義
@Before("execution(public * *..*.Sample.*(..))")
public void beforeExecutionPointcut() throws Throwable {
System.out.println("execution before process.");
}
@After("execution(public * *..*.Sample.*(..))")
public void afterExecutionPointcut() throws Throwable {
System.out.println("execution after process.");
}
デコンパイル結果
public void outputMessage() {
try {
SampleAspect.aspectOf().beforeExecutionPointcut();
System.out.println("this is sample.");
} catch (Throwable throwable) {
SampleAspect.aspectOf().afterExecutionPointcut();
throw throwable;
}
SampleAspect.aspectOf().afterExecutionPointcut();
}
長かったので抜粋しました。
元のソースと見比べると、対象に指定したメソッド内に、以下の処理が新たに追加されていることがわかります。
- 前処理でのbeforeExecutionPointcutメソッド呼び出し
- 後処理でのafterExecutionPointcutメソッド呼び出し
- 処理全体がtry-catchで括られている
- 追加されたcatch句内でのafterExecutionPointcutメソッド呼び出し
call
ウィービング定義
@Before("call(public * *..*.Sample.*(..))")
public void beforeCallPointcut() throws Throwable {
System.out.println("call before process.");
}
@After("call(public * *..*.Sample.*(..))")
public void afterCallPointcut() throws Throwable {
System.out.println("call after process.");
}
デコンパイル結果
private void doProcess() {
Sample sample = new Sample();
try {
SampleAspect.aspectOf().beforeCallPointcut();
sample.outputMessage();
} catch (Throwable throwable) {
SampleAspect.aspectOf().afterCallPointcut();
throw throwable;
}
SampleAspect.aspectOf().afterCallPointcut();
~略~
}
デコンパイル結果を見ると、executionでは対象のメソッド内に前処理・後処理が埋め込まれていたのに対し、メソッドの呼び出し元に処理が埋め込まれていることがわかります。
callは「特定のメソッドを呼び出したとき」を表すポイントカットなので、メソッドの呼び出し元がウィービングの対象となります。
handler
ウィービング定義
@Before("handler(Exception)")
public void beforeHandlerPointcut() throws Throwable {
System.out.println("handler before process.");
}
デコンパイル結果
private void doProcess() {
Sample sample = new Sample();
sample.outputMessage();
sample.getString();
try {
sample.throwException();
} catch (Exception exception) {
SampleAspect.aspectOf().beforeHandlerPointcut();
Exception e = exception;
e.printStackTrace();
}
}
handlerの場合はcatch句の中に処理が埋め込まれるようです。
おわりに
簡単ではありますがデコンパイル結果を掲載してみました。
今回、改めてAOPについて簡単に調べなおしたのですが、思った以上にいろんなことができるので改めて関心しました。
本記事には掲載しきれていない機能も多々ありますので、興味があれば調べてみてもらえると面白いかもです。