Skip to content

Latest commit

 

History

History
389 lines (275 loc) · 13.5 KB

webflux-functional.adoc

File metadata and controls

389 lines (275 loc) · 13.5 KB

Functional Endpoints

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

Overview

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.

HandlerFunction

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

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

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();

Handler Classes

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());
	}
}
  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 contained in the request body. Note that PersonRepository.savePerson(Person) returns Mono<Void>: an empty Mono that emits a completion signal when the person has been read from the request and stored. So we use the build(Publisher<Void>) method to send a response when that completion signal is received, i.e. when the Person has been saved.

  3. getPerson is a handler function that returns a single person, identified via the path variable id. We retrieve that Person via the repository, and create a JSON response if it is found. If it is not found, we use switchIfEmpty(Mono<T>) to return a 404 Not Found response.

RouterFunction

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.

Predicates

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.

Routes

You can compose multiple router functions together via:

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) — shortcut for RouterFunction.and() with nested RouterFunctions.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);

Running a server

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 more RouterFunction<?> beans in the Spring configuration, combines them via RouterFunction.andOther, and routes requests to the resulting composed RouterFunction.

  • HandlerFunctionAdapter — simple adapter that allows the DispatcherHandler to invoke a HandlerFunction that was mapped to a request.

  • ServerResponseResultHandler — handles the result from the invocation of a HandlerFunction by invoking the writeTo method of the ServerResponse.

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...
	}
}

HandlerFilterFunction

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 CorsWebFilter.