Spring AOP 実装パターン — トランザクション・ログ・権限を本流から剥がす
横断的関心事 (クロスカッティングコンサーン) を綺麗に分離する Spring AOP の使い所と Aspect/JoinPoint/Advice/Pointcut の全体像、プロダクションで効くユースケース 4 選。

どういう問題を解くのか
ログ出力・トランザクション境界・権限チェック・監査 — これらは 「どのメソッドでも同じように必要になるが、ドメインロジックとは関係ない」 横断的関心事 (Cross-Cutting Concerns) です。
毎メソッドに手書きすると:
- ドメインロジックが本来の意図から逸脱して読みにくくなる
- 書き忘れが発生する
- 仕様変更時に大量のメソッドを修正することになる
AOP は、これら横断的関心事を 「別のモジュール (Aspect)」 に切り出し、メソッド呼び出しに自動で挟み込む しくみです。
Spring の @Transactional / @Cacheable / @PreAuthorize — どれも Spring AOP の上に成り立っています。
用語の整理
| 用語 | 意味 | 例 |
|---|---|---|
| Aspect | 横断的関心事をまとめたクラス | TransactionAspect, AuditAspect |
| JoinPoint | Advice を挟み込める地点 (メソッド呼び出し等) | OrderService#create の呼び出し |
| Advice | JoinPoint で実行する具体コード | 開始ログを出す、トランザクション開始 |
| Pointcut | どの JoinPoint に Advice を当てるか指定する式 | within(com.sample.service..*) |
| Weaving | Advice を実際のコードに組み込む処理 | Spring は実行時プロキシで実現 |
Spring AOP の制約: Spring Bean に対する メソッド呼び出し のみが JoinPoint。フィールドアクセスや new は介入できません。より強力な介入 (フィールド・コンストラクタ) が必要なら AspectJ を使います。
Advice の 5 種類
@Aspect
@Component
public class AuditAspect {
// 前処理
@Before("execution(* com.example.service..*(..))")
public void before(JoinPoint jp) { ... }
// 後処理 (成功/失敗問わず)
@After("execution(* com.example.service..*(..))")
public void after(JoinPoint jp) { ... }
// 正常終了したときだけ (戻り値を受け取れる)
@AfterReturning(pointcut = "execution(* ..*(..))", returning = "result")
public void afterReturning(JoinPoint jp, Object result) { ... }
// 例外が出たときだけ
@AfterThrowing(pointcut = "execution(* ..*(..))", throwing = "ex")
public void afterThrowing(JoinPoint jp, Throwable ex) { ... }
// 全部包む (もっとも強力、proceed() を呼ばないと本来処理が走らない)
@Around("execution(* ..*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long elapsed = System.nanoTime() - start;
// メトリクス送信など
}
}
}
Pointcut 指示子チートシート
| 指示子 | 何を選ぶか | 例 |
|---|---|---|
within(...) | 指定クラス内の全メソッド | within(com.example.controller..*) |
execution(...) | メソッドシグネチャで指定 | execution(public * com.example..service.*.*(..)) |
target(...) | 指定型のインスタンスのメソッド呼び出し | target(org.springframework.data.repository.Repository+) |
args(...) | 引数の型で指定 | args(java.lang.String, ..) |
@annotation(...) | 指定アノテーションが付いたメソッド | @annotation(org.springframework.transaction.annotation.Transactional) |
bean(name) | Bean 名で指定 | bean(userService) |
複数条件は && / || / ! で合成できます:
@Pointcut(
"within(com.example.service..*) && " +
"!@annotation(com.example.NoAudit)"
)
public void auditedServices() {}
よく使う Pointcut は @Pointcut で名前を付けて再利用。
実用例 4 選
1. API 呼び出しの構造化ログ
@Aspect
@Component
public class ApiLogAspect {
private static final Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
@Around("within(@org.springframework.web.bind.annotation.RestController *)")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
String sig = pjp.getSignature().toShortString();
try {
Object result = pjp.proceed();
log.info("api_ok sig={} ms={}", sig, System.currentTimeMillis() - start);
return result;
} catch (Throwable t) {
log.warn("api_fail sig={} ms={} err={}", sig, System.currentTimeMillis() - start, t.toString());
throw t;
}
}
}
2. 独自アノテーションで監査ログ
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited { String action(); }
@Aspect
@Component
public class AuditAspect {
@AfterReturning("@annotation(audited)")
public void record(JoinPoint jp, Audited audited) {
// DB か監査サーバに書き出す
}
}
呼び出し側:
@Audited(action = "USER_DELETE")
public void deleteUser(String id) { ... }
3. リトライ
spring-retry を使う方が確実ですが、学習用:
@Around("@annotation(retryable)")
public Object retry(ProceedingJoinPoint pjp, Retryable retryable) throws Throwable {
int attempts = retryable.max();
Throwable last = null;
for (int i = 0; i < attempts; i++) {
try { return pjp.proceed(); } catch (Throwable t) { last = t; }
}
throw last;
}
4. メトリクス収集
Micrometer と組み合わせ、@Timed 付きメソッドの実行時間を Prometheus にエクスポート。
落とし穴
1. 自クラスからの this.method() 呼び出しは効かない
Spring AOP はプロキシ経由なので、同じクラス内の別メソッドを this.foo() で呼ぶと プロキシを通らず Advice が効きません 。
@Service
public class OrderService {
@Transactional
public void outer() {
this.inner(); // ← inner の @Transactional は効かない!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() { ... }
}
対策: @Autowired で自分自身のプロキシを注入、または AspectJ コンパイル時 Weaving に切り替える。
2. Pointcut が広すぎてパフォーマンス低下
execution(* ..*(..)) のような無差別 Pointcut は、起動時のマッチング時間が爆発します。
可能な限り within(package..*) で絞る のが鉄則。
3. Advice 内の例外で本来の動作が止まる
監査ログの保存に失敗しただけで業務トランザクションが落ちたら本末転倒。 Aspect 内の例外は原則 try/catch で握りつぶす (ログだけ出して本流を通す)。
4. @Around で proceed() を呼び忘れる
@Around の中で pjp.proceed() を呼ばないと、本来のメソッドが実行されません。レビューで必ずチェック。
まとめ
Spring AOP は ドメインロジックを汚さずに横断的機能を足す ための強力な道具です。ただし 「なんでも AOP で書く」 と可読性が落ちるので、ログ / 監査 / メトリクス / 権限 のような 「明らかに共通」 な関心事に限定するのが実務的。
カスタム Aspect を書く前に、Spring が標準提供している @Transactional / @Cacheable / @Async / @Scheduled / @PreAuthorize / @Validated で済まないか必ず確認を。
関連記事
- Web・モバイル·
Spring Boot プロジェクト構成 — 10 人規模で崩れないパッケージ設計
Controller / Service / Repository の責務境界、ドメイン層の切り方、テストの並べ方まで。Spring Boot API 開発で「後から直せなくなるコード」を防ぐためのパッケージ構成の指針。
- Web・モバイル·
Spring Bean ライフサイクル — スコープ・初期化順・落とし穴
BeanDefinition のロードから @PostConstruct まで、Spring Bean がどの順序で生成されるかを 7 ステップで整理。Singleton/Prototype/Request/Session スコープの違いと実務的な使い分けも。
- Web・モバイル·
Java パフォーマンスチューニングの実務ノート — JVM / コレクション / GC
プリミティブ型・不変オブジェクト・コレクション選択・ストリーム vs for ループ・GC チューニングまで、現場で効くチューニングの型。計測してからチューニングする原則を軸に。