Mastering Spring Web MVC Functional Routing with WebMvc.fn in Spring Boot
Explore Spring Web MVC's functional programming model (WebMvc.fn) in Spring Boot 2.4.12, covering immutable ServerRequest/ServerResponse, RouterFunction routing, handler functions, predicates, nested routes, filters, validation, and practical code examples for building clean, annotation‑free REST endpoints.
Overview
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model where functions are used for routing and handling requests. The model is immutable, provides JDK‑8‑friendly access to HTTP requests and responses, and runs on the same DispatcherServlet as the annotation‑based model.
HandlerFunction
In WebMvc.fn, an HTTP request is processed by a
HandlerFunctionthat receives a
ServerRequestand returns a
ServerResponse. Both request and response objects are immutable.
RouterFunction
A
RouterFunctionroutes incoming requests to a handler function. It matches the request path and returns an optional
HandlerFunction. It is analogous to
@RequestMappingbut also supplies behavior.
<code>@Configuration
public class PersonHandlerConfiguration {
@Bean
public RouterFunction<ServerResponse> person() {
return route()
.GET("/person", accept(MediaType.APPLICATION_JSON), request -> {
return ServerResponse.status(HttpStatus.OK).body("Hello World");
})
.build();
}
}
</code>Expose the
RouterFunctionas a bean in a
@Configurationclass.
GET Method Parameters
First: request path.
Second: predicate that limits which calls match (similar to
consumer,
paramsin
@RequestMapping).
Third: the
HandlerFunctionthat contains the business logic.
ServerRequest and ServerResponse
ServerRequest provides access to HTTP method, URI, headers, query parameters, and body via dedicated methods.
<code>@Bean
public RouterFunction<ServerResponse> student() {
return route()
.GET("/student/{id}", accept(MediaType.APPLICATION_JSON), request -> {
return ServerResponse.ok().body("name = " + request.param("name").get() + ", id = " + request.pathVariable("id"));
})
.POST("/student", accept(MediaType.APPLICATION_JSON), request -> {
return ServerResponse.ok().body(request.body(Student.class));
})
.build();
}
</code>ServerResponse is immutable and built via a fluent API to set status, headers, and body.
Handler Classes
Handler logic can be placed in separate classes, similar to traditional
@RestControllermethods.
<code>@Configuration
public class PersonHandlerConfiguration {
@Resource
private PersonHandler ph;
@Bean
public RouterFunction<ServerResponse> person() {
return route()
.GET("/person/{id}", accept(MediaType.APPLICATION_JSON), ph::queryPerson)
.POST("/person", accept(MediaType.APPLICATION_JSON), ph::save)
.build();
}
}
@Component
public class PersonHandler {
public ServerResponse save(ServerRequest request) throws Exception {
return ServerResponse.ok().body(request.body(Person.class));
}
public ServerResponse queryPerson(ServerRequest request) throws Exception {
return ServerResponse.ok().body(new Person(Integer.valueOf(request.pathVariable("id")), "China"));
}
}
</code>Validation
Spring’s validation utilities can be applied to request bodies.
<code>@Component
public class PersonHandler {
@Resource
private Validator validator;
public ServerResponse save(ServerRequest request) throws Exception {
Person person = request.body(Person.class);
Errors errors = validate(person);
if (errors == null) {
return ServerResponse.ok().body(person);
}
return ServerResponse.ok().body(errors.toString());
}
private Errors validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
return errors.hasErrors() ? errors : null;
}
}
</code>Dependency required:
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</code>RouterFunction Details
Router functions are typically created with
RouterFunctions.route(). The builder provides methods like
GETand
POSTfor mapping HTTP methods.
<code>@Bean
public RouterFunction<ServerResponse> hello() {
return route()
.GET("/hello", accept(MediaType.APPLICATION_JSON), request -> {
return ServerResponse.status(HttpStatus.OK).body("Hello World");
})
.build();
}
</code>Predicates can be combined with
andor
orto add multiple constraints.
Nested Routes
Nested routing can be defined using
pathor
nestto set a common prefix.
<code>@Bean
public RouterFunction<ServerResponse> nestPerson() {
return route()
.path("/persons", builder -> builder
.GET("/{id}", accept(MediaType.APPLICATION_JSON), ph::queryPerson)
.POST("/save", ph::save))
.build();
}
@Bean
public RouterFunction<ServerResponse> nestPerson2() {
return route()
.path("/persons2", b1 -> b1
.nest(accept(MediaType.APPLICATION_JSON), b2 -> b2
.GET("/{id}", accept(MediaType.APPLICATION_JSON), ph::queryPerson)
.POST("/save", ph::save)))
.build();
}
</code>HandlerMapping
Because the functional model still uses
DispatcherServlet, it relies on
RouterFunctionMappingto detect
RouterFunctionbeans and on
HandlerFunctionAdapterto invoke the matched handler.
Filters
Filters can be added with
before,
after, or
filteron the router builder. They apply to all routes generated by the builder, but not to top‑level routes from nested routers.
<code>@Bean
public RouterFunction<ServerResponse> nestPerson2() {
return route()
.path("/persons2", b1 -> b1
.nest(accept(MediaType.APPLICATION_JSON), b2 -> b2
.GET("/{id}", accept(MediaType.APPLICATION_JSON), ph::queryPerson)
.before(request -> ServerRequest.from(request).header("x-pack", "123123").build()))
.POST("/save", ph::save)
.after((request, response) -> {
System.out.println("after execution..." + response.statusCode());
return response;
})
.filter((request, next) -> {
if (request.pathVariable("id").equals("100")) {
return ServerResponse.ok().body("参数错误");
} else {
return next.handle(request);
}
})
.build();
}
</code>before adds a custom header that can be read in the handler; after runs after the response is produced; filter can short‑circuit the chain.
Swagger Note
Swagger documentation is not applicable to this functional routing approach.
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.