Aplicación Reactiva con Spring Boot y Project Reactor

Introducción

La programación reactiva ha estado sonando muy fuerte recientemente. Entenderla para saber cuándo usarla y cuándo no, es esencial para un desarrollador. En este artículo les explicaremos cómo crear una aplicación de Spring Boot Reactiva en poco más de 4 pasos. 

A continuación, para este ejemplo desarrollaremos una aplicación de un CRUD Reactivo utilizando Spring Data Reactive Relational Database Connectivity (R2DBC) con PostgreSQL y Webflux.

Antes de comenzar con la práctica, haré una pequeña explicación sobre los fundamentos de la programación reactiva.

Programación Reactiva

La programación reactiva es un paradigma de desarrollo basado en flujos de datos asíncronos y la propagación del cambio.

Uno de los principales objetivo de la programación reactiva es ayudar a construir servicios más eficientes y resistentes para responder a las realidades de los microservicios distribuidos o nativos de las nubes.

En la programación reactiva, los flujos de datos pueden ser, por ejemplo: eventos, mensajes, llamadas, e incluso fallos, donde hacemos una petición de recursos y empezamos a realizar otras cosas. Entonces, cuando los datos están disponibles, recibimos la notificación junto con los datos que informan la respuesta. Este es el estilo de la programación reactiva que está basado en eventos que hacen que nuestra aplicación sea asíncrona y no se bloquee. Al día de hoy, hay muchos frameworks y librerías que están apareciendo regularmente que se enfrentan a muchas de exigencias en el mercado laboral. Uno de los proyectos más poderosos es Reactor: una librería reactiva de cuarta generación, basada en la especificación Reactive Streams, para construir aplicaciones no bloqueantes en la JVM. Una de las ventajas es que Reactor es creado y mantenido por el equipo de Spring Framework.

Spring WebFlux

Spring WebFlux es un framework para construir aplicaciones asíncronas con un modelo de I/O sin bloqueo. Está basado en el Proyecto Reactor para capacidades reactivas. Por lo tanto, tiene soporte para construir aplicaciones del lado del servidor y del lado del cliente. Spring WebFlux se introduce a partir de la versión Spring 5  y está soportado desde las versiones de Java a partir de la 8 en adelante.

Mono

Un Mono es un Publicador de Flujos Reactivos que emite de 0 a 1 <T> elementos con muchos operadores que pueden ser usados para generar, transformar u orquestar secuencias Mono.

Flux

Un Flux es un Publicador de Flujos Reactivo, que representa una secuencia asincrónica  de 0 a N <T> elementos emitidos que puede opcionalmente terminar usando señales “onComplete” u “onError”.

Construir nuestra primera aplicación reactiva

  • Requisitos / Herramientas a utilizar
Lenguaje de programaciónJava SE 11Oracle:

 

https://www.oracle.com/java/technologies/javase-jdk11-downloads.html
Version Recomendad: AdoptOpenJDK

https://adoptopenjdk.net/installation.html

Entorno Integrado de Desarrollo (IDE)Intellijhttps://www.jetbrains.com/idea/download/
Base de DatosPostgreSQLhttps://www.postgresql.org/download/
Analizador de webservicePostmanhttps://www.postman.com/

 

Ingresamos al sitio web https://start.spring.io/ para este ejemplo. Creamos un proyecto con Maven, en lenguaje seleccionamos Java. Continuamos para seleccionar la versión 2.3.3 de Spring Boot. En el paso siguiente, diligenciamos los datos del proyecto, donde definiremos nuestro ID de grupo que también se convertirá en un paquete base en nuestro proyecto Java. En el campo Artefacto, definiremos el ID del artefacto, que también será el nombre de nuestro proyecto.

Por último, seleccionamos las dependencias que se utilizarán en el ejemplo. Para esta aplicación, seleccionamos las siguientes dependencias Spring Boot DevTools, Lombok, Spring Reactive Web, Spring Data R2DBC y PostgreSQL Driver.

Una de las ventajas de Spring Boot es que proporciona paquetes iniciales que simplifican su configuración de Maven. Los iniciadores Spring Boot son en realidad un conjunto de dependencias que puede incluir en su proyecto. Puede escribir las dependencias en el campo de búsqueda o cambiar a la versión completa y ver todos los paquetes de inicio y las dependencias disponibles como se puede observar en la figura 1.

Figura 1

Estructura de la aplicación

Como se puede observar la figura 2, la estructura de la aplicación estará divida por paquete:

Figura 2

El archivo pom.xml contiene toda la información básica sobre el proyecto. Como pueden observar en este archivo, podemos adicionar las dependencias, configurar los datos, los plugins y las propiedades del proyecto:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.jugnicaragua</groupId>
	<artifactId>demo-app-reactive</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo-app-reactive</name>
	<description>Demo project for Spring Boot Reactive</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-r2dbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>
		<dependency>
			<groupId>org.flywaydb</groupId>
			<artifactId>flyway-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>io.r2dbc</groupId>
			<artifactId>r2dbc-postgresql</artifactId>
		</dependency>
		<dependency>
			<groupId>io.r2dbc</groupId>
			<artifactId>r2dbc-spi</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-r2dbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</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>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>${java.version}</source>
					<target>${java.version}</target>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
							<version>1.18.12</version>
						</path>
					</annotationProcessorPaths>
					<compilerArguments>
						<AaddGeneratedAnnotation>false</AaddGeneratedAnnotation>
						<Adebug>true</Adebug>
					</compilerArguments>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<repositories>
		<repository>
			<id>spring-plugins</id>
			<name>Spring plugins for r2dbc-postgresql</name>
			<url>https://repo.spring.io/plugins-release/</url>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>projectlombok.org</id>
			<url>https://projectlombok.org/edge-releases</url>
		</repository>
	</repositories>
</project>

A continuación daremos una breve explicación de las clases o interfaces que se utilizarán en el ejemplo.

Producto.java

Esta clase de modelo representa un producto. Contiene un id de producto que se  auto-genera, con sus respectivo nombre, descripción, estado y precio, como se puede observar la figura 3.

package com.jugnicaragua.demoappreactive.modelo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import java.math.BigDecimal;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Table("productos")
public class Producto {
    @Id
    private Long id;
    private String nombre;
    private String descripcion;
    private String estado;
    private BigDecimal precio;
    public Producto(String nombre, String descripcion, String estado, BigDecimal precio) {
        this.nombre = nombre;
        this.descripcion = descripcion;
        this.estado = estado;
        this.precio = precio;
    }
}

Figura 3

IProductoRepositorio.java

El Spring Data Reactive Relational Database Connectivity (R2DBC) proporciona una interfaz ReactiveCrudRepository para dar soporte a las operaciones básicas de las operaciones CRUD, dándole funcionalidades a la clase entidad, como se puede observar la figura 4:

package com.jugnicaragua.demoappreactive.repositorio;
import com.jugnicaragua.demoappreactive.modelo.Producto;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
@Repository
public interface ProductoRepositorio extends ReactiveCrudRepository<Producto, Long> {
}

Figura 4

ProductoServicio.java

La clase de ProductoServicio facilita la comunicación del repositorio con las operaciones de guardar, actualizar, eliminar, listar por IdProducto y listar todas los productos, como se puede observar la figura 5.


package com.jugnicaragua.demoappreactive.servicio;
import com.jugnicaragua.demoappreactive.modelo.Producto;
import com.jugnicaragua.demoappreactive.repositorio.ProductoRepositorio;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class ProductoServicio {
    private final ProductoRepositorio productoRepositorio;

    public ProductoServicio(ProductoRepositorio productoRepositorio) {
        this.productoRepositorio = productoRepositorio;
    }
    public Flux<Producto> findAll() {
        return productoRepositorio.findAll();
    }
    public Mono<Producto> findById(Long id) {
        return productoRepositorio.findById(id);
    }
    public Mono<Producto> save(Producto producto) {
        return productoRepositorio.save(producto);
    }
 
    public Mono<Void> deleteById(Long id){
        return this.productoRepositorio.findById(id)
                .flatMap(this.productoRepositorio::delete);
    }
    
}

Figura 5

Como pueden observar, el método findAll() del Repositorio que se usa para tomar “reactivamente” todos los registros de la Base de Datos, por eso su tipo de retorno es un Flux<Producto> y no el clásico List<Producto>. ¿Pero qué es un Flux? Como lo mencione al principio del artículo, es una secuencia asíncrona de elementos 0 a N. Por lo tanto de la misma manera, como pueden observar el Mono, que es un Resultado Asíncrono 0 a 1 elementos.

Ahora, necesitamos crear nuestro RestController, como se puede observar la figura 6.:

ProductoRestController.java

package com.jugnicaragua.demoappreactive.controlador;
import com.jugnicaragua.demoappreactive.modelo.Producto;
import com.jugnicaragua.demoappreactive.servicio.ProductoServicio;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/productos")
public class ProductoRestController {
    private final ProductoServicio productoServicio;

    public ProductoRestController(ProductoServicio productoServicio) {
        this.productoServicio = productoServicio;
    }
    @GetMapping
    public Flux<Producto> list() {
        return productoServicio.findAll();
    }
    @GetMapping("/{id}")
    public Mono<Producto> findById(@PathVariable Long id) {
        return productoServicio.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Producto> create(@RequestBody Producto producto) {
        return productoServicio.save(producto);
    }
    @PutMapping("/{id}")
    public Mono<ResponseEntity<Producto>> update(@PathVariable Long id,
                                                 @RequestBody Producto producto) {
        return productoServicio.findById(id)
                .flatMap(existingProduct -> {
                    existingProduct.setNombre(producto.getNombre());
                    existingProduct.setDescripcion(producto.getDescripcion());
                    existingProduct.setPrecio(producto.getPrecio());
                    return productoServicio.save(existingProduct);
                })
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }
    @DeleteMapping("/{id}")
    public Mono<Void> deleteById(@PathVariable Long id){
        return productoServicio.deleteById(id);
    }
}

Figura 6

PostgresConfiguration.java

Necesitamos configurar PostgreSQL para R2DBC, como se puede observar la figura 7.

package com.jugnicaragua.demoappreactive.config;
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.transaction.ReactiveTransactionManager;
@Configuration
@EnableR2dbcRepositories
public class PostgresConfiguration extends AbstractR2dbcConfiguration {
    @Value("${spring.r2dbc.url}")
    private String url;
    @Value("${spring.r2dbc.username}")
    private String username;
    @Value("${spring.r2dbc.password}")
    private String password;
    @Bean
    @Override
    public ConnectionFactory connectionFactory() {
        return new PostgresqlConnectionFactory(
                PostgresqlConnectionConfiguration.builder()
                        .host(RegexWizard.getJdbcHost(url))
                        .port(RegexWizard.getJdbcPort(url))
                        .username(username)
                        .password(password)
                        .database(RegexWizard.getDbName(url))
                        .build());
    }
    @Bean
    ReactiveTransactionManager transactionManager(ConnectionFactory connectionFactory) {
        return new R2dbcTransactionManager(connectionFactory);
    }
}

Figura 7

RegexWizard.java

La clase de utilidad RegexWizard, como se puede observar la figura 8.

package com.jugnicaragua.demoappreactive.config;
import java.net.URI;
public class RegexWizard {
    public static String getJdbcHost(String jdbc) {
        return getUri(jdbc).getHost();
    }
    public static int getJdbcPort(String jdbc) {
        return getUri(jdbc).getPort();
    }
    public static String getDbName(String jdbc) {
        return getUri(jdbc).getPath().substring(1);
    }
    private static URI getUri(String jdbc) {
        String substring = jdbc.substring(5);
        return URI.create(substring);
    }
}

Figura 8

Para mas información sobre R2DBC pueden visitar la siguiente pagina https://r2dbc.io/

Nuestro archivo application.properties necesita tener nuestras credenciales de la Base de Datos.

Application.properties

Spring Boot application.properties nos permite configurar la aplicación Spring Boot, como se puede observar en la figura 9. Nuestra aplicación se ejecutará en el puerto 8080 conectado a una base de datos PostgreSQL con nombre de usuario postgres y contraseññ postgres.

spring.port=8080
spring.r2dbc.url=jdbc:postgresql://localhost:5432/jugnicaragua
spring.r2dbc.username=postgres
spring.r2dbc.password=postgres
spring.r2dbc.initialization-mode=always

Figura 9

Ejecutar en POSTMAN

Crear un nuevo producto

Crear una nueva solicitud POST para crear un nuevo registro de producto. Una vez que el registro se crea con éxito, obtenemos el ID de la producto como respuesta, como se puede observar en la figura 10:

http://localhost:8080/productos

Figura 10

Ver todos los productos registrados

Es una operación GET devuelve todos los productos registrada en la aplicación, como se puede observar en la figura 11:

http://localhost:8080/productos

Figura 11

Actualizar un producto

Crear una nueva solicitud PUT para actualizar un registro de algún producto, como se puede observar en la figura 12:

http://localhost:8080/productos

Figura 12

Repositorio

Puedes descargar el código de la aplicación desde aquí:

Conclusión

Se desarrolló una aplicación de Spring WebFlux conectada con una base de datos PostgreSQL. La personas que quieran seguir aprendiendo pueden crear un ejemplo con Websockets para transmitir datos de forma reactiva desde una API REST.

Referencias  

Invitación

Por último, te invitamos a un webinar que tendremos en vivo este sábado 26 de septiembre a las 16 horas (GMT-6, hora Centroamérica)
Link de Registro:

https://us02web.zoom.us/webinar/register/7316001055137/WN_xi2FxnQ8Sue5BaYsw1dX1w

Nosotros y terceros seleccionados utilizamos cookies o tecnologías similares con fines técnicos y, con su consentimiento, para otras finalidades (“interacciones y funcionalidades básicas”, “mejora de la experiencia”, “medición” y “segmentación y publicidad”) según se especifica en la política de cookies. Usted es libre de otorgar, denegar o revocar su consentimiento en cualquier momento.    Configurar y más información
Privacidad