Spring Bean の生成フロー:設定読み込みからインスタンス取得までの内部実装解説

Spring イオコンテナにおける Bean の管理は、アプリケーション起動時の複雑なプロセスを通過します。本稿では、バージョン 4.3.7 を基準に、設定ファイルの読み込みから Bean の最終的な取得に至るまでの主要なステップを技術的に深掘りします。

1. コンテナ初期化のトリガー

標準的な Java EE 環境においては、Tomcat や Jetty などの servlet コンテナが初期化される際、web.xml 定義されたリッスナーが動作します。Spring の場合、これは通常 ContextLoaderListener です。

<!-- web.xml での設定例 -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/config.xml</param-value>
</context-param>

ServletContextListener インターフェースを実装するこのクラスは、ウェブアプリケーションの開始時に contextInitialized メソッドを呼び出します。ここでの主たる役割はルートコンテキストの構築です。

class RootInitListener extends ContextLoader implements ServletContextListener {
    // アプリケーション開始時のエントリポイント
    public void contextInitialized(ServletContextEvent event) {
        initRootContext(event.getServletContext());
    }

    // アプリケーション終了時のクリーンアップ
    public void contextDestroyed(ServletContextEvent event) {
        closeRootContext(event.getServletContext());
    }
}

実際の実装である initRootContext メソッドでは、すでにルートコンテキストが存在しているかチェックし、重複防止処理を行います。存在しない場合に限り、新しいコンテナインスタンスを作成して設定し、最後に servlet context に保存されます。

2. BeanFactory と刷新ロジック

コンテナが作成されると、内部的に Bean の登録・管理を行う BeanFactory が準備されます。Spring のコンテナ初期化フローの核心となるのが、抽象ベースクラスの refresh() メソッドです。このメソッドはシリアライズロックを使って並行実行を制御しつつ、以下の流れを実行します。

protected void refresh() {
    synchronized (this.lifecycleMonitor) {
        prepareForRefresh();
        
        // 新規 BeanFactory の取得および設定
        ConfigurableListableBeanFactory factory = obtainFreshBeanFactory();
        
        // 設定を適用し、プロセッサを準備
        prepareBeanFactory(factory);
        invokeFactoryPostProcessors(factory);
        
        // Bean ポストプロセッサの登録
        registerBeanPostProcessors(factory);
        
        // メッセージソースやイベントマクロカスターの初期化
        initMessageSource();
        initEventMulticaster();
        
        // 特定サブクラスでの追加初期化 Hook
        onRefresh();
        
        // リスナーの登録
        registerListeners();
        
        // 非遅延ロードの設定を持つシングルトン Bean のプリインスタンシエーション
        finishBeanFactoryInitialization(factory);
        
        publishRefreshComplete();
    }
}

ここで重要な処理が finishBeanFactoryInitialization メソッド内で行われます。ここでは設定に基づいて、明示的に非 lazy-init として宣言されたシングルトン型 Bean がすべて即時インスタンシエーションされます。

3. XML の構文解析と定義登録

設定ファイル(xml)の内容をメモリ上に保持するためには、まず外部ファイルをパースする必要があります。Spring は XML パーサーを使用して、構成情報をオブジェクト構造へ変換します。

protected void loadResourceDefinitions(BeanFactory targetFactory, String fileUri) throws IOException {
    // パーサーの作成
    XmlDefinitionParser parser = new XmlDefinitionParser(targetFactory);
    
    // ロギング環境やエンコーディングの設定
    parser.setEnvironment(getEnvironment());
    
    // 定義を読み込み開始
    int count = parser.parse(fileUri);
    
    logDebug("Loaded " + count + " definitions");
}

具体的な実装では、XmlBeanDefinitionDocumentReader 類が使用され、DOM 形式のドキュメントツリーを取得した後に、それぞれの要素(import, alias, bean)に対して適切なパーサールールを適用します。

void parseComponent(Element node, ParserDelegate delegate) {
    // ネームスペースごとに分岐
    if (delegate.isStandardNamespace(node)) {
        if ("bean".equals(node.getNodeName())) {
            processBeanTag(node, delegate);
        } else if ("alias".equals(node.getNodeName())) {
            processAliasRegistration(node);
        }
    } else {
        // カスタムネームスペース(AOP など)の処理
        handleCustomElement(node);
    }
}

最終的に各 Bean エントリは BeanDefinitionHolder オブジェクトに変換され、レジストリ( registry )へと登録されます。この時点で、コンテナ内のデータ構造には「名前に紐づく定義情報」のみが含まれる状態となります。

void registerBeanEntry(BeanDefinitionHolder defHolder, Registry store) {
    String name = defHolder.getBeanName();
    
    // メイン名の登録
    store.registerMainDefinition(name, defHolder.getDefinition());
    
    // アリアスの処理
    for(String alias : defHolder.getAliases()) {
        store.registerAlias(name, alias);
    }
}

4. インスタンシエーション戦略

Bean の定義情報が登録された後、実際にオブジェクトを生成するのは getBean() または refresh() 直後のプリインスタンシエーション処理時です。デフォルトではシングルトンかつ遅延ローディングでない Bean は、コンテナ起動時に即座に作成されます。

インスタンス化のメカニズムは、クラスタイプに応じて動的に選択されます。特に、メソッドオーバーライドが指定されている場合は CGLIB サブクラス生成を行い、それ以外は単純なコンストラクタ呼び出しを行います。

Object createInstance(String beanId, RootBeanDef metaDef) {
    BeanWrapper wrapper = null;
    
    if (metaDef.hasMethodOverrides()) {
        // メソッド再定義がある場合、プロキシ生成が必要
        wrapper = proxyInstantiate(metaDef, beanId);
    } else {
        // 標準的なコンストラクタ呼び出し
        Constructor cstr = resolveConstructor(metaDef);
        wrapper = reflectInstantiate(cstr, args);
    }
    
    return wrapper.getWrappedInstance();
}

5. インスタンス取得とキャッシュ機構

アプリケーションコード側から Bean を取り出す際には、一度しか存在しないはずのシングルトン Bean はキャッシュから返却されます。doGetBean メソッド内で複数の検索階層を経由しますが、最も効率的なのは最初のキャッシュ確認です。

T doGetBean(String identifier, Class<T> requiredType) {
    // 単一利用可能か確認(早期参照も考慮)
    Object existing = getSingleton(identifier);
    
    if (existing == null) {
        // キーが見つからない場合、親コンテナをチェック
        
        if (!containsLocalDef(identifier)) {
             return getParentFactory().getBean(identifier, requiredType);
        }
        
        // 依存関係のある Bean の前倒し作成
        injectDependencies(identifier);
        
        // 真正な作成処理
        existing = instantiateSingleton(identifier);
        
        // シングルtons Map に登録してキャッシュ化
        registerSingleton(identifier, existing);
    }
    
    return (T) existing;
}

ただし、Prototype スコープや Request/Session スコープの場合は、毎回または特定のライフサイクル範囲で新たなインスタンスが生成されます。Request スコープの場合は、HTTP リクエスト属性として管理され、リクエスト完了時に破棄されます。

タグ: Spring Framework IoC Bean lifecycle Source Code Analysis Dependency Injection

6月23日 23:17 投稿