Spring Web MVC contains WebMvc.fn, a lightweight functional programming model in which functions are used to route and process requests, and contracts are designed to be immutable. It is an alternative to the annotation-based programming model, but otherwise runs on the same DispatcherServlet.

Brief Description

In WebMvc.fn, an HTTP request is handled using a HandlerFunction: a function that accepts ServerRequest and returns ServerResponse. Both the request and response objects have immutable contracts that provide JDK 8-friendly access to the HTTP request and response. HandlerFunction is the equivalent of a method body marked with the @RequestMapping annotation, in an annotation-based programming model.

Incoming requests are routed to a handler function using RouterFunction: a function that accepts a ServerRequest and returns an optional HandlerFunction (i.e. Optional<HandlerFunction>). If the router function matches, the handler function is returned, otherwise an empty Optional is returned. RouterFunction is the equivalent of the @RequestMapping annotation, but with the significant difference that router functions transfer not only data, but also operating logic.

RouterFunctions.route() provides a router builder that makes it easy to create routers, as shown in the following example:

Java

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .build();
public class PersonHandler {
    // ...
    public ServerResponse listPeople(ServerRequest request) {
        // ...
    }
    public ServerResponse createPerson(ServerRequest request) {
        // ...
    }
    public ServerResponse getPerson(ServerRequest request) {
        // ...
    }
}
Kotlin

import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router { 
    accept(APPLICATION_JSON).nest {
        GET("/person/{id}", handler::getPerson)
        GET("/person", handler::listPeople)
    }
    POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
    // ...
    fun listPeople(request: ServerRequest): ServerResponse {
        // ...
    }
    fun createPerson(request: ServerRequest): ServerResponse {
        // ...
    }
    fun getPerson(request: ServerRequest): ServerResponse {
        // ...
    }
}
  1. Create router using the router DSL.

If registered RouterFunction as a bean, for example, by opening it in a class with the @Configuration annotation, it will be automatically discovered by the servlet.

HandlerFunction

ServerRequest and ServerResponse are immutable interfaces that provide access to the HTTP request and response, including headers, body, method, and JDK 8-friendly status code.

ServerRequest

ServerRequest provides access to the HTTP method, URI, headers and parameters of the request, and access to the body is provided through the body methods.

The following example extracts the request body into a String:

Java
String string = request.body (String.class);
Kotlin
val string = request.body<String>()

The following example retrieves the body into a List<Person>, where the Person objects are decoded from the serialized form, such as JSON or XML format:

Java
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
Kotlin
val people = request.body<Person>()

The following example shows how to access the parameters:

Java
MultiValueMap<String, String> params = request.params();
Kotlin
val map = request.params()

ServerResponse

ServerResponse provides access to the HTTP response, and since it is immutable, you can use the build method to its creation. You can use the build tool to configure the response status, add response headers, or pass the response body. The following example generates a 200 (OK) response with JSON formatted content:

Java

Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON ).body(person);
Kotlin

val person: Person = ...
ServerResponse.ok( ).contentType(MediaType.APPLICATION_JSON).body(person)

The following example shows how to create a 201 (CREATED) response with the header Location and without body:

Java

URI location = ...
ServerResponse.created(location).build();
Kotlin

val location: URI = ...
ServerResponse.created(location).build()

You can also use an asynchronous result as a body in the form of CompletableFuture, Publisher or any other type supported by ReactiveAdapterRegistry. For example:

Java

Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
Kotlin

val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)

If not only the body, but also the status or headers are based on an asynchronous type, you can use the static async method on ServerResponse, which takes CompletableFuture<ServerResponse> , Publisher<ServerResponse>, or any other asynchronous type supported by ReactiveAdapterRegistry. For example:

Java

Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
    .map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);

Events sent by the server can be sent using the static sse method for ServerResponse. The build facility provided by this method allows you to send strings or other objects in JSON format. For example:

Java

public RouterFunction<ServerResponse> sse() {
    return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
                // Save the sseBuilder object somewhere...
            }));
}
// In some other thread sending the string
sseBuilder.send("Hello world");
// Or an object that will be converted to JSON
Person person = ...
sseBuilder.send(person);
// Set up the event using other methods
sseBuilder.id("42")
        .event("sse event")
        .data(person);
// and at some point it will be ready
sseBuilder.complete();
Kotlin

fun sse( ): RouterFunction<ServerResponse> = router {
    GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
        // Save the sseBuilder object somewhere..
    }
}
// In some other thread sending the string
sseBuilder.send("Hello world")
// Or an object that will be converted to JSON
val person = ...
sseBuilder.send (person)
// Set up the event using other methods
sseBuilder.id("42")
        .event("sse event")
        .data(person)
// and at some point it will be ready
sseBuilder.complete()

Handler classes

We can write a handler function as a lambda expression, as shown in the following example:

Java

HandlerFunction<ServerResponse> helloWorld =
    request -> ServerResponse.ok().body("Hello World");
Kotlin

val helloWorld: (ServerRequest ) -> ServerResponse =
    { ServerResponse.ok().body("Hello World") }

This is convenient, but in the application we need several functions, and several built-in Lambda expressions can get confusing. Therefore, it is useful to group related handler functions into a handler class, which plays the same role as the @Controller annotation in an annotation-based application. For example, the following class represents a reactive Person store:

Java

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
    private final PersonRepository repository;
    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }
    public ServerResponse listPeople(ServerRequest request) { 
        List<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people);
    }
    public ServerResponse createPerson(ServerRequest request) throws Exception { 
        Person person = request.body(Person.class);
        repository.savePerson(person);
        return ok().build();
    }
    public ServerResponse getPerson(ServerRequest request) { 
        int personId = Integer.parseInt(request.pathVariable("id"));
        Person person = repository.getPerson(personId);
        if (person != null) {
            return ok().contentType(APPLICATION_JSON).body(person);
        }
        else {
            return ServerResponse.notFound().build();
        }
    }
}
  1. listPeople is a handler function that returns all Person objects found in the repository in JSON format.
  2. createPerson is a handler function that saves a new Person object code> contained in the body of the request.
  3. getPerson is a handler function that returns a single person identified by the path variable id. We retrieve this Person object from storage and generate a JSON response if it is found. If it is not found, we return a 404 Not Found response.
Kotlin

class PersonHandler(private val repository: PersonRepository) {
    fun listPeople(request : ServerRequest): ServerResponse { 
        val people: List<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).body(people);
    } fun createPerson(request: ServerRequest): ServerResponse { 
        val person = request.body<Person>()
        repository.savePerson(person)
        return ok().build()
    }
    fun getPerson(request: ServerRequest): ServerResponse { 
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
                ?: ServerResponse.notFound().build()
    }
}
  1. listPeople is a handler function that returns all Person objects found in the repository, in JSON format.
  2. createPerson is a handler function that saves a new Person object contained in the request body.
  3. getPerson is a handler function that returns a single person identified by the path variable id. We retrieve this Person object from storage and generate a JSON response if it is found. If it is not found, we return a 404 Not Found response.

Validation

The function endpoint can use Spring's validators to apply validation to the body of the request. For example, here is a custom Spring implementation of Validator for Person:

Java

public class PersonHandler {
    private final Validator validator = new PersonValidator() ; 
    // ...
    public ServerResponse createPerson(ServerRequest request) {
        Person person = request.body(Person.class);
        validate(person); 
        repository.savePerson(person);
        return ok().build();
    }
    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString()); 
        }
    }
}
  1. Create an instance of Validator.
  2. Apply validation.
  3. Throw an exception for the 400 response.
Kotlin

class PersonHandler(private val repository: PersonRepository) {
    private val validator = PersonValidator()  /
    / ...
    fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.body<Person>() validate(person) 
        repository.savePerson(person)
        return ok().build()
    }
    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person")
        validator.validate(person, errors)
        if (errors. hasErrors()) {
            throw ServerWebInputException(errors.toString()) 
        }
    }
}
  1. Create an instance of Validator.
  2. Apply validation.
  3. Throw an exception for response 400.

Handlers can also use the standard bean validation API (JSR-303) by creating and injecting a global validator instance based on a LocalValidatorFactoryBean.

RouterFunction

Router functions are used to route requests to the corresponding HandlerFunction. As a rule, router functions are not written independently, but to create them, a method is used for the auxiliary class RouterFunctions. RouterFunctions.route() (no parameters) provides a convenient builder for creating a router function, while RouterFunctions.route(RequestPredicate, HandlerFunction) provides a direct way to create a router .

As a matter of fact, it is recommended to use the route() builder because it provides convenient shortcuts for common display scenarios without requiring the import of static elements that are difficult to detect. For example, the router function builder offers a GET(String, HandlerFunction) method to create a Map for GET requests; and POST(String, HandlerFunction) - for POST requests.

In addition to mapping based on HTTP methods, the route builder offers a way to introduce additional predicates when mapping to requests. For each HTTP method there is an overload that takes RequestPredicate as a parameter through which additional restrictions can be expressed.

Predicates

You can write your own native RequestPredicate, but a helper class RequestPredicates provides commonly used implementations based on request path, HTTP method, content type, and so on. The following example uses a request predicate to create a constraint based on the Accept header:

Java

RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().body("Hello World")).build();
Kotlin

import org.springframework.web.servlet.function.router
val route = router {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().body("Hello World")
    }
}

You can string multiple request predicates together using:

  • RequestPredicate.and(RequestPredicate) - both must match.

  • RequestPredicate.or(RequestPredicate) – any of them can match.

Many predicates from RequestPredicates are compound. For example, RequestPredicates.GET(String) consists of RequestPredicates.method(HttpMethod) and RequestPredicates.path(String). The example above also uses two request predicates because the builder uses RequestPredicates.GET internally and combines it with the accept predicate.

Routes

Router functions are evaluated in an orderly manner: if the first route does not match, then the second is evaluated, and so on. Therefore, it makes sense to declare more specific routes before generic ones. This is also important when registering router functions as Spring beans, as discussed later. Note that this operating logic differs from the annotation-based programming model, where the "most specific" controller method is selected automatically.

When using the router function builder, all defined routes are assembled into a single RouterFunction, which is returned from build(). There are other ways to combine multiple router functions into one:

  • add(RouterFunction) to the build tool RouterFunctions.route()

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) - shorthand for RouterFunction.and() with nested RouterFunctions.route().

The following example shows a layout of four routes:

Java

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route() .
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) 
    .GET ("/person", accept(APPLICATION_JSON), handler::listPeople) 
    .POST("/person", handler::createPerson) 
    .add(otherRoute) 
    .build();
  1. GET /person/{id} with header Accept that matches the JSON format is routed to PersonHandler.getPerson
  2. GET /person with the header Accept that matches the JSON format is routed to PersonHandler.listPeople
  3. POST /person without additional predicates is mapped to PersonHandler.createPerson, and
  4. otherRoute is a router function that is created elsewhere and added to the constructed route.
Kotlin

import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ... val handler = PersonHandler(repository);
val otherRoute = router { }
val route = router {
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) 
    GET("/person", accept(APPLICATION_JSON), handler::listPeople) 
    POST("/person", handler: :createPerson) 
}.and(otherRoute) 
  1. GET /person/{id} with header Accept that matches the JSON format is routed to PersonHandler.getPerson
  2. GET /person with the header Accept that conforms to JSON format is routed to PersonHandler.listPeople
  3. POST /person without additional predicates is mapped to PersonHandler.createPerson, and
  4. otherRoute is a router function that is created elsewhere and added to the constructed route.

Nested routes

Typically a group of router functions have a common predicate, such as a common path. In the example above, the common predicate would be the path predicate that matches /person, used by the three routes. When using annotations, you can eliminate this duplication by using the @RequestMapping annotation at the type level, which maps to /person. In WebMvc.fn, path predicates can be shared using the path method in the router function builder. For example, the last few lines of the example above could be expanded to look like this, using nested routes:

Java

RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder 
        .GET("/{id}", accept (APPLICATION_JSON), handler::getPerson)
        .GET(accept(APPLICATION_JSON), handler::listPeople)
        .POST(handler::createPerson))
    .build();
  1. Note that the second path parameter is the destination that the router builder accepts.
Kotlin

import org.springframework.web.servlet.function.router
val route = router {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON ), handler::getPerson)
        GET(accept(APPLICATION_JSON), handler::listPeople)
        POST(handler::createPerson)
    }
}

Although the attachment is on path-based is the most common, you can nest a predicate of any type using the nest method in the build tool. The above still contains some duplication in the form of the common Accept-header predicate. We will continue to expand the code using the nest method along with accept:

Java

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST(handler::createPerson))
    .build();
Kotlin

import org.springframework.web.servlet.function.router
val route = router {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST(handler::createPerson)
        }
    }
}

Starting the server

Typically router functions in a configuration based on DispatcherHandler are launched through an MVC configuration, which leverages the Spring configuration to declare the components needed to handle requests. The Java MVC configuration declares the following infrastructure components to support functional endpoints:

  • RouterFunctionMapping: Discovers one or more RouterFunction<?> beans in the Spring configuration, orders them, combines them using RouterFunction.andOther and routes requests to the resulting composite function RouterFunction.

  • HandlerFunctionAdapter: A simple adapter that allows the DispatcherHandler to call the HandlerFunction that was mapped to the request.

The previous components allow functional endpoints to fit into the DispatcherServlet request lifecycle, and also (potentially) work side by side with annotated controllers, if declared. This is also the way to enable functional endpoints in the Spring Boot Web launcher.

The following example shows the WebFlux Java configuration:

Java

@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    } @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }
    // ...
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // configure message conversion...
    }
    @Override
    public void addCorsMappings(CorsRegistry registry ) {
        // configure CORS...
    }
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // configure view resolution for HTML rendering...
    }
}
Kotlin

@Configuration
@EnableMvcclass WebConfig : WebMvcConfigurer {
    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }
    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }
    // ...
    override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
        // configure message conversion...
    }
    override fun addCorsMappings(registry: CorsRegistry) {
        // configure CORS...
    }
    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // configure view resolution for HTML rendering...
    }
}

Filter handler functions

You can filter handler functions using the before, after, or filter methods in the routing function builder. Annotations can achieve similar functionality by using @ControllerAdvice, ServletFilter, or both. The filter will be applied to all routes built by the build tool. This means that filters defined in nested routes are not applied to the "top level" routes. For example, consider the following example:

Java

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler ::listPeople)
            .before(request -> ServerRequest.from(request) 
                .header("X-RequestHeader", "Value" )
                .build()))
            .POST(handler::createPerson)) .
        .after((request, response) -> logResponse(response)) 
        .build();
  1. The before filter adding custom request header, applies only to two GET routes.
  2. The after filter that records the response applies to all routes, including nested ones.
Kotlin

import org.springframework.web.servlet.function.router
val route = router {
    "/person".nest {
        GET("/{id}", handler::getPerson)
        GET(handler::listPeople)
        before { 
            ServerRequest.from(it)
                    .header("X-RequestHeader", "Value"). build()
        }
}
    POST(handler::createPerson)
    after { _, response -> 
        logResponse(response)
    }
}
  1. The before filter, which adds a custom request header, applies only to two GET routes.
  2. The after filter, which logs the response, applies to all routes, including nested ones.

The filter method in the router build tool accepts a HandlerFilterFunction: a function that accepts a ServerRequest and HandlerFunction and returns ServerResponse. The handler function parameter is the next element in the chain. Typically this is the handler to which the request is routed, but it could be a different filter if more than one is applied.

We can now add a simple security filter to our route, assuming we have a SecurityManager, which can determine whether a certain path is valid. The following example shows how to do this:

Java

SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler ::listPeople))
        .POST(handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();
Kotlin

import org.springframework.web.servlet.function.router
val securityManager: SecurityManager = ...
val route = router {
    ("/person" and accept(APPLICATION_JSON)).nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        POST(handler::createPerson)
        filter { request, next ->
            if (securityManager.allowAccessTo(request.path())) {
                next(request)
            }
            else {
                status(UNAUTHORIZED).build();
            }
        }
    }
}

The previous example demonstrated that calling next.handle(ServerRequest) is optional. We allow the function to execute -handler only when access is allowed.

In addition to using the filter method in the router function builder, you can apply a filter to an existing router function via RouterFunction.filter( HandlerFilterFunction).

CORS support for function endpoints is provided using a special CorsFilter.