Spring と Spring MVC の親子コンテナ構造

Spring と Spring MVC を統合する際、通常はそれぞれに異なる設定ファイルを使用し、結果としてアプリケーション内に少なくとも2つの ApplicationContext インスタンスが存在することになる。Web アプリケーションでは、これらは WebApplicationContextApplicationContext のサブインタフェース)として実装される。

この構成には以下の2種類のコンテキストが含まれる:

  • Servlet WebApplicationContext:Web 層(コントローラやビュー解決器など)を設定するためのコンテキスト。Spring MVC の DispatcherServlet によって初期化され、一般的に spring-servlet.xml などのファイルで定義される。
  • Root WebApplicationContext:サービス層やデータアクセス層(DAO)の Bean(例:ビジネスロジック、DataSource など)を管理するコンテキスト。ContextLoaderListener によって初期化され、通常は applicationContext.xml で設定される。

web.xml の設定例

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <!-- Root WebApplicationContext の設定 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Servlet WebApplicationContext の設定 -->
    <servlet>
        <servlet-name>appDispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/spring-mvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>appDispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

上記の設定では:

  • ContextLoaderListener が起動時に applicationContext.xml を読み込み、Root WebApplicationContext を生成し、ServletContext の属性キー org.springframework.web.context.WebApplicationContext.ROOT に格納する。
  • DispatcherServlet は自身の設定ファイル(例: spring-mvc-config.xml)を読み込み、独自の Servlet WebApplicationContext を作成する。その際に、既存の Root コンテキストがあれば親として設定する。

この親子関係により、子コンテキスト(Servlet)内で Bean を検索する際、見つからない場合は親コンテキスト(Root)に委譲される仕組みになっている。

なぜ親子コンテキストが必要なのか?

これは主にレイヤー間の責務分離とフレームワークの切り替え可能性を高めるためである。例えば、Web 層を Spring MVC から Struts に変更したい場合、Root コンテキスト(サービス・DAO 層)の設定は一切変更せずに済む。ただし、プロジェクトが Spring + Spring MVC に固定されているなら、すべての Bean を MVC コンテキストにまとめることも可能であり、その場合は Root コンテキストは不要となる。

よくある問題と対応

1. Root コンテキストで @Controller をスキャンしてはいけない理由

@RequestMappingDispatcherServlet の初期化処理(initHandlerMethods())でスキャンされるが、この処理は子コンテキスト(Servlet)内の Bean のみを対象とする。親コンテキストに定義されたコントローラはハンドラとして登録されず、結果としてリクエストが 404 になる。

2. 全ての Bean を Spring MVC コンテキストにまとめるのは可能か?

技術的には可能だが、トランザクションや AOP 機能が正しく動作しない可能性がある。これらの機能は Spring のプロキシ機構に依存しており、コンテキストが分かれていると期待通りに動作しないことがある。

3. 親子コンテキストで重複スキャンを避ける方法

Bean の重複登録を防ぐため、以下のようにパッケージスキャンを分けるのが一般的なベストプラクティスである。

applicationContext.xml(Root)

<context:component-scan base-package="com.example.app">
    <context:exclude-filter type="annotation"
                            expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

spring-mvc-config.xml(Servlet)

<context:component-scan base-package="com.example.app.controller"
                        use-default-filters="false">
    <context:include-filter type="annotation"
                            expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

4. プロパティファイルの読み込みについて

Root コンテキストで読み込んだ .properties ファイルは、子コンテキストからは参照できない。逆も同様である。したがって、両方のコンテキストで共通のプロパティを使用する場合は、それぞれの設定ファイルで明示的に読み込む必要がある。また、@Value("${key}") による値注入も、該当コンテキスト内でプロパティが読み込まれていないと機能しない。

タグ: Spring Spring MVC WebApplicationContext ContextLoaderListener DispatcherServlet

7月3日 19:00 投稿