複数のWebMvcConfigurationSupport継承クラスが存在する場合に最初の設定のみ有効になる理由(ソースコード分析)

はじめに Spring Bootアプリケーションにおいて、WebMvcConfigurationSupportを継承した複数の設定クラスを定義しても、実際には最初にスキャンされたクラスの設定内容しか反映されない現象がよく発生します。多くの解説は「そうなる」という事実に留まりますが、その根本原因をSpringフレームワークの内部処理フロー、特にコンフィギュレーションクラスの解析段階にまで遡って解明していきます。

ConfigurationClassPostProcessorによる設定クラスの解析 Springでは、アノテーション付きのJavaクラス(@Configurationなど)を正しく読み込むためにConfigurationClassPostProcessorが使用されます。このプロセッサは、Bean定義の生成フェーズでprocessConfigurationClassメソッドを呼び出し、設定クラスを再帰的に解析します。

protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) {
    // 中略...
    SourceClass sourceClass = asSourceClass(configClass, filter);
    do {
        sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
    } while (sourceClass != null);
    // 解析済みクラスを登録
    this.configurationClasses.put(configClass, configClass);
}

上記コードのdo-whileループは、スーパークラスが存在する限り解析を続けることを意味しています。つまり、カスタム設定クラス → WebMvcConfigurationSupport の継承チェーンを順にたどって、最終的にMVCインフラ用のBean定義をすべて収集する仕組みです。

スーパークラスの解析とknownSuperclassesの役割 核心となるのはdoProcessConfigurationClass内の以下のロジックです。

if (sourceClass.getMetadata().hasSuperClass()) {
    String superclass = sourceClass.getMetadata().getSuperClassName();
    if (superclass != null && !superclass.startsWith("java") &&
        !this.knownSuperclasses.containsKey(superclass)) {
        
        // 初回のみキャッシュに追加し、スーパークラスを返す
        this.knownSuperclasses.put(superclass, configClass);
        return sourceClass.getSuperClass();
    }
}
// 既に登録済みであればnullを返してループ終了
return null;

ここで使われているknownSuperclassesは、スーパークラスの完全修飾名をキーとして、その解析元となった設定クラスを保持するMapです。したがって:

  1. 最初の設定クラス(例: MyASD)が解析されるとき、WebMvcConfigurationSupportknownSuperclassesに登録される。
  2. 次にWebConfigurationが解析されようとしても、WebMvcConfigurationSupportはすでにマップに存在するため、スーパークラスの解析を行わずnullが返される。
  3. 結果として、WebConfiguration自体の@Beanやオーバーライドメソッドは無視され、Bean定義として登録されない

Beanインスタンス生成時の影響 Bean定義フェーズでWebMvcConfigurationSupport由来のBean(例: requestMappingHandlerAdapter)が最初の設定クラス(MyASD)に関連付けられてしまうため、実際にBeanをインスタンス化する段階で以下のような状況になります。

private Object instantiate(
    String beanName,
    RootBeanDefinition mbd,
    @Nullable Object factoryBean,
    Method factoryMethod,
    Object[] args) {

    // factoryBeanには最初に登録された設定クラスのインスタンスが渡される
    return this.beanFactory.getInstantiationStrategy().instantiate(
        mbd, beanName, this.beanFactory, factoryBean, factoryMethod, args);
}

たとえばextendMessageConverters()メソッドが呼び出される際も、常に最初の設定クラスの実装が実行されることになります。2つ目の設定クラスで同様のメソッドをオーバーライドしていても、そもそもそのクラスがファクトリとして認識されていないため、反映されません。

結論:なぜ片方しか効かないのか?

  • SpringはknownSuperclassesという内部キャッシュにより、同じスーパークラス(WebMvcConfigurationSupport)の再解析を防ぐ
  • そのため、最初にスキャンされた設定クラスのみがMVC関連のBean定義を提供する「ファクトリ」として登録される。
  • 2つ目以降の設定クラスは、独自の@Bean@Overrideメソッドがあっても、Bean定義として登録されないため無効になる

回避策 複数のカスタマイズが必要な場合は、以下のいずれかの方法を推奨します:

  • 一つのメイン設定クラスでWebMvcConfigurationSupportを継承し、他のカスタマイズはDIで注入して適用。
  • WebMvcConfigurerインターフェースの使用(WebMvcConfigurationSupportではなく)。こちらは複数宣言可能で、すべてのaddInterceptors()extendMessageConverters()がコールされます。

タグ: Spring Boot WebMvcConfigurationSupport ConfigurationClassPostProcessor Beanライフサイクル knownSuperclasses

7月9日 21:22 投稿