Mastering Spring WebFlux Functional Routing with WebMvc.fn in Spring Boot 2.7
This guide explains how to use Spring Web MVC's WebMvc.fn functional model—including HandlerFunction, RouterFunction, ServerRequest, ServerResponse, predicates, nested routes, and filters—to build clean, immutable, and reactive APIs in Spring Boot 2.7.
Environment: SpringBoot 2.7.18
1. Introduction
Spring Web MVC provides WebMvc.fn , a lightweight functional programming model that routes and handles requests using functions and immutable contracts, serving as an alternative to the annotation‑based approach.
2. Core Elements
The request is processed by a HandlerFunction , which works together with RouterFunction to map requests to handlers.
ServerRequest and ServerResponse are immutable interfaces that expose HTTP method, URI, headers, query parameters, and body.
Accessing the request body:
<code>String string = request.body(String.class);</code>Reading a parameterized type:
<code>List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});</code>Getting request parameters:
<code>MultiValueMap<String, String> params = request.params();</code>Creating a response with ServerResponse builder methods:
<code>Person person = new Person(1L, "Admin");
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);</code>The body can also be an asynchronous type such as CompletableFuture , Publisher , or any type supported by ReactiveAdapterRegistry :
<code>Mono<Person> person = Mono.just(new Person(1L, "张三"));
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);</code>Server‑Sent Events (SSE) are supported as well:
<code>public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// store sseBuilder for later use
}));
}
// In another thread
sseBuilder.send("Hello world");
Person person = new Person(1L, "李四");
sseBuilder.send(person);
sseBuilder.complete();
</code>Handler Class (Request Handler)
The functional interface HandlerFunction<T extends ServerResponse> defines a single method:
<code>@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
T handle(ServerRequest request) throws Exception;
}</code>Implementation can be expressed with a lambda:
<code>HandlerFunction<ServerResponse> handler = request -> ServerResponse.ok().body("Hello Pack");</code>Typical handler methods are grouped in a component class, similar to a controller:
<code>@Component
public class PersonHandler {
private final PersonRepository personRepository;
public PersonHandler(PersonRepository personRepository) {
this.personRepository = personRepository;
}
public ServerResponse listPeople(ServerRequest request) {
List<Person> people = personRepository.findAll();
return ServerResponse.ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
personRepository.save(person);
return ServerResponse.ok().build();
}
public ServerResponse getPerson(ServerRequest request) {
int id = Integer.valueOf(request.pathVariable("id"));
Person person = personRepository.findById(id).orElse(null);
return (person != null)
? ServerResponse.ok().contentType(APPLICATION_JSON).body(person)
: ServerResponse.notFound().build();
}
}
</code>Router configuration binds routes to handler methods:
<code>@Configuration
public class RouterConfig {
@Bean
public RouterFunction<ServerResponse> router(PersonHandler handler) {
return route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
}
}
</code>Parameter Validation
Validators can be injected into the handler to validate request bodies:
<code>@Component
public class PersonHandler {
private final PersonRepository personRepository;
private final Validator validator;
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new RuntimeException(errors.toString());
}
personRepository.save(person);
return ServerResponse.ok().build();
}
}
</code>3. Advanced Usage
Predicates
Custom RequestPredicate implementations allow fine‑grained matching, while the utility class RequestPredicates provides common predicates such as path, method, and content‑type:
<code>RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/r1", accept(MediaType.TEXT_PLAIN), request -> ServerResponse.ok().body("Hello Pack"))
.build();
</code>Predicates can be combined with and() and or() for complex conditions.
Nested Routes
Nested routing mirrors the @RequestMapping("/persons") prefix used in annotation‑based controllers:
<code>@Bean
public RouterFunction<ServerResponse> person(PersonHandler handler) {
return route()
.path("/persons", builder -> builder
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
// other methods
)
.build();
}
</code>Request Filters
Before, after, and filter methods on the router function act like @ControllerAdvice or servlet filters:
<code>@Bean
RouterFunction<ServerResponse> person() {
return route()
.path("/persons", b1 -> b1
.nest(RequestPredicates.all(), b2 -> b2
.GET("/{id}", accept(MediaType.TEXT_HTML), req -> ServerResponse.ok().body("query person"))
.before(req -> {
System.out.printf("Request Token: %s%n", req.headers().header("access-token"));
return req;
})
.after((req, resp) -> {
System.out.println("after ... ");
return resp;
})
)
)
.build();
}
</code>These hooks let you execute logic at request entry and response exit.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.