Mastering Spring framework 5, Part 2: Spring WebFlux

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

1 2 3 Page 2
Page 2 of 3

Listing 1. Maven pom.xml for the Spring WebFlux example application


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.javaworld.webflux</groupId>
    <artifactId>bookservice</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>bookservice</name>
    <description>Demo project for Spring Boot</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>de.flapdoodle.embed</groupId>
            <artifactId>de.flapdoodle.embed.mongo</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Application dependencies

The <parent> node references version 2.0.3.RELEASE of the spring-boot-starter-parent POM file. The parent POM file ensures that all dependency versions are compatible with this version of Spring Boot. These dependencies include:

  • spring-boot-starter-webflux: Packs everything you need to run a WebFlux application, including spring-web (which gives you all of the Spring MVC capabilities) and Netty, which will be our reactive web server, plus a lot more.
  • spring-boot-starter-data-mongodb-reactive: Includes the MongoDB drivers, reactive support for MongoDB, and Spring Data to make writing persistence code easier.
  • de.flapdoodle.embed.mongo: Includes an embedded MongoDB instance. By default this dependency will be scoped to "test" so that you can write tests that run against an embedded MongoDB instance and then connect to a standalone MongoDB instance in production. For the purpose of this example I removed the test scoping so that we can run our book service against this embedded MongoDB instance.
  • lombok: Adds annotation niceties for generating getters and setters, constructors, and so forth to the application's model classes.
  • spring-boot-starter-test: Includes Spring testing utilities as well as JUnit and Mockito.
  • reactor-test: Includes testing utilities for testing the Reactor engine, which is used by Spring WebFlux for reactive functionality.

The Spring Boot starter class

Listing 2 shows the BookserviceApplication.java file.

Listing 2. BookserviceApplication.java


package com.javaworld.webflux.bookservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BookserviceApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookserviceApplication.class, args);
    }
}

The BookserviceApplication is annotated with the @SpringBootApplication annotation. @SpringBootApplication is a convenience annotation that encompasses the following annotations:

  • @EnabledAutoConfiguration enables auto-configuration of the Spring application context, attempting to guess and configure beans that you are likely to need. Auto-configuration classes are usually applied based on your CLASSPATH and the beans you have defined. For example, when you include the embedded MongoDB dependency in your CLASSPATH, Spring will automatically create an instance in memory and wire it into the application context.
  • @SpringBootConfiguration identifies this class as containing the Spring Boot configuration.
  • @ComponentScan directs Spring to scan the CLASSPATH, in the current package and all sub-packages, for Spring components. In short, this allows you to create a web package and add a @Controller, which Spring will find and make available to the application.

The BookserviceApplication itself defines a main() method that delegates to the SpringApplication.run() method, which starts the application.

Using Spring WebFlux with annotations

In order to build our book service we need to define the following classes and interfaces:

  • Book: A model class representing a book in our service.
  • BookRepository: A Spring Data MongoDB interface telling Spring Data to generate persistence code for books to and from MongoDB.
  • BookService and BookServiceImpl: The "business" service used to interact with the BookRepository to persist books to and from MongoDB. In this example, a service is not necessary and we could place calls to the BookRepository directly in our controller. When building Spring applications it is recommended to create this layer as a business interface between your controllers and persistence repository, however. The business interface enables you to change your repository--such as moving to an SQL-based database or calling another web service--without impacting your controllers.
  • BookController: The web controller that will receive web requests and return reactive responses (Monos and Fluxes).

Example application source code

Listing 3 shows the source code for our model class, Book.java.

Listing 3. Book.java


package com.javaworld.webflux.bookservice.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.mongodb.core.mapping.Document;
@Document
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
    private String id;
    private String title;
    private String author;
}

The Book class is a simple POJO that contains an ID, title, and author. It is annotated with the @Document annotation, which identifies it as a MongoDB document. Spring Data will map documents to collections in MongoDB. The next three annotations-- @Data, @NoArgsConstructor, and @AllArgsConstructor--are Lombok annotations. @Data includes the following capabilities:

  • Generates getters and setters for all fields; setters are only generated for non-final properties.
  • Generates a required arguments constructor.
  • Generates a toString() method.
  • Generates equals() and hashCode() methods that uses all non-transient fields.

In order to work with Spring Data, we need a no-argument constructor so I added @NoArgsConstructor. For testing purposes I aso added an all-argument constructor, @AllArgsConstructor.

As mentioned above, Lombok is not required and you can simply implement getters, setters, and constructors to the class as you normally would do.

Listing 4 shows the source code for the BookRepository interface.

Listing 4. BookRepository.java


package com.javaworld.webflux.bookservice.repository;
import com.javaworld.webflux.bookservice.model.Book;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
public interface BookRepository extends ReactiveMongoRepository<Book, String> {
}

The BookRepository is a Spring Data interface, meaning that you define the interface and Spring Data will generate the code that implements that interface. Specifically, BookRepository extends the ReactiveMongoRepository, which defines the following reactive methods (remember that these are methods that return either monos or fluxes):

  • Mono<Book> save()
  • Flux<Book> saveAll()
  • Flux<Book> findById()
  • Mono<Boolean> existsById()
  • Flux<Book> findAll()
  • Flux<Book> findAllById()
  • Mono<Long> count()
  • Mono<Void> delete()
  • Mono<Void> deleteById()
  • Mono<Void> deleteAll()
  • Flux<Book> insert()

The query methods that return one element (such as findById()) return Mono<Book>. The methods that return more than one element (such as findAll()) return Flux<Book>. It is interesting to note that the delete methods return a Mono<Void>. Mono<Void> means that there is no return type, but when the operation finishes it will publish a completion notification. Recall that these are publishers, so your code, or Spring WebFlux itself, will ultimately define functionality to execute when a message is published to its subscribers.

The BookRepository is defined with two generic parameters: Book, which is the type of document that the repository manages, and String, which is the type of the primary key (the Book's id field). Your code can use the BookRepository methods to execute asynchronous queries against MongoDB.

Listings 5 and 6 show the source code for the BookService and BookServiceImpl, respectively.

Listing 5. BookService.java


package com.javaworld.webflux.bookservice.service;
import com.javaworld.webflux.bookservice.model.Book;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface BookService {
    Mono<Book> findById(String id);
    Flux<Book> findAll();
    Mono<Book> save(Book book);
    Mono<Void> deleteById(String id);
}

Listing 6. BookServiceImpl.java


package com.javaworld.webflux.bookservice.service;
import com.javaworld.webflux.bookservice.model.Book;
import com.javaworld.webflux.bookservice.repository.BookRepository;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class BookServiceImpl implements BookService {
    private BookRepository bookRepository;
    public BookServiceImpl(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    @Override
    public Mono<Book> findById(String id) {
        return bookRepository.findById(id);
    }
    @Override
    public Flux<Book> findAll() {
        return bookRepository.findAll();
    }
    @Override
    public Mono<Book> save(Book book) {
        return bookRepository.save(book);
    }
    @Override
    public Mono<Void> deleteById(String id) {
        return bookRepository.deleteById(id);
    }
}

Services represent business functionality and are identified in Spring using the @Service annotation. In this example, business functionality simply delegates to the underlying repository. If you needed to perform more complex logic on the queries or on the objects being persisted, this is where you would do it.

Listing 7 shows the source code for the BookController class.

Listing 7. BookController.java


package com.javaworld.webflux.bookservice.web;
import com.javaworld.webflux.bookservice.model.Book;
import com.javaworld.webflux.bookservice.service.BookService;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
public class BookController {
    private BookService bookService;
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }
    @GetMapping(value = "/book/{id}")
    public Mono<Book> getBookById(@PathVariable String id) {
        return bookService.findById(id);
    }
    @GetMapping(value = "/books")
    public Flux<Book> getAllBooks() {
        return bookService.findAll();
    }
    @PostMapping(value = "/book")
    public Mono<Book> createBook(@RequestBody Book book) {
        return bookService.save(book);
    }
}

About the code

If you're already familiar with Spring MVC, you’ll notice that the Spring WebFlux application code looks remarkably familiar. The only difference is that all controllers and services return reactive types, namely monos and fluxes. We've also employed a reactive MongoDB driver instead of a nonreactive driver. While the code is familiar, the implementation is quite different. Under the hood, Spring WebFlux will invoke your handler method, capture the reactive response, and then leverage Reactor to wait for the response to be published, all asynchronously.

Here are some points to note about the example application:

  • BookController is annotated with the @RestController annotation, which is a convenience annotation. This annotation includes the @Controller annotation, which is used to identify a class that handles web requests, and @ResponseBody, which indicates that method return values should be bound to the web response body.
  • getBookById() method, which is annotated with the @GetMapping annotation. @GetMapping is a convenience annotation for @RequestMapping(method = RequestMethod.GET). It handles the URI path: /book/{id}, where the id is the value retrieved from the path and passed as the @PathVariable in the method call. The implementation simply delegates to the BookService’s findById() method. Note that this method returns a Mono<Book>, which again is a publisher that will provide WebFlux with a Book instance when it becomes available, ultimately from the reactive MongoDB call to findById().
  • The getAllBooks() method handles the /books URI path and delegates to the BookService’s findAll() method. In this case it returns a Flux<Book>, which is a publisher that sends a stream of Books to Spring WebFlux. When all books have been retrieved from MongoDB, the reactive MongoDB findAll() method will publish a completion notification telling WebFlux that it is finished. WebFlux can then send the response back to the caller.
  • Finally, the createBook() method is annotated with the @PostMapping annotation, which is a convenience annotation for @RequestMapping(method = RequestMethod.POST). @PostMapping handles the /book URI path. The @RequestBody annotation, included when we added @RestContoller tells WebFlux to convert the object received from the caller into a Book instance. The createBook() method delegates to the BookService’s save() method and then returns a Mono<Book> that publishes the newly created Book.

Run the application

1 2 3 Page 2
Page 2 of 3