What this post covers
This is the twenty-fourth post in the "Digging into Spring's Source" series. Part 21, Part 22, and Part 23 showed how Spring MVC, which runs on top of servlets, handles a single request. This post reads Spring WebFlux, which does the same job in a reactive way. On the map in Part 1, this is U22 in Phase 5 (web).
There are four questions to answer.
- Which layers does a request pass through between the server and our code?
HttpHandler,WebFilter,WebExceptionHandler,WebHandler DispatcherHandler.handle()plays the role of MVC'sDispatcherServlet.doDispatch(). What is the same, and what is different?- How do a controller with
@GetMappingand a functional endpoint built withRouterFunctions.route()run inside one dispatcher? - On the sending side, in what order does
WebClientdo its work?
| Item | Details |
|---|---|
| Reference source | Spring Framework 7.1.0-SNAPSHOT |
| Commit | f447f3c310 (2026-10-01, main branch) |
| Modules read most | spring-webflux, spring-web (http.server.reactive, web.server, http.codec) |
| Classes read most | WebHttpHandlerBuilder, HttpWebHandlerAdapter, DispatcherHandler, RequestMappingHandlerAdapter, RouterFunctions, RouterFunctionMapping, DefaultWebClient |
Every line number and behavior in this post refers to that commit. In other versions the line numbers will differ and the details may too. Part 1 explains how to check out the same commit.
WebFlux is wide. This post keeps to the main line that someone new to Spring can follow, and what was left out is listed by name in "What this post leaves out" at the end.
Words to know first
Reactive, Mono, and Flux. An ordinary method waits until the result is ready and then returns the value. A reactive method returns right away, and what it returns is not the value but "a channel the value will flow through later". In the Reactor library those channels are Mono (zero or one value) and Flux (many values). We first met them in Part 19, section 5, with reactive transactions.
Nothing runs until someone subscribes. Holding a Mono means neither that the work is finished nor that it has started. A Mono is a plan — "this is what I will do" — and it only starts running when someone subscribes to it. The examples in this post let you see that twice. They use block(), which means "subscribe, and wait on this thread until it is done".
Three signals. What flows through the channel is a value (onNext), normal completion (onComplete), or an error (onError). In reactive code an exception does not climb the call stack with throw; it travels along the channel as an error signal. So instead of try/catch you receive it with an operator such as onErrorResume. A Mono<Void> is a channel that carries no value, only "done" or "failed".
Operators. These are the methods you chain onto a channel to extend the plan. Only the ones that appear in this post's source are listed.
| Operator | Meaning |
|---|---|
map / flatMap |
Transform the value when it arrives / use the value to create another channel and continue with it |
concatMap |
Create a channel per Flux value, but start the next only after the previous one ends |
next |
Take only the first value of a Flux as a Mono and cancel the rest |
switchIfEmpty |
If it ends without a value, switch to another channel |
onErrorResume |
If an error signal arrives, switch to another channel (the equivalent of catch) |
then |
When the previous one finishes, run the next |
Mono.defer |
Run the code inside, and create the channel, only on subscription |
ServerWebExchange bundles one request, its response, and a map of attributes used during that request (ServerWebExchange line 44, lines 65–75). Where MVC carried HttpServletRequest and HttpServletResponse around as a pair, WebFlux carries this one object.
Handler, HandlerMapping, HandlerAdapter mean the same as in MVC. The handler is our code that actually processes the request, a HandlerMapping finds "who handles this request", and a HandlerAdapter knows how to call that handler. Parts 21 and 22 covered them. WebFlux has its own interfaces with the same names, in the org.springframework.web.reactive package.
1. The floor: everything comes down to one HttpHandler
At the very bottom of WebFlux is an interface with a single method.
public interface HttpHandler {
/**
* Handle the given request and write to the response.
* @param request current request
* @param response current response
* @return indicates completion of request handling
*/
Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
}
spring-web/src/main/java/org/springframework/http/server/reactive/HttpHandler.java lines 43–53
It takes a request and a response and returns a Mono<Void> that says "I will tell you when it is all done". It resembles a servlet's service(request, response), but the fact that the return type is not void changes everything. When the method returns, nothing has finished yet.
Notice that this interface lives in spring-web, not in spring-webflux. So do WebFilter, WebHandler, and ServerWebExchange. That is why spring-web was the shared floor under both MVC and WebFlux on the module map in Part 2. spring-webflux holds the dispatcher and the programming models that sit on top.
The two blue boxes are where we can step in (exception handlers and filters). The red box is the last safety net.
Server adapters — one per server
HttpHandler knows nothing about any particular server. The connection is made by an adapter, one per server. The http.server.reactive package of spring-web has ReactorHttpHandlerAdapter (Reactor Netty), TomcatHttpHandlerAdapter, JettyCoreHttpHandlerAdapter, and ServletHttpHandlerAdapter. They all do the same thing: wrap the server's own request and response objects in Spring's ServerHttpRequest and ServerHttpResponse, then call httpHandler.handle(request, response) (ReactorHttpHandlerAdapter lines 55–76).
So who subscribes? The servlet adapter does it itself — this.httpHandler.handle(httpRequest, httpResponse).subscribe(subscriber) (ServletHttpHandlerAdapter line 197). The Reactor Netty adapter returns the Mono as it is (lines 65–67), and subscribing after that is up to Reactor Netty, which is outside this repository.
WebHttpHandlerBuilder — three layers of wrapping
We rarely implement HttpHandler ourselves. We work with three interfaces one level up, and WebHttpHandlerBuilder weaves them into a single HttpHandler.
| Interface | Method | Role |
|---|---|---|
WebHandler |
Mono<Void> handle(ServerWebExchange) |
The body that handles the request. Usually DispatcherHandler (lines 35–44) |
WebFilter |
Mono<Void> filter(ServerWebExchange, WebFilterChain) |
A filter that steps in before and after the body (lines 32–43) |
WebExceptionHandler |
Mono<Void> handle(ServerWebExchange, Throwable) |
Turns an error signal into a response (lines 27–39) |
The weaving happens in build(). (A // ... line marks something this post trimmed; the rest is the source as it is.)
public HttpHandler build() {
WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
decorated = new ExceptionHandlingWebHandler(decorated, this.exceptionHandlers);
HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
// ...
adapted.afterPropertiesSet();
return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted);
}
There are three layers, from the inside out.
FilteringWebHandlerwraps the body in a filter chain. The chain is aDefaultWebFilterChain. It walks the filter list from the back, building "this filter + the link after it" one at a time and putting it in front (lines 71–78), so the filters run in the order you registered them. One link'sfilter()calls the remaining filter if there is one, and the body if there is not (lines 103–108).ExceptionHandlingWebHandlerwraps that and receives error signals. We read it just below.HttpWebHandlerAdapteris the adapter that turns aWebHandlerinto anHttpHandler. For each request it creates a newServerWebExchange(line 306), hands it inward, and completes the response at the end (lines 321–326).
The body of ExceptionHandlingWebHandler is short enough to read whole.
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Mono<Void> completion;
try {
completion = super.handle(exchange);
}
catch (Throwable ex) {
completion = Mono.error(ex);
}
for (WebExceptionHandler handler : this.exceptionHandlers) {
completion = completion.doOnError(error -> exchange.getAttributes().put(HANDLED_WEB_EXCEPTION, error))
.onErrorResume(ex -> handler.handle(exchange, ex));
}
return completion;
}
Two things to notice. First, there is a try/catch. It covers the case where inner code simply throws before it even returns a Mono, and turns that into an error signal (Mono.error) so there is one road for both. Second, the for loop does not run the exception handlers. It only chains one onErrorResume per handler. If an error occurs the first handler receives it, and if that handler returns an error signal again (meaning "not mine"), the next onErrorResume receives it. The comment on WebExceptionHandler says the same: a completion signal means "handled", an error signal means "still not handled" (lines 30–32).
If nobody handles it all the way out, handleUnresolvedError in the outermost HttpWebHandlerAdapter receives it, sets the status code to 500, and ends there (line 323, lines 359–382).
Example 1: the order of filters and exception handlers
Without starting a server, we build an HttpHandler with WebHttpHandlerBuilder and call it directly with the fake request and response from spring-test (MockServerHttpRequest, MockServerHttpResponse). This is ex/u22/FilterChainExample.java. The example code in this post is trimmed to the essentials (imports and the class and main declarations are left out and the indentation is pulled in; other trimmed spots are marked with // ... comments, and short methods are joined onto one line).
// An example written for this post (not Spring's source)
/** A filter that prints its name before and after. */
static WebFilter named(String name) {
return (exchange, chain) -> {
System.out.println(" " + name + " before");
return chain.filter(exchange)
.doOnSuccess(v -> System.out.println(" " + name + " after (completed)"))
.doOnError(ex -> System.out.println(" " + name + " after (error " + ex.getClass().getSimpleName() + ")"));
};
}
static void send(HttpHandler httpHandler) {
MockServerHttpRequest request = MockServerHttpRequest.get("/hello").build();
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> completion = httpHandler.handle(request, response);
System.out.println(" handle() returned a Mono. status so far = " + response.getStatusCode());
completion.block(); // the subscription happens here
System.out.println(" status = " + response.getStatusCode()
+ ", body = \"" + response.getBodyAsString().block() + "\"");
}
// ... from here on, inside main. hello is a WebHandler that writes 200 and "hello",
// notFound is a WebHandler that returns Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND))
System.out.println("[1] two filters, then the handler");
send(WebHttpHandlerBuilder.webHandler(hello)
.filter(named("filter A"), named("filter B"))
.build());
System.out.println("[2] the handler fails, a WebExceptionHandler is registered");
send(WebHttpHandlerBuilder.webHandler(notFound)
.filter(named("filter A"))
.exceptionHandler(new ResponseStatusExceptionHandler())
.build());
System.out.println("[3] the handler fails, no WebExceptionHandler");
send(WebHttpHandlerBuilder.webHandler(notFound)
.filter(named("filter A"))
.build());
This example was run with the released Spring Framework 7.0.9 to confirm the output below (the 7.1.0-SNAPSHOT this series reads was not built and run directly). The same goes for the other examples in this post.
[1] two filters, then the handler
handle() returned a Mono. status so far = null
filter A before
filter B before
handler
filter B after (completed)
filter A after (completed)
status = 200 OK, body = "hello"
[2] the handler fails, a WebExceptionHandler is registered
handle() returned a Mono. status so far = null
filter A before
handler (signals ResponseStatusException 404)
filter A after (error ResponseStatusException)
status = 404 NOT_FOUND, body = ""
[3] the handler fails, no WebExceptionHandler
handle() returned a Mono. status so far = null
filter A before
handler (signals ResponseStatusException 404)
filter A after (error ResponseStatusException)
status = 500 INTERNAL_SERVER_ERROR, body = ""
- In all three cases, right after
handle()returns neither a filter nor the handler has printed anything. They only run onceblock()subscribes. - In [1] the filters go in in registration order (A, B) and come out in reverse.
- In [2] filter A sees the error signal, because the exception handlers are a layer outside the filters. Only after the error has passed the filter does
ResponseStatusExceptionHandlerreceive it and set the status to 404 (lines 65–68). - [3] is the same 404 exception, yet the response is a 500.
Easy to get wrong
- Throwing
ResponseStatusException(404)does not produce a 404 by itself. A registeredWebExceptionHandleris what carries that exception over to the response. With none at all, the last safety net ends it as a 500, as in [3]. With@EnableWebFlux, a handler that does this job is registered as a bean (section 3). - A filter's "after" code must not go on the line after
chain.filter(exchange). That line has only received a plan. What you want to do afterwards has to be chained onto the returnedMonowith operators, as the example does. - A servlet
Filterand aWebFilterare different things. A servlet filter (jakarta.servlet.Filter) is called by the servlet container. AWebFilteris a Spring interface called by theDefaultWebFilterChainabove and has nothing to do with the Servlet API. Only the names are alike.
2. DispatcherHandler.handle — the reactive doDispatch
What usually sits in the innermost WebHandler slot from section 1 is DispatcherHandler. Its class declaration says it implements WebHandler (line 72). Unlike MVC's DispatcherServlet, a servlet with four levels of inheritance, it is a single 220-line class, and the handle() that processes a request is fourteen lines long.
@Override
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));
}
spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java lines 137–151
The last six lines are all of it. Line by line:
Flux.fromIterable(this.handlerMappings)— puts the list ofHandlerMappings on a channel..concatMap(mapping -> mapping.getHandler(exchange))— asks them one at a time, "do you know the handler for this request?"getHandlerreturns aMono<Object>, and it ends without a value if the mapping does not know (HandlerMappingline 107)..next()— takes only the first handler that comes out and does not ask the remaining mappings..switchIfEmpty(createNotFoundError())— if nobody knows, switches to an error signal carryingResponseStatusException(HttpStatus.NOT_FOUND)(lines 153–158)..onErrorResume(...)— gives the exception-handling side one chance at any error up to this point (the 404 included)..flatMap(handler -> handleRequestWith(exchange, handler))— if a handler was found, goes on to run it.
"Ask each in turn and use the first one that answers", which MVC's getHandler() did with a for loop, has become concatMap and next. The loop and the return turned into operators; the meaning is the same.
Solid lines are the normal flow; dotted lines are the road an error signal takes.
Calling the handler, then handling the result
private Mono<Void> handleRequestWith(ServerWebExchange exchange, Object handler) {
if (ObjectUtils.nullSafeEquals(exchange.getResponse().getStatusCode(), HttpStatus.FORBIDDEN)) {
return Mono.empty(); // CORS rejection
}
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
Mono<HandlerResult> resultMono = adapter.handle(exchange, handler);
return handleResultMono(exchange, resultMono);
}
}
}
return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
}
spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java lines 196–209
The first HandlerAdapter whose supports(handler) is true runs the handler. So far this is the same as MVC. What differs is what comes back. MVC's adapter returned a ModelAndView; WebFlux's adapter returns a Mono<HandlerResult> (HandlerAdapter line 71). A HandlerResult is a container for "which handler returned what, with which declared type" (HandlerResult lines 32–40).
Turning that container into a response is the job of a third strategy, HandlerResultHandler. handleResult() walks the list and hands the result to the first one whose supports(result) is true (lines 181–194). MVC has no interface with this name. In MVC, return value handling lived inside the adapter (HandlerMethodReturnValueHandler); WebFlux pulled it out into a strategy the dispatcher picks directly. So the WebFlux dispatcher has three kinds of strategies.
Errors while running the handler are dealt with by handleResultMono() (lines 160–179). If an adapter also implements DispatchExceptionHandler, it gets the first chance (this is how @ExceptionHandler methods are called; see section 3). An error signal that is still left flows out of DispatcherHandler, and the WebExceptionHandlers from section 1 receive it.
Side by side with MVC
| Spring MVC (Part 21) | Spring WebFlux (this post) | |
|---|---|---|
| Dispatcher | DispatcherServlet (a servlet) |
DispatcherHandler (a WebHandler) |
| Central method | void doDispatch(request, response) |
Mono<Void> handle(exchange) |
| Request and response | HttpServletRequest, HttpServletResponse |
one ServerWebExchange |
| Finding the handler | HandlerMapping → HandlerExecutionChain (handler + interceptors) |
HandlerMapping → Mono<Object> (handler only) |
| Calling the handler | HandlerAdapter → ModelAndView |
HandlerAdapter → Mono<HandlerResult> |
| Handling the result | return value handlers inside the adapter, then view rendering | HandlerResultHandler |
| Stepping in before and after | HandlerInterceptor, servlet Filter |
WebFilter |
| Exceptions | HandlerExceptionResolver |
DispatchExceptionHandler, then WebExceptionHandler outside it |
| Body conversion | HttpMessageConverter |
HttpMessageReader / HttpMessageWriter |
| What "done" means | the method returned | the returned Mono emitted its completion signal |
"Handler only" stands out in that table. A WebFlux HandlerMapping does not return a bundle of interceptors, and the spring-webflux module has no type called HandlerInterceptor at all. Stepping in around the handler is the job of the WebFilter from section 1.
Easy to get wrong
handle()does not process the request. It assembles a plan for processing it and returns that. No line in the code above "waits". Even asking theHandlerMappings happens after subscription.- Not finding a handler is an error signal too. MVC had a separate branch called
noHandlerFound(), but here it is folded into oneResponseStatusExceptionthat takes the exception-handling road. - Same name, different type.
org.springframework.web.servlet.HandlerMappingandorg.springframework.web.reactive.HandlerMappingare unrelated interfaces. An IDE's auto-import easily picks the wrong one.
3. Where the strategy beans come from, and annotated controllers
What @EnableWebFlux registers
DispatcherHandler does not create its three lists itself. When it is given an ApplicationContext, initStrategies() finds every bean of type HandlerMapping, HandlerAdapter, and HandlerResultHandler and keeps them sorted by order (lines 115–134). MVC had a defaults file (DispatcherServlet.properties) for when no beans were present, but DispatcherHandler has no such fallback. No beans, empty lists.
@EnableWebFlux is what registers those beans. The annotation @Imports DelegatingWebFluxConfiguration (EnableWebFlux line 90), and the @Bean methods are gathered in its parent, WebFluxConfigurationSupport. Part 9 showed how @Import pulls in a configuration class. Picking only the ones that matter for this post:
| Bean | Kind | Location |
|---|---|---|
webHandler → DispatcherHandler |
the dispatcher itself | lines 125–128 |
WebFluxResponseStatusExceptionHandler |
WebExceptionHandler |
lines 130–134 |
RouterFunctionMapping (order -1) |
HandlerMapping |
lines 248–258 |
RequestMappingHandlerMapping (order 0) |
HandlerMapping |
lines 136–142 |
RequestMappingHandlerAdapter, HandlerFunctionAdapter, SimpleHandlerAdapter |
HandlerAdapter |
line 305, lines 482–490 |
ResponseEntityResultHandler, ResponseBodyResultHandler, ViewResolutionResultHandler, ServerResponseResultHandler |
HandlerResultHandler |
lines 521–562 |
It is no accident that the dispatcher bean is named webHandler. WebHttpHandlerBuilder.applicationContext(context) looks up the body by exactly that name (WEB_HANDLER_BEAN_NAME, line 72) and collects the WebFilter and WebExceptionHandler beans by type, sorted (lines 174–189). What we put in by hand in section 1 is taken from the container here.
The road an annotated controller takes
The road to a method with @GetMapping has the same names as in MVC. Only the implementations are the reactive ones.
RequestMappingHandlerMappingfinds theHandlerMethodthat matches the request. Even the inheritance has the same shape as MVC (RequestMappingHandlerMapping→RequestMappingInfoHandlerMapping→AbstractHandlerMethodMapping→AbstractHandlerMapping, line 75, line 64, line 67). The matching rules follow the same idea as in Part 22.RequestMappingHandlerAdaptertakes its turn whenhandler instanceof HandlerMethod(lines 247–250). These five lines are the core.
Mono<HandlerResult> resultMono = this.modelInitializer
.initModel(handlerMethod, bindingContext, exchange)
.then(Mono.defer(() -> invocableMethod.invoke(exchange, bindingContext)))
.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
.onErrorResume(ex -> exceptionHandler.handleError(exchange, ex));
Prepare the model, then call the method, attach an exception handler to the result, and on an error hand over to that handler. The exceptionHandler here is a lambda that calls handleException() in the same class (lines 267–268), which finds the matching @ExceptionHandler method and runs it (lines 294–295). The method itself is called by InvocableHandlerMethod.invoke(), which, once all arguments are ready, does method.invoke(getBean(), args) by reflection (InvocableHandlerMethod lines 181–205). Whatever the return value is, it comes out inside a HandlerResult (lines 239–240).
ResponseBodyResultHandlertakes its turn when the class or the method has@ResponseBody(@RestControllercarries it) (lines 98–104), and passes the return value towriteBody()(line 118).
HttpMessageWriter — where the return value becomes bytes
writeBody() is in the parent class AbstractMessageWriterResultHandler (from line 169). It does three things.
- If the return value is a reactive type such as
MonoorFlux, it is used as aPublisheras it is; if it is a plain object, it is wrapped withMono.justOrEmpty(body)(lines 179–199). That is why a WebFlux controller may simply return aString. - It chooses the media type of the response (line 207).
- It walks the list of
HttpMessageWriters and lets the first one whosecanWrite(type, media type)is true do the writing (lines 231–237).
HttpMessageWriter and HttpMessageReader correspond to MVC's HttpMessageConverter (Part 20). In MVC, reading and writing were one interface; here they are split in two, and above all what is passed around is a channel. The writing side takes a Publisher, not a single object (HttpMessageWriter line 89), and the reading side returns a Flux<T> or Mono<T> (HttpMessageReader line 86, line 96). The point is not to hold a thread waiting until the whole body has arrived.
The names are the same as in MVC; the only difference is that each step is chained with Mono. This puts steps 1–3 of this section (①②③ in the figure) on one page.
4. Functional endpoints — functions instead of annotations
WebFlux has one more way to write a controller. Instead of annotations, you assemble it from three functions.
| Interface | Method | In one line |
|---|---|---|
HandlerFunction<T> |
Mono<T> handle(ServerRequest) |
A function that takes a request and produces a response (lines 30–39) |
RequestPredicate |
boolean test(ServerRequest) |
A function that answers "does this request match?" (lines 33–40) |
RouterFunction<T> |
Mono<HandlerFunction<T>> route(ServerRequest) |
A function that takes a request and returns the HandlerFunction that will handle it (lines 36–44) |
All three are @FunctionalInterfaces, so you can write them as lambdas. ServerRequest and ServerResponse are the request and response types that wrap ServerWebExchange to suit the functional model.
The smallest RouterFunction is "one condition + one handler".
@Override
public Mono<HandlerFunction<T>> route(ServerRequest request) {
if (this.predicate.test(request)) {
// ...
return Mono.just(this.handlerFunction);
}
else {
return Mono.empty();
}
}
If the condition matches it returns the handler, otherwise an empty Mono. RouterFunctions.route(predicate, handlerFunction) creates this DefaultRouterFunction (lines 116–120).
With several routes you use the builder. RouterFunctions.route() (the no-argument one) returns a RouterFunctionBuilder (lines 97–99), and the builder's GET("/path", handler) creates one of those small RouterFunctions with a RequestPredicates.GET(pattern) condition and adds it to a list (RouterFunctionBuilder lines 84–86, lines 57–67). build() ties that list into one BuiltRouterFunction (lines 414–428).
@Override
public Mono<HandlerFunction<ServerResponse>> route(ServerRequest request) {
return Flux.fromIterable(this.routerFunctions)
.concatMap(routerFunction -> routerFunction.route(request))
.next();
}
It has the same shape as DispatcherHandler.handle() in section 2: ask the list in turn and use the first that answers. So with functional endpoints, registration order is the priority. That differs from the annotation side, which picks the most specific of several candidates for you.
It stops at the green box. Later routes are not even asked.
The three pieces that plug into DispatcherHandler
Functional endpoints do not use a separate dispatcher. They are simply plugged into the three strategy slots from section 2, one piece each.
RouterFunctionMapping(aHandlerMapping) collects everyRouterFunctionbean in the container and merges them into one withandOther(lines 122–134). When a request arrives it creates aServerRequestand callsrouterFunction.route(request). TheHandlerFunctionthat comes out is the handler, and theServerRequestit just created is stored in the exchange's attributes (lines 158–172).HandlerFunctionAdapter(aHandlerAdapter) is twelve lines even when quoted in full.
@Override
public boolean supports(Object handler) {
return handler instanceof HandlerFunction;
}
@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;
ServerRequest request = exchange.getRequiredAttribute(RouterFunctions.REQUEST_ATTRIBUTE);
return handlerFunction.handle(request)
.map(response -> new HandlerResult(handlerFunction, response, HANDLER_FUNCTION_RETURN_TYPE));
}
It takes the ServerRequest the mapping stored out of the attributes, calls the function, and puts the returned ServerResponse into a HandlerResult. Compare the annotation-side adapter, which was long because it resolves arguments, prepares the model, and validates. In the functional model our own function does all that directly with the ServerRequest, so the adapter has nothing left to do.
3. ServerResponseResultHandler (a HandlerResultHandler) takes its turn when the return value is a ServerResponse (lines 104–107), and calls response.writeTo(exchange, context) so that the response writes itself (lines 109–123). writeTo first copies the status code and headers (DefaultServerResponseBuilder lines 327–344) and then writes the body with the list of HttpMessageWriters it was handed. Those are the same writers as in section 3.
Blue is the functional side, gray the annotation side. They meet again at the bottom.
Example 2: both models pass through the same dispatcher
This is ex/u22/DispatcherHandlerExample.java. One @EnableWebFlux configuration registers a controller and a RouterFunction together, and on purpose both of them declare the same path, /both.
// An example written for this post (not Spring's source)
@RestController
static class HelloController {
@GetMapping("/hello")
Mono<String> hello() { return Mono.just("hello from @GetMapping"); }
@GetMapping("/both")
Mono<String> both() { return Mono.just("both from @GetMapping"); }
}
@Configuration
@EnableWebFlux
static class Config {
@Bean
HelloController helloController() { return new HelloController(); }
@Bean
RouterFunction<ServerResponse> routes() {
return RouterFunctions.route()
.GET("/fn/hello", request -> ServerResponse.ok().bodyValue("hello from HandlerFunction"))
.GET("/both", request -> ServerResponse.ok().bodyValue("both from HandlerFunction"))
.build();
}
/** A filter that, after the chain ends, prints the kind of handler the HandlerMapping chose. */
@Bean
WebFilter handlerTracer() {
return (exchange, chain) -> chain.filter(exchange).doFinally(signal -> {
Object handler = exchange.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
// ... print whether it is a HandlerMethod or a HandlerFunction
});
}
}
// ... main: collect and print the three kinds of strategy beans the way DispatcherHandler.initStrategies does,
// then call the lines below. send, as in example 1, sends a fake request and prints the status and body
HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(context).build();
send(httpHandler, "/hello");
send(httpHandler, "/fn/hello");
send(httpHandler, "/both");
send(httpHandler, "/nope");
[1] bean "webHandler" is a DispatcherHandler
same list as getHandlerMappings()? true
HandlerMapping:
RouterFunctionMapping
RequestMappingHandlerMapping
EmptyHandlerMapping
HandlerAdapter:
WebSocketHandlerAdapter
RequestMappingHandlerAdapter
HandlerFunctionAdapter
SimpleHandlerAdapter
HandlerResultHandler:
ResponseEntityResultHandler
ServerResponseResultHandler
ResponseBodyResultHandler
ViewResolutionResultHandler
[2] requests through WebHttpHandlerBuilder.applicationContext(context)
GET /hello
handler kind = HandlerMethod
status = null, body = "hello from @GetMapping"
GET /fn/hello
handler kind = HandlerFunction
status = 200 OK, body = "hello from HandlerFunction"
GET /both
handler kind = HandlerFunction
status = 200 OK, body = "both from HandlerFunction"
GET /nope
handler kind = null
status = 404 NOT_FOUND, body = ""
(When you run it, two log lines from Hibernate Validator announcing its version are printed first. They have nothing to do with this post and are left out.)
RouterFunctionMappingis first in theHandlerMappinglist, exactly as the source says:mapping.setOrder(-1); // go before RequestMappingHandlerMapping(line 253). The third one,EmptyHandlerMapping, is an empty mapping that fills the slot when no static resource handlers are registered (line 287).- So the functional side wins
/both. BecauseconcatMapandnextstop at the first answer,RequestMappingHandlerMappingis not even asked. - Unlike example 1,
/nopeis a 404. TheWebFluxResponseStatusExceptionHandlerthat@EnableWebFluxregistered received the dispatcher's 404 error signal and carried it over to the response.
Easy to get wrong
status = nullfor/hellois not an error. Without@ResponseStatus, the annotation side does not touch the status code (InvocableHandlerMethodlines 221–224). The fake response object shows a status nobody set asnull(getStatusCode()is@Nullable,AbstractServerHttpResponselines 106–108). On the functional side,writeToalways copies the status created byServerResponse.ok()to the response, so 200 was printed. Whether a real server sends a response with no status set as 200 is the server's business, and this post did not confirm it in the source.- If you use only functional endpoints, you do not need the dispatcher.
RouterFunctions.toHttpHandler(routerFunction)does not go throughDispatcherHandler. It creates a smallWebHandlerthat routes, runs, and writes in one go, and puts that into theWebHttpHandlerBuilderfrom section 1 (lines 318–325, lines 1472–1488). The server in example 3 below works this way.
5. WebClient — the side that sends requests
WebClient is the reactive HTTP client that ships in spring-webflux. Compared with the server side only the direction is reversed; the ideas are the same. There is a filter chain, at the very end there is an adapter that connects to a real HTTP library, and the body is read and written by the same HttpMessageReaders and HttpMessageWriters.
| Server side (section 1) | Client side | Method |
|---|---|---|
WebHandler |
ExchangeFunction |
Mono<ClientResponse> exchange(ClientRequest) (lines 42–55) |
WebFilter |
ExchangeFilterFunction |
Mono<ClientResponse> filter(ClientRequest, ExchangeFunction next) (lines 35–51) |
| Server adapter | ClientHttpConnector |
Mono<ClientHttpResponse> connect(method, uri, requestCallback) (lines 34–49) |
What the builder assembles
WebClient.create() and WebClient.builder() both start from DefaultWebClientBuilder (WebClient lines 147–149, lines 164–166). The first part of build() prepares the three things in the table above.
public WebClient build() {
ClientHttpConnector connectorToUse =
(this.connector != null ? this.connector : initConnector());
ExchangeFunction exchange = (this.exchangeFunction == null ?
ExchangeFunctions.create(connectorToUse, initExchangeStrategies()) :
this.exchangeFunction);
ExchangeFilterFunction filterFunctions = (this.filters != null ? this.filters.stream()
.reduce(ExchangeFilterFunction::andThen)
.orElse(null) : null);
// ...
}
- The connector. If you do not supply one,
initConnector()picks one by looking at which libraries are on the classpath. It checks Reactor Netty, the Jetty client, and Apache HttpComponents in that order, and if none is there it usesJdkClientHttpConnector, built on the JDK'sHttpClient(lines 329–342). - The
ExchangeFunction. TheDefaultExchangeFunctionthatExchangeFunctions.create(...)makes callsconnector.connect(...)and wraps the low-level response that comes back in aClientResponse(ExchangeFunctionslines 97–119). - The filters. Several filters are folded into one with
andThen.a.andThen(b)means "runa, and whenacallsnext,bruns" (lines 59–63), so they are entered in registration order.
One request
The object you write the request on with client.get().uri(...) is DefaultRequestBodyUriSpec, inside DefaultWebClient (line 207). The actual sending is done by its exchange() (lines 442–480). Apart from its first line, which prepares the request builder (line 443), the whole body of that method sits inside Mono.deferContextual(...), so before subscription not even the ClientRequest is built (requestBuilder.build() is on line 456). On subscription it puts the filters over the ExchangeFunction and calls it — finalFilterFunction.apply(exchangeFunction).exchange(request) (lines 462–463).
The blue boxes are the filters. The response comes back along the same road in reverse.
retrieve() and exchangeToMono()
Both call the same exchange(). What differs is who deals with the ClientResponse that comes back.
retrieve()returns aDefaultResponseSpec(lines 408–411). It comes with one default status handler.
private static final StatusHandler DEFAULT_STATUS_HANDLER =
new StatusHandler(code -> code.value() >= 400, ClientResponse::createException);
When you take the body out with bodyToMono(...), the status handlers are applied first (lines 594–598, lines 677–681), so with a status of 400 or above you get a WebClientResponseException error signal instead of the body. A handler you add with onStatus(...) is placed ahead of the default one (lines 572–580).
exchangeToMono(function)passes theClientResponseto your function as it is (lines 413–426). Whatever the status, it is not turned into an exception. In exchange,releaseIfNotConsumed, which cleans up a body you did not read once your function is done, is attached to all three outcomes: a value, no value, and an error.
Example 3: start a server briefly and call it
This is ex/u22/WebClientExample.java. It starts a Reactor Netty server on an arbitrary free port (port(0)), hooks a functional endpoint to it with the ReactorHttpHandlerAdapter from section 1, and calls it with WebClient. The server is closed at the end.
// An example written for this post (not Spring's source)
/** A filter that prints its name when the request goes out and when the response comes back. */
static ExchangeFilterFunction named(String name) {
return (request, next) -> {
System.out.println(" " + name + " -> " + request.method() + " " + request.url().getPath());
return next.exchange(request)
.doOnNext(response -> System.out.println(" " + name + " <- " + response.statusCode().value()));
};
}
// ... from here on, inside main
AtomicInteger hits = new AtomicInteger(); // how many requests the server received
RouterFunction<ServerResponse> routes = RouterFunctions.route()
.GET("/greet/{name}", request -> {
hits.incrementAndGet();
return ServerResponse.ok().bodyValue("hi, " + request.pathVariable("name"));
})
.build();
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routes);
DisposableServer server = HttpServer.create()
.host("localhost")
.port(0) // any free port
.handle(new ReactorHttpHandlerAdapter(httpHandler))
.bindNow(TIMEOUT);
// ... what follows is inside a try block; the last three lines, which close the server, are in its finally block
WebClient client = WebClient.builder()
.baseUrl("http://localhost:" + server.port())
.filter(named("filter A"))
.filter(named("filter B"))
.build();
System.out.println("[1] retrieve(): nothing is sent until someone subscribes");
Mono<String> greeting = client.get().uri("/greet/{name}", "spring")
.retrieve()
.bodyToMono(String.class);
System.out.println(" Mono built. server hits = " + hits.get());
System.out.println(" body = \"" + greeting.block(TIMEOUT) + "\", server hits = " + hits.get());
System.out.println("[2] subscribing to the same Mono again sends a second request");
System.out.println(" body = \"" + greeting.block(TIMEOUT) + "\", server hits = " + hits.get());
System.out.println("[3] retrieve() on a 404");
try {
client.get().uri("/nope").retrieve().bodyToMono(String.class).block(TIMEOUT);
}
catch (WebClientResponseException ex) {
System.out.println(" " + ex.getClass().getSimpleName() + ", status = " + ex.getStatusCode().value());
}
System.out.println("[4] exchangeToMono() on a 404");
String seen = client.get().uri("/nope")
.exchangeToMono(response -> Mono.just("my function saw status " + response.statusCode().value()))
.block(TIMEOUT);
System.out.println(" " + seen);
server.disposeNow(TIMEOUT);
HttpResources.disposeLoopsAndConnections();
System.out.println("server disposed: " + server.isDisposed());
[1] retrieve(): nothing is sent until someone subscribes
Mono built. server hits = 0
filter A -> GET /greet/spring
filter B -> GET /greet/spring
filter B <- 200
filter A <- 200
body = "hi, spring", server hits = 1
[2] subscribing to the same Mono again sends a second request
filter A -> GET /greet/spring
filter B -> GET /greet/spring
filter B <- 200
filter A <- 200
body = "hi, spring", server hits = 2
[3] retrieve() on a 404
filter A -> GET /nope
filter B -> GET /nope
filter B <- 404
filter A <- 404
NotFound, status = 404
[4] exchangeToMono() on a 404
filter A -> GET /nope
filter B -> GET /nope
filter B <- 404
filter A <- 404
my function saw status 404
server disposed: true
- [1] Even after calling
retrieve().bodyToMono(...), the server has received no request (hits = 0). Onceblock()subscribes, the filters run in the order A, B and the response comes back in the order B, A. - The
NotFoundin [3] isWebClientResponseException.NotFound. The commonly used status codes each have their own subclass, and any other code is created as a plainWebClientResponseException(WebClientResponseExceptionline 310, line 385). - [4] is the same 404, but there is no exception and our function received the status.
- No exception handler was added to the server, yet it answers 404 rather than 500. That is because the default
HandlerStrategiesused bytoHttpHandlercontains aWebFluxResponseStatusExceptionHandler(DefaultHandlerStrategiesBuilderline 60).
Easy to get wrong
- Subscribing to the same
Monotwice sends the request twice. In [2]hitsbecame 2. AMonois a plan, not a result, and on every subscription thedeferinsideexchange()runs again from the start. To use the result in several places, pass along the value from a single subscription or use Reactor's caching operator. - If you call
retrieve()and never subscribe, nothing happens. It compiles and throws nothing, which makes it hard to find. Inside a WebFlux controller you have to chain thatMonointo the return value for the framework to subscribe to it. - The
block()calls in the examples are for show. They are there so we can see the results on themainthread. Inside reactive code that handles a request, the rule is said to be that you do not wait withblock()but chain theMonoand return it. That rule belongs to Reactor, and this post did not confirm it in the source.
What this post leaves out
These are named and skipped for the sake of the main line. The reactive HandlerMethodArgumentResolvers that unpack the request body and arguments, and ReactiveAdapterRegistry (the registry that turns other reactive types such as RxJava or coroutines into a Publisher). Encoder/Decoder and DataBuffer, which deal with the actual bytes, and ServerCodecConfigurer, which collects them (WebFluxConfigurationSupport lines 351–356). Backpressure, which makes the sender send only as much as the receiver can take. CORS pre-flight handling (DispatcherHandler lines 211–218, AbstractHandlerMapping lines 186–218). ViewResolutionResultHandler, which renders views, and ResponseEntityResultHandler, which deals with ResponseEntity. HandlerFilterFunction, the filter for functional endpoints, and nest, which groups routes. WebSession, locale, Forwarded header handling, and the Observation integration (HttpWebHandlerAdapter lines 312–324). The setting that makes controller methods judged to be blocking subscribe on a separate scheduler (RequestMappingHandlerAdapter lines 276–279). And WebSocketHandlerAdapter leads into the topic of the next post.
Recap
| Concept | One-line summary |
|---|---|
Mono / Flux |
A plan, not a value. It runs on subscription, and runs again on every subscription |
HttpHandler |
The floor of WebFlux. One method that takes a request and response and returns Mono<Void>. It lives in spring-web |
| Server adapter | Wraps the server's request and response in Spring types and calls the HttpHandler |
WebHttpHandlerBuilder |
Wraps a WebHandler three times: filter chain, exception handling, HttpWebHandlerAdapter |
WebFilter |
Entered in registration order, left in reverse. Takes the place of MVC's interceptors and servlet filters |
WebExceptionHandler |
Turns an error signal into a response, from outside the filters. Without one, 500 |
DispatcherHandler.handle |
Asks the mappings in turn (concatMap, next), calls through an adapter, writes with a result handler |
HandlerResultHandler |
The third strategy, which MVC does not have. Turns a HandlerResult into a response |
HttpMessageReader / Writer |
The reactive counterpart of HttpMessageConverter. They pass channels, not objects |
| Functional endpoint | RequestPredicate + HandlerFunction = RouterFunction. Registration order is the priority |
RouterFunctionMapping |
Order -1, so it is asked before the annotation mapping |
WebClient |
Filters (ExchangeFilterFunction) → ExchangeFunction → ClientHttpConnector |
retrieve() / exchangeToMono() |
Turns 400 and above into an exception / your function looks at the response itself |
What to look at next
We have seen MVC on servlets (Part 21) and WebFlux on reactive streams fill in the same skeleton (mapping → adapter → result) in different ways.
Everything on the web so far has been "one request, one response". The next post goes to a world where a connection stays open and both sides exchange messages. Part 25: WebSocket and Messaging — How a STOMP Message Flows follows which channels a STOMP message travels over a WebSocket connection on its way to an @MessageMapping method.
- Part 2: The Big Map — Modules and Packages
- Part 19: ORM and R2DBC Integration — JPA Support and Reactive Transactions
- Part 20: The spring-web Foundation — HttpMessageConverter, WebApplicationContext, Filters
- Part 21: DispatcherServlet — How One Request Becomes a Response
- Part 22: Handler Mappings and Adapters — How @RequestMapping Methods Are Found and Called
- Part 23: Exception Handling, Interceptors, Views — Before, After, and the End of a Request