Introducción a Spring Reactor y Spring Webflux
Introducción
Este capítulo presenta brevemente las novedades responsivas de Spring 5.
El objetivo de la programación responsiva es simplificar la escritura de aplicaciones asíncronas que, en lugar de utilizar el mecanismo clásico de un pool voluminoso de threads bloqueantes, utiliza un bucle y un número mínimo de threads gracias a un motor basado en un framework NIO como Netty.
Ponemos en cola los procesamientos rápidos de realizar, y se llevan a cabo a lo largo del tiempo. Entonces, es posible modelar estos procesamientos en forma de flujos y propagaciones de cambios. Podemos gestionar flujos estáticos con tablas o flujos dinámicos con transmisores y receptores de eventos, como en el design pattern observador.
En un contexto de servidor de un clúster elástico, este tipo de procesamiento «en serie» permite saber si la carga en un servidor está alineada con la potencia del clúster a través de la backpressure, que mide el volumen del flujo aceptable en la entrada del servidor. Si se supera un umbral, podemos aumentar el número activo de nodos en el clúster para aligerar la carga por servidor y, seguidamente, reducir el número de nodos si la carga baja.
El objetivo es que los productores (Publisher) no abrumen a los consumidores (Consumer). Esto es particularmente adecuado para microservicios en el cloud.
El diseño de servidores...
Spring Reactor
1. Presentación
Históricamente, teníamos RxJava 1 y 2 para hacer programación responsiva.
Spring decidió implementar Reactor, su propia versión de 4.ª generación de un motor responsivo, para hacer el mejor uso de las tecnologías actuales.
El reactor está particularmente bien adaptado a Spring. Puede interactuar fácilmente con RxJava y con el equivalente del JDK9: java.util.concurrent.Flow.
Una aplicación responsiva se caracteriza por estos puntos:
-
Responsivo: proporciona tiempos de respuesta rápidos y consistentes.
-
Resiliente: sigue siendo responsivo en caso de error y se debe recuperar.
-
Elástico: sigue siendo responsivo y es capaz de manejar varias cargas de trabajo.
-
Message driven: se comunica mediante mensajes asíncronos.
Las aplicaciones responsivas no son más rápidas que las tradicionales, sino que tienen un comportamiento mucho mejor controlado y predecible cuando el servidor llega a saturarse. Aunque la aplicación Java esté saturada, el sistema host sigue siendo totalmente accesible. Las aplicaciones son más intuitivas de programar y mejor estructuradas a través de la programación funcional.
Por un lado, tenemos el Publisher (editor) que produce uno o más elementos, que posteriormente los consume un Consumer (consumidor). Se debe considerar que estos elementos intercambiados son eventos.
Cabe destacar que la programación es asíncrona. El código ya no se ejecuta secuencialmente. Dos procesos de flujo que se suceden en el código se inician en paralelo. No esperamos a que una secuencia termine de procesarse para pasar al siguiente flujo. Paradójicamente, el sistema solo inicia un proceso a la vez en su cola. Por lo tanto, será necesario sincronizar los flujos.
Hay dos tipos de Publisher:
-
El que emite 0 o 1 elemento:
Mono: reactor.core.publisher.Mono<T>
-
El que emite de 0 a N elementos:
Flux: reactor.core.publisher.Flux<T>
Es posible utilizar Mono y Flux sin utilizar programación funcional. Sin embargo, las lambdas son más concisas e integradas en java 8. Por lo tanto, nos centramos solo en ejemplos que usan la programación funcional.
La programación funcional implica conocer bien las lambdas.
La programación de un Publisher se realiza en tres pasos:
-
Crear el flujo.
-
Rellenar...
WebFlux
Normalmente usaremos Reactor como parte de las aplicaciones WebFlux.
De hecho, las API Responsive en flujos de eventos estáticos a menudo se sustituyen por Streams tradicionales. WebFlux es el equivalente responsivo de Spring MVC y utiliza el motor Reactor internamente.
Diseñar una aplicación WebFlux desde cero es complejo. Recientemente, JHipster permite generar aplicaciones completas usándolo, y su uso puede ahorrar mucho tiempo.
Para utilizar Spring WebFlux, debe añadir la dependencia Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.6.7</version>
</dependency>
Hay disponible un ejemplo completo generado con JHipster en los ejemplos descargables del libro. Los detalles al respecto se estudian en un capítulo específico.
1. Definición del término responsivo
Las nociones de responsividad en WebFlux son de varios tipos. En lugar de asignar o asociar a través de un pool un thread a una consulta para gestionar todo el procesamiento de una consulta hasta que se responde, dividimos el procesamiento en pequeños elementos que se ponen en cola y se ejecutan lo antes posible.
Procesamos este elemento en un entorno asíncrono sin bloqueo.
En un servidor Spring MVC, las consultas se procesan siempre que haya threads disponibles. Cuando estos se agotan, el cliente HTTP se pone a la espera. El cliente está esperando una conexión disponible. |
En un servidor Spring WebFlux, los clientes no se bloquean. Si hay demasiadas llamadas, también saturamos el sistema, pero en otro lugar, en el bucle de eventos. |
La diferencia radica principalmente en el hecho de que no tenemos threads bloqueados esperando el resultado de una etapa algo larga.
Podríamos pensar que tenemos el equivalente usando Spring MVC de manera asíncrona, pero Spring Async soporta las especificaciones Servlet 3.0, mientras que Spring WebFlux soporta Servlet 3.1+. Se puede bloquear el modelo de E/S Spring Async al comunicarse con el cliente, lo que provoca un problema de rendimiento con clientes lentos, mientras que WebFlux proporciona un modelo de E/S sin bloqueo.
La lectura del cuerpo de la consulta o de partes de la consulta se bloquea en Spring Async, mientras...
Cliente responsivo
Podemos usar la clase WebClient, que se introdujo en Spring 5 y es la equivalente del RestTemplate tradicional. Es un cliente sin bloqueo que soporta flujos responsivos. Permite recuperar datos de los endpoints proporcionados por el controlador WebFlux. La clase WebClient va a sustituir a RestTemplate, que quedará obsoleta en breve.
Vamos a crear un UsuarioWebClient sencillo:
public class UsuarioWebClient {
WebClient client = WebClient.create("http://localhost:8080");
// ...
}
Recuperación de un único recurso:
Mono<Usuario> usuarioMono = client.get()
.uri("/usuarios/{id}", "1")
.retrieve()
.bodyToMono(Usuario.class);
usuarioMono.subscribe(System.out::println);
Recuperación de una colección:
Flux<Usuario> usuariosFlux = client.get()
.uri("/usuarios")
.retrieve()
.bodyToFlux(Usuario.class);
employeeFlux.subscribe(System.out::println);
Pruebas con WebFlux
Spring ofrece una serie de herramientas para facilitar las pruebas.
1. Pruebas unitarias
Las pruebas unitarias se corresponden con pruebas de bajo nivel.
a. Pruebas unitarias con aplicaciones responsivas
Podemos utilizar el tradicional Mockito.
Para una clase:
public class UsuarioService {
public Mono<Usuario> getUsuarioById(Integer usuarioId) {
return webClient
.get()
.uri("http://localhost:8080/usuario/{id}", usuarioId)
.retrieve()
.bodyToMono(Usuario.class);
}
}
Podemos tener la prueba unitaria:
@ExtendWith(MockitoExtension.class)
public class UsuarioServiceTest {
@Test
void givenUsuarioId_whenGetUsuarioById_thenReturnUsuario() {
Integer usuarioId = 100;
Usuario mockUsuario = new Usuario(1, "Pepe", 51);
when(webClientMock.get())
.thenReturn(requestHeadersUriSpecMock);
when(requestHeadersUriMock.uri("/usuario/{id}", usuarioId))
.thenReturn(requestHeadersSpecMock);
when(requestHeadersMock.retrieve())
.thenReturn(responseSpecMock);
when(responseMock.bodyToMono(Usuario.class))
.thenReturn(Mono.just(mockUsuario));
Mono<Usuario> usuarioMono =
usuarioService.getUsuarioById(usuarioId);
StepVerifier.create(usuarioMono)
.expectNextMatches(usuario -> usuario.getNombre()
.equals("Pepe"))
.verifyComplete();
}
}
b. Uso de MockWebServer
MockWebServer fue desarrollado por el equipo Squaere, cuyo trabajo encontrará en el sitio web https://github.com/square/okhttp/tree/master/mockwebserver
MockWebServer es un servidor web que permite ejecutar scripts para probar clientes HTTP enviando solicitudes y verificando respuestas.
Se utiliza como mockito:
-
Codificar los mocks en script.
-
Ejecutar el código...
Server Site Event con Spring
En nuestras aplicaciones, a menudo se requieren refrescos para llamadas asíncronas. Hay varias tecnologías para esto. La más sencilla es el pooling, que consiste en consultar regularmente al servidor para conocer el avance de un procesamiento. Otra evolución es el long pooling, que consiste en mantener abierta la conexión HTTP; pero estas dos técnicas son costosas a nivel de red. Existe una evolución con webhooks y Publish-Subscribe. También tenemos los websockets, que son bidireccionales, y Server Site Event o SSE, para abreviar, que es un estándar HTTP que permite a una aplicación web manejar un flujo unidireccional de eventos y recibir actualizaciones cada vez que el servidor emite datos. Esta solución suele ser más adecuada en un contexto web que no necesite la característica en tiempo real de los websockets. Por lo tanto, perdemos algo de tiempo, pero algunas veces esto es sostenible.
La versión Spring 4.2 ya soportaba SSE, pero, a partir de Spring 5, hay una forma más idiomática y práctica de gestionarlo a través de un flujo y un elemento ServerSiteEvent, gracias a las API responsivas de Spring WebFlux. Creamos un endpoint de streaming SSE, siguiendo las especificaciones del W3C que designan el tipo MIME como text/event-stream:
@GetMapping(path = "/stream-flux"...
Para ir más allá
La adopción de aplicaciones responsivas es muy lenta. A priori, tenemos los mismos obstáculos que para el uso de Domain Driven Design, arquitecturas hexagonales, Event Sourcing, etc. Por el momento, a menudo se considera más fácil trabajar de la manera tradicional, incluso si esto implica más servidores.
Spring proporciona un conjunto completo, que funciona con Java y Kotlin. Los servidores más innovadores están en Kotlin, utilizan API responsivas y funcionan con Flux en el Cloud. Las aplicaciones responsivas con frecuencia usan bases de datos Kafka y Cassandra y están basadas en eventos.
El número de servidores se convierte en un criterio importante cuando se utiliza un Cloud de Amazon, Google, Azure u otra nube porque se busca reducir el número de máquinas.
Puntos clave
-
La adopción de aplicaciones responsivas es lenta.
-
Reactor es un motor que permite programar aplicaciones responsivas.
-
WebFlux es el equivalente de Spring MVC en el mundo responsivo.
-
WebFlux soporta la backpressure.
-
Para crear un servicio web responsivo, necesita una base de datos que soporte las API responsivas.
-
Spring Data se puede utilizar para repositorios responsivos.
-
R2DBC se puede utilizar para bases de datos SQL responsivas soportadas.