Spring Bootで、独自アノテーションを目印にAOPを行う方法

こんにちは。 エキサイト株式会社の三浦です。

こちらは、エキサイトホールディングス Advent Calendar 2023の6日目の記事になります。

qiita.com

良ければ他の記事もどうぞ!

さて、コーディングをしている時、「このメソッドが実行される際はログを取りたい」など、メソッド実行時に追加で何かをさせたいという要件はたまに存在します。

その際には「AOP」という方法が役に立つのですが、今回はSpring Bootにおいて、独自のアノテーションを目印にしてAOPを行う方法を紹介します。

AOPとは

AOPは、「Aspect Oriented Programming(アスペクト指向プログラミング)」のことです。

上記で上げた「メソッド実行時にログを取る」などの共通化可能な処理を抜き出して、通常の処理と同等に書かなくても、メソッド名やアノテーションなどを目印に裏側で実行してくれるようにするというものです。

通常

通常は、以下のようにメソッド内に毎回ログ用の処理を書く必要があります。

public void method1(String arg1, String arg2) {
    // ログ用処理
    this.logMethod(arg1, arg2);

    this.mothod1Process(arg1, arg2);
}

public void method2(String arg1, String arg2) {
    // ログ用処理
    this.logMethod(arg1, arg2);

    this.mothod2Process(arg1, arg2);
}

AOP

AOPを使えば、特定のアノテーション等を目印にして、裏側でログを取ることが可能です。

// このアノテーションを目印にログを取る
@LogMethod
public void method1(String arg1, String arg2) {
    this.mothod1Process(arg1, arg2);
}

// このアノテーションを目印にログを取る
@LogMethod
public void method2(String arg1, String arg2) {
    this.mothod2Process(arg1, arg2);
}

今回の例のように、使用箇所が少なかったり同一クラス内でしか使わない場合は、そこまでメリットが感じられないかもしれません。

しかし、使用箇所が増えたり、クラスを横断してその処理が必要になっていくにつれ、AOPによる共通化の恩恵は大きくなっていきます。

では、Spring Bootでは具体的にどのように実装すれば良いのでしょうか。

Spring BootでAOPを使う

今回は簡単に、以下の要件を考えます。

  • メソッド実行前に、メソッド名やクラス名、その引数をログに取る
  • 独自のアノテーションが付いたメソッドでのみログを取る

build.gradle

まずは build.gradle に以下の設定を行います。

なお、Spring Bootはすでに読み込んでいる前提です。

implementation 'org.springframework.boot:spring-boot-starter-aop'

独自アノテーション

次に、目印とする独自アノテーションを作成します。

@Target(ElementType.METHOD)  // メソッドのみに付与可能
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogMethod {
}

AOPの処理本体

続いて、AOPの処理本体を設定します。

@Aspect
@Component
public class LogMethodAspect {
    @Before("@annotation(LogMethod)") // @LogMethodが付いているメソッドが実行された時、処理が実行される直前に以下が行われる
    public void beforeLogMethod(JoinPoint joinPoint) {
        // クラス名とメソッド名を取得
        final String path = "{className}.{methodName}"
                .replace("{className}", joinPoint.getSignature().getDeclaringTypeName())
                .replace("{methodName}", joinPoint.getSignature().getName());

        // 引数を取得
        final String body = Arrays.toString(joinPoint.getArgs());

        // 取得した情報を、何かしらに出力する
        this.log(path, body);
    }
}

AOPを適用

ここまで来れば、後は使うだけです。

@Component
public class SampleClass {
    @LogMethod
    public void method1(String arg1, String arg2) {
        this.mothod1Process(arg1, arg2);
    }

    @LogMethod
    public void method2(String arg1, String arg2) {
        this.mothod2Process(arg1, arg2);
    }
}

これで無事、ログを取ることができます!

呼び出し例

// DIコンテナから呼び出し
sampleClass.method1("hello", "world");

ログ

今回は「ログを取る」ことを目的としましたが、もちろんそれ以外の処理を行わせることもできます。

また、

  • 特定のパッケージのメソッドを対象にする
  • 特定のメソッド名のメソッドを対象にする

であったり、

  • メソッド実行後に処理を行う
  • メソッド実行の前後に処理を行う

など、色々なやり方があります。

spring.pleiades.io

ぜひいろいろ試してみてください!

最後に

コードが複雑になればなるほど、AOPはその真価を発揮していきます。

一度理解してしまえば使いたくなること請け合いなので、使えそうな場面があったらぜひやってみてください!