Dev Study
← 解説「AOP プロキシと自己呼び出し」に戻る

サンプルコードで身につける: AOP プロキシと自己呼び出し

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

1Bean のクラス名を出してみる

から取り出した に getClass().getName() を尋ねた出力です。の対象でない PlainService は元ののままですが、対象のを持つ ReportService は別のクラスになっていて、そのが元の ReportService です。を実装した GreeterImpl も、 の既定では同じ形になりました(末尾の数字は環境で変わる連番です)。

ProxyApp の出力(Spring Boot 4.1.1、既定の設定、該当部分)コマンド出力
spring.aop.proxy-target-class = (not set)
PlainService : l13.proxy.PlainService
superclass=java.lang.Object jdkProxy=false (AopUtils: aop=false jdk=false cglib=false)
ReportService: l13.proxy.ReportService$$SpringCGLIB$$0
superclass=l13.proxy.ReportService jdkProxy=false (AopUtils: aop=true jdk=false cglib=true)
greeterImpl : l13.proxy.GreeterImpl$$SpringCGLIB$$0
superclass=l13.proxy.GreeterImpl jdkProxy=false (AopUtils: aop=true jdk=false cglib=true)
instanceof Greeter=true instanceof GreeterImpl=true

2サブクラス型の代理を手で書く

が実行時に生成するものを、手書きのサブクラスで真似た形です。利用側の変数の型は ReportService のままで、中身だけが代理になっています。外から呼ぶと代理を通りますが、本物の outer の中の this.inner() は本物に対する呼び出しなので、inner の前後には何も出ません。真似ているのは呼び出しの通り道だけで、本物はクラスを実行時に生成し、リファレンスによれば代理のを という仕組みで作ります(を2回呼ばないため)。

Java

3設定を変えると JDK の動的プロキシになる

起動時に --spring.aop.proxy-target-class=false を付けた場合の出力です。インターフェースを実装した GreeterImpl の Bean は $Proxy56 という形の名前の になり、Greeter ではあっても GreeterImpl ではなくなりました。インターフェースの無い ReportService は、false にしても CGLIB のままです。

ProxyApp の出力(--spring.aop.proxy-target-class=false、差が出る部分)コマンド出力
spring.aop.proxy-target-class = false
PlainService : l13.proxy.PlainService
superclass=java.lang.Object jdkProxy=false (AopUtils: aop=false jdk=false cglib=false)
ReportService: l13.proxy.ReportService$$SpringCGLIB$$0
superclass=l13.proxy.ReportService jdkProxy=false (AopUtils: aop=true jdk=false cglib=true)
greeterImpl : l13.proxy.$Proxy56
superclass=java.lang.reflect.Proxy jdkProxy=true (AopUtils: aop=true jdk=true cglib=false)
instanceof Greeter=true instanceof GreeterImpl=false

4うまくいかない場合: final メソッド

を付けたメソッドはサブクラスで上書きできないので、代理は割り込めません。起動時に WARN が出るだけで起動はしますが、呼ぶとアドバイスは走らず、 name が null と表示されました。本体が代理のインスタンスの上で動き、代理の側のフィールドは初期化されていないためで、final でない normal() では field-value と出ています(この書き方での観測です)。

起動時の WARN と、normal() / sealed() を呼んだ出力(Spring Boot 4.1.1)追跡ログ
2026-10-01T13:21:41.725Z WARN 1 --- [ main] o.s.aop.framework.CglibAopProxy : Public final method [public final void l13.proxy.FinalMethodService.sealed()] cannot get proxied via CGLIB, consider removing the final marker or using interface-based JDK proxies.
...
--- final method
FinalMethodService: l13.proxy.FinalMethodService$$SpringCGLIB$$0
superclass=l13.proxy.FinalMethodService jdkProxy=false (AopUtils: aop=true jdk=false cglib=true)
[advice] before normal
normal(): name = field-value
[advice] after normal
sealed(): name = null, this = FinalMethodService$$SpringCGLIB$$0
// l13/proxy/ProxyApp.java(抜粋)
@Component
class FinalMethodService {
    private final String name = new String("field-value");

    @Logged
    public void normal() { System.out.println("    normal(): name = " + name); }

    @Logged
    public final void sealed() { System.out.println("    sealed(): name = " + name + ", this = " + this.getClass().getSimpleName()); }
}

5うまくいかない場合: final クラス

クラスそのものに final を付けると、サブクラスを作れないので代理を生成できず、起動に失敗しました(終了コード 1)。下はそのときの WARN 行で、原因の候補として final クラスと、見えないクラスの2つを挙げています。 のリファレンスにも、final クラスはできないのでにできないとあります。

起動失敗のログ(Spring Boot 4.1.1、終了コード 1)追跡ログ
2026-10-01T13:21:50.795Z WARN 1 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sealedService' defined in file [/work/aop/target/classes/l13/finalclass/SealedService.class]: Could not generate CGLIB subclass of class l13.finalclass.SealedService: Common causes of this problem include using a final class or a non-visible class
// l13/finalclass/FinalClassApp.java(抜粋)
@Component
final class SealedService {   // final なのでサブクラスを作れない
    @Logged
    public void work() {}
}

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