Springに必要なjarについてはSpringの導入&サンプルプログラムを参照。(SpringAOPのコアとなるのはspring-aop-4.2.4.RELEASE.jar、spring-aspects-4.2.4.RELEASE.jar)
また下記のjarも必要。
両方ともMaven Repositoryのサイト(http://mvnrepository.com/)からダウンロードできる。(それぞれ"aopalliance"、"AspectJ Weaver"などで検索すると表示される。)
Springの設定ファイルを作成する。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
<-- ①component-scanの定義 -->
<context:component-scan base-package="aoptest" />
<-- ②AspectJを有効にする設定 -->
<aop:aspectj-autoproxy />
<-- ③Aspectクラスの定義 -->
<bean id="myAspect" class="aoptest.SpringSampleAspect" />
</beans>
①component-scanの定義
<context:component-scan base-package="aoptest" />
component-scanを利用して、aoptestパッケージ配下のクラスを自動的にインジェクションさせる。Springの一般的な定義。
②AspectJを有効にする設定
<aop:aspectj-autoproxy />
AspectJのアノテーションスタイルが利用できるようにする設定。ソースに@Configuration、@EnableAspectJAutoProxyを付与すれば省略可能(後述)。
③Aspectクラスの定義
<bean id="myAspect" class="aoptest.SpringSampleAspect" />
挿入する処理を持つAspectクラス(のちに自作)のbean定義。①の自動スキャン対象外のため、別途bean定義が必要。id属性は省略してもよい。
まずは処理が挿入される通常のメソッドを作成する。サンプルでは単純な文字列を出力するoutメソッドにする。本クラスは@Componentアノテーションが付与されているため、component-scanによって自動インジェクションされる。
package aoptest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class SpringSample {
public static void main(String[] args){
ApplicationContext context =
new ClassPathXmlApplicationContext("ApplicationContext7.xml");
SpringSample ss = context.getBean(SpringSample.class);
ss.out();
}
// 文字列を出力するメソッド
public void out(){
System.out.println("SpringAOP Test");
}
}
[補足]
クラス宣言に以下のアノテーションを付与すれば、Spring設定ファイルの<aop:aspectj-autoproxy />の記述は不要となる。但し、クラス個別に付けなくてはいけないので<aop:aspectj-autoproxy />の方が楽ではある。
package aoptest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;
@Component
@Configuration
@EnableAspectJAutoProxy
public class SpringSample {
前述で作成したSpringSampleクラスのoutメソッドの前後に処理を挿入してみる。下記のアノテーションを使用する。
package aoptest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
// ①クラス宣言に@Aspectを付加
@Aspect
public class SpringSampleAspect {
// ②メソッドの前で実行する処理
@Before("execution(* aoptest.*.*(..))")
public void before(JoinPoint joinPoint){
System.out.println("before");
}
// ③メソッドの後で実行する処理
@After("execution(* aoptest.*.*(..))")
public void after(JoinPoint joinPoint){
System.out.println("after");
}
}
①クラス宣言に@Aspectを付加
@Aspect
public class SpringSampleAspect {
クラス宣言の上部に@Aspectアノテーションを付ける。
なお@Aspectは@Componentと違い、Springの自動インジェクション対象とはならないため、ApplicationContext.xmlにbean定義を記述してインジェクトさせる。
<bean id="myAspect" class="aoptest.SpringAopSample" />
②メソッドの前で実行する処理
@Before("execution(* aoptest.*.*(..))")
public void before(JoinPoint joinPoint){
System.out.println("before");
}
メソッドの実行前に処理を挿入させたい場合、@Beforeアノテーションを付与する。@Beforeのパラメータにはexecutionに続けて挿入対象となるメソッド(の条件)を記述する。上記サンプルの場合aoptestパッケージ配下のメソッドが対象となる。
上記の記述を順番に分解すると、
ワイルドカード「*」やメソッド引数の「..」は何でもOKを意味する。パッケージ、クラス名、メソッド名は「"execution(* *(..))"」のようにまとめて「*」にしてもよい。
なお、全てフルで記述した場合、以下のような記述になる。(※サンプルと違って戻り値、引数ありの例)
@Before("execution(String aoptest.SpringSample.out(String, String))")
この記述方式は@Afterや後述する@Aroundなどでも同じ。
なお、execution以外にも、within、target、argsなどで対象のメソッドを絞ることができる。
表現例 | 説明 |
---|---|
within(test.*) | パッケージで絞る |
target(test.SampleInterface) | インタフェースで絞る |
args(java.lang.String) | 引数で絞る |
最後にbeforeメソッドの引数にはJoinPointクラスを指定する。これに実行するメソッドの情報(シグニチャや引数の値など)が格納されている。特に使用しない場合は引数に指定しなくてもよい。
public void before(JoinPoint joinPoint){
③メソッドの後で実行する処理
@After("execution(* aoptest.*.*(..))")
public void after(JoinPoint joinPoint){
System.out.println("after");
}
メソッドの後で実行させたい場合、@Afterアノテーションを付与する。記述方式やメソッド引数などは@Beforeと同じ。
なお、アノテーションには@Before、@After含めて以下の種類がある。@Around、@AfterReturning、@AfterThrowingは後述する。
アノテーション | 説明 |
---|---|
@Before | メソッド実行前に呼ばれる |
@After | メソッド実行後に呼ばれる |
@Around | メソッドは自分で実行し、前後に処理を記述する。 |
@AfterReturning | メソッド実行後によばれる。例外スロー時は呼ばれない。 |
@AfterThrowing | 例外スロー時に呼ばれる。 |
ApplicationContext.xml、SpringSample.java、SpringSampleAspect.javaが揃ったところでSpringSampleのmainメソッドを実行してみる。SpringSampleのoutメソッドの前後で、それぞれ"before"、"after"が出力されているのがわかる。
before
SpringAOP Test
after
@Beforeで前処理、@Afterで後処理を記述していたが、@Aroundを使えば1メソッドだけで実現できる。下記のように記述する。
// ①@Aroundアノテーション記述&メソッド宣言
@Around("execution(* aoptest.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
// -- 前処理 --
System.out.println("around before");
// ②メソッドの実行
Object ret = joinPoint.proceed();
// -- 後処理 --
System.out.println("around after");
// ③戻り値の返却
return ret;
}
①@Aroundアノテーション記述&メソッド宣言
@Around("execution(* aoptest.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
メソッド宣言の前に@Aroundアノテーションを付与する。記述方法は@Beforeや@Afterと同じ。
メソッドの引数にはProceedingJoinPointクラスを指定する。ProceedingJoinPointはJoinPointのサブクラスで、実行するメソッドの情報が取得できるほか、メソッド自体の実行も行える。戻り値とthrows節については後述。
②メソッドの実行&③戻り値の返却
System.out.println("around before");
Object ret = joinPoint.proceed();
System.out.println("around after");
return ret;
ProceedingJoinPointクラスのproceed()を呼ぶ。
proceed()を実行することで、実際のメソッドが呼ばれる。前処理、後処理は、このproceed()の前後に記述する。
proceed()の戻り値には実際のメソッドの戻り値が格納されている。 aroundメソッドの戻り値には、この値をそのまま返却する(編集して返却も可能)。aroundメソッドの戻り値をvoidにすると、呼び出し元にはnullが返却される。逆に戻り値を返却しても実際のメソッド戻り値がvoidだった場合、返却値は無視される。
なおproceed()はThrowableをスローするため、明示的にcatchしないのであれば、aroundメソッドのthrows節を記述する。
メソッド実行後の出力結果は以下のとおり。
around before
SpringAOP Test
around after
@AfterReturningは、メソッドが正常終了した場合(例外がスローされなかった場合)に呼ばれる。
@AfterReturningのパラメータはpointcutとreturningの2つあるので、key=value形式で記述する。
@AfterReturning(
pointcut = "execution(* aoptest.*.*(..))",
returning= "ret")
public void afterReturning(JoinPoint joinPoint, Object ret){
System.out.println("afterReturning");
}
pointcutパラメータは対象メソッドの条件を記述する。@Beforeなどと同じようにexecution(...)形式で記述。
returningパラメータには実際のメソッド戻り値を格納する変数名を記述する。afterReturningメソッドの引数にこの名前で変数を記述しておくと、実際のメソッド戻り値を格納してくれる。
@AfterThrowingは、異常終了した場合(例外がスローされた場合)に呼ばれる。
@AfterThrowing(
pointcut = "execution(* aoptest.*.*(..))",
throwing= "ex")
public void afterThrowing(JoinPoint joinPoint, Throwable ex){
System.out.println("afterThrowing");
}
@AfterThrowingもパラメータは2つある。pointcutパラメータは@AfterReturningと同じ。throwingパラメータは実際のメソッドでスローされた例外を格納する変数名を記述する。afterThrowingメソッドの引数にこの名前で変数を記述しておくと、実際のメソッドでスローされた例外を格納してくれる。
@AfterReturning、@AfterThrowingともに利用頻度は少ないかもしれないが、例外発生時の処理を切り分けたい場合などに便利。
@Before、@After、@Around、@AfterReturning、@AfterThrowingのすべてを実装して、各メソッドで標準出力してみると、以下の順に出力される。
around before
before
Spring Test
around after
after
afterReturning
出力結果より、以下の順に実行されるのがわかる。
①@Around(proceed呼び出し前)
②@Before
③メソッド実行(proceedで呼び出される)
④@Around(proceed呼び出し後)
⑤@After
⑥@AfterReturning
例外が発生していないので、@AfterThrowingは呼ばれない。例外を発生させると以下のようになる。
around before
before
Spring Test
after
afterThrowing
出力結果より、以下の順に実行されるのがわかる。
①@Around(proceed呼び出し前)
②@Before
③メソッド実行(proceedで呼び出される)
④@After
⑤@AfterThrowing
@AfterReturningの代わりに、@AfterThrowingが呼ばれる。また@Aroundで例外を捕捉しない場合、@Around(proceed呼び出し後)は呼ばれない。
実行対象メソッドの条件はexecuteに記述するが、下記サンプルのように"execution(* aoptest.*.*(..))"を何度も記述するとメンテナンス性が損なわれる。
package aoptest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class SpringSampleAspect {
@Before("execution(* aoptest.*.*(..))")
public void before(){
System.out.println("before");
}
@After("execution(* aoptest.*.*(..))")
public void after(){
System.out.println("after");
}
@AfterReturning("execution(* aoptest.*.*(..))")
public void afterReturning(){
System.out.println("afterReturning");
}
@AfterThrowing("execution(* aoptest.*.*(..))")
public void afterThrowing(){
System.out.println("afterThrowing");
}
@Around("execution(* aoptest.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("around before");
Object ret = joinPoint.proceed();
System.out.println("around after");
return ret;
}
}
@Pointcutアノテーションを使用することにより、条件を共有(再利用)できる。@Pointcutを使った修正版が以下である。
package aoptest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class SpringSampleAspect {
@Pointcut("execution(* aoptest.*.*(..))")
public void myPointcut(){};
@Before("aoptest.SpringSampleAspect.myPointcut()")
public void before(){
System.out.println("before");
}
@After("aoptest.SpringSampleAspect.myPointcut()")
public void after(){
System.out.println("after");
}
@AfterReturning("aoptest.SpringSampleAspect.myPointcut()")
public void afterReturning(){
System.out.println("afterReturning");
}
@AfterThrowing("aoptest.SpringSampleAspect.myPointcut()")
public void afterThrowing(){
System.out.println("afterThrowing");
}
@Around("aoptest.SpringSampleAspect.myPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("around before");
Object ret = joinPoint.proceed();
System.out.println("around after");
return ret;
}
}
まず適当なメソッドを1つ用意し、@Pointcutアノテーションを付与する。パラメータには条件(execute)を記述する。@Pointcutが付与されたメソッドは何も実装は必要ない。これはメソッド名を単にIDのような形で利用するためである。
@Pointcut("execution(* aoptest.*.*(..))")
public void myPointcut(){};
次に@Before、@After、@Around、@AfterReturning、@AfterThrowingのパラメータに、@Pointcutが付いたメソッドのパッケージ付きクラス名.メソッド名()を指定する。 これで@Pointcutで指定した条件が共有できる。
@Before("aoptest.SpringSampleAspect.myPointcut()")
SpringAOPでは、JoinPoint、Pointcut、Adviceという言葉がよくでてくるが、それぞれ以下のような意味を持つ。
用語 | 説明 |
---|---|
Advice | 挿入する処理(ふるまい)。@Before、@Afterなどが付いたメソッドのこと。 |
Pointcut | 処理が挿入されるメソッドの条件。"execution(* aoptest.*.*(..))"などの対象メソッドを特定する表現のこと。 |
JoinPoint | 処理が挿入される候補のメソッドのこと。「public void out()」などのこと。 |
PointcutとJoinPointは似ているが、Pointcutが対象メソッドを絞り込むための表現なのに対し、JoinPointは個々のメソッドだと思えばよい。Aspectクラスの各メソッドでも引数のJoinPointクラスから具体的なメソッドの情報が取得できる。
またJoinPointの説明で候補のメソッドと書いたが、どうやらJoinPointはPointcutで指定可能な全てのメソッドのことを指すらしい。つまり全てのJoinPointの中から、絞り込むための条件がPointcutである。(なんともわかりづらいし、用語もイメージしにくい。。)
なお、対象メソッドが以下の場合、処理の挿入はできないようである。(JoinPointになれない)