How are functional endpoints registered and dispatched, and how do they coexist with annotated @Controllers in the same application?
answer
- @Bean RouterFunction -> RouterFunctionMapping combines all
- DispatcherServlet -> ordered HandlerMappings
- HandlerFunctionAdapter invokes handle()
- RequestMappingHandlerMapping order 0 wins over functional
- First-match-wins within a router
basics
~20 sYou expose RouterFunction<ServerResponse> beans. Spring's RouterFunctionMapping combines them and the DispatcherServlet dispatches matching requests to them. Annotated controllers use RequestMappingHandlerMapping. Both work together; by default the annotated handler mapping is consulted before the functional one.
solid answer
~40 sFunctional endpoints are registered by declaring @Bean methods returning RouterFunction<ServerResponse>. At startup, RouterFunctionMapping (a HandlerMapping) discovers and combines every such bean into one composite routing table. At request time, the DispatcherServlet iterates its ordered HandlerMappings; RouterFunctionMapping matches a request against the router's predicates (first match wins) and yields the HandlerFunction, which a HandlerFunctionAdapter invokes. Annotated controllers go through RequestMappingHandlerMapping instead. The two models fully coexist in one app. Crucially, the mappings are ordered: RequestMappingHandlerMapping has order 0 and is consulted before RouterFunctionMapping (ordered later by Spring's config), so if an annotated @RequestMapping and a functional route both match the same path, the annotated controller wins. Within a single RouterFunction, routes are still first-match-wins in declaration order.
code
java · 19 lines// Handler is a bean so DI works; router references its methods
@Component
class OrderHandler {
private final OrderService svc;
OrderHandler(OrderService svc) { this.svc = svc; }
ServerResponse list(ServerRequest r) { return ServerResponse.ok().body(svc.all()); }
}
@Configuration
class OrderRoutes {
@Bean
RouterFunction<ServerResponse> orderRoutes(OrderHandler h) {
return RouterFunctions.route()
.GET("/orders", h::list) // combined with all other RouterFunction beans
.build();
}
}
// RouterFunctionMapping discovers this bean; if a @GetMapping("/orders")
// controller also existed, the annotated one (order 0) would win.go deeper
Know routers are @Beans discovered by Spring and that annotated and functional endpoints can coexist.
Name RouterFunctionMapping + HandlerFunctionAdapter and know multiple router beans are combined.
Explain HandlerMapping ordering: annotated (order 0) wins over functional; first-match-wins within a router.
Reason about mapping precedence conflicts, modular router composition, and how functional routes fit the DispatcherServlet pipeline vs annotation-only features (CORS, etc.).
## Registration You declare functional routes as beans: ```java @Configuration class Routes { @Bean RouterFunction<ServerResponse> a() { ... } @Bean RouterFunction<ServerResponse> b() { ... } } ``` At context startup, **`RouterFunctionMapping`** — a `HandlerMapping` auto-configured by Spring MVC (`WebMvcConfigurationSupport` / Spring Boot's `WebMvcAutoConfiguration`) — finds **all** `RouterFunction` beans in the context and **combines** them (via `RouterFunction.andOther`/`and`) into a single composite router. So multiple `@Bean` router functions across many config classes all merge into one routing table. (You *can* alternatively build one big router yourself, but multiple beans is idiomatic and modular.) ## Dispatch flow 1. A request hits the **`DispatcherServlet`**. 2. It asks each registered **`HandlerMapping`**, in order, "do you have a handler for this request?" 3. `RouterFunctionMapping` evaluates the composite `RouterFunction`: it walks routes in order and returns the **first** matching `HandlerFunction` (wrapped as the handler). 4. The `DispatcherServlet` finds the matching **`HandlerAdapter`** — here **`HandlerFunctionAdapter`** — which invokes `handlerFunction.handle(serverRequest)` and writes the returned `ServerResponse`. 5. If no route matches, that mapping returns null and the `DispatcherServlet` moves on to the next mapping. Annotated controllers use a parallel pair: **`RequestMappingHandlerMapping`** + **`RequestMappingHandlerAdapter`**. ## Coexistence and ordering — the key gotcha Both models live happily in one application; you can mix `@RestController`s and functional routers freely. But `HandlerMapping`s are **ordered**, and the `DispatcherServlet` uses the **first** one that produces a handler: - `RequestMappingHandlerMapping` is ordered **0** (highest priority among these). - `RouterFunctionMapping` is ordered **after** it (Spring MVC's config assigns it a later order value). Consequence: if an annotated `@GetMapping("/foo")` **and** a functional `GET("/foo")` both match the same request, the **annotated controller wins** because its mapping is consulted first. Two endpoints answering the same path is a design smell — but knowing the precedence matters when it happens accidentally. Within a single (composite) `RouterFunction`, matching is **first-match-wins in declaration order** — so order specific routes before catch-all ones. ## Beans, dependencies, and testing - Handlers are just objects/lambdas; you typically make a `@Component` handler class and inject services into it, then reference `handler::method` from the router `@Bean`. This keeps DI intact. - Because routers are beans, you can compose/generate them programmatically and unit-test handlers directly (call `handle` with a mock `ServerRequest`) or integration-test via `MockMvc` against the full context. ## Edge cases - **CORS, content negotiation, path matching**: functional routes participate in the same `DispatcherServlet` pipeline, but some annotation-oriented features (e.g. `@CrossOrigin`) don't apply; you configure such concerns via predicates/filters or global config instead. - **Multiple router beans**: their relative order in the combined router is generally the order Spring assembles them; avoid relying on cross-bean ordering — keep mutually exclusive predicates. - **Actuator / static resources**: those use their own handler mappings with their own orders, unaffected by your routers.
- If both an annotated @GetMapping and a functional route match the same path, which runs?The annotated controller. RequestMappingHandlerMapping is ordered 0 and is consulted before RouterFunctionMapping, so the DispatcherServlet uses the annotated handler it finds first.
- Do multiple RouterFunction @Beans conflict, or are they combined?They're combined. RouterFunctionMapping discovers all RouterFunction beans and merges them into one composite routing table, so you can modularize routes across config classes.
- What component actually invokes a matched HandlerFunction?HandlerFunctionAdapter — the HandlerAdapter for functional endpoints. It calls handlerFunction.handle(serverRequest) and writes the returned ServerResponse.
saying these in an interview costs you the question
- Claiming functional and annotated endpoints cannot coexist
- Saying functional routes take precedence over @RequestMapping by default (it's the opposite)
- Thinking each RouterFunction bean is dispatched by a separate mapping rather than combined
- Assuming routes are matched most-specific-first rather than declaration order