Mastering Spring framework 5, Part 2: Spring WebFlux

Build reactive web applications using Spring WebFlux annotations and functional programming techniques

1 2 3 Page 3
Page 3 of 3

You can run your new service by executing the following command from the root directory of your project:

mvn spring-boot:run

Now take out your favorite REST service testing tool, like Poster, or execute the following cURL commands on your command-line. See the responses below each cURL command:


$ curl --header "Content-Type: application/json" --request POST --data '{"title": "Book 1", "author": "Mr Author"}' http://localhost:8080/book
{"id":"5b2ea197c0f951f7354085d7","title":"Book 1","author":"Mr Author"}
$ curl --header "Content-Type: application/json" --request POST --data '{"title": "Book 2", "author": "Other Author"}' http://localhost:8080/book
{"id":"5b2ea1b0c0f951f7354085d8","title":"Book 2","author":"Other Author"}
$ curl http://localhost:8080/books
[{"id":"5b2ea197c0f951f7354085d7","title":"Book 1","author":"Mr Author"},{"id":"5b2ea1b0c0f951f7354085d8","title":"Book 2","author":"Other Author"}]
$ curl http://localhost:8080/book/5b2ea197c0f951f7354085d7
{"id":"5b2ea197c0f951f7354085d7","title":"Book 1","author":"Mr Author"}

Functional reactive services with Spring WebFlux

Spring WebFlux applications can be built using either Spring MVC annotations (which you just saw) or functional programming techniques. Functional programming has many benefits, such as immutable data objects, inherent thread safety, the ability to pass functions to other functions, and the ability to program declaratively rather than imperatively (meaning that you describe the problem you are solving, not the steps that define how to solve the problem).

Pure functions--or functions that provide the same result every time they are given the same input--limit side-effects, which makes testing easier. They also allow for easy parallelization and caching. If you haven't taken the time to start learning functional programming, I encourage you to do so; it will change how you approach and solve problems.

Arjen Poutsma, a member of the Spring WebFlux team, posted a video on YouTube entitled "New in Spring Framework 5.0: Functional Web Framework" that describes the motivation behind building functional web applications and how Spring WebFlux can be used functionally. In short, he argues for more library, less framework, meaning that WebFlux can be used as a library that leaves you in control of your web application. This is an efficient alternative to utilizing the full Spring framework, which is the approach we took in the previous section.

We'll conclude this tutorial by using Spring WebFlux to build another BookHandler application, this time using functional techniques.

Router and handler

Our functional Spring WebFlux application will be based on two main components, a router and a handler. The router is responsible for routing HTTP requests to handler functions. Handler functions are responsible for executing business functionality and building responses.

Listing 8 shows the source code for the BookHandler class.

Listing 8. BookHandler.java


package com.javaworld.webflux.bookservice.web;
import com.javaworld.webflux.bookservice.model.Book;
import com.javaworld.webflux.bookservice.service.BookService;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.BodyInserters.fromPublisher;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
@Component
public class BookHandler {
    private final BookService bookService;
    public BookHandler(BookService bookService) {
        this.bookService = bookService;
    }
    public Mono<ServerResponse> findById(ServerRequest request) {
        String id = request.pathVariable("id");
        return ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(bookService.findById(id), Book.class);
    }
    public Mono<ServerResponse> findAll(ServerRequest request) {
        return ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(bookService.findAll(), Book.class);
    }
    public Mono<ServerResponse> save(ServerRequest request) {
        final Mono<Book> book = request.bodyToMono(Book.class);
        return ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(fromPublisher(book.flatMap(bookService::save), Book.class));
    }
    public Mono<ServerResponse> delete(ServerRequest request) {
        String id = request.pathVariable("id");
        return ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(bookService.deleteById(id), Void.class);
    }
}

The BookHandler is annotated with @Component, a generic annotation that identifies the class as being a Spring-managed bean. Spring will discover this component when it does its component scan and add it to the application context. This is not a controller, but rather a standard Spring bean that will be wired into the BookRouter, defined below.

The functions in the BookHandler return Mono<ServerResponse>. This component is a little different from the BookController built in the previous section, which returned Mono<Book> and Flux<Book>. When building a handler function, you are responsible for building the response that will ultimately be returned to the caller. All methods are required to return a Mono<ServerResponse>, even if the body of the response contains a Flux.

Each method is passed a ServerRequest argument, which provides access to request parameters, such as path variables, query parameters, and, in the case of the save() method, the body of a POST or PUT.

In order to build a response body, we construct it using a BodyBuilder. The ok() method returns a BodyBuilder with an HTTP status code of 200; it is a convenience method for status(HttpStatus.OK). The BodyBuilder interface defines methods for setting the content type, content length, as well as HTTP header values. The body() method sets the contents to be returned to the caller and returns a Mono<ServerResponse>.

Important methods: save() and flatMap()

The method in this class that deserves special attention is the save() method. First, in order to deserialize the body payload to a class instance, we invoke the bodyToMono() method. This method returns a Mono<Book>, which is a publisher that will provide a Book instance asynchronously when it is available. With the Mono<Book> in hand, we construct the response using the ok() method, as usual, and then the body() method is implemented as follows:

fromPublisher(book.flatMap(bookService::save), Book.class)

The fromPublisher() method returns a BodyInserter, which the body() method expects, from a publisher function and the class of the object that will be published, Book.class in this case. The publisher function is passed the following:

book.flatMap(bookService::save)

You're probably already familiar with the Java 8 map() function, which converts every input item in a stream (or in the Mono in this case) into another object. The Java 8 flatMap() function is similar, but it "flattens" the response. For example, if we were constructing a list of objects and the map function returned an embedded list, rather than a list of lists, the flatMap() function would return a single list that contained all of the elements in all embedded lists.

We can read this function as follows: for each Book in the book object, which is a Mono so there will only be one, call the BookService’s save() method, and return the result of BookService::save to the caller (fromPublisher() in this case) as a single Mono object (not a Mono<Mono<Book>>. The flatMap() function takes care of flattening embedded Monos into a single Mono.

Example application code

Listing 9 shows the source code for the BookRouter class.

Listing 9. BookRouter.java


package com.javaworld.webflux.bookservice.web;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
@Configuration
public class BookRouter {
    @Bean
    public RouterFunction<ServerResponse> route(BookHandler handler) {
        return RouterFunctions
                .route(GET("/fbooks").and(accept(MediaType.APPLICATION_JSON)), handler::findAll)
                .andRoute(GET("/fbook/{id}").and(accept(MediaType.APPLICATION_STREAM_JSON)), handler::findById)
                .andRoute(POST("/fbook").and(accept(MediaType.APPLICATION_JSON)), handler::save)
                .andRoute(DELETE("/fbook/{id}").and(accept(MediaType.APPLICATION_JSON)), handler::delete);
    }
}

The BookRouter class is annotated with @Configuration, which is a Spring annotation that identifies a class as a configuration class whose methods create other Spring beans. In this example, the route() method creates a bean of type RouterFunction<ServerResponse>. Router functions are responsible for translating HTTP routes (HTTP verb and URI path) into handler functions. For example, the first route reads: if there is a request of type GET for the URI path /fbooks and a media accept type of APPLICATION_JSON, then invoke the BookHandler’s findAll() method.

The syntax might look a little strange, so let’s take it apart. First, consider the GET() method:

GET("/fbooks").and(accept(MediaType.APPLICATION_JSON)), handler::findAll

The GET() method is statically imported from the RequestPredicates class and returns a RequestPredicate instance. A predicate is a boolean-valued function with a test() method that evaluates the predicate and returns true or false if the predicate’s conditions are met. A RequestPredicate evaluates a ServerRequest to determine whether or not this route should handle the request. So our goal is to define the criteria under which our handler function should be called.

GET() is a convenience method for

method(HttpMethod.GET).and(path(String Pattern))

This means that the RequestPredicate will compare the HTTP verb in the ServerRequest to HttpMethod.GET and the path to the specified URI pattern. We then chain accept(MediaType.APPLICATION_JSON) to the predicate using the and() method, which is a standard Predicate function that evaluates two predicates using AND boolean logic. The accept() method adds a condition to the predicate that verifies the "Accept" HTTP header against the provided media type. In the end, the handler::findAll method will be invoked if the following conditions are true:

  • The HTTP verb is GET
  • The URI path is /fbooks
  • The HTTP "Accept" header is "application/json"

The RouterFunctions::route method returns a RouterFunction that allows you to add additional routes by invoking the addRoute() method. As you can see, we leverage this capability to chain together several different routes: GET with an id request parameter, POST, and DELETE.

The only other magic in the BookRouter::route method is the Spring injection of the BookHandler. The route() method is annotated with @Bean, which means that it returns a Spring-managed bean. When Spring invokes this method it will see that it requires a BookHandler argument. Having already discovered the BookHandler (annotated with @Component), and having added it to the application context, it will pass the Spring-managed BookHandler to the route() method.

In summary, the BookRouter::route creates a RouterFunction, which is composed of several router functions that define the conditions for which specific handler functions should be invoked.

Run and test the application

You can test this code by starting the Spring Boot application with the following command:

mvn spring-boot:run

Now you have two sets of end-points: /book uses the BookController and /fbook uses the functional BookRouter and BookHandler. The following are sample cURL commands to invoke these services:


$ curl --header "Content-Type: application/json" --request POST --data '{"title": "Book 1", "author": "Author"}' http://localhost:8080/fbook
{"id":"5b394748aaac8a7c67f94367","title":"Book 1","author":"Author"}
$ curl --header "Content-Type: application/json" --request POST --data '{"title": "Book 2", "author": "Author"}' http://localhost:8080/fbook
{"id":"5b39474daaac8a7c67f94368","title":"Book 2","author":"Author"}
$ curl http://localhost:8080/fbooks
[{"id":"5b394748aaac8a7c67f94367","title":"Book 1","author":"Author"},
 {"id":"5b39474daaac8a7c67f94368","title":"Book 2","author":"Author"}]
$ curl http://localhost:8080/fbook/5b39474daaac8a7c67f94368
{"id":"5b39474daaac8a7c67f94368","title":"Book 2","author":"Author"}
$ curl --header "Content-Type: application/json" --request DELETE http://localhost:8080/fbook/5b39474daaac8a7c67f94368
$ curl http://localhost:8080/fbooks
[{"id":"5b394748aaac8a7c67f94367","title":"Book 1","author":"Author"}]

Conclusion

Spring WebFlux is Spring’s reactive web framework that uses the Reactor library to asynchronously manage web requests. I started this article by reviewing reactive systems and the Reactive Streaming API, and described the problems they're designed to solve. I then showed you two ways to create a Spring WebFlux application: the traditional annotation-based approach and the functional approach. Spring WebFlux was introduced in Spring framework 5, and is new to the Spring ecosystem. It will undoubtedly continue to evolve. Still, it is already a powerful framework and library for building very scalable and reactive web applications.

1 2 3 Page 3
Page 3 of 3