Spring Web Reactive | 1. Spring WebFlux | 1.5. Functional Endpoints
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
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
Coroutinesrouter DSL. A reactive alternative is also available throughrouter { }.
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
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)
listPeopleis a handler function that returns allPersonobjects found in the repository as JSON. - (2)
createPersonis a handler function that stores a newPersonincluded in the request body. Note thatPersonRepository.savePerson(Person)returnsMono<Void>. The empty Mono emits a completion signal when the person read from the request has been stored. Therefore, thebuild(Publisher<Void>)method is used to send the response when the completion signal is received, that is, when thePersonis saved. - (3)
getPersonis a handler function that returns one person identified by theidpath variable. It searches thePersonrepository and creates a JSON response if found. If not found, it usesswitchIfEmpty(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)
listPeopleis a handler function that returns allPersonobjects found in the repository as JSON. - (2)
createPersonis a handler function that stores a newPersonincluded in the request body. Note thatPersonRepository.savePerson(Person)is a suspending function with no return value. - (3)
getPersonis a handler function that returns one person identified by theidpath variable. It searches thePersonrepository 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
Validatorinstance. - (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
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 theRouterFunctions.route()builderRouterFunction.and(RouterFunction)RouterFunction.andRoute(RequestPredicate, HandlerFunction)- a concise form of nestedRouterFunctions.route()andRouterFunction.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 anAcceptheader that matches JSON is routed toPersonHandler.getPerson. - (2)
GET /personwith anAcceptheader that matches JSON is routed toPersonHandler.listPeople. - (3)
POST /personwithout additional predicates is mapped toPersonHandler.createPerson. - (4)
otherRouteis 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
pathis 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
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 moreRouterFunction<?>beans in Spring configuration, combines them through orderableRouterFunction.andOther, and routes requests to the resultingRouterFunction.HandlerFunctionAdapter: A simple adapter that letsDispatcherHandlerinvoke theHandlerFunctionmapped to the request.ServerResponseResultHandler: Handles the result of aHandlerFunctioninvocation by calling thewriteTomethod ofServerResponse.
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
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
beforefilter that adds a custom request header applies only to the two GET routes. - (2) The
afterfilter 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.