Spring MVCの構造、動作原理、および処理フロー
Webサーバー (静的ページ)
ユーザーにサーバーにあるページを表示します。
ウェブの初期には、サーバーに静的なドキュメント(HTML)を保存し、ユーザーがリクエストすると、そのファイルをユーザーのブラウザにダウンロードして表示する方式でした。例えば、特定のサーバー aaron.com にある hello.html ドキュメントを見たい場合、ブラウザで aaron.com/hello.html を呼び出す形式です。大学で教授が自身の研究室サーバーを利用して講義資料を配布する際に、以下のようなページを見た読者もいるかもしれません。

この方式では、サーバーが提供したいファイルを実際にサーバー内部に一つずつ配置する必要があり、サーバーに存在しないファイルにアクセスすると 404 Not Found Error が返されました。このようにユーザーに静的なページを提供するサーバーをWebサーバーと呼び、よく知られているApacheやNginxがこれに該当します。
例) Nginxのリクエスト/処理フロー
Webサーバーの例として、Nginxではユーザーのリクエストを以下の過程で処理します。

- ユーザーはWebサーバーに特定のページ(
index.html)をリクエストします。 - Webサーバーは
index.htmlを検索し、存在すればユーザーに返します。
Webアプリケーション (動的ページ)
サーバーにないページを、ユーザーの各リクエストごとに動的に生成して表示します。
Webサーバーがユーザーに単純なドキュメントを共有する一方的なサービスを提供するに留まらず、ユーザーとのインタラクションを通じて会員登録が可能になり、記事の投稿や、投稿された記事を相互に閲覧できるといった双方向サービスの要求を受けるようになりました。一般的なアプリケーションのようにデータベースとの接続も必要になり、会員の状態に応じた動的なページレンダリングのための非同期API呼び出しなどが必要になったのです。サーバーはもはやサーバーにある静的リソースを返すだけでなく、ユーザーが要求した情報に対応するリソース(ページ)を動的に作成して返すようになりました。
ウェブでアプリケーションのような要求を処理するためには、Webサーバーと様々な言語で開発されたプログラムを接続し、ユーザーのリクエストをサーバー経由でプログラムに渡す必要がありました。このようにWebサーバーとプログラムの間を接続する方式をCGI (Common Gateway Interface)と呼び、様々な言語で開発されてきましたが、その中でもJavaにおいてはWebサーバーのリクエスト/レスポンスとJavaアプリケーションの間を接続するServletオブジェクトの概念が登場します。
Servletはユーザーのリクエストごとに一つずつ生成されるため、複数のリクエストに応じたServletリソースの管理が必要になります。この役割を担うのがWebコンテナであり、Servletの視点からはServletコンテナとも呼ばれます。
- ユーザーのリクエスト/レスポンスを管轄するWebサーバー、
- ここに、リクエストに応じた適切なJavaアプリケーションの稼働を担うServlet管理者であるWebコンテナを追加すると、
この両者を合わせたものがWebアプリケーションです。
- Webアプリケーション = Webサーバー + Webコンテナ (= Servletコンテナ)
- Webコンテナは、ユーザーのリクエストに応じてServletリソースのライフサイクルを管理します。
- 生成 (init) -> 処理 (service) -> 破棄 (destroy)

例) Tomcatのリクエスト/処理フロー
Webアプリケーションの例として、Tomcatではユーザーのリクエストを以下の過程で処理します。

Webサーバーの図と比較すると、Webサーバーの下に追加されたものはすべてWebコンテナに関連するものです。Webコンテナから始めて、下から上に逆順で見ていきましょう。隣に灰色で表示されている名称は実際のクラス/インターフェース名です。
- ServletContext (Webコンテナ)
- Servletオブジェクトのライフサイクル管理のためのWebコンテナ(管理という意味でのContext)
- すべてのリクエストに対するServletのライフサイクルは、このServletContextがすべて管理します。
- ServletContextListener
- ServletContextが最初に起動する際(Listener)に実行する処理を定義します。
- web.xml (デプロイメント記述子)
- デプロイメント記述子という名称が示すように、Webコンテナ起動時のServletのための設定です。
- B) どの
ServletContextListenerインターフェース実装を実行するか - A) 「どのリクエスト」に対して「どのタイプ」のServletオブジェクトを生成するかについてのマッピング
- 例)
/hello.htmlへのアクセスはHelloServletが動的に処理
- 例)
- B) どの
- デプロイメント記述子という名称が示すように、Webコンテナ起動時のServletのための設定です。
構成が分かったところで、上記の図に従ってWebアプリケーションの起動時の手順を見ていきましょう。
初回起動時
- Tomcat Webアプリケーションが初回起動する際、最も先に**Webコンテナ (ServletContext)**が起動します。
- B) ServletContext起動時、
web.xmlに設定された**ServletContextListener**が同時に実行されます。
リクエスト処理時
- ユーザーは**Webサーバー**に特定のページ(
index.html)をリクエストします。 - Webサーバーは
index.htmlを検索しますが、存在しないため、**Webコンテナ (ServletContext)**にリクエストを移管します。 - A) **ServletContext**は、
web.xmlで定義されたindex.htmlリクエストに合致するタイプのServletを生成します。- 生成されたServletは、ユーザーがリクエストしたページを動的に生成してユーザーに返し、その後破棄 (destroy) されます。
Spring Framework
Java Servletを活用したWebアプリケーション開発が活発になるにつれて、様々なデザインパターンを適用し、Java Web開発をより容易にするSpring Frameworkが登場しました。初期のWebアプリケーションがページを動的にレンダリングするために各リクエストごとにServletを割り当ててリクエストを処理していたのに対し、Springは各リクエストごとにServletよりも小さい単位であるBeanを割り当ててリクエストを処理します。これは、多数のServletを置かず、単一のServlet(後述しますが、これがDispatcherServletです)を置き、バックエンドで多様かつ多数のBeanを配置することで、リクエストに応じた適切な処理を行うという意味です。
- Servletコンテナは、各URLリクエストをServletを単位として処理します。
- リクエストを処理する単位がServletであるため、ServletコンテナがServletを管理します。
- Springコンテナは、各URLリクエストをBeanを単位として処理します。
- リクエストを処理する単位がBeanであるため、Spring (Bean) コンテナがBeanを管理します。
Springは基本的にMVCモデルで、Model、View、Controllerの3つのグループに役割を分離して開発を支援するフレームワークであるため、デザインパターンに関する知識が全くない開発者でも、保守性、再利用性に優れたWebアプリケーションを作成できます。また、データベースアクセス用のJPA、トランザクション、セキュリティなど、Webアプリケーションで必要とされるすべてのものをBean設定で提供するため、どんな初心者でもしっかりとした理解があればWebアプリケーションを簡単に作成できます。実際、しっかりとした理解がなくても作成できるのがSpringの利点です。これはつまり、ジュニア/シニアに関わらず最高の生産性を意味します。
Spring MVCの概念
Spring MVCの動作過程を理解するには、MVCとFront Controllerパターン (2レベルController) を知っていれば十分です。
MVC (Model, View, Controller)
- ユーザーがあるページをリクエストすると、リクエストに適合するControllerがリクエストを受け取り、
- リクエストされたページに必要な情報であるModelを検索/生成し、
- 検索/生成したModelを通じて最終ページであるViewを生成してユーザーに返します。
Front (2レベル) Controllerパターン
上記のMVCにおいて、リクエストを受け取る部分をControllerとしましたが、
- Tomcatの立場では、リクエストを処理するServletがControllerでしょう。
- Springの立場では、リクエストを処理するBeanがControllerでしょう。
では、2レベルControllerの意味は以下の通りです。
- Front Controller: すべてのユーザーリクエストを、最初にTomcatの**単一Servlet (DispatcherServlet)**で受け取り、
- Page Controller: リクエストURLが何であるかに応じて、SpringのController Beanにマッピング/ページ生成および返却を行います。
一番前のTomcat単一Servlet (DispatcherServlet)を、「リクエストを一番最初に受け取る」という意味でFront Controllerと呼び、その後のSpring Controller Beanを、実際のページ生成に使用されるという意味でPage Controllerと呼びます。
Spring MVCのリクエスト/処理フロー
初回起動時
まず、Spring + Tomcatが最初に起動する際にどのようなオブジェクトが生成されて準備されるかを見ていきましょう。Webコンテナの下にSpringコンテナが新しく追加されているのが分かります。

上記の図のようにTomcatにSpringを接続するためには、Tomcatの設定ファイルであるweb.xmlに2つの設定が必要です。
- web.xml (デプロイメント記述子)
- B) ServletContextListenerインターフェース実装を通じて
- 1: ServletContextを起動するだけでなく = Front Controller
- A) すべてのリクエストはFront Controllerに該当する単一のServletオブジェクト(DispatcherServlet)が処理します。
- 2: Root WebApplicationContextも同時に(次に)起動します = Page Controller
- これはSpring共通Bean (@Service, @Repository, @Component…) オブジェクトを事前に生成しておくためです。
- 1: ServletContextを起動するだけでなく = Front Controller
- B) ServletContextListenerインターフェース実装を通じて
リクエスト処理時
初回起動後、Tomcatはすべてのリクエストを単一のDispatcherServletで受け取る準備が完了し、SpringもController Beanが結果を返すための様々なBeanがRoot WebApplicationContextに準備完了します。それでは、ユーザーがリクエストを送信した際の処理を見ていきましょう。少し複雑に見えるかもしれませんが、上記の初回起動時で見た内容に少し拡張が加えられただけですので、心配する必要はありません。

SpringのキーワードはIoC、DIと言えますが、簡単に説明すると、従来は開発者が new を通じてオブジェクトを直接生成し、直接注入していたのに対し、Springではインターフェースだけを明示し、ApplicationContext (= Spring Container, BeanFactoryを継承) がBeanと呼ばれるオブジェクトを生成し、開発者の設定によって自動的に注入してくれる概念です。このようにSpringでは、基本単位のJavaオブジェクトをBeanと呼んで使用します。
- Spring Container = ApplicationContext
SpringにおけるBeanは、Webアプリケーションの観点から、以下のように大きく2つのタイプに区分できます。それに伴い、Beanのライフサイクルを管理するSpringコンテナも2つに分けられます。
- リクエストが来た際に適切な処理のために、リクエストに関わらずすべてのBeanが相互に共有する共通Bean
- 例)
@ComponentScanで登録された @Service, @Repository, @Component など - ライフサイクル管理
- Root WebApplicationContext (図中のSpring Container 1)
- 例)
- リクエストが来た時に割り当てられるServletのように、リクエストが来た時にのみ生成すればよいBean
- 例:
@ComponentScanで登録された @Controller, @Interceptor など - ライフサイクル管理
- Servlet WebApplicationContext (図中のSpring Container 2)
- 例:
上記の図を見ると、初回起動時に生成されたSpring Container 1の下にもう一つのSpring Container 2が生成されているのが分かります。特に重要な内容ではありませんが、「parent」と「child」と書かれているのは、2つのコンテナ間に階層があることを意味し、単に子であるServlet WebApplicationContextのBeanは親であるRoot WebApplicationContextのBeanを参照できますが、その逆は参照できないことを意味します。
リクエスト処理は次の流れで進行します。上記の赤線(リクエスト)と緑線(応答)の流れに従って見ていきましょう。
- ユーザーは**Webサーバー**に特定のページ(
index.html)をリクエストします。 - Webサーバーは
index.htmlを検索しますが、存在しないため、**Webコンテナ (ServletContext)**にリクエストを移管します。 - A) ServletContextは、
web.xmlで定義された、どのリクエストに対しても/**単一のDispatcherServlet**を生成します。 - **DispatcherServletは、ユーザーがリクエストしたページに該当するSpring Controllerがあるか、HandlerMapping**を探索します。
- DispatcherServletは、見つけた**Spring Controller BeanをHandlerAdapter**を介して呼び出します。
- HandlerAdapterは**HelloController Bean**を呼び出します。
- HelloControllerは**Root WebApplicationContext内のBeanを活用してDispatcherServlet**に結果を返します。
- DispatcherServletはControllerの結果を受け取り、ViewResolverで結果ページ (= View) (
index.html) を生成します。 - DispatcherServletは**結果ページ (= View) (
index.html)** をユーザーに返します。
上記の過程のコードレベルでの流れは、こちらのブログリンクに詳しくまとめられているので、参考にすると良いでしょう。
このように、Spring MVCがどのようにユーザーのリクエストを受け取り、処理し、返すのかを図で見てきました。これほど詳細に見ていくと、ControllerやServiceでExceptionが発生した際にSpringのログに残る数多くのスタックトレース内のメソッドやクラス(invoke, DispatcherServlet, preHandle, postHandleなど)の意味をより深く理解できるようになるでしょう。
長い記事でしたが、ここからもう少し踏み込んでみようと思います。それはInterceptorとFilterです。この2つの違いは、単純にServletが管理するかSpringが管理するかであり、呼び出しの順序とタイミングを知っていれば、将来Spring Securityを学習/適用する際に大きな問題はないでしょう。もちろん、ここまで来るのに十分に大変だった場合は、後日でも構いませんのでぜひ読んでいただきたいと思います。
Spring InterceptorとFilterの違い
Springに限らず、どのようなWebアプリケーションでも一部のユーザーにサービスを提供するために公開されているため、セキュリティは絶対に必要です。Spring Securityは、基本的にログインとセッションに関連するモジュールや設定を簡単に利用できるように提供するだけでなく、別途開発した認証モジュールを適用したり、リクエストURLに応じて異なるセキュリティ処理を行ったりと、開発者が望むセキュリティ要素を、Spring Controllerに実際のリクエストが渡される前に実行されるように追加できます。この際に使用されるのがInterceptorとFilterです。
私たちはこれまで、Springを使用したWebアプリケーションが大きくTomcat (Webコンテナ)とSpring (Springコンテナ)の2つで構成されることを学びました。InterceptorとFilterは、管理主体および実行時間がこの2つの構成要素、TomcatとSpringに分かれると理解すれば良いでしょう。
FilterはServlet仕様の一部であり、サーバー(Tomcat)によって呼び出されます。一方、InterceptorはSpringによって呼び出されます。^2
- FilterはServlet仕様の一部であり、Servlet (Tomcat) によって呼び出されますが、
- InterceptorはSpringによって呼び出されます。
以下は、InterceptorとFilterの管理主体および実行時間を分かりやすく表現した図です。

Filter (Tomcat)
- Servlet (J2EE 7標準) 仕様に定義されています。
- Webアプリケーション (Tomcat) のデプロイメント記述子 (web.xml) に設定されます。
- この部分は最新のSpringでも設定可能です。
- Webアプリケーション (Tomcat) のデプロイメント記述子 (web.xml) に設定されます。
- 1つの関数でDispatcherServletの前/後に呼び出されます。
doFilter()- リクエストが
DispatcherServlet.service()に入る直前(init()の後)に呼び出されます。 DispatcherServlet.service()が結果を返す直後(destroy()の前)に呼び出されます。
- リクエストが
doFilter 関数がリクエスト進入時と結果返却時の2回呼び出されるため、暗号化/復号化のような、リクエスト前と返却後の両方でグローバルに処理する必要があるロジックに適しています。
Interceptor (Spring)
- Spring Framework仕様に定義されています。
- SpringのWebApplicationContextに設定されます。
- 3つの関数でControllerの前/後に呼び出されます。
preHandle()= リクエストがControllerに入る直前に呼び出されます。postHandle()= Controllerが結果を返した直後に呼び出されます。afterCompletion()= Controllerの結果に基づいてViewを生成した直後に呼び出されます。
Controller進入時、または結果返却時に詳細な処理が必要なロジックに適しています。例えば、特定のURLに進入するリクエストに対しては、Controller進入直前にそのURLに特化した情報をセッションに事前に設定し、Controller内部ロジックで活用できるようにすることができます。他のURLであれば、このロジックを実行しないように条件を追加することも可能です。
重要な内容として、FilterとInterceptorは管理主体が異なるため、以下のような状況が発生する可能性があります。
FilterはSpring Containerの管理主体ではないため、Filterロジック内部でSpringのBeanを使用するには、@AutowiredのようなBean注入ではなく、まずSpring WebApplicationContextオブジェクトを取得し、その中に設定されたBeanをハードコーディングを通じて直接取得して使用する必要があります。
複数InterceptorとFilterの呼び出し順序
FilterとInterceptorは、状況に応じて複数指定して使用できます。多数のFilterあるいはInterceptorを使用する際、FilterとInterceptorそれぞれにおける呼び出し順序は設定できますが、Filter - Interceptor - Filter - Interceptor のように混ぜる形式では不可能です。
2つのFilterと2つのInterceptorを使用する際にどのように動作するのか順序を調べるために、DispatcherServletとHandlerAdapterに焦点を当てて見てみると以下のようになります。

簡潔に要約すると、下記の図/流れのようになります。

doFilter(F1)doFilter(F2)preHandler(I1)preHandler(I2)- Controller リクエスト処理
postHandler(I2)postHandler(I1)- View レンダリング
afterCompletion(I2)afterCompletion(I1)doFilter(F2)doFilter(F1)
WebサーバーからWebアプリケーションへ、WebサーバーとWebアプリケーションを接続するためのCGIの例としてServlet、そしてコンテナについて学んだ後、SpringコンテナとFilter、Interceptorの違い、そして実行順序について考察しました。Springを学習されている方や使用されている他の開発者の皆様にとって、この記事が役立つことを願っています。参照した記事も素晴らしい内容ですので、お時間があれば一度目を通すことをお勧めして締めくくります。