Read OSS

リクエストディスパッチ: DispatcherServlet と Reactive DispatcherHandler の内側

上級

前提知識

  • 第3回: ApplicationContext のリフレッシュ — DispatcherServlet は onRefresh() 中に初期化される
  • Servlet API とリアクティブストリームの基本的な理解

リクエストディスパッチ: DispatcherServlet と Reactive DispatcherHandler の内側

Spring Framework の Web レイヤーには、2つの並行する世界があります。従来のブロッキング I/O を担う DispatcherServlet と、リアクティブなノンブロッキング I/O を担う DispatcherHandler です。どちらも同じ Strategy ベースのアーキテクチャ(HandlerMappingHandlerAdapter、そして結果のハンドリング)を用いたフロントコントローラーパターンを実装していますが、その実装は大きく異なります。DispatcherServlet は 1,400 行以上の命令型コードで構成されています。一方 DispatcherHandler は、同じ概念的なフローをわずか約 220 行で実現しています。この差こそが、リアクティブプログラミングがいかに複雑さを再分配するかを雄弁に語っています。

spring-web: 共有 HTTP 抽象化レイヤー

ディスパッチャーの詳細へ入る前に、モジュール構成を把握しておきましょう。第1回で整理したように、spring-webmvcspring-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-webmvcspring-aopspring-contextspring-expression を API 依存として取り込み、さらに jakarta.servlet-api も必要とします。一方 spring-webfluxreactor-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 は、どちらも二つのプログラミングモデルをサポートしています。

  1. アノテーションベース@Controller + @RequestMappingRequestMappingHandlerMapping + RequestMappingHandlerAdapter が処理します
  2. ファンクショナルRouterFunction + HandlerFunctionRouterFunctionMapping + HandlerFunctionAdapter が処理します

ファンクショナルモデルはアノテーション処理を完全に回避するため、わずかに効率が高く、AOT フレンドリーでもあります(ルートの検索にリフレクションが不要です)。

初期化のブリッジ: onRefresh() と setApplicationContext()

ディスパッチャーは、第3回で見たリフレッシュライフサイクルとどのように繋がるのでしょうか。

DispatcherServlet はリフレッシュシーケンスのステップ 9 にあたる onRefresh() をオーバーライドします。DispatcherServletFrameworkServletHttpServletBean という継承チェーンを持ちます。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 ネイティブイメージに適した静的生成コードへどのように変換するかを追っていきます。実行時のクラスパススキャンをビルド時のコード生成で置き換え、RuntimeHintsBeanRegistrationAotProcessor・新しい BeanRegistrar API がそれをどう実現しているかを解説します。