Master Spring Web MVC.fn: Functional Routing and Handlers in Spring Boot 2.7
This article explains Spring Web MVC.fn's functional programming model, covering HandlerFunction, immutable request/response contracts, parameter validation, routing predicates, nested routes, filters, and provides complete code examples for building functional endpoints in Spring Boot 2.7.
1. Introduction
Spring Web MVC includes WebMvc.fn , a lightweight functional programming model where functions handle routing and requests while contracts remain immutable. It serves as an alternative to the annotation‑based model but still relies on the same underlying DispatcherServlet for request processing.
2. Core Concepts
2.1 HandlerFunction
In WebMvc.fn , an HTTP request is processed by a HandlerFunction —a function that receives a ServerRequest and returns a ServerResponse . The request and response objects are immutable and provide JDK 8‑friendly access to headers, body, method, and status.
<code>import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...;
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
public ServerResponse listPeople(ServerRequest request) { /* ... */ }
public ServerResponse createPerson(ServerRequest request) { /* ... */ }
public ServerResponse getPerson(ServerRequest request) { /* ... */ }
}</code>2.2 Request and Response APIs
ServerRequest gives access to HTTP method, URI, headers, query parameters, and body via body(Class) . ServerResponse is built immutably using a builder to set status, headers, and body.
<code>String body = request.body(String.class);
MultiValueMap<String, String> params = request.params();
ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(new Person());
Mono<Person> person = webClient.get()
.retrieve()
.bodyToMono(Person.class);
ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(person);
</code>2.3 Parameter Validation
Handler functions can perform bean validation using Spring’s Validator .
<code>@Component
public class PersonHandler {
private final Validator validator;
public PersonHandler(Validator validator) { this.validator = validator; }
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person);
repository.savePerson(person);
return ok().build();
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString());
}
}
}
</code>3. Routing Functions
3.1 Predicates
RequestPredicates provides common predicate implementations based on path, method, content type, etc.
<code>RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/demo", accept(MediaType.TEXT_PLAIN), req -> ServerResponse.ok().body("test"))
.build();
</code>3.2 Router Builders
Use route() for a fluent builder; .add(otherRoute) can combine additional routes.
<code>import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...;
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...;
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.add(otherRoute)
.build();
</code>3.3 Nested Routes
<code>RouterFunction<ServerResponse> route = route()
.path("/person", b -> b
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
</code>3.4 Route Filters
Filters can be applied globally with before , after , or filter . They affect all routes built by the same builder; nested route filters do not apply to top‑level routes.
<code>RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(req -> ServerRequest.from(req)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((req, res) -> logResponse(res))
.build();
</code>4. Complete Example
<code>@Configuration
public class RouterConfig {
@Bean
public UserHandler userHandler() {
return new UserHandler();
}
@Bean
public RouterFunction<ServerResponse> r1(UserHandler userHandler) {
return route()
.GET("/r1/{id}", userHandler::queryUser)
.build();
}
private class UserHandler {
public Mono<ServerResponse> queryUser(ServerRequest request) {
return ok().bodyValue("查询 - " + request.pathVariable("id"));
}
}
}
</code>The article ends with a brief note that the content is complete and hopes it is helpful.
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.