Spring AOPって何? Spring AOPでメソッド前後に処理を挟んでみよう
Spring AOPは、ログを出す処理や権限チェックなど「どこでも共通でやりたい処理」を、業務ロジックとは別にまとめて書ける仕組みです。この記事では、Spring AOPの考え方を分かりやすく説明しながら、Spring Bootでの入れ方と基本的な書き方をサンプル付きで紹介します。
Spring AOPを一言でいうと
Spring AOPは、メソッドが動く前後に「追加の処理」を差し込める仕組みです。
例えばこんな処理は、どの機能にも必要になりがちです。
- いつ、どのメソッドが呼ばれたかをログに出す
- 実行にかかった時間を測る
- 権限がある人だけ通す
- 失敗したときに共通のエラー処理をする
こういった処理を、各メソッドに毎回書くとコードが散らかります。Spring AOPを使うと、共通処理を一箇所にまとめて、必要な場所にだけ適用できます。
まず覚える用語
最初は用語が多く見えますが、意味はシンプルです。
- Aspect
共通処理をまとめたクラスです。ログや計測などの「まとめ役」です。 - Pointcut
どこに適用するかの条件です。「このパッケージのこのメソッドだけ」みたいに絞ります。 - Advice
実際に差し込む処理そのものです。「前に動かす」「後に動かす」などの種類があります。
Spring AOPでは、基本的に「メソッドが呼ばれるタイミング」に処理を差し込むと覚えると理解しやすいです。
Spring Bootで使えるようにする
Spring Bootなら、依存関係を足すだけでだいたい準備完了です。
依存関係の追加
Gradleの場合です。
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
Mavenの場合です。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
実行時間を測ってログに出してみよう
ここでは「このメソッドは時間を測りたい」という場所にだけ、計測を付けてみます。
目印になるアノテーションを作る
このアノテーションを付けたメソッドだけを計測します。
package com.example.demo.aop;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timed {
}
アスペクトを書く
@Aspect を付けると「AOP用のクラスです」とSpringに伝えられます。@Around は「前後どちらにも処理を入れたい」ときによく使います。
package com.example.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimingAspect {
private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);
@Around("@annotation(com.example.demo.aop.Timed)")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
// ここで本来のメソッドを実行します
return pjp.proceed();
} finally {
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
log.info("method={} elapsedMs={}", pjp.getSignature().toShortString(), elapsedMs);
}
}
}
ポイントはここです。
@Aroundの中でpjp.proceed()を呼ぶと、本来のメソッドが実行されるproceed()の前に書けば「前の処理」、後に書けば「後の処理」になる
使いたいメソッドに付ける
package com.example.demo.service;
import com.example.demo.aop.Timed;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
@Timed
public String hello(String name) {
return "Hello " + name;
}
}
これで hello が呼ばれるたびに、実行時間がログに出ます。業務ロジック側に「計測のためのコード」を書かなくていいのが嬉しいところです。
差し込むタイミングの種類
よく使うものだけ覚えれば大丈夫です。
@Before
メソッドの前に動く@AfterReturning
正常に終わった後に動く@AfterThrowing
例外で終わった後に動く@After
成功でも失敗でも最後に動く@Around
前後まとめて書ける
迷ったら @Around を選ぶことが多いです。前後の両方に書けて、戻り値もそのまま返せるからです。
例外のときだけログを出す
「失敗したときだけログを出したい」なら @AfterThrowing が読みやすいです。
@AfterThrowing(
pointcut = "execution(* com.example.demo..*Service.*(..))",
throwing = "ex"
)
public void logError(Exception ex) {
log.warn("service error", ex);
}
どこに適用するかの書き方
大きく分けると2つです。
- 文字のルールで場所を指定する
- アノテーションで場所を指定する
文字のルールで指定する
例えば「Serviceで終わるクラスの全メソッド」を対象にしたいなら、次のように書けます。
@Around("execution(* com.example.demo..*Service.*(..))")
public Object aroundServices(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
ただし、範囲が広すぎると「思ってないところにも効く」ことがあります。最初は狭い範囲で試すのがおすすめです。
アノテーションで指定する
実務では「付けたところだけに効かせたい」ことが多いので、アノテーション指定はかなり使いやすいです。
@Around("@annotation(com.example.demo.aop.Timed)")
public Object timedOnly(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
よくあるつまずきどころ
Spring AOPは便利ですが、仕組みの都合で引っかかりやすい点があります。
同じクラスの中で呼ぶと効かないことがある
同じクラスの中で別メソッドを呼ぶとき、AOPが効かない場合があります。
public void a() {
this.b(); // この呼び方だと、AOPが効かないことがある
}
対策としては、次のどちらかがよく使われます。
- メソッドを別クラスに切り出して、Bean同士で呼ぶようにする
- AOPに頼らない設計にする
「AOPは、Springが管理しているBeanの呼び出し経由で効く」と覚えておくと、原因が見つけやすいです。
privateメソッドに付けても期待通りにならない
AOPは基本的に「外から呼ばれるメソッド」に効かせるものだと考えると安全です。サービスの入口になる public メソッドに付けるのが無難です。
トランザクションも似た仕組み
@Transactional も、内部的には同じような考え方で動いています。例外を握りつぶしたり、途中で飲み込んだりすると、想定と違う結果になることがあります。
AOPで例外ログを出すときは「例外をそのまま投げ直すのか」を意識すると事故が減ります。
まとめ
Spring AOPは、ログや計測などの共通処理を、業務ロジックから切り離して書ける仕組みです。Spring Bootならスターターを追加するだけで始められます。
まずは、カスタムアノテーションを作って「付けたところだけに効かせる」形から試すと理解しやすいです。慣れてきたら、適用範囲を少しずつ広げていくのがおすすめです。