How to develop the reactive web server?
I used Spring WebFlux, which has been available since Spring 5 and is included in the Spring Boot 2.5 version. We'll use Spring Data and Spring Boot to connect to a Mongo database via its reactive driver.
Within the same backend application, I used two different web approaches: A reactive design that makes use of WebFlux and a MongoDB ReactiveCrudRepository. A blocking REST controller and a blocking MongoDB query are used in the traditional Spring boot Web stack.
In addition, we will employ: Docker will make MongoDB deployment easier.
Backend Application Design
Spring Boot integrates WebFlux capabilities, so let's harness their power to build our backend. The Spring Boot Initializr has made the first change in our dependency management. We don't include the traditional web starter.
But we do include the WebFlux one (spring-boot-starter-webflux in pom.xml). This starter includes the Reactor Netty server library dependencies, so Spring Boot knows which one to start at runtime. During development, we'll also have access to the Reactor API.
You'll build a standard three-layer backend application. Controllers will use the repositories directly because there is no business logic involved.
In addition, I created reactive methods from two coexisting stacks:
- The reactive method
- The traditional method
We plan to expose a list of quotes via a reactive web API, namely the ExampleReactiveController and ExampleMongoReactiveRepository, and compare it to the traditional blocking REST API, namely the ExampleBlockingController and ExampleMongoBlockingRepository.
The CorsFilter configuration class allows cross-origin requests to simplify the frontend connection, and we create the data loader (ExampleDataLoader) to insert the quotes into the Mongo database if they aren't already there.
A script to preload data into MongoDB. We'll use Spring Boot's CommandLineRunner implementation for this.
I have mentioned here the following topics:
- CorsWebFilter bean
- Returning a Flux from a controller
We want to connect to the backend from a client app that is deployed at a different origin and uses a different port. That means the frontend will make a cross-origin request. That connection will be rejected unless we explicitly allow CORS (Cross-Origin Resource Sharing) in our configuration.
One option for enabling CORS with WebFlux globally is to inject a CorsWebFilter bean in the context with custom configuration of the allowed origins, methods, headers, and so on. I've set it up to accept any method and header from the origin, localhost:4200, where our frontend will be hosted.
WebFlux:Standard controllers and router functions
WebFlux also includes a reactive web client, WebClient. You can use it to send requests to controllers that use reactive streams while also benefiting from backpressure. It is a reactive and fluent implementation of the well-known RestTemplate. This component is being popularly used across all the E-commerce application.
As stated in the first section, we will not be using WebClient. That is only useful for backend-to-backend communication and it is simple to use. Instead, you can use Angular to build a real frontend that consumes the reactive API.
Behind the scenes of returning a Flux from a controller
- The reactive types (including Flux) are listed as possible return types in the Spring WebFlux documentation. So, what happens when we ask the server for content? How does WebFlux transform the data into an acceptable response? It all depends on how we ask for it:
- We'll get a blocking process and a JSON-formatted response if we don't use an Accept header or set it to application/json.
- We must support an Event-Stream response if we want to go full reactive and use Spring's Server-Sent Events support to implement our full reactive stack. To accomplish this, we must explicitly or implicitly set the Accept header to text/event-stream, thereby activating Spring's reactive functionality in Spring to open an SSE channel and publish server-to-client events.
Let us now concentrate on the most critical component of our backend application: the reactive controller.
public class ExampleReactiveController
private static final int DELAY_IN_MS = 100;
private final QuoteMongoReactiveRepository quoteMongoReactiveRepository;
public QuoteReactiveController(final QuoteMongoReactiveRepository quoteMongoReactiveRepository)
this.quoteMongoReactiveRepository = quoteMongoReactiveRepository;
public Flux getQuoteFlux(final @RequestParam(name = "pg") int pge,
final @RequestParam(name = "sze") int quote_size)
return quoteMongoReactiveRepository.findAllByIdNotNullOrderByIdAsc(PageRequest.of(pge, quote_size))
- If you're familiar with Spring controllers and their annotations, you'll notice that the only difference in the code is the Flux object we're returning as a result of the methods. Instead, in Spring MVC, we'd probably return a Java collection (e.g., List).
- Hold on to the delayElements method and the paging arguments for a moment; we'll get to them in the following sections.
- It's worth noting that you can use the router functions instead of the annotated controllers (@RestController and @GetMapping annotations). The implementation would differ significantly, but the resulting functionality would be identical. We're sticking with the classic style because it adds no value to our code.
- The controller queries the ExampleMongoRepository for all quotes. Don Quixote's printed editions can easily exceed 500 pages, so you can imagine how many quotes there are. We don't need the full list of results in the backend to get them because of the reactive approach; we can consume the quotes one by one as soon as the MongoDB driver publishes results.
Simulating poor performance:
- To properly evaluate the reactive web, we must simulate issues such as irregular network latency or an overloaded server. To keep things simple, we'll go with the latter and pretend that each quote takes 100 milliseconds to process.
- The entire dataset will take nearly ten minutes to retrieve (more than 5000 quotes divided by 100ms each). If you want to try a smaller set of quotes, use the take method at the end of the expression to limit the Flux.
- A simulated delay will also assist us in visualizing the differences between reactive and MVC strategies. The client and server will be run on the same machine. So, if we don't add the delay, the response times will be so good that it will be hard to spot the differences.
- When we built the repository, we added a method for retrieving paginated results. We’re exposing that behavior in the controller mapped to the URL /reactive-example-paged?page=x&size=y.
- Whether you're calling the backend in a blocking or non-blocking style, creating pages for results is always a good practice. On the one hand, the client may not be interested in receiving all of the results at once; on the other hand, you want to make the best use of resources by processing the request as quickly as possible.
By calling PageRequest.of, we created a Pageable object from the query parameters.
Reactive: Spring 5 and Spring data reactive repository
- Creating basic reactive repository in Spring Data is the same as creating a classic one: create an interface that extends ExampleReactiveCrudRepository, which is the reactive version of CrudRepository. You'll then have access to default methods for creating, reading, updating, and deleting (CRUD) quotes.
- The ExampleReactiveSortingRepository interface we're extending adds sorting capabilities to the base ExampleReactiveCrudRepository, which contains the basic CRUD operations.
- We get everything we need except for one thing: retrieving Quotes pages. To do so, we use Spring Data's query methods and pass a Pageable argument to specify the offset and results per page.
- Because our filter looks for non-null IDs, the query will match all quotes, so it's only there because the query method, findAllBy(), always expects a filter.
- We'd also like to order the results by quote ID. So, we add the OrderByIdAsc suffix, and Spring Data will handle the conversion into a proper MongoDB sort clause.
- Both the provided findAll() method and the controller's findAllByIdNotNullOrderByIdAsc() method return a Flux. It means that our subscriber (the controller) has control over how quickly the data is retrieved from the database.
Saving the Entities in reactive Way:
Now imagine: Let's say we want to save a Quote. We're so sure of the outcome that we ignore the possibility that it won't save and use:
- ExamplequottionRepository.save(quote): You can do this by extending CrudRepository with an interface, and the entity will be saved. When you do this with a ReactiveCrudRepository, however, the entity is not saved. Because the reactive repository returns a Mono, which is a publisher, it will not function until you subscribe to it. If you want to emulate CrudRepository's blocking behavior, you must instead call:
- ExamplequotRepository.save(quote).block(): However, then you are not leveraging the reactive advantages, and you could keep it with a classic repository definition.
Blocking Controller and Mongo Repository:
- Let's say we want to save a Quote. We're so sure of the outcome that we ignore the possibility that it won't save and use:
- Let's create a separate controller with different request paths and connect it to a standard CrudRepository to better demonstrate the difference between the blocking and reactive approaches. The code is very straightforward and familiar to java full stack developers.
Loading Data Into MongoDB With an ApplicationRunner:
We now have all of the code necessary to run our Spring Boot application. However, the quotes are not yet stored in the database. We'll fix this by reading them from the book's text version and storing them in MongoDB the first time the application runs.
- The first time we run the application, each paragraph will be saved in MongoDB as a quotation. To accomplish this, we inject an ApplicationRunner implementation into the context of the application: the ExampleQuotationDataLoader class.
- More information about how the ApplicationRunner approach works can be found in the Spring Boot reference documentation.
- In our case, we'll start by seeing if the data is already present on line 17. If it isn't, we'll create a Flux from a BufferedReader stream and convert each line to a Quote object before storing it in the database. To generate a sequence of identifiers, we use a functional Supplier interface.
Blocking logic and self-subscription:
- Because the ApplicationRunner interface is not prepared for a reactive approach, the data loader is a good example of using a reactive programming style with blocking logic and self-subscription:
- Because the repository is reactive, we must block() to await the outcome of the one-element publisher (Mono) containing the number of quotes in the repository.
- We use a reactive pattern to subscribe to the save() result from the reactive repository. Keep in mind that if we do not consume the result, the quote is not saved.
- If the ApplicationRunner interface had a reactive signature, indicating a Flux or Mono return type, we could instead subscribe to the count() method and chain the Mono and Fluxes. In our example, we must instead call block() to keep the runner executor thread alive. We wouldn't be able to load the data if the executor didn't finish first.
- In the case of the loader, it would be more logical to use the classic repository in a purely functional manner. We used this example, however, to demonstrate how Fluxes work in a blocking programming style.
Running MongoDB with Docker#
Remember that the full source code (Spring Boot, Angular, and Docker) can be found on GitHub: Full-Reactive Stack repository. Please read through this entire step-by-step process for detailed implementations:
- Let's quickly go over how to install MongoDB on your machine before you try it out on the Educative platform.
- This is an optional step. MongoDB can also be installed on your machine and used in the same way. However, in the course appendix, you'll see how to run the entire reactive system with Docker, so it's best to take this approach to get your first contact with this tool.
- If you haven't already, you should install Docker. Then, make a file called named: docker-compose-mongo-only.yml with this content:
- Remember that this will launch a Docker container with a MongoDB instance in your Docker host (which can be localhost or a virtual machine IP) and expose the default connection port, 27017. It will also create a persistent volume, so the quotes' data will remain after the container is stopped. To run Mongo with this configuration, run the following commands:
$ docker-compose -f docker-compose-mongo-only.yml up