Spring Cloud Stream:event-driven microservices

Existen muchas opciones para conectar microservicios entre sí, las comunicaciones no tienen porque ser todas síncronas mediante invocaciones directas haciendo uso del servicio de descubrimiento; también podemos realizar invocaciones asíncronas haciendo uso de brokers de mensajería.

Índice de contenidos.


1. Introducción.

Spring Cloud Stream es un proyecto de Spring Cloud construido sobre la base de Spring Boot y Spring Integration que nos facilita la creación de microservicios bajo los patrones de message-driven y event-driven.

El hecho de estar construido sobre la base de Spring Boot hace que sea muy sencilla su configuración mediante el soporte de starters, anotaciones y propiedades de configuración susceptibles de elevarse a un servicio de configuración centralizada y es el soporte de Spring Integration el que proporciona la conectividad con brokers de mensajería.

El patrón message-driven, en el entorno de microservicios, introduce el concepto de comunicación asíncrona entre microservicios dirigida por el envío de mensajes; un mensaje es un dato que se envía a un destino específico.

El patrón event-driven, sobre la base del patrón anterior y usando el mismo canal de comunicaciones, se orienta al envío de eventos; un evento es una señal emitida por un componente al alcanzar un determinado estado.

Sobre la base de estos principios se construyen patrones como CQRS y Sagas que, en un entorno de microservicios nos ayudan a mantener, eventualmente, consistente la información, mediante la generación de eventos de dominio (conceptos de DDD).

En este tutorial haremos una introducción al proyecto Spring Cloud Stream, como candidato para implementar dichos patrones en un entorno de microservicios, abstrayéndonos totalmente de la capa de transporte y sin necesidad de conocer detalles sobre el protocolo de comunicación.

Se presuponen conocimientos de Spring Boot y Spring Cloud, puesto que no se generará ninguno de los proyectos en los que se basan las pruebas desde cero.


2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Oracle Java: 1.8.0_25
  • Spring Cloud Ditmars.SR3
  • Spring Boot 1.5.9.RELEASE

3. Configuración.

Se recomienda la configuración mediante el sistema de gestión de dependencias de maven y, para ello, lo primero es incluir las siguientes dependencias en nuestro pom.xml

En cuanto incluimos la dependencia de Spring Cloud Stream, que arrastra de forma transitiva la de Spring Cloud Stream Binder, cualquier aplicación Spring Boot con la anotación @EnableBinding intentará engancharse al broker de mensajería externo que encuentre en el classpath (Rabbit MQ, Apache Kafka,…).

A continuación definiremos los canales de comunicaciones en el fichero de configuración de nuestras aplicaciones (application.yml) y aquí vamos a hacer una distinción entre el productor de los eventos (command) y el consumidor de los mismos (query).

Esta sería la configuración del productor (command):

A continuación la configuración del consumer (query):

La diferencia está en la semántica del canal, en este segundo usamos “input” en vez de “output”.

Por último, antes de comenzar con nuestra emisión de eventos, vamos a incluir las dependencias necesarias para poder ejecutar tests de integración, como sigue:


4. Publicación de un evento (command).

Para publicar un evento lo primero es definir la estructura de datos del evento a emitir, en nuestro caso vamos a publicar un evento de generación de un pedido, con la información del pedido creado:

En este punto debemos tomar una decisión, ¿compartimos estructuras de los datos de los eventos entre microservicios?. Si añadimos a la estructura de los eventos tipos complejos como en este ejemplo, se convertirá en un problema de dependencias y no queremos hacer que nuestros microservicios dependan de una única estructura de dominio con lo que, pensadlo primero, la estructura de datos de los eventos debería tener su propia estructura de datos independiente del dominio de negocio de cada microservicio. No deberíamos incluir en el evento objetos específicos de dominio como, en el ejemplo, el pedido.

A continuación, vamos a añadir a nuestra configuración la anotación @EnableBinding(Processor.class) para realizar un binding con el stream correspondiente (por defecto con Processor.class tendremos los canales “output” e “input” que ya hemos definido en el application.yml).

Podemos configurar una fuente (Source.class), un destino (Sink.class) o ambos (Producer.class), lo único que estamos haciendo es declarar como canales “input” y/o “output”, según el caso.

Lo siguiente es publicar el evento y lo vamos a enviar en nuestro servicio de negocio justo después de persistir la entidad; para ello debemos recibir la inyección del repositorio (Spring Data) y la fuente para poder realizar el envío.

Una vez hecho esto vamos a comprobar que realmente se envía escribiendo el siguiente test de integración:

Declaramos por inyección de dependencias un recolector de mensajes y el canal fuente definido en el binder; con este último configuramos el recolector y comprobamos que una vez invocado el servicio de negocio podemos consumir un evento almacenado en el recolector que tiene como payload la estructura de datos del evento enviado.

En este punto deberíamos estar en verde.


5. Consumo de un evento (query).

Consumir el evento desde un test de integración esta más que bien, pero el objetivo es consumirlo desde otro microservicio, para lo cuál debemos realizar la misma configuración en cuanto a dependencias de maven en nuestro segundo microservicio y crear la estructura de datos a la que se mapeará la recepción del evento.

Lo siguiente es configurar el binder y un método de listener que escuchará los eventos:

El listener solo traza la recepción del evento con el sistema de logging estándar (Simple Logging Facade For Java).

Y, como no, vamos a probar el envío y la recepción con un test de integración dentro del contexto del propio microservicio:

Simulamos el servicio que emite un evento generándolo nosotros manualmente y como el listener simplemente traza un mensaje y en el entorno de test la salida es por consola, la capturamos y esperamos 10 segundos a que se imprima.

En este punto deberíamos estar también en verde.


6. Integración del productor (command) y el consumidor (query).

Tenemos ambos tests de integación funcionando y ahora vamos a realizar una prueba integrada de los dos microservicios para lo cuál necesariamente tendremos que levantar el broker de mensajería en local para comprobar el envío y la recepción.

Para levantar en local un Rabbit MQ, que mejor que docker y con el siguiente comando lo tendremos funcionando en un solo paso, exponiendo los mismos puertos que ya tenemos configurados en el application.yml de ambos microservicios (en entornos productivos en nuestro servicio de configuración centralizada).

Al finalizar el comando anterior, con el siguiente docker ps deberíamos tener un contenedor levantado como el que se muestra a continuación:

Si levantamos los dos microservicios con un perfil de desarrollo, sin necesidad de depender de los servicios estructurales de Spring Cloud y probamos a lanzar una operación contra el API que termina generando un pedido podremos comprobar como se emite el evento desde el microservicio de command y podemos consumirlo desde el microservicio de query.

A continuación os muestro un ejemplo de invocación desde postman con la confirmación de la creación del recurso con la cabecera de location:

Si accedemos a la consola del segundo microservicio podremos comprobar cómo se imprime por consola la traza del evento aunque también podemos comprobar, desde la consola de administración del propio Rabbit MQ, las estadísticas de envío de mensajes en el canal que hemos definido.

Para ello no tenemos más que acceder a través de un navegador a http://localhost:15672/


7. Referencias.


8. Conclusiones.

Continuamos explotando las posibilidades del stack de Spring Cloud y nos sorprendemos de la sencillez; cada vez más nos olvidamos de la configuración y uso de plantillas para acercarnos a los conceptos de los patrones que queremos implementar.

Un saludo.

Jose