Read OSS

Spring の起動プロセスを解剖する:refresh() シーケンスと @Configuration クラスの処理

上級

前提知識

  • 第2回:BeanFactory の階層と BeanPostProcessor
  • Java のアノテーションとリフレクションの基礎知識

Spring の起動プロセスを解剖する:refresh() シーケンスと @Configuration クラスの処理

コマンドラインツールであれ、Web サーバーであれ、マイクロサービスであれ、すべての Spring アプリケーションは1つのメソッド呼び出しから始まります。それが AbstractApplicationContext.refresh() です。このメソッドは、環境の準備、コンポーネントのスキャン、@Configuration クラスの処理、post-processor の登録、そしてすべての singleton bean のインスタンス化まで、起動シーケンス全体を統括します。第2回で見た IoC コンテナがどのように構築・起動されるのかを理解するには、refresh() を理解することが不可欠です。

ApplicationContext:BeanFactory を超えた存在

refresh() の詳細へ入る前に、ApplicationContext が前回解説した BeanFactory へ何を加えているのかを整理しておきましょう。インターフェースの宣言がその答えを示しています。

spring-context/src/main/java/org/springframework/context/ApplicationContext.java#L59-L60

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory,
        HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher,
        ResourcePatternResolver {
classDiagram
    class ApplicationContext {
        <<interface>>
    }
    class ListableBeanFactory {
        <<interface>>
        Bean enumeration
    }
    class HierarchicalBeanFactory {
        <<interface>>
        Parent-child contexts
    }
    class EnvironmentCapable {
        <<interface>>
        Profiles + properties
    }
    class MessageSource {
        <<interface>>
        i18n message resolution
    }
    class ApplicationEventPublisher {
        <<interface>>
        Event broadcasting
    }
    class ResourcePatternResolver {
        <<interface>>
        Classpath scanning
    }

    ListableBeanFactory <|-- ApplicationContext
    HierarchicalBeanFactory <|-- ApplicationContext
    EnvironmentCapable <|-- ApplicationContext
    MessageSource <|-- ApplicationContext
    ApplicationEventPublisher <|-- ApplicationContext
    ResourcePatternResolver <|-- ApplicationContext

ApplicationContext は、ListableBeanFactoryHierarchicalBeanFactory を通じて BeanFactory の機能を取り込みつつ、さらに4つの機能を追加しています。環境の抽象化(プロファイルとプロパティソース)、国際化(i18n)、イベントの発行、そしてクラスパスのリソーススキャンです。これらは「あると便利な機能」ではなく、設定処理パイプラインが依存する本質的なインフラです。

代表的な実装クラスは2つあります。

  • AnnotationConfigApplicationContext — スタンドアロンのアノテーションベースアプリケーション向けです。内部で DefaultListableBeanFactory を生成し、AnnotatedBeanDefinitionReader を登録します。
  • GenericWebApplicationContext — Web アプリケーション向けで、通常は Spring Boot の SpringApplication が生成します。

refresh() の13ステップ

refresh() は Spring Framework で最も重要なメソッドです。全体像を確認してみましょう。

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java#L582-L662

sequenceDiagram
    participant App as Application
    participant AAC as AbstractApplicationContext
    participant BF as BeanFactory
    participant BPPs as BeanPostProcessors

    App->>AAC: refresh()
    
    Note over AAC: 1. prepareRefresh()
    Note right of AAC: Set startup time, active flag,<br/>validate required properties

    AAC->>BF: 2. obtainFreshBeanFactory()
    Note right of BF: Get or create the<br/>DefaultListableBeanFactory

    AAC->>BF: 3. prepareBeanFactory(beanFactory)
    Note right of BF: Register environment beans,<br/>system properties, ClassLoader

    Note over AAC: 4. postProcessBeanFactory()
    Note right of AAC: Template method for subclasses<br/>(web contexts register scopes)

    AAC->>BF: 5. invokeBeanFactoryPostProcessors()
    Note right of BF: 🔥 ConfigurationClassPostProcessor<br/>runs here — discovers all beans

    AAC->>BF: 6. registerBeanPostProcessors()
    Note right of BF: Sort & register all BPPs<br/>by PriorityOrdered/Ordered

    Note over AAC: 7. initMessageSource()
    Note over AAC: 8. initApplicationEventMulticaster()
    Note over AAC: 9. onRefresh()
    Note right of AAC: Template method<br/>(web contexts start server)

    Note over AAC: 10. registerListeners()
    
    AAC->>BF: 11. finishBeanFactoryInitialization()
    Note right of BF: Pre-instantiate ALL<br/>non-lazy singletons

    Note over AAC: 12. finishRefresh()
    Note right of AAC: Publish ContextRefreshedEvent,<br/>start Lifecycle beans

    Note over AAC: 13. Exception handling
    Note right of AAC: destroyBeans() + cancelRefresh()<br/>on any failure

特に重要な3つのステップを見ていきましょう。

ステップ5 — invokeBeanFactoryPostProcessors() がすべての起点となります。このステップでは、すべての BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor が呼び出されます。なかでも重要なのが ConfigurationClassPostProcessor です。クラスパスのスキャン、@Configuration クラスの処理、@ComponentScan の解決、@Import チェーンの追跡、そして発見されたすべての bean 定義の登録を担います。ステップ5の実行前、ファクトリが保持しているのはわずかなインフラ bean のみです。ステップ5が完了すると、アプリケーション全体の bean 定義が揃います。

ステップ6 — registerBeanPostProcessors() では、ステップ5で生成されたすべての BeanPostProcessor の bean 定義を発見し、優先度順にインスタンス化します。第2回で見たように、BeanPostProcessor は汎用の拡張フックです。このステップによって、ファクトリはその後の処理に向けた「武装」を完了します。

ステップ11 — finishBeanFactoryInitialization() では、DefaultListableBeanFactory.preInstantiateSingletons() が呼び出されます。登録されているすべての bean 定義を走査し、遅延初期化でない singleton ごとに getBean() を実行します。第2回で解説した bean 生成パイプラインが、アプリケーションの bean に対して実際に動作するのはこのタイミングです。

ヒント: 起動が遅い場合、ボトルネックはほぼ間違いなくステップ5(クラスパスのスキャン)かステップ11(singleton のインスタンス化)です。Spring Boot の startup actuator はステップごとの処理時間を記録しています。spring.context.beans.post-processspring.context.refresh の起動ステップを確認してみましょう。

ConfigurationClassPostProcessor:アノテーションベース設定の中核を担う

ConfigurationClassPostProcessor は Spring の中で最も複雑なクラスと言っても過言ではありません。1,100行を超えるコードの中で、BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor の両方を実装しており、ステップ5の2つの異なるフェーズに介入できる構造になっています。

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java#L133-L139

インポート文を見るだけで、その責務の広さが伝わります。AOT コード生成(44〜75行目)、bean 登録、アノテーション処理、さらにはプロパティソースの処理まで、幅広い役割を担っています。

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java#L44-L75

処理フローは ConfigurationClassParser に委譲されます。

flowchart TD
    CCPP["ConfigurationClassPostProcessor<br/>postProcessBeanDefinitionRegistry()"]
    
    CCPP --> FindCandidates["Find @Configuration candidates<br/>in existing bean definitions"]
    FindCandidates --> CCP["ConfigurationClassParser.parse()"]
    
    CCP --> ProcessConfig["Process @Configuration class"]
    ProcessConfig --> CS["@ComponentScan<br/>→ ClassPathBeanDefinitionScanner"]
    ProcessConfig --> Import["@Import<br/>→ Recursive processing"]
    ProcessConfig --> Bean["@Bean methods<br/>→ BeanDefinition registration"]
    ProcessConfig --> PS["@PropertySource<br/>→ PropertySource registration"]
    ProcessConfig --> IC["@ImportResource<br/>→ XML import"]
    ProcessConfig --> BR["BeanRegistrar<br/>→ Programmatic registration"]
    
    Import --> IS["ImportSelector<br/>(Spring Boot auto-config)"]
    Import --> IBR["ImportBeanDefinitionRegistrar"]
    Import --> RecurseConfig["Another @Configuration<br/>→ Recurse"]
    
    CS --> NewConfigs["Discovered @Configuration classes"]
    NewConfigs --> CCP

このパーサーは再帰的に動作します。@ComponentScan を見つけると classpath スキャナーを起動し、新たに発見した @Configuration クラスを処理対象に加えます。@Import を見つけると、インポートチェーンを再帰的に追跡します。@SpringBootApplication 一つで数百もの auto-configuration クラスが読み込まれるのは、この仕組みによるものです。

ConfigurationClassParser@Conditional アノテーションを、設定クラスを処理する前に評価します。@ConditionalOnClass@ConditionalOnMissingBean など、Spring Boot の条件付き auto-configuration を支える仕組みはまさにここにあります。

遅延インポート処理:Spring Boot の起点

パーサーは ImportSelector の実装を2通りの方法で処理します。

  1. 通常の ImportSelector — パース中に即座に処理されます。
  2. DeferredImportSelector — すべての設定クラスが処理されたにまとめて処理されます。

Spring Boot の AutoConfigurationImportSelectorDeferredImportSelector を実装しています。これにより、ユーザー定義の bean が先に登録されるため、@ConditionalOnMissingBean チェックがカスタム実装の有無を正確に判断できます。

この微妙な順序の保証こそが、Spring Boot の「意見を持ちつつも簡単に上書きできる」設計パターンを成立させています。遅延処理がなければ、auto-configuration はユーザーの設定と競合し、予測不可能な結果を生むことになるでしょう。

BeanRegistrar:AOT 時代のプログラマティックな bean 登録

Spring 7.x では、プログラムで bean を登録するための新しい API が導入されました。

spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java#L22-L80

class MyBeanRegistrar implements BeanRegistrar {
    @Override
    public void register(BeanRegistry registry, Environment env) {
        registry.registerBean("foo", Foo.class);
        registry.registerBean("bar", Bar.class, spec -> spec
                .prototype()
                .lazyInit()
                .supplier(context -> new Bar(context.bean(Foo.class))));
    }
}

なぜ新しい登録メカニズムが必要なのでしょうか。その答えは AOT(Ahead-of-Time)コンパイルにあります。詳細は第6回で解説しますが、従来の @Bean メソッドはランタイムのリフレクションに依存しており、フレームワークがメソッドをリフレクティブに呼び出して bean を生成する必要があります。一方、BeanRegistrarsupplier() ラムダによる明示的なコードレベルの bean 生成を提供し、ビルド時に解析・最適化できます。

flowchart LR
    subgraph "@Configuration + @Bean"
        A1["Runtime reflection"] --> A2["Invoke method reflectively"]
        A2 --> A3["Return bean instance"]
    end
    
    subgraph "BeanRegistrar"
        B1["Explicit supplier lambda"] --> B2["Direct constructor call"]
        B2 --> B3["Return bean instance"]
    end
    
    A1 -.->|"AOT must generate<br/>reflection hints"| AOT1["AOT overhead"]
    B1 -.->|"Already AOT-friendly<br/>no reflection needed"| AOT2["Zero AOT overhead"]

BeanRegistrar@Import を通じて ConfigurationClassParser に統合されます。

@Configuration
@Import(MyBeanRegistrar.class)
class MyConfiguration { }

重要な点として、BeanRegistrar の実装クラスは Spring コンポーネントではありません。引数なしコンストラクタを持ち、依存性の注入も使えません。この制約は意図的なものです。AOT 処理中に簡単にインスタンス化できることを保証するためです。

ヒント: BeanRegistrar の Javadoc には明記されています。実装クラスに @Component を付けたり @Bean から返したりすると bean として登録はされますが、register() メソッドは呼び出されません。登録を起動するのは @Import だけです。

同期されたシャットダウンのセーフティネット

refresh()startupShutdownLock(583行目)を取得し、シーケンス全体を try-finally ブロックで囲んでいます。いずれかのステップで RuntimeExceptionError がスローされると、628行目の catch ブロックがクリーンアップ処理を実行します。

  1. 起動済みの Lifecycle bean をすべて停止する
  2. destroyBeans() を呼び出し、失敗前に生成された singleton をクリーンアップする
  3. cancelRefresh() を呼び出し、アクティブフラグをリセットする

これにより、起動に失敗しても未解放のリソースが残ることはありません。中途半端な状態のデータベース接続、スレッドプール、ネットワークリスナーも、すべて適切にシャットダウンされます。

次回予告

refresh() から ConfigurationClassPostProcessor、そして BeanRegistrar に至る起動シーケンス全体を追ってきました。次回は、これらの bean に横断的な振る舞いが必要になったとき何が起きるのかを掘り下げます。Spring の AOP プロキシインフラが @Transactional@Async などのインターセプターを bean に透過的に適用する仕組み、すなわち第2回で確認した postProcessAfterInitialization フェーズの詳細を解説します。