Spring Web Reactive | 1. Spring WebFlux | 1.5. Functional Endpoints

Web MVC

Spring WebFlux includes WebFlux.fn. WebFlux.fn uses functions to route and handle requests and is designed around immutability. It is an alternative to the annotation-based programming model and otherwise runs on the same reactive core.

1.5.1. Overview

Web MVC

In WebFlux.fn, an HTTP request is handled with a HandlerFunction. It is a function that takes a ServerRequest and returns a deferred, asynchronous ServerResponse, that is, Mono<ServerResponse>. Both request and response objects are immutable objects that provide JDK 8-friendly access to HTTP requests and responses. A HandlerFunction is equivalent to the body of an @RequestMapping method in the annotation-based programming model.

Incoming requests are routed to handler functions with a RouterFunction. It is a function that takes a ServerRequest and returns a deferred HandlerFunction, that is, Mono<HandlerFunction>. If the router function matches, a handler function is returned. Otherwise, an empty Mono is returned. RouterFunction is equivalent to the @RequestMapping annotation, with the important difference that router functions provide behavior as well as data.

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 {  // (1)
    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 the router with the Coroutines router DSL. A reactive alternative is also available through router { }.

One way to run a RouterFunction is to convert it to an HttpHandler and install it through one of the built-in server adapters.

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

Most applications can run through WebFlux Java configuration. See Running a Server.

1.5.2. HandlerFunction

Web MVC

ServerRequest and ServerResponse are immutable interfaces that provide JDK 8-friendly access to HTTP requests and responses. Both request and response provide Reactive Streams back pressure for body streams. Request content is represented as Reactor Flux or Mono. The response body is represented as a Reactive Streams Publisher, including Flux and Mono. For details, see Reactive Libraries.

ServerRequest

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

The following example extracts the request body as Mono<String>.

Java

Mono<String> string = request.bodyToMono(String.class);

Kotlin

val string = request.awaitBody<String>()

The following example extracts the body as Flux<Person>, or Kotlin Flow<Person>. Person objects are decoded from a serialized format such as JSON or XML.

Java

Flux<Person> people = request.bodyToFlux(Person.class);

Kotlin

val people = request.bodyToFlow<Person>()

The preceding examples are convenient shortcuts for the more general ServerRequest.body(BodyExtractor), which accepts the BodyExtractor functional strategy interface. The utility class BodyExtractors provides access to many instances. For example, the preceding examples can 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 parts one at a time in streaming fashion.

Java

Flux<Part> parts = request.body(BodyExtractors.toParts());

Kotlin

val parts = request.body(BodyExtractors.toParts()).asFlow()

ServerResponse

ServerResponse provides access to the HTTP response. Since it is immutable, you can create an HTTP response with the build method. You can use the builder to set the response status, add response headers, or provide a body. The following example creates a 200 (OK) response with JSON 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 in use, you can pass hint parameters to specify how the body is serialized or deserialized. For example, you can specify a Jackson JSON 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, 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 applications need multiple functions, and many inline lambdas can become messy. It is convenient to group related handler functions into a handler class that plays the same role as a Controller in an annotation-based application. For example, the following class handles operations related to a reactive Person repository.

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) {   // (1) 
        Flux<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) {   // (2) 
        Mono<Person> person = request.bodyToMono(Person.class);
        return ok().build(repository.savePerson(person));
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) {   // (3) 
        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 objects found in the repository as JSON.
  • (2) createPerson is a handler function that stores a new Person included in the request body. Note that PersonRepository.savePerson(Person) returns Mono<Void>. The empty Mono emits a completion signal when the person read from the request has been stored. Therefore, the build(Publisher<Void>) method is used to send the response when the completion signal is received, that is, when the Person is saved.
  • (3) getPerson is a handler function that returns one person identified by the id path variable. It searches the Person repository and creates a JSON response if found. If not found, it uses switchIfEmpty(Mono<T>) to return a 404 Not Found response.

Kotlin

class PersonHandler(private val repository: PersonRepository) {

    suspend fun listPeople(request: ServerRequest): ServerResponse {   // (1)	
        val people: Flow<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse {   // (2)	
        val person = request.awaitBody<Person>()
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse {   // (3)	
        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 repository as JSON.
  • (2) createPerson is a handler function that stores a new Person included in the request body. Note that PersonRepository.savePerson(Person) is a suspending function with no return value.
  • (3) getPerson is a handler function that returns one person identified by the id path variable. It searches the Person repository and creates a JSON response if found. Otherwise, it returns a 404 Not Found response.

Validation

Functional endpoints can use Spring validation features to apply validation to request bodies. The following example uses a custom Spring Validator implementation for Person.

Java

public class PersonHandler {

    private final Validator validator = new PersonValidator();   // (1)

    // ...

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);   // (2)
        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());   // (3)
        }
    }
}

Kotlin

class PersonHandler(private val repository: PersonRepository) {

    private val validator = PersonValidator()   // (1)

    // ...

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.awaitBody<Person>()
        validate(person)   // (2)
        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())   // (3)
        }
    }
}
  • (1) Create a Validator instance.
  • (2) Apply validation.
  • (3) Raise an exception for a 400 response.

You can also use the standard Bean Validation API (JSR-303) by creating and injecting a global Validator instance based on LocalValidatorFactoryBean into the handler. See Spring Validation.

1.5.3. RouterFunction

Web MVC

Router functions are used to route requests to the corresponding HandlerFunction. Generally, router functions are not created directly but with methods on the RouterFunctions utility class. RouterFunctions.route() with no parameters provides a fluent builder for creating router functions, while RouterFunctions.route(RequestPredicate, HandlerFunction) provides a direct way to create a router.

In general, it is recommended to use the route() builder because it does not require static imports that are hard to discover and provides shortcuts useful for common mapping scenarios. For example, the router function builder provides GET(String, HandlerFunction) for creating a GET request mapping and POST(String, HandlerFunction) for POST.

In addition to HTTP method-based mappings, the route builder provides a way to introduce additional predicates when mapping requests. HTTP methods have overloaded variants that take a RequestPredicate parameter so you can express additional constraints.

Predicates

You can create your own RequestPredicate, but the RequestPredicates utility class provides commonly used implementations based on request path, HTTP method, content type, and more. 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 compose multiple request predicates with the following.

  • RequestPredicate.and(RequestPredicate) - both must match.
  • RequestPredicate.or(RequestPredicate) - either one can match.

Many RequestPredicates predicates are composed. For example, RequestPredicates.GET(String) is composed from RequestPredicates.method(HttpMethod) and RequestPredicates.path(String). The preceding example also uses two request predicates because the builder uses RequestPredicates.GET internally and composes it with the accept predicate.

Routes

Router functions are evaluated in order. If the first route does not match, the second route is evaluated. It makes sense to declare more specific routes before more general routes. This is also important when registering router functions as Spring beans, as explained later. Note that this behavior 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 composed into one RouterFunction returned from build(). There are also other ways to compose multiple router functions together.

  • add(RouterFunction) on the RouterFunctions.route() builder
  • RouterFunction.and(RouterFunction)
  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) - a concise form of nested RouterFunctions.route() and RouterFunction.and().

The following example shows a composition 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)   (1)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)   (2)
    .POST("/person", handler::createPerson)   (3)
    .add(otherRoute)   (4)
    .build();

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)   // (1)
    GET("/person", accept(APPLICATION_JSON), handler::listPeople)   // (2)
    POST("/person", handler::createPerson)   // (3)
}.and(otherRoute)   // (4)
  • (1) GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson.
  • (2) GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople.
  • (3) POST /person without additional predicates is mapped to PersonHandler.createPerson.
  • (4) otherRoute is a router function created elsewhere and added to the built routes.

Nested Routes

It is common for a group of router functions to share a predicate, such as a shared path. In the preceding example, the shared predicate is the path predicate matching /person, used by three routes. With annotations, this duplication can be removed by using a type-level @RequestMapping annotation mapped to /person. In WebFlux.fn, routing predicates can be shared through the path method of the router function builder. For example, the last few lines of the preceding example can be improved with nested paths as follows.

Java

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

Kotlin

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

Path-based nesting is the most common, but you can nest on any kind of predicate by using the nest method on Builder. The preceding example still contains duplication in the form of a shared Accept header predicate. You can improve it further by using nest together 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("/person", handler::createPerson))
    .build();

Kotlin

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

1.5.4. Running a Server

Web MVC

How do you run a router function on an HTTP server? A simple option is to convert the router function to an HttpHandler by using one of the following.

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

Then you can use the returned HttpHandler with various server adapters by running the HttpHandler according to server-specific procedures.

A common option, also used by Spring Boot, is to run through a DispatcherHandler-based setup through WebFlux configuration. WebFlux configuration declares the components required to process requests by using Spring configuration. WebFlux Java configuration declares the following infrastructure components to support functional endpoints.

  • RouterFunctionMapping: Detects one or more RouterFunction<?> beans in Spring configuration, combines them through orderable RouterFunction.andOther, and routes requests to the resulting RouterFunction.
  • HandlerFunctionAdapter: A simple adapter that lets DispatcherHandler invoke the HandlerFunction mapped to the request.
  • ServerResponseResultHandler: Handles the result of a HandlerFunction invocation by calling the writeTo method of ServerResponse.

The preceding components make functional endpoints fit into the DispatcherHandler request processing lifecycle and potentially run alongside annotated controllers, if any. This is also how the Spring Boot WebFlux starter uses functional endpoints.

The following example shows WebFlux Java configuration. For how to run it, see DispatcherHandler.

Java

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }

    // ...

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // 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
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }

    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }

    // ...

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // configure message conversion...
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        // configure CORS...
    }

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // configure view resolution for HTML rendering...
    }
}

1.5.5. Filtering Handler Functions

Web MVC

You can filter handler functions by using the before, after, or filter methods on the routing function builder. With annotations, similar functionality is provided with @ControllerAdvice, ServletFilter, or both. Filters apply to all routes created by the builder. This means that filters defined in nested routes do not apply to top-level routes. 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)   // (1)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST("/person", handler::createPerson))
    .after((request, response) -> logResponse(response))   // (2)
    .build();

Kotlin

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

The filter method on the router builder takes a HandlerFilterFunction as an argument. This is a function that takes a ServerRequest and HandlerFunction and returns a ServerResponse. The handler function parameter represents the next element in the chain. This is usually the routed handler, but it can be another filter if multiple filters are applied.

Assume there is a SecurityManager that can determine whether a specific path is allowed. You can add a simple security filter to the routes. The following example shows how.

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("/person", 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("/person", handler::createPerson)
            filter { request, next ->
                if (securityManager.allowAccessTo(request.path())) {
                    next(request)
                }
                else {
                    status(UNAUTHORIZED).build();
                }
            }
        }
    }

The preceding example shows that calling next.handle(ServerRequest) is optional. The handler function is executed only when access is allowed.

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

CORS support for functional endpoints is provided through the dedicated CorsWebFilter.