Backend Development 10 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring WebFlux Functional Routing with WebMvc.fn 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&lt;Person&gt; people = request.body(new ParameterizedTypeReference&lt;List&lt;Person&gt;&gt;() {});</code>

Getting request parameters:

<code>MultiValueMap&lt;String, String&gt; 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&lt;Person&gt; 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&lt;ServerResponse&gt; 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&lt;ServerResponse&gt; 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&lt;Person&gt; 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&lt;ServerResponse&gt; 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&lt;ServerResponse&gt; 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&lt;ServerResponse&gt; 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&lt;ServerResponse&gt; 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 Bootfunctional routingWebMvc.fnhandlerfunctionServerResponse
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.