Spring WebFlux contains WebFlux.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 works on the same basis from Reactive Core.

Brief Description

In WebFlux.fn, an HTTP request is processed using HandlerFunction: A function that accepts a ServerRequest and returns a deferred ServerResponse (i.e. Mono<ServerResponse>). Both the request and response objects have immutable contracts that provide JDK 8-compliant access to the HTTP request and response. HandlerFunction is the equivalent of a method body with the @RequestMapping in the annotation-based programming model.

Incoming requests are routed to a handler function using RouterFunction: a function that accepts a ServerRequest and returns a deferred HandlerFunction (i.e. Mono<HandlerFunction>). If the router function matches, a handler function is returned; otherwise, an empty Mono function is returned. RouterFunction is the equivalent of the @RequestMapping annotation, but with the significant difference that router functions provide 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.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.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 Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
        
Kotlin

val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = coRouter { 
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
suspend fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
suspend fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
suspend fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
        
  1. Create router using Coroutines router DSL, a Reactive alternative is also available via router { }.

One way to run a RouterFunction is to turn it into a HttpHandler and install via one of the built-in server adapters:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

Most applications can be launched through the WebFlux Java configuration.

HandlerFunction

ServerRequest and ServerResponse are immutable interfaces that provide JDK 8 compliant access to HTTP request and response. Both request and response provide callback Reactive Streams for body streams. The request body is represented using Flux or Mono from Reactor. The response body is presented using any Publisher from Reactive Streams, including Flux and Mono.

ServerRequest

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

In The following example extracts the request body into a Mono<String>:

Java
Mono<String> string = request.bodyToMono(String.class);
Kotlin
val string = request.awaitBody<String>()

In the following example, the body is retrieved in Flux<Person> (or Flow<Person> in Kotlin), where Person objects are decoded from some serialized form, such as JSON or XML:

Java
Flux<Person> people = request.bodyToFlux(Person.class);
Kotlin
val people = request.bodyToFlow<Person>()

The previous examples are shortcuts using the more generic ServerRequest.body (BodyExtractor), which accepts the BodyExtractor functional strategy interface. The BodyExtractors helper class provides access to multiple instances. For example, the previous examples could be written as follows:

Java

Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
Kotlin

val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()

The following example shows how to access form data:

Java
Mono<MultiValueMap<String, String>> map = request.formData();
Kotlin
val  map = request.awaitFormData()

The following example shows how to access multipart data as a Map:

Java
Mono<MultiValueMap<String, Part>> map = request.multipartData();
Kotlin
val  map = request.awaitMultipartData()

The following example shows how to access multiple components, one at a time, in streaming mode :

Java
Flux<Part> parts = request.body(BodyExtractors.toParts());
Kotlin
val parts = request.body(BodyExtractors.toParts()).asFlow()

ServerResponse

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

Java

Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
Kotlin

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

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

Java

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

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

Depending on the codec used, you can pass hint parameters to customize how the body is serialized or deserialized. For example, to define a Jackson JSON-based view:

Java
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
Kotlin
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java) .body(...)

Handler classes

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

Java

HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
        
Kotlin
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }

This is convenient , but we need multiple functions in an application, and multiple inline lambda expressions can lead to confusion. 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 Mono<ServerResponse> listPeople(ServerRequest request) { 
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) { 
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) { 
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
    .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
    .switchIfEmpty(ServerResponse.notFound().build());
}
}
  1. listPeople is a handler function that returns all Person found in the store, in JSON format.
  2. createPerson is a handler function that saves a new Person object contained in the body of the request. Note that PersonRepository.savePerson(Person) returns Mono<Void>: an empty Mono that generates a termination signal if Person has been read from request and saved. Therefore, the build(Publisher<Void>) method is used to send a response when a completion signal is received (that is, when Person is saved).
  3. getPerson is a handler function that returns a single Person object 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, switchIfEmpty(Mono<T>) is used to return a 404 Not Found response.
Kotlin

class PersonHandler(private val repository: PersonRepository) {
suspend fun listPeople(request: ServerRequest): ServerResponse { 
val people: Flow<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
}
suspend fun createPerson(request: ServerRequest): ServerResponse { 
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ok().buildAndAwait()
}
suspend fun getPerson(request: ServerRequest): ServerResponse { 
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
        ?: ServerResponse.notFound().buildAndAwait()
}
}
  1. listPeople is a handler function that returns all Person objects found in the store in JSON format.
  2. createPerson is a handler function that stores a new Person object contained in the request body. Note that PersonRepository.savePerson(Person) returns Mono<Void>: an empty Mono that generates a termination signal if Person has been read from request and saved. Therefore, the build(Publisher<Void>) method is used to send a response when a completion signal is received (that is, when Person is saved).
  3. getPerson is a handler function that returns a single Person object 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, switchIfEmpty(Mono<T>) is used to return a 404 Not Found response.

Validation

A function endpoint can use Spring validators to apply validation to the request body. For example, given a custom implementation of Validator from Spring for the Person object:

Java

public class PersonHandler {
private final Validator validator = new PersonValidator(); 
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); 
return ok().build(repository.savePerson(person));
}
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 when the response is 400.
Kotlin

class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() 
// ...
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
validate(person) 
repository.savePerson(person)
return ok().buildAndAwait()
}
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. Generate an exception when the response is 400.

Handlers can also use standard bean validation API (JSR-303), 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().bodyValue("Hello World")).build();
Kotlin

val route = coRouter {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().bodyValueAndAwait("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 build multiple router functions into one:

  • add(RouterFunction) on the RouterFunctions.route() builder

  • 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.reactive.function.server.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 an Accept header that matches JSON is routed to PersonHandler.getPerson
  2. GET /person with a Accept header that conforms to JSON format is sent 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
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter {  }
val route = coRouter {
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 an Accept header that matches JSON is routed to PersonHandler.getPerson
  2. GET /person with a Accept header that matches the format JSON, sent to PersonHandler.listPeople
  3. POST /person without additional predicates is mapped to PersonHandler.createPerson, and
  4. otherRoute is a router function that is created in another place 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 WebFlux.fn, path predicates can be shared using the path method in the router function constructor. 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 parameter is path is the recipient that the router builder receives.
Kotlin

val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}

Although path-based nesting 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

val route = coRouter {
"/person".nest {
accept(APPLICATION_JSON).nest {
    GET("/{id}", handler::getPerson)
    GET(handler::listPeople)
    POST(handler::createPerson)
}
}
}

Starting the server

How to start router function in HTTP server? A simple option is to convert the router function to HttpHandler using one of the following:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

You can then use the returned HttpHandler with different server adapters, following the instructions regarding HttpHandler for a specific server.

A more typical option, also used in Spring Boot, is to work with DispatcherHandler-based configuration via WebFlux configuration, which uses Spring configuration to declare the components needed to handle requests . The WebFlux Java configuration declares the following infrastructure components to support functional endpoints:

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

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

  • ServerResponseResultHandler: Processes the result of a call to HandlerFunction by calling the writeTo method on ServerResponse.

The previous components allow functional endpoints points to “fit” into the request processing lifecycle of DispatcherServlet, and also (potentially) work with annotated controllers, if declared, simultaneously. This is also a way to activate functional endpoints using a starter from Spring Boot for WebFlux.

The following example shows the WebFlux Java configuration:

Java

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// configure converting messages...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure the view resolution for HTML visualization...
}
}
Kotlin

@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// configure converting messages...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure the view resolution for HTML visualization...
}
}

Filtering Handler Functions

You can filter handler functions using the before, after or filter in the forty routing functions tool. 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 "higher level" routes. For example, consider the following:

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. before filter that adds a custom title request, applies only to two routes of the GET method.
  2. The after filter, which records the response, applies to all routes, including nested ones.
Kotlin

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 the two GET method routes.
  2. The after filter, which logs the response, applies to all routes, including nested.

The filter method in the router constructor 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 another filter if there is more than one applied.

We can now add a simple security filter to our route, assuming we have a SecurityManager , which can determine whether a particular 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
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 shows that calling next.handle(ServerRequest) is optional. We allow the handler function to run only when access is allowed.

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

CORS support for functional endpoints is provided through a special CorsWebFilter.