Backend Development 10 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Spring Boot Functional Routing with WebMvc.fn: A Complete Guide

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.

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

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.

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

<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 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 can be applied to request bodies using Spring’s validation infrastructure.

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

Dependency required:

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt;
&lt;/dependency&gt;
</code>

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.

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

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.

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

<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("参数错误");
      }
      return next.handle(request);
    })
    .build();
}
</code>

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.

Spring Bootfunctional routingWebMvc.fnhandlerfunctionrouterfunction
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.