
Full Reactive Stack with Spring Boot, WebFlux and MongoDB
Within this chapter, you’ll learn how to develop the Reactive Web Server. We’ll use Spring WebFlux, available since Spring 5, and included from the Spring Boot 2.0 version. We’ll connect to a Mongo database using its reactive driver with Spring Data, also via Spring Boot.
- Project Reactor – Main Features
- WebFlux – Main Features
- Creating the application
- Repository Layer
- The Reactive Controller
- The Blocking Controller and Repository
- Loading data into MongoDB with an ApplicationRunner
- Running the backend
- Playing with Reactive and Classic endpoints
This post is a blog version of one of the parts of the Full Reactive Stack with Spring Boot, WebFlux, Angular, RxJS and Eventsource Guide. The complete code sources are on GitHub.
Do you prefer an interactive way of learning or want a certificate of completion? Start this course now on Educative.io: Full Reactive Stack: Spring Boot 2 & Spring WebFlux
Get the complete guide in eBook formats now on Leanpub
We’ll include two different web approaches within the same backend application:
- A reactive style, using WebFlux and a MongoDB ReactiveCrudRepository.
- The classical Spring Web MVC stack, using a blocking REST controller and a blocking MongoDB query.
Additionally, we’ll use:
- Docker, to simplify MongoDB deployment.
- A script to pre-load data into MongoDB. For that, we’ll use a Spring Boot’s
CommandLineRunner
implementation.
Before diving into the implementation details, we’ll introduce some topics. This will help those not familiar with reactive programming. If you already have experience with these concepts, feel free to jump directly into the code explanation.
All the source code (Spring Boot, Angular, Docker) is available on GitHub: Full-Reactive Stack repository. If you find it useful, please star it!
Project Reactor – Main Features
Overview
Project Reactor is a framework built by Pivotal and powered by Spring. It implements Reactive Programming patterns and, more specifically, the Reactive Streams specification.
If you’re familiar with Java 8 Streams you’ll quickly find many similarities between a Stream and a Flux (or its single-element version, Mono). The main characteristics that make Fluxes and Monos different from the Stream API are that the first two follow a Publisher-Subscriber pattern and implement backpressure.
For instance, if you declare a Flux that takes elements from a database, maps them applying an operation, and filters them according to some random criteria, then nothing happens. It will do all these operations only when a subscriber subscribes to the Flux. Furthermore, the items flow through the processing logic only when the subscriber is ready to consume more elements. This is the backpressure concept we mentioned earlier in this guide.
As a direct conclusion of this very brief summary, the main advantage of using Reactor is that you’re in total control of the data flow: you can rely on the subscriber’s ability to ask for more information when it’s ready to process it, or buffer some results on the publisher’s side, or even use a full push-approach without backpressure.
To deep dive into Reactor’s knowledge, the Project Reactor official docs are a very good place to start. Anyway, we’ll cover some of its features in the next sections.
Fluxes and Monos
In our application, we’ll use the Flux class, an asynchronous sequence of 0-N items. It implements the Publisher interface and, as we briefly introduced, it is just a reactive stream that pushes elements whenever the subscriber instructs it to do so.
There is a handy version of a Flux for the special case in which the reactive stream will either emit only one item, or none: Mono. It contains a different set of methods to, for instance, concatenate Mono streams into a Flux.
Reactor Integrations
Spring is including Reactor in some of their popular Spring modules, thus enforcing reactive programming patterns when we use them. Following a smart approach, they’re not getting rid of the previous programming style in the majority of these modules. That means we are not forced to adopt reactive programming.
One of the most popular modules where Spring is leveraging Reactor is, as you may guess, the Web framework. Starting with the Spring 5 you can use WebFlux, which comes with major updates like a new way of declaring the controller routes, and transparent support for Server-Sent Events using the Reactor API.
Spring Data has also embraced Reactive Patterns through its Reactive module, with the inclusion of the ReactiveCrudRepository
. That means we can perform reactive queries to databases like MongoDB, which already have a reactive version of its driver. Then, the database driver pushes data as a flow controlled by the subscribers, instead of pushing the query results all at once.
WebFlux – Main Features
Standard Controllers and Router Functions
Spring WebFlux comes with an extra new feature called Router Functions. The idea is to apply Functional Programming to the Web layer and get rid of the declarative way of implementing Controllers and RequestMapping’s – see the full docs here. However, as you can see depicted in the picture below (based on the one available on the official docs), Spring 5 still allows you to use the @Controller
and @RequestMapping
annotations, so you can decide.
In this guide, we’ll use the classic, simple way of declaring routes and controllers: declarative annotations. The main reason is that I don’t want to distract you with multiples changes at once but also, to be honest, I’m not yet convinced about the convenience of that new style of declaring routes (feel free to judge for yourself).
The dotted orange frame in the figure above represents the stack we’ll use: declarative annotations with WebFlux and reactive streams.
WebClient
WebFlux comes with a reactive web client too: WebClient. You can use it to perform requests to controllers that use reactive streams and benefit from backpressure as well. It’s a kind of reactive and fluent version of the well-known RestTemplate.
As described in the first part of this guide, we won’t use WebClient. That would be only useful for backend-to-backend communication and it’s straightforward to use. Instead, we’ll create a real frontend with Angular to consume the reactive API. We’ll detail the differences of a frontend client when compared to a WebFlux’s WebClient.
Creating the application
To keep this post short, we won’t demonstrate how to initialize the Spring Boot application. You can clone the GitHub repo or get the full version of the book on Leanpub.
Designing our app
You’ll create a typical 3-layered backend application. The exception here is that there is no business logic involved, so the Controllers will use directly the repositories. Besides, we’ll create two co-existing stacks: the reactive way and the classic way. In any case, we’ll focus on the reactive side while explaining the ideas. See the image for the final structure of the backend application (as you find it on the GitHub repo).
We have duplicated the Repository and the Controller elements as our main logic. The Quote is our domain class. We intend to expose a list of Quotes using a Reactive Web API – with QuoteReactiveController and QuoteMongoReactiveRepository -, and compare it with the classic blocking REST API – QuoteBlockingController and QuoteMongoBlockingRepository. The CorsFilter configuration class enables cross-origin requests to simplify the connection from the frontend, and we create the data loader (QuijoteDataLoader) to put the quotes in the Mongo database if they’re not there yet.
We’ll implement two endpoints per controller: one to retrieve all the quotes available in the repository and another one that supports pagination. This is useful for us to compare both approaches in a more realistic scenario, in which pagination is usually required.
Repository Layer
Let’s start from the source of our Reactive Data Stream: MongoDB. We need to create a class that represents the domain object, Quote
, and a Spring Data Repository to collect and map this data to Java objects.
The Quote class
Below you can find the Quote class implementation. It contains just an identifier, the book title, and the quote contents.
package com.thepracticaldeveloper.reactiveweb.domain;
public final class Quote {
private String id;
private String book;
private String content;
// Empty constructor is required by the data layer and JSON de/ser
public Quote() {
}
public Quote(String id, String book, String content) {
this.id = id;
this.book = book;
this.content = content;
}
public String getId() {
return id;
}
public String getBook() {
return book;
}
public String getContent() {
return content;
}
@Override
public String toString() {
return "Quote{" +
"id='" + id + '\'' +
", book='" + book + '\'' +
", content='" + content + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Quote quote = (Quote) o;
if (id != null ? !id.equals(quote.id) : quote.id != null) return false;
if (book != null ? !book.equals(quote.book) : quote.book != null) return false;
return content != null ? content.equals(quote.content) : quote.content == null;
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (book != null ? book.hashCode() : 0);
result = 31 * result + (content != null ? content.hashCode() : 0);
return result;
}
}
Reactive Repository with Spring 5 and Spring Data
Creating a basic Reactive repository is as simple as creating a classic one in Spring Data: you just need to create an interface that extends ReactiveCrudRepository
, which is the reactive version of CrudRepository
. You’ll have access then to default methods to create, read, update, and delete (CRUD) Quotes.
Additional resources
If you’re curious about Spring Data and how it can be used in a typical Spring Boot application for easy management of databases, consider getting a copy of my new book: Learn microservices with Spring Boot 2nd Edition.
Let’s have a look at the interface, and then we’ll describe what it does.
package com.thepracticaldeveloper.reactiveweb.repository;
import com.thepracticaldeveloper.reactiveweb.domain.Quote;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.reactive.ReactiveSortingRepository;
import reactor.core.publisher.Flux;
public interface QuoteMongoReactiveRepository extends ReactiveSortingRepository<Quote, String> {
Flux<Quote> findAllByIdNotNullOrderByIdAsc(final Pageable page);
}
The interface we’re extending, ReactiveSortingRepository
, adds sorting capabilities to the base ReactiveCrudRepository
, which contains the basic CRUD operations. We get everything we need except for one requirement: retrieving pages of Quotes. To accomplish that, we make use of Spring Data’s Query Methods and we pass a Pageable
argument to define the offset and the results per page. Note that the query will match all the quotes since our filter looks for non-null ids so it’s there only because the query method findAllBy...
always expects a filter. We also want to sort the results by quote ID so we add the OrderByIdAsc
suffix and Spring Data will take care of translating this to a proper MongoDB sort clause.
Both the provided findAll
method and the findAllByIdNotNullOrderByIdAsc
that we’ll use from the controller return a Flux. That means that our subscriber (the controller) can control how fast the data should be pulled from the database.
Saving entities the reactive way
One thing that draws the attention of people not familiar with reactive patterns is how the save
method (and its variants) works. Let’s compare the CrudRepository
and the ReactiveCrudRepository
signatures:
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
// ... other methods
}
public interface ReactiveCrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> Mono<S> save(S entity);
// ... other methods
}
Now imagine that we want to save a Quote and we’re so confident about the result that we just ignore it, so we use:
quoteRepository.save(quote);
You can do that with an interface extending CrudRepository
and the entity will be persisted. However, if you do that with a ReactiveCrudRepository
, the entity is not saved. The reactive repository returns a Mono
, which is a Publisher, so it won’t start working until you subscribe to it. If you want to mimic the blocking behavior that CrudRepository offers, you need to call instead:
quoteRepository.save(quote).block();
But then you are not leveraging the reactive advantages and you could keep it simpler with a classic repository definition. We’ll see an example of this when we get to the QuijoteDataLoader
class.
The Reactive Controller
Controller’s Code
Let’s focus now on the most important part of our backend application for the purpose we have in this guide: the Reactive Controller. First, let’s see the full code source, and then we’ll navigate through the different parts.
package com.thepracticaldeveloper.reactiveweb.controller;
import com.thepracticaldeveloper.reactiveweb.domain.Quote;
import com.thepracticaldeveloper.reactiveweb.repository.QuoteMongoReactiveRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
@RestController
public class QuoteReactiveController {
private static final int DELAY_PER_ITEM_MS = 100;
private final QuoteMongoReactiveRepository quoteMongoReactiveRepository;
public QuoteReactiveController(final QuoteMongoReactiveRepository quoteMongoReactiveRepository) {
this.quoteMongoReactiveRepository = quoteMongoReactiveRepository;
}
@GetMapping("/quotes-reactive")
public Flux<Quote> getQuoteFlux() {
return quoteMongoReactiveRepository.findAll().delayElements(Duration.ofMillis(DELAY_PER_ITEM_MS));
}
@GetMapping("/quotes-reactive-paged")
public Flux<Quote> getQuoteFlux(final @RequestParam(name = "page") int page,
final @RequestParam(name = "size") int size) {
return quoteMongoReactiveRepository.findAllByIdNotNullOrderByIdAsc(PageRequest.of(page, size))
.delayElements(Duration.ofMillis(DELAY_PER_ITEM_MS));
}
}
If you’re familiar with Spring Controllers and their annotations, you’ll find out quickly that the only part of the code that seems different is the Flux object we’re returning as a result of the methods. In Spring MVC, we would probably return a Java collection (e.g. List) instead. Let’s park the delayElements
method and the paging arguments for a moment; we’ll cover them in the next sub-sections.
Note that you can also choose the Router Functions alternative instead of the annotated controllers (@RestController and @GetMapping annotations). The implementation would be quite different, but the resulting functionality would be exactly the same. Since that doesn’t add any value to our code, I’m sticking to the classic style.
The controller is calling the QuoteMongoRepository to retrieve all quotes. The printed versions of Don Quixote can easily get to more than 500 pages, so you can imagine that there are a lot of quotes (more than 5000). Thanks to the reactive approach, we don’t need the full list of results to be available in the backend before getting them: we can consume the quotes one by one as soon as the MongoDB driver is publishing results.
Simulating poor performance
To evaluate the Reactive Web properly, we should simulate issues like an irregular network latency or an overloaded server. To keep it simple, we’ll go for the latter and mimic a situation where every quote has a processing time of 100 milliseconds. It will take almost ten minutes (more than 5000 quotes divided by 100ms each) to retrieve the whole dataset. If you want to try a smaller set of quotes you can limit the Flux using the method take
at the end of the expression.
Having a simulated delay will also help us visualize the differences between Reactive and MVC strategies. We’ll run client and server on the same machine so, if we don’t introduce the delay, the response times are so good that it would be hard to spot the differences.
Pagination
When we created the Repository, we introduced an additional method to retrieve paginated results. We’re exposing that behavior in the Controller, mapped to the URL /reactive-quotes-paged?page=x&size=y
.
Creating pages for results is always a good practice, no matter if you’re calling the backend in a blocking or non-blocking style. On one hand, the client might be not interested in getting all the results at once; on the other hand, you want to make the best use of resources by keeping the request’s processing time as short as possible.
We build a Pageable
object from the query parameters just by calling PageRequest.of(page, size)
.
Enabling CORS in Spring WebFlux
We want to connect to the backend from a client application that is deployed at a different origin because it uses a different port (at least during development). In Web terms, that means the frontend will do a Cross-Origin Request. Unless we explicitly allow CORS (Cross-Origin Resource Sharing) in our configuration, that connection is going to be rejected.
To enable CORS with WebFlux globally, one of the options is to inject a CorsWebFilter
bean in the context with custom configuration of the allowed origins, methods, headers, etc. We’ll configure it to allow any method and header from the origin localhost:4200
, where our frontend will be deployed.
package com.thepracticaldeveloper.reactiveweb.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class WebConfiguration {
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:4200");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
Returning a Flux from a Controller: behind the scenes
Let’s dive a bit more into details. The Spring WebFlux documentation lists the reactive types (Flux included) as possible return types. So, what does it happen when we request content to the server? How does WebFlux convert the data into a valid response? It actually depends on how we request it:
- If we request content without using an
Accept
header, or we set it toapplication/json
, we’ll get a blocking process and a JSON-formatted response. - If we want to go Full Reactive and use the Server-Sent Events support in Spring to implement our full reactive stack, we have to support an Event-Stream response. To do that, we have to set (explicitly or implicitly) the
Accept
header totext/event-stream
, therefore activating the reactive functionality in Spring to open an SSE channel and publish server-to-client events.
We’ll test these two different cases at the end of this chapter.
The Blocking Controller and Repository
To better show the comparison between the blocking and reactive approaches, let’s create a separate Controller with different request paths, and connect them to a standard CrudRepository
. This code is pretty straightforward and well-known to all developers familiar with Spring Web MVC.
Let’s see the Controller’s code. Note that here we also apply the same delay as in the reactive approach. This time, the delay is just a big one, since the query is returning results at once too.
package com.thepracticaldeveloper.reactiveweb.controller;
import com.thepracticaldeveloper.reactiveweb.domain.Quote;
import com.thepracticaldeveloper.reactiveweb.repository.QuoteMongoBlockingRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class QuoteBlockingController {
private static final int DELAY_PER_ITEM_MS = 100;
private final QuoteMongoBlockingRepository quoteMongoBlockingRepository;
public QuoteBlockingController(final QuoteMongoBlockingRepository quoteMongoBlockingRepository) {
this.quoteMongoBlockingRepository = quoteMongoBlockingRepository;
}
@GetMapping("/quotes-blocking")
public Iterable<Quote> getQuotesBlocking() throws Exception {
Thread.sleep(DELAY_PER_ITEM_MS * quoteMongoBlockingRepository.count());
return quoteMongoBlockingRepository.findAll();
}
@GetMapping("/quotes-blocking-paged")
public Iterable<Quote> getQuotesBlocking(final @RequestParam(name = "page") int page,
final @RequestParam(name = "size") int size) throws Exception {
Thread.sleep(DELAY_PER_ITEM_MS * size);
return quoteMongoBlockingRepository.retrieveAllQuotesPaged(PageRequest.of(page, size));
}
}
The Repository is very similar to the reactive version, but this time we extend PagingAndSortingRepository
, which adds Paging and Sorting functionality on top of the basic CrudRepository
. As you can see, we return a List in this case:
package com.thepracticaldeveloper.reactiveweb.repository;
import java.util.List;
import com.thepracticaldeveloper.reactiveweb.domain.Quote;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface QuoteMongoBlockingRepository extends PagingAndSortingRepository<Quote, String> {
List<Quote> findAllByIdNotNullOrderByIdAsc(final Pageable page);
}
Loading data into MongoDB with an ApplicationRunner
We have all the code we need to run our Spring Boot application. However, we don’t have the Quotes stored in the database yet. We’ll solve this by reading them from a text version of the book and storing them into MongoDB the first time the application runs.
In the project’s GitHub repository, you’ll see a file containing the ebook in text mode: pg2000.txt
. The first time we run the application, every paragraph will be stored as a Quote in MongoDB. To achieve this, we inject an ApplicationRunner
implementation in the application’s context: the QuijoteDataLoader
class.
You can learn more about how the ApplicationRunner
approach works in the Spring Boot’s reference documentation.
In our case, we’ll check first if the data is already there. If it isn’t, we create a Flux from a BufferedReader
stream, and, for each line, we convert it to a Quote
object and store it in the database. For the identifiers, we use a functional Supplier
interface to generate a sequence.
package com.thepracticaldeveloper.reactiveweb.configuration;
// ... imports
@Component
public class QuijoteDataLoader implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(QuijoteDataLoader.class);
private final QuoteMongoReactiveRepository quoteMongoReactiveRepository;
QuijoteDataLoader(final QuoteMongoReactiveRepository quoteMongoReactiveRepository) {
this.quoteMongoReactiveRepository = quoteMongoReactiveRepository;
}
@Override
public void run(final ApplicationArguments args) {
if (quoteMongoReactiveRepository.count().block() == 0L) {
var idSupplier = getIdSequenceSupplier();
var bufferedReader = new BufferedReader(
new InputStreamReader(getClass()
.getClassLoader()
.getResourceAsStream("pg2000.txt"))
);
Flux.fromStream(
bufferedReader.lines()
.filter(l -> !l.trim().isEmpty())
.map(l -> quoteMongoReactiveRepository.save(
new Quote(idSupplier.get(),
"El Quijote", l))
)
).subscribe(m -> log.info("New quote loaded: {}", m.block()));
log.info("Repository contains now {} entries.",
quoteMongoReactiveRepository.count().block());
}
}
private Supplier<String> getIdSequenceSupplier() {
return new Supplier<>() {
Long l = 0L;
@Override
public String get() {
// adds padding zeroes
return String.format("%05d", l++);
}
};
}
}
The data loader is a good example of using a reactive programming style with a blocking logic and self-subscription since the ApplicationRunner
interface is not prepared for a reactive approach:
- Since the repository is reactive, we need to block() to wait for the result of the one-element publisher (Mono) containing the number of quotes in the repository (the
count
method). - We apply a reactive pattern to subscribe to the result of
save()
from the reactive repository. Remember that, if we don’t consume the result, the quote is not stored.
If the ApplicationRunner
interface would offer a reactive signature, meaning a Flux
or Mono
return type, we could subscribe to the count()
method instead and chain the mono and fluxes. Instead, we need to call block()
in our example to keep the runner executor thread alive. Otherwise, we wouldn’t be able to load the data before the executor finishes.
In the loader case, it would make more sense to switch to a purely functional approach with the classic repository. However, we used this as an example of how fluxes work in a blocking programming style.
Running the backend
Remember that the complete source code (Spring Boot, Angular, Docker) is available on GitHub: Full-Reactive Stack repository. If you find it useful, please star it!
Running MongoDB with Docker
This step is optional, you can also install MongoDB on your machine. It’ll work the same way. However, in the guide’s appendix, you’ll see how to run the complete reactive system using Docker, so it’s good to follow this approach to get first contact with this tool.
You need to install Docker if you haven’t done it yet. Then, create a file named docker-compose-mongo-only.yml
with this content:
version: "2"
services:
mongo:
image: mongo:3.4
hostname: mongo
ports:
- "27017:27017"
volumes:
- mongodata:/data/db
volumes:
mongodata:
I’ll explain the contents in more detail in the dedicated Docker section in this guide. For now, just keep in mind that this will create 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’ll create a persistent volume too, so the quotes’ data will remain after stopping the container. To run mongo with this configuration, you need to execute:
$ docker-compose -f docker-compose-mongo-only.yml up
Running the Spring Boot Reactive application
We now run our backend application to see if everything works as expected. To do so, we can either use our preferred IDE to run the ReactiveWebApplication
class, or we can use the included maven wrapper from the spring-boot-reactive-web folder:
$ ./mvnw spring-boot:run
If everything goes well you should see the application starting and, immediately after, the loader storing all quotes. This will happen only the first time you run the backend application.
INFO 36399 --- [ main] c.t.reactiveweb.ReactiveWebApplication : Starting ReactiveWebApplication on /target/classes started by moises in /frs/
INFO 36399 --- [ main] c.t.reactiveweb.ReactiveWebApplication : No active profile set, falling back to default profiles:
INFO 36399 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Reactive MongoDB repositories in
INFO 36399 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 57ms. Found 1 ry interfaces.
INFO 36399 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data MongoDB repositories in DEFAULT mode.
INFO 36399 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 5ms. Found 1 aces.
INFO 36399 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator'
INFO 36399 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
INFO 36399 --- [ main] c.t.reactiveweb.ReactiveWebApplication : Started ReactiveWebApplication in 1.911 seconds (JVM running
INFO 36305 --- [ main] c.t.r.configuration.QuijoteDataLoader : New quote loaded: Quote{id='03142', book='El Quijote', el mancebo-, yo llevo en este envoltorio unos greguescos de terciopelo, compañeros desta ropilla; si los gasto en el camino, no me podré udad, y no tengo con qué comprar otros; y, así por esto como por orearme, voy desta manera, hasta alcanzar unas compañías de infantería de aquí, donde asentaré mi plaza, y no faltarán bagajes en que caminar de allí adelante hasta el embarcadero, que dicen ha de ser en ener por amo y por señor al rey, y servirle en la guerra, que no a un pelón en la corte. '}
INFO 36305 --- [ main] c.t.r.configuration.QuijoteDataLoader : New quote loaded: Quote{id='03143', book='El Quijote', merced alguna ventaja por ventura? -preguntó el primo. '}
INFO 36305 --- [ main] c.t.r.configuration.QuijoteDataLoader : New quote loaded: Quote{id='03144', book='El Quijote', ervido a algún grande de España, o algún principal personaje -respondió el mozo-, a buen seguro que yo la llevara, que eso tiene el servir elo suelen salir a ser alférez o capitanes, o con algún buen entretenimiento; pero yo, desventurado, serví siempre a catarriberas y a ón y quitación tan mísera y atenuada, que en pagar el almidonar un cuello se consumía la mitad della; y sería tenido a milagro que un paje na siquiera razonable ventura. '}
...
Playing with Reactive and Classic endpoints
Let’s test our API endpoints using cURL, a command-line tool that supports Server-Sent Events, so we can have a look at the blocking and non-blocking alternatives in our application. On Windows, you’ll need either the native Linux shell or a Linux shell simulator (like Git Bash, included with the official Git distribution package, or Cygwin). You can also download cURL for Windows from this site or use any other HTTP client of your choice (e.g. Postman).
Then we try the paged version of the reactive endpoint:
$ curl "http://localhost:8080/quotes-reactive-paged?page=0&size=50"
Something is going wrong with that request. It takes at least 5 seconds to complete (due to the delay of 100ms per element). After that time, we’ll get a big JSON output in the console. It’s still a blocking call.
The reason is that we didn’t specify what are the contents we accept in our first request so Spring WebFlux, by default, is returning us a JSON and treating the request as a blocking one. To activate the Reactive patterns via a Server-Sent Events channel, we need to request a text/event-stream
content instead. Let’s do that:
$ curl -H "Accept: text/event-stream" "http://localhost:8080/quotes-reactive-paged?page=0&size=50"
data:{"id":"00000","book":"El Quijote","content":"El ingenioso hidalgo don Quijote de la Mancha"}
data:{"id":"00001","book":"El Quijote","content":"TASA "}
data:{"id":"00002","book":"El Quijote","content":"Yo, Juan Gallo de Andrada, escribano de Cámara del Rey nuestro señor, de los que residen en su Consejo, certifico y doy fe que, habiendo visto por los señores dél un libro intitulado El ingenioso hidalgo de la Mancha, compuesto por Miguel de Cervantes Saavedra, tasaron cada pliego del dicho libro a tres maravedís y medio; el cual tiene ochenta y tres pliegos, que al dicho precio monta el dicho libro docientos y noventa maravedís y medio, en que se ha de vender en papel; y dieron licencia para que a este precio se pueda vender, y mandaron que esta tasa se ponga al principio del dicho libro, y no se pueda vender sin ella. Y, para que dello conste, di la presente en Valladolid, a veinte días del mes de deciembre de mil y seiscientos y cuatro años. "}
data:{"id":"00003","book":"El Quijote","content":"Juan Gallo de Andrada. "}
...
Much better! The output format is different now and so it is the time that it takes to get each quote on the console. This time, WebFlux is creating a Server-Sent Events connection and publishing a quote every 100 milliseconds. The cURL tool supports SSE so it’s consuming these events. We’re not employing any kind of backpressure here so our client is just pulling data continuously and printing it on screen.
Let’s keep that header in our request and evaluate what happens in the rest of the cases:
curl -H "Accept: text/event-stream" "http://localhost:8080/quotes-blocking-paged?page=0&size=50"
This call will block for five seconds since we’re requesting data to the classic controller (non-reactive). The same will happen if we change the URL to http://localhost:8080/quotes-blocking
. In that case, you’ll need some patience since it will take almost ten minutes to complete.
curl -H "Accept: text/event-stream" "http://localhost:8080/quotes-reactive"
That one will also take almost ten minutes to finish but, in this reactive scenario, we’re getting quotes as soon as they’re dispatched. If you’re a fast reader, you can try to read the book as it’s printed to console. Otherwise, you can always cancel it with a Ctrl-C.
At this point, we proved one of the most important takeaways from this guide: it doesn’t matter if you use a Reactive Web approach in the backend, it won’t be really reactive and non-blocking unless your client can handle it as well.
We’ll implement a more realistic Reactive Web Client in the next section of this guide.
Did you like this post? Start the interactive course on Educative.io or get the complete Full Reactive series in a book format on Leanpub.
Comments