Skip to content

Full Reactive Stack con Spring Boot, WebFlux y MongoDB

Construyendo el backend de una aplicación reactiva con Spring Boot, WebFlux y MongoDB

Este post es una versión de blog de una parte de la guía Full Reactive Stack with Spring Boot, WebFlux, Angular, RxJS, and Eventsource. El código fuente completo está en GitHub.

Esta guía tiene cuatro capítulos principales y un apéndice:

  1. La Web Reactiva. Este capítulo te guía a través de los conceptos principales de un enfoque reactivo y lo compara con las interfaces web clásicas.

  2. El backend. Esta sección cubre la creación de la aplicación Spring Boot usando las dependencias requeridas y el desarrollo de las capas del backend, prestando especial atención a los Controllers con WebFlux y el driver reactivo de MongoDB.

  3. El frontend. Recorre el desarrollo de una aplicación Angular que consume el endpoint reactivo creado en el lado del servidor.

  4. Conclusiones. Esta parte completa la guía, resumiendo lo que has construido y comparando los enfoques Web Reactivo y Clásico. Da algunas perspectivas sobre rendimiento, testabilidad, etc.

  5. Ejecutar la app con Docker. Este apéndice te ayuda a construir y ejecutar la aplicación full-stack en Docker.

Todo el código fuente (spring-boot, Angular, Docker) está disponible en GitHub: repositorio Full-Reactive Stack. Si te resulta útil, por favor dale una estrella!

Dentro de este capítulo, aprenderás cómo desarrollar el Servidor Web Reactivo. Usaremos Spring WebFlux, que ha estado disponible desde Spring 5 y está incluido en la versión Spring Boot 2.0. Nos conectaremos a una base de datos Mongo usando su driver reactivo con Spring Data, también vía Spring Boot.

Incluiremos dos enfoques web diferentes dentro de la misma aplicación backend:

  1. Un estilo reactivo, usando WebFlux y un ReactiveCrudRepository de MongoDB.

  2. El clásico stack Spring Web MVC usa un controlador REST bloqueante y una consulta bloqueante de MongoDB.

Además, usaremos:

  • Docker para simplificar el despliegue de MongoDB.

  • Un script para pre-cargar datos en MongoDB. Para eso, usaremos una implementación de CommandLineRunner de Spring Boot.

Antes de sumergirnos en los detalles de implementación, introduciremos algunos temas. Esto ayudará a aquellos que no están familiarizados con la programación reactiva. Si ya tienes experiencia con estos conceptos, por favor salta directamente a la explicación del código.

Todo el código fuente (Spring Boot, Angular, Docker) está disponible en GitHub: repositorio Full-Reactive Stack. Si te resulta útil, por favor márcalo con una estrella!

Project Reactor – Características Principales

Resumen

Project Reactor es un framework construido por Pivotal y potenciado por Spring. Implementa patrones de Programación Reactiva, específicamente la especificación Reactive Streams.

Si estás familiarizado con Java 8 Streams, rápidamente encontrarás muchas similitudes entre un Stream y un Flux (o su versión de un solo elemento, Mono). Las principales características que hacen que Fluxes y Monos sean diferentes de la API Stream son que los primeros dos siguen un patrón Publisher-Subscriber e implementan backpressure.

Por ejemplo, si declaras un Flux que toma elementos de una base de datos, los mapea aplicando una operación, y los filtra según algunos criterios aleatorios, entonces no sucede nada. Realizará todas estas operaciones solo cuando un subscriber se suscriba al Flux. Además, los elementos fluyen a través de la lógica de procesamiento solo cuando el subscriber está listo para consumir más elementos. Este es el concepto de backpressure que mencionamos anteriormente en esta guía.

Como conclusión directa de este breve resumen, la principal ventaja de usar Reactor es que tienes control total del flujo de datos: puedes confiar en la capacidad del subscriber para solicitar más información cuando esté listo para procesarla, o hacer buffer de algunos resultados en el lado del publisher, o incluso usar un enfoque de push completo sin backpressure.

Los documentos oficiales de Project Reactor son un buen lugar para comenzar a explorar el conocimiento de Reactor. En las siguientes secciones, cubriremos algunas de sus características.

Fluxes y Monos

Nuestra aplicación usará la clase Flux, una secuencia asíncrona de 0-N elementos. Implementa la interfaz Publisher y es, como introdujimos brevemente, solo un stream reactivo que empuja elementos cuando el subscriber le indica que lo haga.

Hay una versión conveniente de un Flux para el caso especial en el que el stream reactivo emitirá solo un elemento o ninguno: Mono. Contiene un conjunto diferente de métodos para concatenar streams Mono en un Flux.

Integraciones de Reactor

Spring incluye Reactor en algunos de sus módulos populares de Spring, reforzando así los patrones de programación reactiva cuando los usamos. Siguiendo un enfoque inteligente, no están eliminando el estilo de programación anterior en la mayoría de estos módulos. Eso significa que no estamos obligados a adoptar la programación reactiva.

Uno de los módulos más populares donde Spring está aprovechando Reactor es, como puedes adivinar, el framework Web. Comenzando con Spring 5 puedes usar WebFlux, que viene con actualizaciones importantes como una nueva forma de declarar las rutas del controlador, y soporte transparente para Server-Sent Events usando la API de Reactor.

Spring Data también ha adoptado Patrones Reactivos a través de su módulo Reactive, con la inclusión del ReactiveCrudRepository. Eso significa que podemos realizar consultas reactivas a bases de datos como MongoDB, que ya tiene una versión reactiva de su driver. Entonces, el driver de la base de datos empuja datos como un flujo controlado por los subscribers en lugar de empujar los resultados de la consulta simultáneamente.

WebFlux – Características Principales

Controladores Estándar y Router Functions

Spring WebFlux viene con una nueva característica adicional llamada Router Functions. La idea es aplicar Programación Funcional a la capa Web y eliminar la forma declarativa de implementar Controllers y RequestMapping’s – ver los documentos completos aquí. Sin embargo, como puedes ver representado en la imagen a continuación (basada en la disponible en los documentos oficiales), Spring 5 todavía te permite usar las anotaciones @Controller y @RequestMapping, así que puedes decidir.

Alternativas de Spring WebFlux
Alternativas de Spring WebFlux

En esta guía, usaremos la forma clásica y simple de declarar rutas y controladores: anotaciones declarativas. La razón principal es que no quiero distraerte con múltiples cambios a la vez, pero para ser honesto, todavía no estoy convencido de la conveniencia de ese nuevo estilo de declarar rutas (siéntete libre de juzgar por ti mismo).

El marco naranja punteado en la figura anterior representa el stack que usaremos: anotaciones declarativas con WebFlux y streams reactivos.

WebClient

WebFlux viene también con un cliente web reactivo: WebClient. Puedes usarlo para realizar peticiones a controladores que usan streams reactivos y beneficiarte del backpressure también. Es una especie de versión reactiva y fluida del bien conocido RestTemplate.

Como se describe en la primera parte de esta guía, no usaremos WebClient. Eso solo sería útil para comunicación backend-a-backend, y es sencillo de usar. En su lugar, crearemos un frontend real con Angular para consumir la API reactiva. Detallaremos las diferencias entre un cliente frontend y el WebClient de WebFlux.

Creando la aplicación

No demostraremos cómo inicializar la aplicación Spring Boot para mantener este post corto. Puedes clonar el repositorio de GitHub u obtener la versión completa del libro.

Diseñando nuestra app

Crearás una aplicación backend típica de tres capas. La excepción aquí es que no hay lógica de negocio involucrada, así que los Controllers usarán los repositories directamente. Además, crearemos dos stacks coexistentes: el reactivo y el clásico. En cualquier caso, nos enfocaremos en el lado reactivo mientras explicamos las ideas. Mira la imagen para la estructura final de la aplicación backend (como puedes encontrarla en el repositorio de GitHub).

Estructura del Proyecto Reactivo
Estructura del Proyecto Reactivo

Hemos duplicado los elementos Repository y Controller como nuestra lógica principal. El Quote es nuestra clase de dominio. Pretendemos exponer una lista de Quotes usando una API Web Reactiva – con QuoteReactiveController y QuoteMongoReactiveRepository – y compararla con la API REST bloqueante clásica – QuoteBlockingController y QuoteMongoBlockingRepository. La clase de configuración CorsFilter habilita peticiones cross-origin para simplificar la conexión desde el frontend, y creamos el cargador de datos (QuijoteDataLoader) para poner las citas en la base de datos Mongo si aún no están allí.

Implementaremos dos endpoints por controlador: uno para recuperar todas las citas disponibles en el repository y otro que soporta paginación. Esto nos permitirá comparar ambos enfoques en un escenario más realista, en el que usualmente se requiere paginación.

UML Reactivo de Spring WebFlux
UML Reactivo de Spring WebFlux

Capa Repository

Comencemos desde la fuente de nuestro Reactive Data Stream: MongoDB. Necesitamos crear una clase que represente el objeto de dominio, Quote, y un Spring Data Repository para recopilar y mapear estos datos a objetos Java.

La clase Quote

A continuación, puedes encontrar la implementación de la clase Quote. Contiene un identificador, el título del libro, y el contenido de la cita.

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;
    }
}

Repository Reactivo con Spring 5 y Spring Data

Crear un Repository Reactivo básico es tan simple como crear uno clásico en Spring Data: solo necesitas crear una interfaz que extienda ReactiveCrudRepository, que es la versión reactiva de CrudRepository. Tendrás acceso entonces a métodos predeterminados para crear, leer, actualizar y eliminar (CRUD) Quotes.

Recursos adicionales

Si tienes curiosidad sobre Spring Data y cómo puede usarse en una aplicación típica de Spring Boot para la gestión fácil de bases de datos, considera obtener una copia de mi nuevo libro: Learn microservices with Spring Boot 2nd Edition.

Veamos la interfaz, y luego describiremos qué hace.

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);
}

La interfaz que estamos extendiendo, ReactiveSortingRepository, añade capacidades de ordenamiento al ReactiveCrudRepository base, que contiene las operaciones CRUD básicas. Obtenemos todo lo que necesitamos excepto un requisito: recuperar páginas de Quotes. Para lograr eso, usamos los Query Methods de Spring Data, y pasamos un argumento Pageable para definir el offset y los resultados por página. Ten en cuenta que la consulta coincidirá con todas las citas ya que nuestro filtro busca ids no nulos, así que solo está ahí porque el método de consulta findAllBy… siempre espera un filtro. También queremos ordenar los resultados por ID de cita, así que añadimos el sufijo OrderByIdAsc, y Spring Data traducirá esto a una cláusula de ordenamiento apropiada de MongoDB.

Tanto el método findAll proporcionado como el findAllByIdNotNullOrderByIdAsc que usaremos desde el controlador devuelven un Flux. Eso significa que nuestro subscriber (el controlador) puede controlar qué tan rápido se deben extraer los datos de la base de datos.

Guardando entidades de forma reactiva

Una cosa que llama la atención de las personas no familiarizadas con patrones reactivos es cómo funciona el método save (y sus variantes). Comparemos las firmas de CrudRepository y ReactiveCrudRepository:

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
}

Ahora imagina que queremos guardar un Quote, y estamos tan seguros del resultado que simplemente lo ignoramos, así que usamos:

quoteRepository.save(quote);

Puedes hacer eso con una interfaz que extienda CrudRepository y la entidad será persistida. Sin embargo, si haces eso con un ReactiveCrudRepository, la entidad no se guarda. El repository reactivo devuelve un Mono, que es un Publisher, así que no comenzará a funcionar hasta que te suscribas a él. Si quieres imitar el comportamiento bloqueante que ofrece CrudRepository, necesitas llamar en su lugar:

quoteRepository.save(quote).block();

Pero entonces no estás aprovechando las ventajas reactivas, y podrías mantenerlo más sencillo con una definición de repository clásica. Veremos un ejemplo de esto cuando lleguemos a la clase QuijoteDataLoader.

El Controlador Reactivo

Código del Controlador

Enfoquémonos en la parte más importante de nuestra aplicación backend para el propósito de esta guía: el Controlador Reactivo. Primero, veamos el código fuente completo, y luego navegaremos a través de las diferentes partes.

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));
    }

}

Si estás familiarizado con los Controllers de Spring y sus anotaciones, descubrirás rápidamente que la única parte del código que parece diferente es el objeto Flux que estamos devolviendo como resultado de los métodos. En Spring MVC, probablemente devolveríamos una colección Java (por ejemplo, List) en su lugar. Dejemos el método delayElements y los argumentos de paginación por un momento; los cubriremos en las siguientes subsecciones.

También puedes elegir la alternativa de Router Functions en lugar de los controladores anotados (anotaciones @RestController y @GetMapping). La implementación sería bastante diferente, pero la funcionalidad resultante sería la misma. Como eso no añade ningún valor a nuestro código, me quedo con el estilo clásico.

El controlador llama al QuoteMongoRepository para recuperar todas las citas. Las versiones impresas de Don Quijote pueden llegar fácilmente a más de 500 páginas, así que puedes imaginar que hay muchas citas (más de 5000). Gracias al enfoque reactivo, no necesitamos que la lista completa de resultados esté disponible en el backend antes de obtenerlos: podemos consumir las citas una por una tan pronto como el driver de MongoDB publique resultados.

Simulando bajo rendimiento

Para evaluar la Web Reactiva adecuadamente, deberíamos simular problemas como una latencia de red irregular o un servidor sobrecargado. Para mantenerlo simple, iremos por lo último y simularemos una situación donde cada cita tiene un tiempo de procesamiento de 100 milisegundos. Tomará casi diez minutos (más de 5000 citas divididas por 100ms cada una) recuperar todo el conjunto de datos. Si quieres probar un conjunto más pequeño de citas, puedes limitar el Flux usando el método take al final de la expresión.

Un retraso simulado también nos ayudará a visualizar las diferencias entre las estrategias Reactiva y MVC. Ejecutaremos el cliente y el servidor en la misma máquina, así que si no introducimos el retraso, los tiempos de respuesta serán tan buenos que sería difícil detectar las diferencias.

Paginación

Cuando creamos el Repository, introdujimos otro método para recuperar resultados paginados. Estamos exponiendo ese comportamiento en el Controlador, mapeado a la URL /reactive-quotes-paged?page=x&size=y.

Crear páginas para los resultados es siempre una buena práctica, ya sea que estés llamando al backend en un estilo bloqueante o no bloqueante. Por un lado, el cliente podría no estar interesado en obtener todos los resultados de una vez; por otro lado, quieres hacer el mejor uso de los recursos manteniendo el tiempo de procesamiento de la petición lo más corto posible.

Construimos un objeto Pageable desde los parámetros de consulta simplemente llamando a PageRequest.of(page, size).

Habilitando CORS en Spring WebFlux

Queremos conectarnos al backend desde una aplicación cliente desplegada en un origen diferente porque usa un puerto diferente (al menos durante el desarrollo). En términos Web, eso significa que el front end hará una Cross-Origin Request. A menos que permitamos explícitamente CORS (Cross-Origin Resource Sharing) en nuestra configuración, esa conexión será rechazada.

Para habilitar CORS con WebFlux globalmente, una de las opciones es inyectar un bean CorsWebFilter en el contexto con una configuración personalizada de los orígenes permitidos, métodos, headers, etc. Lo configuraremos para permitir cualquier método y header desde el origen localhost:4200, donde nuestro frontend será desplegado.

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);
    }

}

Devolviendo un Flux desde un Controlador: detrás de escena

Profundicemos un poco más en los detalles. La documentación de Spring WebFlux lista los tipos reactivos (Flux incluido) como posibles tipos de retorno. Entonces, ¿qué sucede cuando solicitamos contenido del servidor? ¿Cómo convierte WebFlux los datos en una respuesta válida? Depende de cómo lo solicitemos:

  • Si solicitamos contenido sin usar un header Accept, o lo configuramos a application/json, obtendremos un proceso bloqueante y una respuesta formateada en JSON.

  • Si queremos ir Full Reactive y usar el soporte de Server-Sent Events en Spring para implementar nuestro stack reactivo completo, tenemos que soportar una respuesta Event-Stream. Para hacer eso, tenemos que configurar (explícita o implícitamente) el header Accept a text/event-stream, activando así la funcionalidad reactiva en Spring para abrir un canal SSE y publicar eventos servidor-a-cliente.

Probaremos estos dos casos diferentes al final de este capítulo.

El Controlador y Repository Bloqueantes

Para mostrar mejor la comparación entre los enfoques bloqueante y reactivo, creemos un Controlador separado con diferentes rutas de petición y conectémoslos a un CrudRepository estándar. Este código es sencillo y bien conocido por todos los desarrolladores familiarizados con Spring Web MVC.

Veamos el código del Controlador. Ten en cuenta que también aplicamos el mismo retraso que en el enfoque reactivo. Esta vez, el retraso es solo uno grande ya que la consulta también está devolviendo resultados de una vez.

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));
    }
}

El Repository es muy similar a la versión reactiva, pero esta vez extendemos PagingAndSortingRepository, que añade funcionalidad de Paginación y Ordenamiento sobre el CrudRepository básico. Como puedes ver, devolvemos una List en este caso:

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);
}

Cargando datos en MongoDB con un ApplicationRunner

Tenemos todo el código que necesitamos para ejecutar nuestra aplicación Spring Boot. Sin embargo, las Quotes aún no están almacenadas en la base de datos. Resolveremos esto leyéndolas de una versión de texto del libro y almacenándolas en MongoDB la primera vez que se ejecute la aplicación.

En el repositorio de GitHub del proyecto, verás un archivo que contiene el ebook en modo texto: pg2000.txt. La primera vez que ejecutemos la aplicación, cada párrafo será almacenado como un Quote en MongoDB. Para lograr esto, inyectamos una implementación de ApplicationRunner en el contexto de la aplicación: la clase QuijoteDataLoader.

Puedes aprender más sobre cómo funciona el enfoque ApplicationRunner en la documentación de referencia de Spring Boot.

En nuestro caso, primero verificaremos si los datos ya están ahí. Si no están, creamos un Flux desde un stream de BufferedReader, y, para cada línea, la convertimos a un objeto Quote y la almacenamos en la base de datos. Para los identificadores, usamos una interfaz funcional Supplier para generar una secuencia.

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++);
            }
        };
    }
}

El cargador de datos es un buen ejemplo de usar un estilo de programación reactivo con lógica bloqueante y auto-suscripción ya que la interfaz ApplicationRunner no está preparada para un enfoque reactivo:

  • Como el repository es reactivo, necesitamos block() para esperar el resultado del publisher de un elemento (Mono) que contiene el número de citas en el repository (el método count).

  • Aplicamos un patrón reactivo para suscribirnos al resultado de save() del repository reactivo. Recuerda que si no consumimos el resultado, la cita no se almacena.

Si la interfaz ApplicationRunner ofreciera una firma reactiva, significando un tipo de retorno Flux o Mono, podríamos suscribirnos al método count() en su lugar y encadenar el mono y los fluxes. En cambio, necesitamos llamar a block() en nuestro ejemplo para mantener vivo el hilo ejecutor del runner. De lo contrario, no podríamos cargar los datos antes de que el ejecutor termine.

En el caso del loader, cambiar a un enfoque puramente funcional con el repository clásico tendría más sentido. Sin embargo, usamos esto como un ejemplo de cómo funcionan los fluxes en un estilo de programación bloqueante.

Ejecutando el backend

Recuerda que el código fuente completo (Spring Boot, Angular, Docker) está disponible en GitHub: repositorio Full-Reactive Stack. Si te resulta útil, por favor márcalo con una estrella!

Ejecutando MongoDB con Docker

Este paso es opcional; también puedes instalar MongoDB en tu máquina. Funcionará de la misma manera. Sin embargo, en el apéndice de la guía, verás cómo ejecutar el sistema reactivo completo usando Docker, así que es bueno seguir este enfoque para obtener un primer contacto con esta herramienta.

Necesitas instalar Docker si aún no lo has hecho. Luego, crea un archivo llamado docker-compose-mongo-only.yml con este contenido:

version: "2"

services:
  mongo:
    image: mongo:3.4
    hostname: mongo
    ports:
      - "27017:27017"
    volumes:
      - mongodata:/data/db

volumes:
  mongodata:

Explicaré los contenidos con más detalle en la sección dedicada a Docker en esta guía. Por ahora, solo recuerda que esto creará un contenedor docker con una instancia de MongoDB en tu host Docker (que puede ser localhost o una IP de máquina virtual) y expondrá el puerto de conexión predeterminado, 27017. También creará un volumen persistente, así que los datos de las citas permanecerán después de detener el contenedor. Para ejecutar mongo con esta configuración, necesitas ejecutar:

$ docker-compose -f docker-compose-mongo-only.yml up

Ejecutando la aplicación Spring Boot Reactive

Ahora ejecutamos nuestra aplicación backend para ver si todo funciona como se espera. Para hacerlo, podemos usar nuestro IDE preferido para ejecutar la clase ReactiveWebApplication, o podemos usar el maven wrapper incluido desde la carpeta spring-boot-reactive-web:

$ ./mvnw spring-boot:run

Si todo va bien, deberías ver la aplicación iniciándose e, inmediatamente después, el loader almacenando todas las citas. Esto sucederá solo la primera vez que ejecutes la aplicación backend.

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. '}
...

Jugando con endpoints Reactivos y Clásicos

Probemos nuestros endpoints de API usando cURL, una herramienta de línea de comandos que soporta Server-Sent Events, para que podamos ver las alternativas bloqueantes y no bloqueantes en nuestra aplicación. En Windows, necesitarás ya sea el shell nativo de Linux o un simulador de shell de Linux (como Git Bash, incluido con el paquete de distribución oficial de Git, o Cygwin). También puedes descargar cURL para Windows desde este sitio o usar cualquier otro cliente HTTP de tu elección (por ejemplo, Postman).

Luego probamos la versión paginada del endpoint reactivo:

$ curl "http://localhost:8080/quotes-reactive-paged?page=0&size=50"

Algo está yendo mal con esa petición. Tarda al menos 5 segundos en completarse (debido al retraso de 100ms por elemento). Después de ese tiempo, obtendremos una enorme salida JSON en la consola. Todavía es una llamada bloqueante.

La razón es que no especificamos los contenidos que aceptamos en nuestra primera petición, así que Spring WebFlux, por defecto, nos está devolviendo un JSON y tratando la petición como una bloqueante. Para activar los patrones Reactivos vía un canal de Server-Sent Events, necesitamos solicitar un contenido text/event-stream en su lugar. Hagamos esto:

$ 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. "}

...

¡Mucho mejor! El formato de salida es diferente ahora, y también lo es el tiempo que tarda en obtener cada cita en la consola. Esta vez, WebFlux está creando una conexión de Server-Sent Events y publicando una cita cada 100 milisegundos. La herramienta cURL soporta SSE, así que está consumiendo estos eventos. No estamos empleando ningún tipo de backpressure aquí, así que nuestro cliente simplemente está extrayendo datos continuamente y imprimiéndolos en pantalla.

Mantengamos ese header en nuestra petición y evaluemos qué sucede en el resto de los casos:

curl -H "Accept: text/event-stream" "http://localhost:8080/quotes-blocking-paged?page=0&size=50"

Esta llamada se bloqueará por cinco segundos ya que estamos solicitando datos al controlador clásico (no reactivo). Lo mismo sucederá si cambiamos la URL a http://localhost:8080/quotes-blocking. En ese caso, necesitarás algo de paciencia ya que tomará casi diez minutos en completarse.

curl -H "Accept: text/event-stream" "http://localhost:8080/quotes-reactive"

Ese también tomará casi diez minutos en terminar pero, en este escenario reactivo, estamos obteniendo citas tan pronto como se despachan. Si eres un lector rápido, puedes intentar leer el libro mientras se imprime en consola. De lo contrario, siempre puedes cancelarlo con un Ctrl-C.

En este punto, probamos una de las conclusiones más esenciales de esta guía: no importa si usas un enfoque Web Reactivo en el backend; no será reactivo y no bloqueante a menos que tu cliente también pueda manejarlo.

Implementaremos un Cliente Web Reactivo más realista en la siguiente sección de esta guía: el Frontend.

Continúa Leyendo Esta Serie

Este artículo es parte de la serie Full Reactive Stack:

  1. Parte 1: Introducción
  2. Parte 2: El Backend - Spring Boot, WebFlux y MongoDB (este post)
  3. Parte 3: El Frontend Angular
  4. Parte 4: Conclusiones

¿Quieres la guía completa? Obtén Esta Serie de Blog y Más como un eBook Completo—¡Disponible Ahora para Comprar!

Reactive Systems in Java

Build resilient, scalable applications with reactive patterns

Explore Book

Get More Architecture Insights

Join hundreds of software architects and engineers getting practical insights delivered to their inbox.

Volver al Blog