Seasar2でAOPは散々使ってきたが、Springではまだ使ったことなかったので、どんな感じになるのか簡易なコマンドラインアプリケーションで試してみた。今更感があるのでAOPとは?の説明はここでは省略。Spring AOPはXMLで設定を記述する方法もあるが、今回はアノテーションベース + Spring Bootで試してみた。
用語の整理
とはいえまず最初に用語の整理をしてみる。正直AOPでよく聞く用語は公式を直訳すると結構分かりづらく、あまり直観的ではないので語弊を恐れず簡潔に定義したい。
Aspect(アスペクト)
複数のクラスにまたがる関心事をモジュール化したもの(まとめたもの)。
JoinPoint(ジョインポイント)
Advice(下記参照)を挿入する場所。場所といってもソースの特定の位置というわけではなく、メソッド(やコンストラクタ)の実行前、メソッド(やコンストラクタ)の実行後、、といったように実行されるタイミングのこと。
Advice(アドバイス)
JoinPointで実行される処理(差し込みたい処理)のこと。これには以下の種類がある。
・Before advice
JoinPointの前に実行されるAdviceのこと。例外がスローされない限り実行を止めることは出来ない。
・After (finally) advice
JoinPointの後に実行されるAdviceのこと。
メソッドが正常終了、例外で終了に関わらずメソッドの実行後に実行される。
・After returning advice
JoinPointの処理が正常終了時に実行されるAdviceのこと。
例外がスローされた場合は無効となる。
・After throwing advice
JoinPointで例外が発生した直後に実行されるAdviceのこと。
・Around advice
Joinpointの前後で実行されるAdviceのこと。実行タイミングは自身で定義できる等、最も汎用的なAdvice。
PointCut(ポイントカット)
処理がJoinPointに到達した時、Adviceを実行するかどうかを判定するもの。例えば、メソッド名がgetで始まる時だけ処理する、のような条件を定義したもの。
これらで重要なことは、上記の仕組みは全てクラス本来の機能として用意されているものではなく、AOPによって外部から組み込まれている機能だということ。
Aspectの実装
複数のクラスにまたがる関心事をモジュール化する。
package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AspectExample {
}
Aspectとなるクラスには@Aspectアノテーションが必要。次に各Adviceを追加していく。
Before Advice
package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AspectExample {
@Before("execution(* com.example.service.*.*(..))")
public void before() {
System.out.println("before !!");
}
}
@Beforeアノテーションを付与する。
executionとはポイントカット指示子(メソッド実行条件)と呼ばれるもので、Spring AOPでは最もよく利用される。
ポイントカット指示子とは、メソッドを実行するかどうかを判定するもので、この条件にマッチした場合はメソッドが呼ばれることになる。
例えば上記の場合、com.example.serviceパッケージ配下のクラスのメソッドなら全て条件にマッチしたことになり、それらのマッチしたメソッドの実行前にbeforeメソッドが呼ばれることになる。
executionのフォーマットは以下の通り。
execution(メソッド修飾子 メソッド戻り値 パッケージ名.クラス名.メソッド名(引数の型,引数の型) throws 例外)
※メソッド修飾子(public等)とthrows 例外は省略可能。
上記の通り、ポイントカット指示子は正規表現のような形で表現される。
1箇所のみを何でも一致させたい場合は「*」を、個数に関係なく全てを一致させたい場合は「..」を利用する。
上記をの場合、引数部分が「..」となっているが、これは引数の個数に関係なく「どんな型で引数が何個指定されていてもOK」なメソッドが対象となる(ほぼ全てのメソッドが対象となる)。
ポイントカット指示子について
少し話がそれるが、一般的によく使われるポイントカット指示子の表現としては以下のようなものがある。
※全てBefore Adviceで表現しているが実行タイミングを変えたい場合は後述する@Afterや@AfterReturning等にする
publicメソッドの実行前
@Before("execution(public * *(..))")
メソッド名が"set"で始まるメソッドの実行前
@Before("execution(* set*(..))")
AccountServiceで定義されているメソッドの実行前
@Before("execution(* com.xyz.service.AccountService.*(..))")
serviceパッケージに定義されているメソッドの実行前
@Before("execution(* com.xyz.service..(..))")
serviceもしくはそのサブパッケージ内に定義されているメソッドの実行前
@Before("execution(* com.xyz.service...(..))")
ポイントカット指示子はexecution以外にも以下のようなものがある。
※代表的なものだけを抜粋
within
指定したクラス(型)で定義されたメソッドに対する呼び出しに適用するポイントカット。
within(com.example.service.ExampleService)のようにすることでExampleServiceで定義されたメソッドに対する呼び出しに適用される。
@Before("within(com.example.service.ExampleService)")
target
指定したクラス(型)のインスタンス(その型を実装するインスタンス)のメソッド呼び出しに適用するポイントカット。
例えばExampleServiceがParentExampleServiceを継承している場合、target(com.example.service.ParentExampleService)とすることで、ExampleService及びParentExampleServiceで定義されたメソッド呼び出しに適用される(withinでは指定したクラスのみで親クラス、子クラスの呼び出しには適用されない)。
@Before("target(com.example.service.ParentExampleService)")
args
指定した引数の型にマッチするメソッド呼び出しに適用するポイントカット。
例えば以下のようにすることで、ExampleServiceクラスでString型の引数を取るメソッド呼び出しに適用される。
@Before("within(com.example.service.ExampleService) && args(java.lang.String)")
public void beforeArgs() {
また、以下のようにargsに実際の引数名を指定することでAdvice(ここではbeforeArgsメソッド)の引数としてバインディングされ、Advice内で利用することができる。
@Before("within(com.example.service.ExampleService) && args(something)")
public void beforeArgs(String something) {
ちなみにExampleServiceはこんな感じ。例えばsetSomethingに文字列"hoge"を渡すと、上記beforeArgsでも"hoge"が取得できる。
public void setSomething(String something) {
System.out.println("setSomething");
}
@annotation
指定されたアノテーションが付与されたメソッド呼び出しに適用するポイントカット。
例えば以下のようにすることで、ExampleServiceクラスで@Beanアノテーションが付与されたメソッド呼び出しに適用される。
@Before("within(com.example.service.ExampleService) && @annotation(org.springframework.context.annotation.Bean)")
その他のポイントカット指示子
bean、this、@target、@args、@within等がある。
After Advice
@Afterアノテーションを付与する。挙動としてはBefore Adviceの逆でメソッド呼び出し後に適用されるだけなので省略。
After Returning Advice
@AfterReturningアノテーションを付与する。挙動としてはAfter Adviceと似ているが以下の点が異なる。
- After Adviceはメソッドが正常終了、例外で終了に関わらずメソッドの実行後に実行されるのに対して、After Returning Adviceはメソッドが正常終了の場合のみ実行される。
- 以下のようにAfter Advice、After Returning Adviceが両方適用されていた場合(こんなケースある?)、After Advice → After Returning Adviceの順序で実行される。
@After("within(com.example.service.ExampleService) && args(java.lang.String)")
public void after() {
System.out.println("after !!");
}
@AfterReturning("within(com.example.service.ExampleService) && args(java.lang.String)")
public void afterReturning() {
System.out.println("afterReturning !!");
}
After Throwing Advice
@AfterThrowingアノテーションを付与する。
例えば以下のようにすることで、ExampleServiceクラスでString型の引数を取るメソッド呼び出しで例外が発生した場合に適用される。
@AfterThrowing("within(com.example.service.ExampleService) && args(java.lang.String)")
public void afterThrowing() {
System.out.println("afterThrowing !!");
}
Around Advice
@Aroundアノテーションを付与する。
例えば以下のようにすることで、ExampleServiceクラスでString型の引数を取るメソッド呼び出しの前後に処理が差し込める。
@Around("within(com.example.service.ExampleService) && args(java.lang.String)")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around - before !!"); // 前処理
pjp.proceed(); // ExampleServiceクラスでString型の引数を取るメソッド呼び出し
System.out.println("around - after !!"); // 後処理
}
Around AdviceではProceedingJoinPointを利用して対象のメソッドの前後に処理を差し込む。
上記の例では、Adviceであるaroundメソッドに引数ProceedingJoinPointを指定しpjp.proceed();で対象のメソッドを実行する。つまり、pjp.proceed();の前後に差し込みたい処理を記載するだけ(ここではコンソールにaround - before !!、around - after !!と出力)。非常に汎用的なAdviceだが、例えばBefore Adviceで良いところを手抜きしてAround Adviceを使用してはいけない。全てのAdviceに言えることだが、要求に最も合致する最も狭い形式のAdviceを採用することが重要。
試した環境
Spring Boot 1.3.2.RELEASE
Spring AOP 4.2.4.RELEASE (Springが提供するAOPのフレームワーク)
Aspectj Weaver 1.8.8 (AspectJライブラリ)
参考にしたサイト・書籍
SpringFramework4プログラミング入門
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
http://javatechnology.net/spring/