Mastering Spring Web MVC Functional Routing with WebMvc.fn in Spring Boot
This article explains Spring Web MVC's functional programming model (WebMvc.fn), covering immutable ServerRequest/ServerResponse, RouterFunction routing, HandlerFunction handling, 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. The request and response objects are immutable and provide JDK‑8‑friendly access. HandlerFunction corresponds to the body of a @RequestMapping method, and RouterFunction routes incoming requests to the appropriate HandlerFunction.
HandlerFunction Objects
ServerRequest and ServerResponse are immutable interfaces offering access to HTTP method, URI, headers, query parameters, and body.
ServerRequest
<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>ServerResponse
ServerResponse is immutable; use builder methods to set status, headers, and body.
Handler Classes
Handler classes can be defined separately, similar to @RestController, and injected with DAO components for database operations.
<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();
}
}
</code> <code>@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")), "中国"));
}
}
</code>Validation
Spring's validation tools 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 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;
}
}
</code> <code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</code>RouterFunction
RouterFunction maps requests to HandlerFunction. Use RouterFunctions.route() to build routes fluently.
<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>Nested Routing
<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();
}
</code> <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))
.POST("/save", ph::save))
.build();
}
</code>HandlerMapping
Functional routing still uses DispatcherServlet, so corresponding HandlerMapping and Adapter are present:
RouterFunctionMapping : Detects RouterFunction beans, sorts and combines them.
HandlerFunctionAdapter : Simple adapter that lets DispatcherHandler invoke the mapped HandlerFunction.
Filters
Filters can be added with before, after, or filter methods on the router builder. They apply to all routes generated by the builder, but not to top‑level routes when defined inside nested routes.
<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();
}
public ServerResponse queryPerson(ServerRequest request) throws Exception {
System.out.println(request.headers().header("x-pack"));
return ok().body(new Person(Integer.valueOf(request.pathVariable("id")), "中国"));
}
</code>Swagger
Swagger support is not applicable in this functional routing context.
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.