Internally, how does Spring WebFlux dispatch a request to a functional route, and how are multiple RouterFunction beans, predicates, and filters composed and ordered?
answer
- DispatcherHandler -> ordered HandlerMappings
- RouterFunctionMapping combines beans (andOther)
- route(request) -> Mono<HandlerFunction>, first match wins
- HandlerFunctionAdapter + ServerResponseResultHandler
- HandlerFilterFunction (per-router) vs WebFilter (global)
basics
~10 sRouterFunctionMapping collects all RouterFunction<ServerResponse> beans, combines them, and for each request calls RouterFunction.route(request) to get a Mono<HandlerFunction>. HandlerFunctionAdapter invokes it; ServerResponse writes the result. Routes are tried in order; first match wins.
solid answer
~30 sAt startup RouterFunctionMapping (a HandlerMapping) gathers every RouterFunction<ServerResponse> bean and combines them into one composite via RouterFunction.andOther, in bean order (influenced by @Order). Per request, DispatcherHandler consults its ordered HandlerMappings; RouterFunctionMapping calls composite.route(ServerRequest), which walks routes in declaration/combination order calling each RequestPredicate.test — the first match returns a Mono<HandlerFunction>. The matched HandlerFunction is the handler, adapted by HandlerFunctionAdapter, and its Mono<ServerResponse> is rendered by ServerResponseResultHandler (writeTo). filter() wraps handlers as HandlerFilterFunction (per-router, distinct from global WebFilter). Predicates compose via and/or/negate; nest() ANDs a shared predicate and can short-circuit. Ordering across the annotated RequestMappingHandlerMapping is by HandlerMapping order.
code
java · 29 lines// Combining beans, ordering, group filter, and a custom predicate.
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import org.springframework.web.reactive.function.server.*;
import org.springframework.core.annotation.Order;
import org.springframework.context.annotation.*;
@Configuration
class RoutingConfig {
@Bean @Order(1) // consulted before the catch-all bean when paths overlap
RouterFunction<ServerResponse> apiRoutes(ApiHandler h) {
return RouterFunctions.route()
.nest(path("/api").and(accept(org.springframework.http.MediaType.APPLICATION_JSON)), b -> b
.GET("/items/{id}", h::getItem)
.GET("/items", h::listItems)
// HandlerFilterFunction scoped to this nest only
.filter((request, next) ->
next.handle(request)
.onErrorResume(ex -> ServerResponse.status(500).bodyValue("failed"))))
.build();
}
@Bean @Order(2)
RouterFunction<ServerResponse> fallback(ApiHandler h) {
return RouterFunctions.route()
.GET("/**", h::notFound) // catch-all last
.build();
}
}go deeper
Aware there is some mapping that finds the handler; details not expected.
Knows RouterFunctionMapping discovers beans and first-match-wins ordering.
Can trace route()->HandlerFunctionAdapter->ServerResponseResultHandler and explain filters vs WebFilter.
Reasons end-to-end about composition, ordering across HandlerMappings, short-circuiting nests, and non-blocking predicate/handler discipline.
**The dispatch pipeline.** WebFlux's front controller is `DispatcherHandler` (the reactive analog of MVC's `DispatcherServlet`). It holds ordered lists of `HandlerMapping`, `HandlerAdapter`, and `HandlerResultHandler` beans. For each `ServerWebExchange` it: (1) asks each `HandlerMapping` in order for a handler, taking the first non-empty; (2) finds a `HandlerAdapter` that `supports(handler)` and calls `handle(...)` to get a `Mono<HandlerResult>`; (3) finds a `HandlerResultHandler` that `supports(result)` to write the response. **Where functional routes plug in.** - **`RouterFunctionMapping`** is the `HandlerMapping` for functional endpoints. On init it finds all `RouterFunction<ServerResponse>` beans in the context and **combines** them into a single composite `RouterFunction` (equivalent to chaining `RouterFunction.andOther(...)`), preserving bean order. Bean order can be influenced with `@Order` / `Ordered`. - Per request it wraps the exchange in a `ServerRequest` and calls `composite.route(serverRequest)`, which returns `Mono<HandlerFunction<ServerResponse>>`. The composite evaluates constituent routes **in order**: each `RouterFunction` tests its `RequestPredicate` against the `ServerRequest`; the **first predicate that matches** yields its `HandlerFunction` and evaluation stops (first-match-wins). If none match, the `Mono` is empty and `DispatcherHandler` moves on to the next `HandlerMapping`. - The resolved handler is the `HandlerFunction` itself. **`HandlerFunctionAdapter`** is the `HandlerAdapter` that `supports` `HandlerFunction`; it invokes `handlerFunction.handle(serverRequest)` producing `Mono<ServerResponse>`, wrapped as a `HandlerResult`. - **`ServerResponseResultHandler`** is the `HandlerResultHandler` that recognizes a `ServerResponse` result and calls its `writeTo(exchange, context)` to serialize status, headers, and body (using the configured `HttpMessageWriter`s / codecs). **Predicate composition mechanics.** `RequestPredicate` exposes `test(ServerRequest)`, plus combinators `and`, `or`, `negate`. `RequestPredicates` provides factories (`GET`, `POST`, `path`, `pathExtension`, `accept`, `contentType`, `headers`, `queryParam`, `method`). Path matching uses `PathPattern` (the same parser as annotated mappings), so `{var}` capture and `**` behave consistently, and captured variables are stored as request attributes readable via `ServerRequest.pathVariable`. **nest() internals.** `nest(predicate, consumer)` produces a `NestedRouterFunction`-style structure: the outer predicate is tested first; only if it matches are the inner routes evaluated (their predicates effectively ANDed with the outer). This gives a natural **short-circuit**: a large group whose prefix doesn't match is skipped wholesale, and nested path variables from the outer predicate remain available to inner handlers. `path(pattern, consumer)` is `nest(RequestPredicates.path(pattern), consumer)`. **Filters.** `filter(HandlerFilterFunction)` wraps the matched `HandlerFunction`: a `HandlerFilterFunction<T,R>` has `Mono<R> filter(ServerRequest, HandlerFunction<T> next)`, so it can inspect/transform the request, short-circuit, or post-process the response by composing around `next.handle(request)`. `before`/`after`/`onError` are conveniences over it. Crucially these are **per-RouterFunction** and only wrap routes within their builder/nest scope — distinct from a global **`WebFilter`**, which sits in the `WebHandler` chain ahead of `DispatcherHandler` and applies to *all* requests (both functional and annotated). **Ordering across models.** `RouterFunctionMapping` and `RequestMappingHandlerMapping` are both `HandlerMapping`s with `Ordered` values; `DispatcherHandler` consults them in that order and the first to return a handler wins. So if a path could be claimed by both models, the relative `HandlerMapping` order decides — teams typically avoid overlap by giving each distinct paths. Within functional routing, ordering is purely by declaration/combination sequence, which is why more specific routes must precede catch-alls to avoid shadowing. **Practical implications / gotchas:** - Combining many beans: control precedence with `@Order` on the `@Bean RouterFunction` methods when overlap is possible. - `route()` returns a *stateful builder*; reusing predicates across builders is fine because predicates are immutable values. - Because routing is a `Mono`, predicate evaluation itself stays non-blocking; avoid heavyweight/blocking logic in custom `RequestPredicate.test`. - Errors thrown while producing the response should be handled reactively (in the handler or a filter's `onError`) — there's no annotated advice fallback for functional routes.
- What's the difference between a HandlerFilterFunction and a WebFilter?HandlerFilterFunction is functional-endpoint-scoped: it wraps the matched HandlerFunction within a specific RouterFunction/nest via .filter(). A WebFilter is global, sits in the WebHandler chain before DispatcherHandler, and applies to every request regardless of programming model.
- If two RouterFunction beans could both match a path, how is the winner decided?By combination/bean order — RouterFunctionMapping combines beans in order (influenceable via @Order), and route() evaluation is first-match-wins, so the earlier-ordered bean's matching route claims the request.
- Which components adapt and render a functional handler's result?HandlerFunctionAdapter invokes the HandlerFunction to get Mono<ServerResponse>; ServerResponseResultHandler then calls ServerResponse.writeTo to serialize status/headers/body via the configured codecs.
saying these in an interview costs you the question
- Saying annotated and functional routes are matched by the same HandlerMapping.
- Claiming all routes are evaluated and 'best match' wins (it's first-match in order).
- Confusing per-router HandlerFilterFunction with a global WebFilter.
- Doing blocking work inside a RequestPredicate.test or handler on the event loop.