12.2 Defining functional request handlers
Spring MVC’s annotation-based programming model has been around since Spring 2.5 and is widely popular. It comes with a few downsides, however.
First, any annotation-based programming involves a split in the definition of what the annotation is supposed to do and how it’s supposed to do it. Annotations themselves define the what; the how is defined elsewhere in the framework code. This division complicates the programming model when it comes to any sort of customization or extension because such changes require working in code that’s external to the annotation. Moreover, debugging such code is tricky because you can’t set a breakpoint on an annotation.
Also, as Spring continues to grow in popularity, developers new to Spring from other languages and frameworks may find annotation-based Spring MVC (and WebFlux) quite unlike what they already know. As an alternative to WebFlux, Spring offers a functional programming model for defining reactive APIs.
This new programming model is used more like a library and less like a framework, letting you map requests to handler code without annotations. Writing an API using Spring’s functional programming model involves the following four primary types:
- RequestPredicate —— Declares the kind(s) of requests that will be handled
- RouteFunction —— Declares how a matching request should be routed to the handler code
- ServerRequest —— Represents an HTTP request, including access to header and body information
- ServerResponse —— Represents an HTTP response, including header and body information
As a simple example that pulls all of these types together, consider the following HelloWorld example:
package hello;
import static org.springframework.web
.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web
.reactive.function.server.RouterFunctions.route;
import static org.springframework.web
.reactive.function.server.ServerResponse.ok;
import static reactor.core.publisher.Mono.just;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
@Configuration
public class RouterFunctionConfig {
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"),
request -> ok().body(just("Hello World!"), String.class))
;
}
}The first thing to notice is that you’ve chosen to statically import a few helper classes that you can use to create the aforementioned functional types. You’ve also statically imported Mono to keep the rest of the code easier to read and understand.
In this @Configuration class, you have a single @Bean method of type RouterFunction<?>. As mentioned, a RouterFunction declares mappings between one or more RequestPredicate objects and the functions that will handle the matching request(s).
The route() method from RouterFunctions accepts two parameters: a RequestPredicate and a function to handle matching requests. In this case, the GET() method from RequestPredicates declares a RequestPredicate that matches HTTP `GET`` requests for the /hello path.
As for the handler function, it’s written as a lambda, although it can also be a method reference. Although it isn’t explicitly declared, the handler lambda accepts a ServerRequest as a parameter. It returns a ServerResponse using ok() from ServerResponse and body() from BodyBuilder, which was returned from ok(). This was done to create a response with an HTTP 200 (OK) status code and a body payload that says "Hello World!"
As written, the helloRouterFunction() method declares a RouterFunction that handles only a single kind of request. But if you need to handle a different kind of request, you don’t have to write another @Bean method, although you can. You only need to call andRoute() to declare another RequestPredicate to function mapping. For example, here’s how you might add another handler for GET requests for /bye:
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"),
request -> ok().body(just("Hello World!"), String.class))
.andRoute(GET("/bye"),
request -> ok().body(just("See ya!"), String.class))
;
}Hello World samples are fine for dipping your toes into something new. But let’s amp it up a bit and see how to use Spring’s functional web programming model to handle requests that resemble real-world scenarios.
To demonstrate how the functional programming model might be used in a realworld application, let’s reinvent the functionality of TacoController in the functional style. The following configuration class is a functional analog to TacoController:
package tacos.web.api;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import java.net.URI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import tacos.Taco;
import tacos.data.TacoRepository;
@Configuration
public class RouterFunctionConfig {
@Autowired
private TacoRepository tacoRepo;
@Bean
public RouterFunction<?> routerFunction() {
return route(GET("/api/tacos").
and(queryParam("recent", t->t != null )),
this::recents)
.andRoute(POST("/api/tacos"), this::postTaco);
}
public Mono<ServerResponse> recents(ServerRequest request) {
return ServerResponse.ok()
.body(tacoRepo.findAll().take(12), Taco.class);
}
public Mono<ServerResponse> postTaco(ServerRequest request) {
return request.bodyToMono(Taco.class)
.flatMap(taco -> tacoRepo.save(taco))
.flatMap(savedTaco -> {
return ServerResponse
.created(URI.create(
"http://localhost:8080/api/tacos/" +
savedTaco.getId()))
.body(savedTaco, Taco.class);
});
}
}As you can see, the routerFunction() method declares a RouterFunction<?> bean, like the Hello World example. But it differs in what types of requests are handled and how they’re handled. In this case, the RouterFunction is created to handle GET requests for /api/tacos?recent and POST requests for /api/tacos.
What stands out even more is that the routes are handled by method references. Lambdas are great when the behavior behind a RouterFunction is relatively simple and brief. In many cases, however, it’s better to extract that functionality into a separate method (or even into a separate method in a separate class) to maintain code readability.
For your needs, GET requests for /api/tacos?recent will be handled by the recents() method. It uses the injected TacoRepository to fetch a Flux<Taco>, from which it takes 12 items. It then wraps the Flux<Taco> in a Mono<ServerResponse> so that we can ensure that the response has an HTTP 200 (OK) status by calling ok() on the ServerResponse. It’s important to understand that even though up to 12 tacos are returned, there is only one server response—that’s why it is returned in a Mono and not a Flux. Internally, Spring will still stream the Flux<Taco> to the client as a Flux.
Meanwhile, POST requests for /api/tacos are handled by the postTaco() method, which extracts a Mono<Taco> from the body of the incoming ServerRequest. The postTaco() method then uses a series of flatMap() operations to save that taco to the TacoRepository and create a ServerResponse with an HTTP 201 (CREATED) status code and the saved Taco object in the response body.
The flatMap() operations are used to ensure that at each step in the flow, the result of the mapping is wrapped in a Mono, starting with a Mono<Taco> after the first flatMap() and ultimately ending with a Mono<ServerResponse> that is returned from postTaco().
