Backend Development 11 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring Web MVC Functional Routing with WebMvc.fn in Spring Boot

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

HandlerFunction

that receives a

ServerRequest

and returns a

ServerResponse

. Both request and response objects are immutable.

RouterFunction

A

RouterFunction

routes incoming requests to a handler function. It matches the request path and returns an optional

HandlerFunction

. It is analogous to

@RequestMapping

but 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

RouterFunction

as a bean in a

@Configuration

class.

GET Method Parameters

First: request path.

Second: predicate that limits which calls match (similar to

consumer

,

params

in

@RequestMapping

).

Third: the

HandlerFunction

that 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

@RestController

methods.

<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

GET

and

POST

for 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

and

or

or

to add multiple constraints.

Nested Routes

Nested routing can be defined using

path

or

nest

to 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

RouterFunctionMapping

to detect

RouterFunction

beans and on

HandlerFunctionAdapter

to invoke the matched handler.

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 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.

Javabackend developmentSpring BootSpring MVCfunctional routingWebMvc.fn
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.