CLOVER🍀

That was when it all began.

はじめてのSpring AOP

SpringでのAOPをやったことがないなと思いまして、Interceptorの書き方を軽く見るとともに、挙動について把握しておこうかと思いまして。

Interceptorのかかり方について、気になるのは

  • 可視性
  • Interceptorを動かすには、拡張されたインスタンス(要は@Autowiredなりで取得したもの)である必要があるのか?
  • Interceptorの適用順

といったところですね。

実装はSpring Bootで行ったのですが、AOPを使ったプログラムを書くにあたり、参考にしたのは以下あたり。

Spring BootのAOPのサンプル

Aspect Oriented Programming with Spring

Declaring a pointcut Examples

Advice ordering

Spring AOP APIs

@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の適用順を制御しようということで、どうすればいいのかな?と思いましたが、ここを見ればいい感じですかね。

Advice ordering

とりあえず、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に指定する値って、どのくらいから始めれば適切なのかな。