Dev Study
← Spring コース

46. AOP プロキシと自己呼び出し

の前後に共通の処理()を挟む の仕組みを と呼びます( はアスペクト指向プログラミングの略)。対象の は、元ののではなく代理()に置き換わります。実測でのクラス名は ReportService$$SpringCGLIB$$0 で、親が ReportService でした。の回の差し込みを、Spring が自動で行っています。

作り方は2通りです。を実装する JDK の動的プロキシと、対象のを実行時に生成する です。 単体の既定は、インターフェースがあれば前者です。 4.1.1 の既定では、インターフェースがあっても CGLIB になりました。リファレンスもこの違いに触れています。 を false にすると前者になりました。

つまずきどころはです。外からの呼び出しは代理を通りますが、本物のメソッドの中で this.inner() と呼ぶと、this は本物なので代理を通らず、アドバイスは走りません。また CGLIB はで作るので、アドバイスの対象になった クラスは起動に失敗し、final メソッドにはアドバイスが効きません。実測では、final メソッドの中でが null に見えました(この書き方での観測です)。

点線の付いた語はタップすると説明が出ます。用語集を開く →

外からの呼び出しと自己呼び出しの通り道(Spring Boot 4.1.1 での実測をもとに作図)
呼び出す側ctx.getBean(ReportService.class) で受け取った参照
指しているのは代理
outer() を呼ぶ
代理 ReportService$$SpringCGLIB$$0ReportService のサブクラス。アドバイスを挟む
[advice] before outer の後、本物へ
本物の ReportService.outer()この中の this は本物
this.inner() — 代理を通らない
本物の ReportService.inner()アドバイスは走らない
外から inner() を呼べば代理を通る
  • ・Spring Framework のリファレンスは「self invocation via an explicit or implicit this reference will bypass the advice」と書いています
  • ・同リファレンスの制約: final classes cannot be proxied, because they cannot be extended. / final methods cannot be advised, because they cannot be overridden.
  • ・既定の違いについての同リファレンスの文: While the core framework suggests interface-based proxies by default, Spring Boot may - depending on configuration properties - enable class-based proxies by default.
  • ・同じ仕組みを使う @Transactional は、後のレッスンで扱います

サンプルコード(フレームワーク環境が必要なため表示のみ)

// l13/proxy/ProxyApp.java(抜粋)— 依存は spring-boot-starter-webmvc と spring-boot-starter-aspectj
package l13.proxy;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Logged {}

// @Logged の付いたメソッドの前後に出力を挟む
@Aspect
@Component
class LoggingAspect {
    @Around("@annotation(l13.proxy.Logged)")
    Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("    [advice] before " + pjp.getSignature().getName());
        try { return pjp.proceed(); }
        finally { System.out.println("    [advice] after  " + pjp.getSignature().getName()); }
    }
}

@Component
class ReportService {
    @Logged
    public void outer() {
        System.out.println("    outer() body, this = " + this.getClass().getSimpleName());
        this.inner();                     // 自己呼び出し
    }

    @Logged
    public void inner() { System.out.println("    inner() body"); }
}

// main では、コンテナから取り出した ReportService のクラス名と親クラス名を出力し、
// 続けて outer() と inner() をそれぞれ外から呼んでいる

// Spring Boot 4.1.1 / JDK 25 での出力(該当部分。末尾の数字は環境で変わる連番)
//
// ReportService: l13.proxy.ReportService$$SpringCGLIB$$0
//     superclass=l13.proxy.ReportService ...
// --- call outer() from outside
//     [advice] before outer
//     outer() body, this = ReportService
//     inner() body                        ← inner の前後にアドバイスが無い
//     [advice] after  outer
// --- call inner() from outside
//     [advice] before inner
//     inner() body
//     [advice] after  inner

図やコードに出てくる用語(タップで説明)

サンプルコードで身につける(5本) →

学んだ概念を、素朴な例から実務寄りの例まで5つの具体例で確認します。

公式ドキュメントで詳しく ↗