Spring WebFlux includes a lightweight, functional programming model in which functions are used to route and handle requests and contracts are designed for immutability. It is an alternative to the annotated-based programming model but otherwise running on the same web-reactive.adoc foundation
An HTTP request is handled with a HandlerFunction
that takes ServerRequest
and
returns Mono<ServerResponse>
, both of which are immutable contracts that offer JDK-8
friendly access to the HTTP request and response. HandlerFunction
is the equivalent of
an @RequestMapping
method in the annotation-based programming model.
Requests are routed to a HandlerFunction
with a RouterFunction
that takes
ServerRequest
and returns Mono<HandlerFunction>
. When a request is matched to a
particular route, the HandlerFunction
mapped to the route is used. RouterFunction
is
the equivalent of an @RequestMapping
annotation.
RouterFunctions.route(RequestPredicate, HandlerFunction)
provides a router function
default implementation that can be used with a number of built-in request predicates.
For example:
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> route =
route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson)
.andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople)
.andRoute(POST("/person"), handler::createPerson);
public class PersonHandler {
// ...
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
One way to run a RouterFunction
is to turn it into an HttpHandler
and install it
through one of the built-in server adapters:
-
RouterFunctions.toHttpHandler(RouterFunction)
-
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
Most applications will run through the WebFlux Java config, see Running a server.
ServerRequest
and ServerResponse
are immutable interfaces that offer JDK-8 friendly
access to the HTTP request and response with
Reactive Streams back pressure against the request
and response body stream. The request body is represented with a Reactor Flux
or Mono
.
The response body is represented with any Reactive Streams Publisher
, including Flux
and Mono
. For more on that see
Reactive Libraries.
ServerRequest
provides access to the HTTP method, URI, headers, and query parameters
while access to the body is provided through the body
methods.
To extract the request body to a Mono<String>
:
Mono<String> string = request.bodyToMono(String.class);
To extract the body to a Flux<Person>
, where Person
objects are decoded from some
serialized form, such as JSON or XML:
Flux<Person> people = request.bodyToFlux(Person.class);
The above are shortcuts that use the more general ServerRequest.body(BodyExtractor)
which accepts the BodyExtractor
functional, strategy interface. The utility class
BodyExtractors
provides access to a number of instances. For example, the above can
also be written as follows:
Mono<String> string = request.body(BodyExtractors.toMono(String.class)); Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
To access form data:
Mono<MultiValueMap<String, String> map = request.body(BodyExtractors.toFormData());
To access multipart data as a map:
Mono<MultiValueMap<String, Part> map = request.body(BodyExtractors.toMultipartData());
To access multiparts, one at a time, in streaming fashion:
Flux<Part> parts = request.body(BodyExtractos.toParts());
ServerResponse
provides access to the HTTP response and since it is immutable, you use
a build to create it. The builder can be used to set the response status, to add response
headers, or to provide a body. Below is an example with a 200 (OK) response with JSON
content:
Mono<Person> person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
This is how to build a 201 (CREATED) response with "Location"
header, and no body:
URI location = ... ServerResponse.created(location).build();
We can write a handler function as a lambda. For example:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body(fromObject("Hello World"));
That is convenient but in an application we need multiple functions and useful to group
related handler functions together into a handler (like an @Controller
). For example,
here is a class that exposes a reactive Person
repository:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.ServerResponse.ok;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
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).body(fromObject(person)))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
-
listPeople
is a handler function that returns allPerson
objects found in the repository as JSON. -
createPerson
is a handler function that stores a newPerson
contained in the request body. Note thatPersonRepository.savePerson(Person)
returnsMono<Void>
: an empty Mono that emits a completion signal when the person has been read from the request and stored. So we use thebuild(Publisher<Void>)
method to send a response when that completion signal is received, i.e. when thePerson
has been saved. -
getPerson
is a handler function that returns a single person, identified via the path variableid
. We retrieve thatPerson
via the repository, and create a JSON response if it is found. If it is not found, we useswitchIfEmpty(Mono<T>)
to return a 404 Not Found response.
RouterFunction
is used to route requests to a HandlerFunction
. Typically, you do not
write router functions yourself, but rather use
RouterFunctions.route(RequestPredicate, HandlerFunction)
. If the predicate applies, the
request is routed to the given HandlerFunction
, or otherwise no routing is performed,
and that would translate to a 404 (Not Found) response.
You can write your own RequestPredicate
, but the RequestPredicates
utility class
offers commonly implementations, based on the request path, HTTP method, content-type,
and so on. For example:
RouterFunction<ServerResponse> route =
RouterFunctions.route(RequestPredicates.path("/hello-world"),
request -> Response.ok().body(fromObject("Hello World")));
You can compose multiple request predicates together via:
-
RequestPredicate.and(RequestPredicate)
— both must match. -
RequestPredicate.or(RequestPredicate)
— either may match.
Many of the predicates from RequestPredicates
are composed. For example
RequestPredicates.GET(String)
is composed from RequestPredicates.method(HttpMethod)
and RequestPredicates.path(String)
.
You can compose multiple router functions into one, such that they’re evaluated in order, and if the first route doesn’t match, the second is evaluated. You can declare more specific routes before more general ones.
You can compose multiple router functions together via:
-
RouterFunction.and(RouterFunction)
-
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
— shortcut forRouterFunction.and()
with nestedRouterFunctions.route()
.
Using composed routes and predicates, we can then declare the following routes, referring
to methods in the PersonHandler
, shown in [webflux-fn-handler-class], through
method-references:
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> personRoute =
route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson)
.andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople)
.andRoute(POST("/person"), handler::createPerson);
How do you run a router function in an HTTP server? A simple option is to convert a router
function to an HttpHandler
using one of the following:
-
RouterFunctions.toHttpHandler(RouterFunction)
-
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
The returned HttpHandler
can then be used with a number of servers adapters by following
HttpHandler for server-specific instructions.
A more advanced option is to run with a DispatcherHandler-based setup through the web-reactive.adoc which uses Spring configuration to declare the components quired to process requests. The WebFlux Java config declares the following infrastructure components to support functional endpoints:
-
RouterFunctionMapping
— detects one or moreRouterFunction<?>
beans in the Spring configuration, combines them viaRouterFunction.andOther
, and routes requests to the resulting composedRouterFunction
. -
HandlerFunctionAdapter
— simple adapter that allows theDispatcherHandler
to invoke aHandlerFunction
that was mapped to a request. -
ServerResponseResultHandler
— handles the result from the invocation of aHandlerFunction
by invoking thewriteTo
method of theServerResponse
.
The above components allow functional endpoints to fit within the DispatcherHandler
request
processing lifecycle, and also potentially run side by side with annotated controllers, if
any are declared. It is also how functional endpoints are enabled the Spring Boot WebFlux
starter.
Below is example WebFlux Java config (see DispatcherHandler for how to run):
@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
default void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
Routes mapped by a router function can be filtered by calling
RouterFunction.filter(HandlerFilterFunction)
, where HandlerFilterFunction
is essentially 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 typically the
HandlerFunction
that is routed to, but can also be another FilterFunction
if multiple filters
are applied.
With annotations, similar functionality can be achieved using @ControllerAdvice
and/or a ServletFilter
.
Let’s add a simple security filter to our route, assuming that we have a SecurityManager
that
can determine whether a particular path is allowed:
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = ...
RouterFunction<ServerResponse> filteredRoute =
route.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
});
You can see in this example that invoking the next.handle(ServerRequest)
is optional: we only
allow the handler function to be executed when access is allowed.
Note
|
CORS support for functional endpoints is provided via a dedicated |