リクエストディスパッチ: DispatcherServlet と Reactive DispatcherHandler の内側
前提知識
- ›第3回: ApplicationContext のリフレッシュ — DispatcherServlet は onRefresh() 中に初期化される
- ›Servlet API とリアクティブストリームの基本的な理解
リクエストディスパッチ: DispatcherServlet と Reactive DispatcherHandler の内側
Spring Framework の Web レイヤーには、2つの並行する世界があります。従来のブロッキング I/O を担う DispatcherServlet と、リアクティブなノンブロッキング I/O を担う DispatcherHandler です。どちらも同じ Strategy ベースのアーキテクチャ(HandlerMapping、HandlerAdapter、そして結果のハンドリング)を用いたフロントコントローラーパターンを実装していますが、その実装は大きく異なります。DispatcherServlet は 1,400 行以上の命令型コードで構成されています。一方 DispatcherHandler は、同じ概念的なフローをわずか約 220 行で実現しています。この差こそが、リアクティブプログラミングがいかに複雑さを再分配するかを雄弁に語っています。
spring-web: 共有 HTTP 抽象化レイヤー
ディスパッチャーの詳細へ入る前に、モジュール構成を把握しておきましょう。第1回で整理したように、spring-webmvc と spring-webflux はどちらも spring-web に依存しています。これは共通の HTTP 抽象化を提供する共有モジュールです。
spring-web/spring-web.gradle#L1-L9
spring-webmvc/spring-webmvc.gradle#L6-L13
spring-webflux/spring-webflux.gradle#L6-L10
flowchart TD
subgraph "spring-web (shared)"
HC["HttpMessageConverter"]
RC["RestClient"]
HA["HTTP abstractions"]
end
subgraph "spring-webmvc"
DS["DispatcherServlet"]
SA["Servlet API"]
SE["spring-expression"]
SA2["spring-aop"]
end
subgraph "spring-webflux"
DH["DispatcherHandler"]
R["reactor-core"]
end
DS --> HC
DS --> SA
DS --> SE
DS --> SA2
DH --> HC
DH --> R
依存関係の主な違いを見てみましょう。spring-webmvc は spring-aop、spring-context、spring-expression を API 依存として取り込み、さらに jakarta.servlet-api も必要とします。一方 spring-webflux は reactor-core を API 依存として取り込み、spring-context はオプション扱いにしています。WebFlux は意図的に軽量に設計されており、完全な application context がなくても動作できます。
DispatcherServlet.doDispatch(): MVC のリクエストフロー
Spring MVC の中核を担うのが DispatcherServlet.doDispatch() です。すべての HTTP リクエストを処理するこのメソッドを見ていきましょう。
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java#L935-L1004
sequenceDiagram
participant Client
participant DS as DispatcherServlet
participant HM as HandlerMapping
participant HEC as HandlerExecutionChain
participant HA as HandlerAdapter
participant Handler as @Controller Method
participant VR as ViewResolver
Client->>DS: HTTP Request
DS->>DS: checkMultipart(request)
DS->>HM: getHandler(request)
HM-->>DS: HandlerExecutionChain<br/>(handler + interceptors)
DS->>HEC: applyPreHandle(request, response)
Note right of HEC: Each interceptor's preHandle()<br/>Can reject the request
DS->>HA: getHandlerAdapter(handler)
DS->>HA: handle(request, response, handler)
HA->>Handler: invoke @RequestMapping method
Handler-->>HA: Return value
HA-->>DS: ModelAndView
DS->>HEC: applyPostHandle(request, response, mv)
DS->>DS: processDispatchResult()
DS->>VR: resolveViewName(viewName)
VR-->>DS: View
DS->>DS: view.render(model, request, response)
DS-->>Client: HTTP Response
コードのキーステップを順に確認していきましょう。
951 行目 — ハンドラーの検索: getHandler(processedRequest) は登録済みのすべての HandlerMapping Bean を順に参照し、各 HandlerMapping にリクエストとのマッチングを問い合わせます。最初に非 null の結果を返したものが採用されます。@RequestMapping / @GetMapping アノテーションを解決するのは RequestMappingHandlerMapping です。
957 行目 — インターセプターの前処理: HandlerExecutionChain はハンドラーを HandlerInterceptor のリストでラップします。applyPreHandle() は各インターセプターの preHandle() メソッドを呼び出し、いずれかが false を返した時点でリクエストはただちに拒否されます。
962〜963 行目 — ハンドラーのアダプテーション: getHandlerAdapter() はハンドラーの型に応じた適切なアダプターを探します。RequestMappingHandlerAdapter は @Controller メソッドを担当し、メソッドの引数(@RequestBody、@PathVariable など)を解決してメソッドを呼び出し、戻り値を ModelAndView に変換します。
970 行目 — インターセプターの後処理: ハンドラーが正常に実行された後、applyPostHandle() によってインターセプターがレンダリング前に ModelAndView を変更する機会が与えられます。
980 行目 — 結果の処理: processDispatchResult() は通常フロー(ビューのレンダリング)と例外(HandlerExceptionResolver への委譲)の両方を処理します。
広範な例外ハンドリングにも注目してください。982〜988 行目の外側の try-catch はクリーンアップのために triggerAfterCompletion() が確実に呼ばれることを保証し、989〜1003 行目の finally ブロックは非同期リクエストのシナリオに対応しています。DispatcherServlet はフレームワークコードとユーザーコードの境界に位置し、あらゆる障害が発生しうるため、このような防御的なコーディングが必要となります。
DispatcherHandler: リアクティブな対応物
今度はリアクティブな対応物と比較してみましょう。
spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java#L72-L151
handle() メソッド全体が、一つのリアクティブパイプラインとして構成されています。
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return handlePreFlight(exchange);
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.onErrorResume(ex -> handleResultMono(exchange, Mono.error(ex)))
.flatMap(handler -> handleRequestWith(exchange, handler));
}
sequenceDiagram
participant Client
participant DH as DispatcherHandler
participant HM as HandlerMapping (Flux)
participant HA as HandlerAdapter
participant HRH as HandlerResultHandler
Client->>DH: ServerWebExchange
DH->>HM: Flux.fromIterable(handlerMappings)<br/>.concatMap(mapping.getHandler())
HM-->>DH: Mono~Handler~
DH->>DH: .next() — take first match
DH->>DH: .switchIfEmpty(404 error)
DH->>HA: handleRequestWith(exchange, handler)
Note right of HA: adapter.handle() returns<br/>Mono~HandlerResult~
HA-->>DH: Mono~HandlerResult~
DH->>HRH: handleResult(exchange, result)
HRH-->>DH: Mono~Void~
DH-->>Client: Response written reactively
コードが少ないのは、DispatcherHandler がやることが少ないからではありません。ハンドラーの検索、アダプテーション、結果のハンドリング、エラーハンドリングと、同じことをすべてやっています。違うのはやり方です。
| 関心事 | DispatcherServlet | DispatcherHandler |
|---|---|---|
| ハンドラーの検索 | null チェック付き for ループ | Flux.concatMap().next() |
| エラーハンドリング | ネストした try-catch ブロック | .onErrorResume() |
| 非同期サポート | WebAsyncManager + 特殊ケース処理 |
Mono/Flux に組み込み済み |
| インターセプター | 明示的な applyPreHandle()/applyPostHandle() |
WebFilter チェーン(ディスパッチャーとは別) |
| ビューのレンダリング | インラインの processDispatchResult() |
HandlerResultHandler に委譲 |
リアクティブプログラミングでは、エラーハンドリング・非同期処理・処理の合成がパイプラインの演算子そのものに組み込まれます。DispatcherHandler に try-catch が不要なのは、エラーがリアクティブストリームのシグナルとして伝播されるからです。WebAsyncManager が不要なのは、すべてが最初からノンブロッキングだからです。
ヒント: MVC と WebFlux ではインターセプターのパターンが大きく異なります。MVC は
preHandle/postHandle/afterCompletionコールバックを持つHandlerInterceptorを使います。WebFlux はWebFilterを使います。これはより汎用的なフィルターチェーンで、ディスパッチャーがハンドラーを探す前に実行されます。MVC から WebFlux へ移行する場合、インターセプターはフィルターとして書き直す必要があります。
あらゆる場所に見られる Strategy パターン: プラガブルなコンポーネント
両ディスパッチャーは Strategy パターンで設定されます。初期化時(MVC では refresh() のステップ 9 で onRefresh() が、WebFlux では setApplicationContext() がトリガー)に、application context から Strategy Bean を検索します。
spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java#L115-L134
classDiagram
class DispatcherServlet {
-handlerMappings: List~HandlerMapping~
-handlerAdapters: List~HandlerAdapter~
-handlerExceptionResolvers: List~HandlerExceptionResolver~
-viewResolvers: List~ViewResolver~
}
class HandlerMapping {
<<interface>>
+getHandler(request) HandlerExecutionChain
}
class HandlerAdapter {
<<interface>>
+supports(handler) boolean
+handle(request, response, handler) ModelAndView
}
class RequestMappingHandlerMapping {
Resolves @RequestMapping
}
class RequestMappingHandlerAdapter {
Invokes @Controller methods
}
class RouterFunctionMapping {
Resolves RouterFunction routes
}
class HandlerFunctionAdapter {
Invokes functional handlers
}
DispatcherServlet --> HandlerMapping
DispatcherServlet --> HandlerAdapter
HandlerMapping <|.. RequestMappingHandlerMapping
HandlerMapping <|.. RouterFunctionMapping
HandlerAdapter <|.. RequestMappingHandlerAdapter
HandlerAdapter <|.. HandlerFunctionAdapter
Strategy ベースの設計により、ディスパッチの仕組み全体がプラガブルになっています。カスタムの HandlerMapping を追加してリクエストの処理方法を変えたり(例:パスではなくヘッダーによるルーティング)、カスタムの HandlerAdapter を追加して新しいハンドラータイプをサポートすることも可能です。
MVC と WebFlux は、どちらも二つのプログラミングモデルをサポートしています。
- アノテーションベース —
@Controller+@RequestMapping。RequestMappingHandlerMapping+RequestMappingHandlerAdapterが処理します - ファンクショナル —
RouterFunction+HandlerFunction。RouterFunctionMapping+HandlerFunctionAdapterが処理します
ファンクショナルモデルはアノテーション処理を完全に回避するため、わずかに効率が高く、AOT フレンドリーでもあります(ルートの検索にリフレクションが不要です)。
初期化のブリッジ: onRefresh() と setApplicationContext()
ディスパッチャーは、第3回で見たリフレッシュライフサイクルとどのように繋がるのでしょうか。
DispatcherServlet はリフレッシュシーケンスのステップ 9 にあたる onRefresh() をオーバーライドします。DispatcherServlet は FrameworkServlet → HttpServletBean という継承チェーンを持ちます。Web application context は起動時に onRefresh() を呼び出し、initStrategies() がすべてのハンドラーマッピング、アダプター、ビューリゾルバー、例外リゾルバーを context から検索します。
DispatcherHandler はより単純なアプローチを取ります。ApplicationContextAware を実装しているため、Bean の初期化中に context が setApplicationContext() を呼び出し、initStrategies() がトリガーされます。
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
initStrategies(applicationContext);
}
DispatcherHandler の 115〜134 行目にある initStrategies() は驚くほどシンプルです。型で Bean を検索し、@Order でソートして、イミュータブルなリストとして保持するだけです。フォールバックのデフォルト値も、XML 設定サポートも、DispatcherServlet.properties ファイルも存在しません。WebFlux はモダンなコンベンションのもとでゼロから構築されています。
次のステップ
Spring MVC と WebFlux、双方のリクエストライフサイクル全体をカバーしました。最終回では、Spring の最新かつ最も将来志向のサブシステムである Ahead-of-Time コンパイルを取り上げます。AOT エンジンが、ここまで見てきた動的でリフレクション多用のフレームワークを、GraalVM ネイティブイメージに適した静的生成コードへどのように変換するかを追っていきます。実行時のクラスパススキャンをビルド時のコード生成で置き換え、RuntimeHints・BeanRegistrationAotProcessor・新しい BeanRegistrar API がそれをどう実現しているかを解説します。