Read OSS

AOPプロキシの内部構造:@TransactionalからランタイムInterceptionまで

上級

前提知識

  • 第2回:BeanPostProcessorのライフサイクル
  • 第3回:refresh()シーケンスとBeanの初期化
  • Javaダイナミックプロキシの基本的な理解

AOPプロキシの内部構造:@TransactionalからランタイムInterceptionまで

@Transactionalをメソッドに付けても、Springはクラス自体を書き換えるわけではありません。代わりに、元のBeanをプロキシに差し替えます。プロキシとは実行時に生成されるラッパーで、メソッド呼び出しを横取りし、コードが実行される前にトランザクションを開始し、終了後にコミットまたはロールバックを行います。この仕組みは@Async@Cacheable、Spring Securityの@PreAuthorizeなど、アノテーションで実現するあらゆる横断的関心事を支える共通基盤です。本記事では、AOPプロキシの概念的なモデルから実行時のバイトコード生成まで、その全体像を追っていきます。

AOP Allianceモデル:Advice、Pointcut、Advisor

Spring AOPは、org.aopallianceパッケージのAOP Allianceインターフェースを基盤としており、そこにSpring固有の抽象化を加えた形で構成されています。核となる概念は次の4つです。

  • Advice — 何をするか(インターセプターのロジック)。トランザクションの開始、権限チェック、結果のキャッシュなどが該当する
  • Pointcut — どこに適用するか(ジョインポイントにマッチする述語)。「@Transactionalが付いたすべてのメソッド」「serviceパッケージのすべてのpublicメソッド」などが例として挙げられる
  • Advisor — AdviceとPointcutを組み合わせたもの。プロキシに登録される単位である
  • AopProxy — 対象のBeanをラップし、Adviceチェーンを通じてメソッド呼び出しをディスパッチするプロキシオブジェクトそのものである

ProxyFactoryは、プロキシ設定をプログラムから構築するためのエントリーポイントです。

spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java#L36

classDiagram
    class ProxyConfig {
        +proxyTargetClass: boolean
        +optimize: boolean
        +exposeProxy: boolean
    }
    
    class AdvisedSupport {
        -targetSource: TargetSource
        -advisors: List~Advisor~
        +addAdvisor(Advisor)
        +getInterceptorsAndDynamicInterceptionAdvice() List
    }
    
    class ProxyCreatorSupport {
        -aopProxyFactory: AopProxyFactory
        +createAopProxy() AopProxy
    }
    
    class ProxyFactory {
        +getProxy() Object
    }
    
    class AopProxy {
        <<interface>>
        +getProxy() Object
    }
    
    class JdkDynamicAopProxy {
        implements InvocationHandler
    }
    
    class ObjenesisCglibAopProxy {
        Generates subclass
    }
    
    ProxyConfig <|-- AdvisedSupport
    AdvisedSupport <|-- ProxyCreatorSupport
    ProxyCreatorSupport <|-- ProxyFactory
    AopProxy <|.. JdkDynamicAopProxy
    AopProxy <|.. ObjenesisCglibAopProxy

AdvisedSupportはプロキシの設定情報——対象オブジェクト、Advisorのリスト、インターフェース情報——を保持します。ProxyCreatorSupportAopProxyFactoryに処理を委譲して実際のAopProxy実装を生成します。このファクトリが下す判断こそが重要で、JDKダイナミックプロキシを使うか、CGLIBサブクラスを使うかをここで決定します。

プロキシの選択:JDKダイナミックプロキシ vs CGLIB

選択ロジックはDefaultAopProxyFactory.createAopProxy()に集約されており、コードは驚くほどコンパクトです。

spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java#L60-L76

flowchart TD
    Start["createAopProxy(config)"]
    
    Start --> Check1{"optimize || proxyTargetClass<br/>|| no user interfaces?"}
    Check1 -->|No| JDK["JdkDynamicAopProxy<br/>(interface-based proxy)"]
    
    Check1 -->|Yes| Check2{"targetClass is null<br/>|| is interface<br/>|| is Proxy class<br/>|| is lambda?"}
    Check2 -->|Yes| JDK
    Check2 -->|No| CGLIB["ObjenesisCglibAopProxy<br/>(subclass-based proxy)"]

61行目のロジックでは、CGLIBを優先する3つの条件をチェックしています。

  1. config.isOptimize() — 直接使われることはほとんどありません
  2. config.isProxyTargetClass()proxyTargetClass=trueフラグが立っている場合
  3. !config.hasUserSuppliedInterfaces() — インターフェースが設定されていない場合

CGLIBが選ばれた場合でも、インターフェース・既存のプロキシ・ラムダクラスに対しては67〜69行目でJDKプロキシにフォールバックします。CGLIBはこれらをサブクラス化できないためです。

Spring BootはデフォルトでproxyTargetClass=trueに設定されているため、実質的にすべてのBeanにCGLIBプロキシが使われます。これは実用的な判断です。JDKプロキシはインターフェースで宣言されたメソッドしか公開できないため、具体クラスにしか存在しないメソッドをコードから呼び出すと、気づきにくいバグの原因になります。CGLIBプロキシは具体クラスをサブクラス化するので、この問題が生じません。

ヒント: Springがどちらのプロキシ型を生成したかを確認するには、AopUtils.isCglibProxy(bean)またはAopUtils.isJdkDynamicProxy(bean)を使いましょう。デバッグログでは、CGLIBプロキシのクラス名に$$SpringCGLIB$$が含まれているのでひと目でわかります。

AbstractAutoProxyCreator:BeanPostProcessorによる自動プロキシラッピング

ProxyFactoryを直接操作する機会はほとんどありません。Springは第2回で解説したBean初期化フェーズに自動的にプロキシを組み込みます。その仕組みを担うのがAbstractAutoProxyCreatorです。これはSmartInstantiationAwareBeanPostProcessorの実装であり、Bean生成の2つのフェーズにフックを差し込みます。

最初のフックはcreateBean()内のresolveBeforeInstantiation()呼び出しです。

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#L512-L517

// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
    return bean;
}

これは「ショートサーキット」パスです。BeanPostProcessorpostProcessBeforeInstantiation()からnull以外の値を返すと、通常のBean生成パイプライン全体をスキップできます。スクリプトBeanなどの特殊ケースで使われます。

より一般的なパスはpostProcessAfterInitialization()です。第2回で触れたinitializeBean()の最終ステップがこれに当たります。ここでAbstractAutoProxyCreatorは各Beanを検査し、適用可能なAdvisorがあれば、BeanをプロキシでラップしてBeanFactoryに返します。

sequenceDiagram
    participant BF as BeanFactory
    participant AAPC as AbstractAutoProxyCreator
    participant DPF as DefaultAopProxyFactory
    participant Proxy as Generated Proxy

    BF->>AAPC: postProcessAfterInitialization(bean, "myService")
    AAPC->>AAPC: wrapIfNecessary(bean, "myService", cacheKey)
    AAPC->>AAPC: getAdvicesAndAdvisorsForBean()
    Note right of AAPC: Find all Advisors whose<br/>Pointcuts match this bean
    
    alt No matching advisors
        AAPC-->>BF: return original bean
    else Has matching advisors
        AAPC->>AAPC: createProxy(bean, advisors)
        AAPC->>DPF: createAopProxy(config)
        DPF-->>AAPC: JdkDynamicAopProxy or CglibAopProxy
        AAPC->>Proxy: aopProxy.getProxy()
        AAPC-->>BF: return proxy (replaces original bean)
    end

ポイントはgetAdvicesAndAdvisorsForBean()の動作です。コンテナに登録されているすべてのAdvisor Beanを取得し、それぞれのPointcutを対象BeanのクラスにマッチングさせてAdvisorを絞り込みます。@Transactionalの場合、AdvisorのPointcutは@Transactionalが付いたメソッドにマッチします。@Asyncの場合も同様です。

実際に最もよく目にする具象サブクラスはAnnotationAwareAspectJAutoProxyCreatorです。SpringネイティブのAdvisorとAspectJスタイルの@Aspectクラスの両方を処理できます。

実行時のInvocationチェーン

プロキシ化されたメソッドが呼び出されると、プロキシは直接メソッドを実行するのではなく、適用可能なすべてのインターセプターからInvocationチェーンを構築します。

JDKプロキシでは、JdkDynamicAopProxyInvocationHandlerを実装します。

spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java

CGLIBプロキシでは、CglibAopProxyがコールバックインターセプターを持つサブクラスを生成します。

spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java

どちらも同じパターンに従います。AdvisedSupportからMethodInterceptorのチェーンを取得し、ReflectiveMethodInvocation(JDK)またはCglibMethodInvocation(CGLIB)を通じて順に実行します。各インターセプターは次のことができます。

  1. proceed()を呼び出す前に処理を行う(例:トランザクションの開始)
  2. proceed()を呼び出して次のインターセプター(または対象メソッド)に処理を委ねる
  3. proceed()の戻り後に処理を行う(例:トランザクションのコミット)
  4. 例外をキャッチする(例:トランザクションのロールバック)

これはChain of Responsibilityパターンの典型的な実装であり、@Transactional@Cacheable@Asyncを1つのメソッドに重ねて適用できる理由もここにあります。

MergedAnnotations:アノテーション探索エンジン

@Transactional内包するカスタムメタアノテーション@TransactionalServiceが付いているとき、Springはどうやって@Transactionalの存在を発見するのでしょうか。その答えがMergedAnnotationsです。

spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java#L31-L60

MergedAnnotationsは、Javaの要素(クラス・メソッド・フィールド)に付けられたアノテーションを統合的なビューとして提供します。次の3種類を網羅しています。

  1. 直接付与されたアノテーション — 要素に物理的に存在するもの
  2. メタアノテーション — アノテーションに付けられたアノテーション(再帰的に探索)
  3. @AliasForの解決 — アノテーションとメタアノテーション間の属性エイリアス
flowchart TD
    Element["@PostMapping('/users')"]
    
    Element --> Direct["Direct: @PostMapping"]
    Direct --> Meta1["Meta: @RequestMapping(method=POST)"]
    Meta1 --> Meta2["Meta: @Mapping"]
    
    Element --> Merged["MergedAnnotations.from(method)"]
    Merged --> Get["mergedAnnotations.get(RequestMapping.class)"]
    Get --> Result["MergedAnnotation with merged attributes:<br/>path='/users', method=POST"]
    
    Note1["@AliasFor resolves<br/>@PostMapping.value → @RequestMapping.path"]

@GetMapping("/users")@RequestMapping(path="/users", method=GET)のショートカットとして機能するのも、この仕組みのおかげです。MergedAnnotationsがメタアノテーション階層をたどって属性を透過的にマージします。AOPのPointcutマッチングでも同様です。@Transactionalが合成アノテーションの奥に隠れていても、MergedAnnotationsを使えば確実に発見できます。

ヒント: @AliasForを使ったカスタムアノテーションを作成する際は、MergedAnnotations.from(element).get(YourAnnotation.class)でテストして、属性のマージが意図通りに動作しているか確認しましょう。@AliasFor宣言のミスはサイレントに失敗するため、気づくのが遅れがちです。

全体像をつなぐ

AOPプロキシの完全なフローは、このシリーズでここまで解説してきたすべての要素を結びつけています。

  1. 第3回refresh()のステップ5が設定クラス上の@EnableTransactionManagementを検出し、ProxyTransactionManagementConfiguration@Importします。これによりTransactionInterceptor(Advice)とTransactionAttributeSourceAdvisor(Advisor)が登録されます。

  2. 第3回refresh()のステップ6がAnnotationAwareAspectJAutoProxyCreatorBeanPostProcessorとして登録します。

  3. 第2回:ステップ11(finishBeanFactoryInitialization)で@Service Beanが生成される際、initializeBean()がすべてのBeanPostProcessorpostProcessAfterInitialization()を呼び出します。

  4. 本記事AbstractAutoProxyCreator.postProcessAfterInitialization()TransactionAttributeSourceAdvisorのPointcutを評価し、対象のBeanが@Transactionalメソッドを持つことを確認します。その後、トランザクションインターセプターを組み込んだCGLIBプロキシを生成してBeanをラップします。

  5. 実行時に@Transactionalメソッドが呼ばれると、処理はプロキシを経由します。プロキシはTransactionInterceptorを呼び出し、トランザクションを開始して実際のメソッドを実行し、結果に応じてコミットまたはロールバックを行います。

次回予告

これでシリーズの4つの内部的な柱、モジュール構成、IoCコンテナ、ブートストラップシーケンス、AOPプロキシをすべてカバーしました。次回はWebレイヤーに移ります。DispatcherServlet.doDispatch()とそのリアクティブ版DispatcherHandler.handle()を通じてリクエストの道筋を追い、同じStrategyパターンから生まれる全く異なる実装を比較します。