Request Dispatch: Inside DispatcherServlet and the Reactive DispatcherHandler
Prerequisites
- ›Article 3: ApplicationContext refresh — DispatcherServlet initializes during onRefresh()
- ›Basic understanding of the Servlet API and reactive streams
Request Dispatch: Inside DispatcherServlet and the Reactive DispatcherHandler
Spring Framework's web layer exists in two parallel universes: DispatcherServlet for traditional blocking I/O and DispatcherHandler for reactive non-blocking I/O. Both implement the Front Controller pattern using the same Strategy-based architecture — HandlerMapping, HandlerAdapter, and result handling — but their implementations differ dramatically. DispatcherServlet is 1,400+ lines of imperative code. DispatcherHandler achieves the same conceptual flow in about 220 lines. This difference tells a profound story about how reactive programming redistributes complexity.
spring-web: The Shared HTTP Abstraction Layer
Before examining the dispatchers, it's important to understand the module structure. As we mapped in Part 1, both spring-webmvc and spring-webflux depend on spring-web — a shared module that provides common HTTP abstractions.
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
The key difference in dependencies: spring-webmvc pulls in spring-aop, spring-context, and spring-expression as API dependencies, plus jakarta.servlet-api. spring-webflux pulls in reactor-core as an API dependency and keeps spring-context as optional. WebFlux is deliberately lighter — it can run without a full application context.
DispatcherServlet.doDispatch(): The MVC Request Flow
The heart of Spring MVC is DispatcherServlet.doDispatch() — a method that handles every HTTP request:
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
Let's walk through the key steps in the code:
Line 951 — Handler lookup: getHandler(processedRequest) iterates through all registered HandlerMapping beans, asking each one to match the request. The first non-null result wins. RequestMappingHandlerMapping is the one that resolves @RequestMapping/@GetMapping annotations.
Line 957 — Interceptor pre-handle: The HandlerExecutionChain wraps the handler with a list of HandlerInterceptor instances. applyPreHandle() calls each interceptor's preHandle() method. If any returns false, the request is rejected immediately.
Line 962–963 — Handler adaptation: getHandlerAdapter() finds the right adapter for the handler type. RequestMappingHandlerAdapter handles @Controller methods — it resolves method parameters (@RequestBody, @PathVariable, etc.), invokes the method, and processes the return value into a ModelAndView.
Line 970 — Interceptor post-handle: After successful handler execution, applyPostHandle() gives interceptors a chance to modify the ModelAndView before rendering.
Line 980 — Result processing: processDispatchResult() handles both normal flow (view rendering) and exceptions (delegating to HandlerExceptionResolver).
Notice the extensive exception handling — the outer try-catch at line 982–988 ensures triggerAfterCompletion() is called for cleanup, and the finally block at line 989–1003 handles async request scenarios. This defensive coding is necessary because DispatcherServlet sits at the boundary between framework code and user code, where anything can go wrong.
DispatcherHandler: The Reactive Mirror
Now compare with the reactive equivalent:
spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java#L72-L151
The entire handle() method is a single reactive pipeline:
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
The size difference is not because DispatcherHandler does less. It does the same things — handler lookup, adaptation, result handling, error handling. The difference is in how:
| Concern | DispatcherServlet | DispatcherHandler |
|---|---|---|
| Handler lookup | For loop with null checks | Flux.concatMap().next() |
| Error handling | Nested try-catch blocks | .onErrorResume() |
| Async support | WebAsyncManager + special cases |
Built into Mono/Flux |
| Interceptors | Explicit applyPreHandle()/applyPostHandle() |
WebFilter chain (separate from dispatcher) |
| View rendering | Inline processDispatchResult() |
Delegated to HandlerResultHandler |
Reactive programming pushes error handling, async behavior, and composition into the pipeline operators themselves. The DispatcherHandler doesn't need try-catch blocks because errors are propagated as signals in the reactive stream. It doesn't need WebAsyncManager because everything is already non-blocking.
Tip: The interceptor pattern differs significantly between MVC and WebFlux. MVC uses
HandlerInterceptorwithpreHandle/postHandle/afterCompletioncallbacks. WebFlux usesWebFilter— a more general-purpose filter chain that runs before the dispatcher even finds a handler. If you're migrating from MVC to WebFlux, your interceptors need to be rewritten as filters.
The Strategy Pattern Everywhere: Pluggable Components
Both dispatchers are configured through the Strategy pattern. During initialization (triggered by onRefresh() in step 9 of refresh() for MVC, or setApplicationContext() for WebFlux), they discover strategy beans from the application context:
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
This strategy-based design means the entire dispatch mechanism is pluggable. You can add custom HandlerMapping implementations to handle requests differently (e.g., routing based on headers rather than paths), or custom HandlerAdapter implementations to support new handler types.
Both MVC and WebFlux support two programming models:
- Annotation-based —
@Controller+@RequestMapping, handled byRequestMappingHandlerMapping+RequestMappingHandlerAdapter - Functional —
RouterFunction+HandlerFunction, handled byRouterFunctionMapping+HandlerFunctionAdapter
The functional model bypasses annotation processing entirely, making it slightly more efficient and fully AOT-friendly (no reflection needed to discover routes).
The Initialization Bridge: onRefresh() and setApplicationContext()
How do the dispatchers connect to the refresh lifecycle from Part 3?
DispatcherServlet overrides onRefresh() — step 9 of the refresh sequence. Since DispatcherServlet extends FrameworkServlet which extends HttpServletBean, the web application context calls onRefresh() during startup, which triggers initStrategies() to discover all handler mappings, adapters, view resolvers, and exception resolvers from the context.
DispatcherHandler takes a simpler path: it implements ApplicationContextAware, so the context calls setApplicationContext() during bean initialization, which triggers initStrategies():
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
initStrategies(applicationContext);
}
The initStrategies() method at line 115–134 of DispatcherHandler is refreshingly straightforward — it just looks up beans by type, sorts them by @Order, and stores them as immutable lists. No fallback defaults, no XML configuration support, no DispatcherServlet.properties file. WebFlux was built from scratch with modern conventions.
What's Next
We've now covered the full request lifecycle for both Spring MVC and WebFlux. In the final article, we'll look at Spring's newest and most forward-looking subsystem: Ahead-of-Time compilation. We'll trace how the AOT engine transforms the dynamic, reflection-heavy framework we've been exploring into statically generated code suitable for GraalVM native images — replacing runtime classpath scanning with build-time code generation, and explaining how RuntimeHints, BeanRegistrationAotProcessor, and the new BeanRegistrar API make it possible.