はじめに
今回はSpring AOPの実行順序について書いていきます。
同じAspect内にあるAdvice間の優先順位や、別Aspect同士の同じAdviceの優先順位づけ方法などをご紹介いたします!
Spring AOPそのものの簡潔な説明はこちらのQiita(はじめてのSpring AOP)が詳しいので、まずAOPについてざっくり知りたい方はそちらをご参照いただいてから読むとスムーズです。
使用環境とバージョン
- macOS Catalina
- jdk13
- STS 4.6.1
- Spring Boot 2.3.0(spring-core5.2.6)
Adviceの優先順位
Adviceの実行順には優先順位があります。1
@Around
@Before
@After
@AfterReturning
@AfterThrowing
すなわち、同じジョインポイントに@Around
と@Before
が存在する場合、@Around
内で定義されているジョインポイント実行前の処理が先に実行されます。
それでは同じAspectの同じジョインポイントに@Before
、 @After
、@AfterReturning
、そしてジョインポイント実行前後にそれぞれ処理がある@Around
が存在する場合は、どのようになるでしょうか?
以下3クラスからなる簡単なHelloWorldコードで実験してみました。
- メインクラス
- コントローラークラス
- Aspectを定義しているクラス
それぞれ、簡単な紹介とソースコードを記載します。
①メインクラス
シンプルにSpringを実行するクラスです。特別な処理は行っていません。
@EnableAspectJAutoProxy
はAOPを有効にするために必要なアノテーションなので、今回すべてのクラスに付与しています。
package com.example.practiceaop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@EnableAspectJAutoProxy
@SpringBootApplication
public class AopSampleApplication {
public static void main(String[] args) {
SpringApplication.run(AopSampleApplication.class, args);
}
}
②コントローラークラス
http://localhost:8080/ を叩くとコンソールに【Hello World!】を表示します。
以前書いたQiita記事のコードをかなり流用しました(作っておいて良かった!)。
package com.example.practiceaop;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@EnableAspectJAutoProxy
public class AopSampleController {
@RequestMapping("/")
@ResponseBody
public void helloWorld() {
System.out.println("【Hello World!】");
}
}
③Aspectを定義しているクラス
ジョインポイントは全Advice共通でAopSampleControllerのhelloWorld()としています。
package com.example.practiceaop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
@Aspect
@Component
@EnableAspectJAutoProxy
public class Aspect1 {
@Before("execution(* com.example.practiceaop.AopSampleController.helloWorld(..))")
public void before1(JoinPoint jp) {
System.out.println("Before1");
}
@After("execution(* com.example.practiceaop.AopSampleController.helloWorld(..))")
public void after1(JoinPoint jp) {
System.out.println("After1");
}
@AfterReturning("execution(* com.example.practiceaop.AopSampleController.helloWorld(..))")
public void afterReturning1(JoinPoint jp) {
System.out.println("AfterReturning1");
}
@Around("execution(* com.example.practiceaop.AopSampleController.helloWorld(..))")
public Object around1(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Around1 メソッド実行前");
try {
Object result = pjp.proceed();
System.out.println("Around1 メソッド実行後");
return result;
} catch (Throwable e) {
throw e;
}
}
}
実行結果は以下のとおりです。
Around1 メソッド実行前
Before1
【Hello World!】
AfterReturning1
After1
Around1 メソッド実行後
Aroundの優先順位は1位であるはずなのに、ジョインポイント後は実行順が最下位になっているのは何故でしょうか?理解を助けるために、公式ドキュメントをDeepl翻訳で日本語にしたもの(少し手動で体裁を整えています)を記載します。
Spring AOPは、AspectJと同じ優先順位ルールに従って、アドバイスの実行順序を決定します。アドバイスの優先順位が高いものが最初に実行されます(つまり、2つのBeforeアドバイスがあった場合、優先順位の高いものが最初に実行されます)。ジョインポイントから「出て行く途中」では、優先順位の高いアドバイスが最後に実行されます(したがって、2つのAfterアドバイスが与えられた場合、優先順位の高い方が2番目に実行されます)。
ジョインポイントから「出て行く途中」では、優先順位の高いアドバイスが最後に実行されます
、この法則がややこしいですね。
つまり、ジョインポイントのメソッド実行前は、優先順位=実行順ですが、ジョインポイントのメソッド実行後は、優先順位の低いものから実行されるということです。
同列の場合の優先順位づけ
例えば、同じジョインポイントの前にトランザクション処理とログ出力を挟みたいとします。また、その実行順はトランザクションが絶対先に行われてほしいとします。
Adviceのうちの一つ、Beforeを使用すればジョインポイントの前で実行するのは可能です。しかし、公式ドキュメントによると、同じ種類の複数のAdviceが同じジョインポイントに存在する場合、実行順は不定で、実行ごとに変化してしまいます。
同じアスペクトで定義された2つのアドバイスが、両方とも同じ結合点で実行される必要がある場合、順序は不定です(javacでコンパイルされたクラスでは、反射を通して宣言順序を取得する方法がないため)。
これを解決するには、同じAdviceにトランザクションとログ出力を入れるか、別のAspectに切り出して@Order
を使用するかの、2つの方法があります。
同じAdviceに含めて問題のない処理同士であれば良いのですが、そうでない場合がほとんどだと思うので、@Order
を使用する方が現実的です。@Order
を使用すると、同列に存在するAspectの実行順位を指定することができます。
試しに、先ほど使用したAspect1に@Order(1)
をつけ、Aspect1をコピーしてコンソール出力文字だけ変更したAspect2クラスを作成し@Order(2)
をつけて実行してみます。
・
・
・
(前略)
@Aspect
@Component
@EnableAspectJAutoProxy
@Order(1) // アノテーション追加
public class Aspect1 {
(後略)
・
・
・
・
・
・
(前略)
@Aspect
@Component
@EnableAspectJAutoProxy
@Order(2) // アノテーション追加
public class Aspect2 { //Aspect1のコピー
@Before("execution(* com.example.practiceaop.AopSampleController.helloWorld(..))")
public void before1(JoinPoint jp) {
System.out.println("Before2"); // Before1→Before2に変更。以下Adviceも同様。
}
(後略)
・
・
・
結果は以下のとおりです。
Aspect1と2間で実行順序が入り乱れることなく実行できていることが分かります。
Around1 メソッド実行前
Before1
Around2 メソッド実行前
Before2
【Hello World!】
AfterReturning2
After2
Around2 メソッド実行後
AfterReturning1
After1
Around1 メソッド実行後
念のため、Aspect1に@Order(2)
を、Aspect2に@Order(1)
をつけて実行してみました。
こちらも以下のように、Aspect1と2間で実行順序が入り乱れることなく、またAspect同士の優先順位だけをきれいに入れ替えることができました。
Around2 メソッド実行前
Before2
Around1 メソッド実行前
Before1
【Hello World!】
AfterReturning1
After1
Around1 メソッド実行後
AfterReturning2
After2
Around2 メソッド実行後
おわりに
今回はSpring AOPの実行順序について見ていきました。
特にジョインポイント後の挙動がいまいち分かっておらず、検索してもドキュメントと矛盾する結果が出てくることもあり、かなり混乱していました。
ドキュメント作成者自身もややこしいと感じているのか、新しいバージョンになるにつれ記載が丁寧になっていってる感ありますね…
この記事がどなたかの参考になれば幸いです。また、何か間違いなどございましたら、そっと教えてください…!
お読みいただき、ありがとうございました!
-
Spring-core5.3系のドキュメントになってしまうため、今回使用したバージョン(5.2.6)とは若干異なりますが、5.2.6の方はAdviceの優先順位が明記されていなかったことと、後述の実験結果とも齟齬がなかったことから、この部分だけ5.3系を参照しました。https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/core.html#aop-ataspectj-advice-ordering ↩