Spring と Spring MVC を統合する際、通常はそれぞれに異なる設定ファイルを使用し、結果としてアプリケーション内に少なくとも2つの ApplicationContext インスタンスが存在することになる。Web アプリケーションでは、これらは WebApplicationContext(ApplicationContext のサブインタフェース)として実装される。
この構成には以下の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 をスキャンしてはいけない理由
@RequestMapping は DispatcherServlet の初期化処理(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}") による値注入も、該当コンテキスト内でプロパティが読み込まれていないと機能しない。