SpringでのAOPをやったことがないなと思いまして、Interceptorの書き方を軽く見るとともに、挙動について把握しておこうかと思いまして。
Interceptorのかかり方について、気になるのは
- 可視性
- Interceptorを動かすには、拡張されたインスタンス(要は@Autowiredなりで取得したもの)である必要があるのか?
- Interceptorの適用順
といったところですね。
実装はSpring Bootで行ったのですが、AOPを使ったプログラムを書くにあたり、参考にしたのは以下あたり。
Aspect Oriented Programming with Spring
@AspectJ cheat sheet | Java and Spring development
このうち可視性については、Springのドキュメントに「publicメソッドだけだよ!」と書かれているので、OKとしましょう。
Due to the proxy-based nature of Spring’s AOP framework, protected methods are by definition not intercepted, neither for JDK proxies (where this isn’t applicable) nor for CGLIB proxies (where this is technically possible but not recommendable for AOP purposes). As a consequence, any given pointcut will be matched against public methods only!
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html#aop-pointcuts
あとは、Interceptorを書きつつ、動作を確認していってみます。
準備
Maven依存関係は、以下のように設定。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> </dependencies>
「spring-boot-starter-aop」が要るらしいです。
エントリポイントのコード
プログラムのエントリポイントは、以下のように実装。
src/main/java/org/littlewings/springboot/App.java
package org.littlewings.springboot; import org.littlewings.springboot.service.JdkProxyCalcService; import org.littlewings.springboot.service.OuterCalcService; import org.littlewings.springboot.service.SimpleCalcService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.ExitCodeGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; @SpringBootApplication public class App implements CommandLineRunner, ExitCodeGenerator { /** @Autowiredでいろいろ書く */ public static void main(String... args) { ApplicationContext context = SpringApplication.run(App.class, args); App app = context.getBean(App.class); int exitCode = SpringApplication.exit(context, app); System.exit(exitCode); } public void run(String... args) throws Exception { // @Autowiredでインジェクションしたインスタンスを操作する } public int getExitCode() { return 0; } }
コメントで書いている部分は、後で埋めていきます。
Interceptorを書く
それでは、Interceptorを書いてみます。
今回は、こんな感じのよくあるトレース用Interceptorを用意。
src/main/java/org/littlewings/springboot/trace/TraceInterceptor.java
package org.littlewings.springboot.trace; import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; 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.stereotype.Component; @Aspect @Component public class TraceInterceptor { @Before("execution(* org.littlewings.springboot.service.*.*(..))") public void invokeBefore(JoinPoint joinPoint) { System.out.printf("[AOP at before] called parameters = %s, by %s#%s%n", Arrays.toString(joinPoint.getArgs()), joinPoint.getTarget().getClass(), joinPoint.getSignature().getName()); // JoinPoint#getThisの場合は、拡張されたオブジェクトが返る } @Around("execution(* org.littlewings.springboot.service.*.*(..))") public Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object ret = null; try { System.out.printf("[AOP at around] before invoke, parameters = %s, by %s#%s%n", Arrays.toString(proceedingJoinPoint.getArgs()), proceedingJoinPoint.getTarget().getClass(), proceedingJoinPoint.getSignature().getName()); ret = proceedingJoinPoint.proceed(); return ret; } finally { System.out.printf("[AOP at around] after invoke, result = %s, %s#%s%n", ret, proceedingJoinPoint.getTarget().getClass(), proceedingJoinPoint.getSignature().getName()); } } }
@Aspectを付けて、@Componentとすればよいと。
@Aspect @Component public class TraceInterceptor {
Interceptorを実行する場所ですが、ドキュメントを見て今回は@Beforeと@Aroundとしました。
内容的には、半分被っていますが…。
Interceptorを適用する対象は、自作のserviceパッケージ内の任意のクラス/メソッドを対象としました。
@Before("execution(* org.littlewings.springboot.service.*.*(..))") @Around("execution(* org.littlewings.springboot.service.*.*(..))")
Interceptorを適用するクラスを書いて、適用範囲を確認する
Interceptorを適用するクラスとして、まずはこういうのを用意。
src/main/java/org/littlewings/springboot/service/SimpleCalcService.java
package org.littlewings.springboot.service; import org.springframework.stereotype.Service; @Service public class SimpleCalcService { public int add(int a, int b) { System.out.println(SimpleCalcService.class + "#add" + " called."); return a + b; } public int add2(int a, int b) { return add2Internal(a, b); } public int add2Internal(int a, int b) { System.out.println(SimpleCalcService.class + "#add2" + " called."); return a + b; } }
ひとつ、実体がなく別のpublicメソッドを呼んですぐ終了するケースを用意。
これを、先ほどのエントリポイントのクラスに@Autowiredして確認してみます。
@Autowired private SimpleCalcService simpleCalcService;
コードと結果をそれぞれ載せていきます。
単純にメソッド呼び出し。
public void run(String... args) throws Exception { System.out.printf("simpleCalcService.add = %d%n", simpleCalcService.add(1, 2)); }
※以降、runメソッドの宣言および閉じ括弧は端折ります。
結果。
[AOP at around] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add [AOP at before] called parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add class org.littlewings.springboot.service.SimpleCalcService#add called. [AOP at around] after invoke, result = 3, class org.littlewings.springboot.service.SimpleCalcService#add simpleCalcService.add = 3
ちゃんと動作しましたと。
続いて、別のpublicメソッドを呼び出すケース。
System.out.printf("simpleCalcService.add2 = %d%n", simpleCalcService.add2(1, 2));
結果。
[AOP at around] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add2 [AOP at before] called parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add2 class org.littlewings.springboot.service.SimpleCalcService#add2 called. [AOP at around] after invoke, result = 3, class org.littlewings.springboot.service.SimpleCalcService#add2 simpleCalcService.add2 = 3
こちらの場合は、@Autowiredしたものを直接呼んだケースのみInterceptorが動作しています。
ということは、publicメソッドであってもクラス内で別のメソッドを呼び出してもダメだということですね。あくまで、インジェクションしたインスタンス越しにメソッド呼び出しせよ、と。
当然ですが、別メソッドとなったものを直接呼び出せば
System.out.printf("simpleCalcService.add2 = %d%n", simpleCalcService.add2Internal(1, 2));
Interceptorも動きますよと。
[AOP at around] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add2Internal [AOP at before] called parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add2Internal class org.littlewings.springboot.service.SimpleCalcService#add2 called. [AOP at around] after invoke, result = 3, class org.littlewings.springboot.service.SimpleCalcService#add2Internal simpleCalcService.add2 = 3
続いて、別のクラスに委譲するケース。
src/main/java/org/littlewings/springboot/service/OuterCalcService.java
package org.littlewings.springboot.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OuterCalcService { @Autowired private InternalCalcService internalCalcService; public int add(int a, int b) { return internalCalcService.add(a, b); } }
外側、内側と言うのも変ですが、便宜上。
src/main/java/org/littlewings/springboot/service/InternalCalcService.java
package org.littlewings.springboot.service; import org.springframework.stereotype.Service; @Service public class InternalCalcService { public int add(int a, int b) { return a + b; } }
これを@Autowiredして
@Autowired private OuterCalcService outerCalcService;
実行。
System.out.printf("outerCalcService.add = %d%n", outerCalcService.add(1, 2));
結果。
[AOP at around] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.OuterCalcService#add [AOP at before] called parameters = [1, 2], by class org.littlewings.springboot.service.OuterCalcService#add [AOP at around] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.InternalCalcService#add [AOP at before] called parameters = [1, 2], by class org.littlewings.springboot.service.InternalCalcService#add [AOP at around] after invoke, result = 3, class org.littlewings.springboot.service.InternalCalcService#add [AOP at around] after invoke, result = 3, class org.littlewings.springboot.service.OuterCalcService#add outerCalcService.add = 3
両方動作しますね、と。そりゃそうですが…。
あと、JDKのProxyを使う版も確認してみました。
インターフェースの定義。
src/main/java/org/littlewings/springboot/service/JdkProxyCalcService.java
package org.littlewings.springboot.service; public interface JdkProxyCalcService { int add(int a, int b); }
実装側。
src/main/java/org/littlewings/springboot/service/JdkProxyCalcServiceImpl.java
package org.littlewings.springboot.service; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Service; @Service @Scope(proxyMode = ScopedProxyMode.INTERFACES) public class JdkProxyCalcServiceImpl implements JdkProxyCalcService { @Override public int add(int a, int b) { return a + b; } }
@Autowiredして
@Autowired private JdkProxyCalcService jdkProxyCalcService;
実行。
System.out.printf("jdkProxyCalcService.add = %d%n", jdkProxyCalcService.add(1, 2));
結果。
[AOP at around] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.JdkProxyCalcServiceImpl#add [AOP at before] called parameters = [1, 2], by class org.littlewings.springboot.service.JdkProxyCalcServiceImpl#add [AOP at around] after invoke, result = 3, class org.littlewings.springboot.service.JdkProxyCalcServiceImpl#add jdkProxyCalcService.add = 3
特に問題なさそうです。
Interceptorの順番を制御する
Interceptorの適用順を制御しようということで、どうすればいいのかな?と思いましたが、ここを見ればいい感じですかね。
とりあえず、2つめのInterceptorを用意。先のInterceptorのコピーですが。
src/main/java/org/littlewings/springboot/trace/TraceInterceptor2.java
package org.littlewings.springboot.trace; import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Aspect @Component public class TraceInterceptor2 { @Before("execution(* org.littlewings.springboot.service.*.*(..))") public void invokeBefore(JoinPoint joinPoint) { System.out.printf("[AOP at before-2] called parameters = %s, by %s#%s%n", Arrays.toString(joinPoint.getArgs()), joinPoint.getTarget().getClass(), joinPoint.getSignature().getName()); // JoinPoint#getThisの場合は、拡張されたオブジェクトが返る } @Around("execution(* org.littlewings.springboot.service.*.*(..))") public Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object ret = null; try { System.out.printf("[AOP at around-2] before invoke, parameters = %s, by %s#%s%n", Arrays.toString(proceedingJoinPoint.getArgs()), proceedingJoinPoint.getTarget().getClass(), proceedingJoinPoint.getSignature().getName()); ret = proceedingJoinPoint.proceed(); return ret; } finally { System.out.printf("[AOP at around-2] after invoke, result = %s, %s#%s%n", ret, proceedingJoinPoint.getTarget().getClass(), proceedingJoinPoint.getSignature().getName()); } } }
出力内容を、「-2」をつけてちょこっと変更。
System.out.printf("[AOP at before-2] called parameters = %s, by %s#%s%n",
この状態で、実行してみましょう。
System.out.printf("simpleCalcService.add = %d%n", simpleCalcService.add(1, 2));
こういう結果になりました。
[AOP at around] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add [AOP at before] called parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add [AOP at around-2] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add [AOP at before-2] called parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add class org.littlewings.springboot.service.SimpleCalcService#add called. [AOP at around-2] after invoke, result = 3, class org.littlewings.springboot.service.SimpleCalcService#add [AOP at around] after invoke, result = 3, class org.littlewings.springboot.service.SimpleCalcService#add simpleCalcService.add = 3
これを逆転させるには?@Orderアノテーションを指定すればよさそう?
import org.springframework.core.annotation.Order;
※Orderインターフェースを実装するでも良さそうですが
先に作ったInterceptorを200に
@Aspect @Component @Order(200) public class TraceInterceptor {
あとで作ったInterceptorを100に設定。
@Aspect @Component @Order(100) public class TraceInterceptor2 {
実行結果。
[AOP at around-2] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add [AOP at before-2] called parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add [AOP at around] before invoke, parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add [AOP at before] called parameters = [1, 2], by class org.littlewings.springboot.service.SimpleCalcService#add class org.littlewings.springboot.service.SimpleCalcService#add called. [AOP at around] after invoke, result = 3, class org.littlewings.springboot.service.SimpleCalcService#add [AOP at around-2] after invoke, result = 3, class org.littlewings.springboot.service.SimpleCalcService#add simpleCalcService.add = 3
結果が入れ替わりました。数字が小さい方が、先に実行される、と。
基礎的な使い方はわかった感じ?ですが、Orderに指定する値って、どのくらいから始めれば適切なのかな。