Master Spring Boot Functional Routing with WebMvc.fn: A Complete Guide
This article explains Spring Web MVC's functional programming model (WebMvc.fn), covering immutable request/response handling, RouterFunction routing, HandlerFunction implementation, validation, nested routes, filters, and provides comprehensive code examples for Spring Boot 2.4.12.
Overview
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model where functions are used for routing and handling requests. HandlerFunction processes a ServerRequest and returns a ServerResponse. The model is immutable and runs on the same DispatcherServlet as the annotation‑based model.
Routing is performed by RouterFunction, which maps a request to an optional HandlerFunction. When a router matches, the corresponding handler is invoked; otherwise the result is empty.
@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();
}
}Exposing a RouterFunction as a @Bean is sufficient. The three parameters of a GET route are the path, a predicate (similar to @RequestMapping attributes), and the HandlerFunction that contains the business logic.
HandlerFunction Objects
ServerRequest provides immutable access to HTTP method, URI, headers, query parameters, and body via dedicated methods.
@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();
}ServerResponse is also immutable; it is built using a fluent API to set status, headers, and body.
Handler Classes can be defined separately, similar to traditional @RestController classes.
@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 ok().body(request.body(Person.class));
}
public ServerResponse queryPerson(ServerRequest request) throws Exception {
return ok().body(new Person(Integer.valueOf(request.pathVariable("id")), "中国"));
}
}Validation can be applied to request bodies using Spring’s validation infrastructure.
@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 ok().body(person);
}
return ok().body(errors.toString());
}
private Errors validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
return errors.hasErrors() ? errors : null;
}
}Dependency required:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>RouterFunction
Router functions are typically created with RouterFunctions.route(). The builder provides methods such as GET and POST for common mappings, and predicates can be added to impose additional constraints.
@Bean
public RouterFunction<ServerResponse> hello() {
return route().GET("/hello", accept(MediaType.APPLICATION_JSON), request -> {
return ServerResponse.status(HttpStatus.OK).body("Hello World");
}).build();
}Multiple predicates can be combined with and or or.
Nested Routes
Nested routing can be expressed with path or nest to set a common prefix.
@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();
}HandlerMapping
Because the functional model still uses DispatcherServlet, it relies on RouterFunctionMapping (which aggregates all RouterFunction beans) and HandlerFunctionAdapter (which invokes the matched HandlerFunction).
Filters
Filters can be added with before, after, or filter on the router builder. They apply to all routes generated by the builder, but not to routes defined in a separate nested router.
@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("参数错误");
}
return next.handle(request);
})
.build();
}before adds a custom header that can be read later; after runs after the handler; filter can short‑circuit the chain based on request data.
Swagger
Swagger support is not applicable in this functional routing approach, so use it cautiously.
End of tutorial.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
