Primeros pasos hacia la creación de microservicios reactivos con Java moderno

Juan Irabedra
.
August 14, 2023
Primeros pasos hacia la creación de microservicios reactivos con Java moderno

La programación reactiva definitivamente se está haciendo un hueco. Desde RxJS hasta Akka Streams, surgieron bibliotecas reactivas en muchos lenguajes de programación para fomentar los principios del paradigma reactivo, tal como se establece en el Manifiesto reactivo.

La programación reactiva a menudo se explica en términos demasiado teóricos. Según el Manifiesto Reactivo, todos los sistemas responsivos, resilientes, elásticos y basados en mensajes son reactivos. La capacidad de respuesta tiene que ver con el tiempo de respuesta y el rendimiento. La resiliencia tiene que ver con la tolerancia a fallos y la disponibilidad. La elasticidad implica la capacidad de soportar diferentes cargas en diferentes momentos, lo que puede afectar al rendimiento, la disponibilidad y los gastos operativos. Además, la gestión de mensajes permite que el sistema se comporte de acuerdo con los mensajes que pasan de un componente a otro, estableciendo límites claros entre los componentes y manteniendo la transparencia de las interacciones entre ellos.

Nota sobre las transmisiones: las transmisiones son una abstracción de datos ilimitados. A menudo se comparan con lotes. Imagina que tenemos dos números enteros. Aplicar una función común a este tipo de números enteros, como el operador de suma (+), parece factible. Ahora, ¿qué pasa si los operandos no dejan de aparecer porque, por ejemplo, provienen de alguna entrada de usuario, que utiliza alguna aplicación en tiempo real? Entonces, la suma se convierte en un desafío. El uso de las transmisiones aprovecha un enfoque diferente para este problema. Profundizaremos en la composición de transmisiones más adelante.

Los flujos de eventos son particularmente eficaces en los lenguajes de programación que admiten la programación funcional hasta cierto punto. Los lenguajes multiparadigmáticos, como C# o Scala, tienen algunas abstracciones integradas para representar los flujos de datos. En este artículo, exploraremos una implementación particular de flujos reactivos utilizando Java moderno y Proyecto Reactor. Esto nos servirá como capa de servicio en una aplicación web. En futuros artículos analizaremos cómo esta capa de servicio podría interoperar con una WebFlux capa de acceso web. ¡Síguenos!

Lo que necesitarás

Para seguir esta guía, asegúrese de tener:

Basic knowledge of Java applications and functional programming

Experience developing some kind of asynchronous algorithms

Some development environment, such as Visual Studio Code or IntelliJ IDEA

Java 17 (which can be installed directly on IntelliJ)

Qué puede esperar

Después de completar esta guía, usted debería poder:

Identify basic use cases for reactive programming

Explain how to set up a basic Reactor application

Feel comfortable reading basic algorithms written in a reactive style

Recommend articles and blog posts written by experts to sustain decisions on architecture and design similar to the ones used in the example.

En esta guía utilizaremos Java, Gradle, Spring-Boot e IntelliJ CE.

La aplicación

Crearemos una PoC de microservicio de procesamiento de interacciones con el usuario. Nos centraremos en la capa de servicio y suprimiremos la capa de acceso web (que implementaremos en WebFlux en otro artículo) y la capa de acceso a los datos.

Creación de la aplicación

Para empezar, crea una aplicación básica de Spring con Gradle. Puedes hacerlo usando Spring Initializer, configurándolo manualmente o clonando nuestro proyecto inicial en GitHub. Maven también funciona, pero preferimos la sintaxis sucinta de Gradle en lugar de XML. Vamos a buscar nuestras dependencias de Repositorio Mvn. Vamos a presentar brevemente las principales dependencias utilizadas en nuestro proyecto:

Nuestro bloque de dependencias build.gradle tiene este aspecto:

no-line-numbers|dark|gradledependencies { implementation ‘org.springframework.boot:spring-boot-starter’ testImplementation ‘org.springframework.boot:spring-boot-starter-test’ compileOnly ‘org.projectlombok:lombok:1.18.28’ annotationProcessor ‘org.projectlombok:lombok:1.18.28’ testCompileOnly ‘org.projectlombok:lombok:1.18.28’ testAnnotationProcessor ‘org.projectlombok:lombok:1.18.28’ implementation ‘io.projectreactor:reactor-core:3.5.8’}

En primer lugar, veamos algunas de las características de Lombok. Una de nuestras funciones favoritas es el patrón de construcción incorporado y las clases de datos. Con esto, podemos escribir una clase de modelo simple en un archivo llamado DomElement.java dentro del paquete de modelos con este código:

no-line-numbers|dark|java@Data@Builder(setterPrefix = “with”)public class DomElement { private String name; private String type;}

El @Data La anotación lombok nos permite prescindir de los constructores, los descriptores de acceso, los modificadores, el comparador de igualdad y los métodos ToString. La función de datos de Lombok se comporta de forma similar a la de los registros de Java, aunque no son exactamente iguales. Las clases de @Data se pueden ajustar con precisión y la personalización se puede encontrar en la documentación de Lombok. Por ejemplo, los campos marcados como transitorio no se tendrá en cuenta para los comparadores de igualdad o los códigos hash.

La anotación @Builder nos permite evitar la creación de numerosos constructores sobrecargados o saturar nuestras clases con un exceso de setters. En vez de eso, nos permite usar el patrón Builder e invocar en cadena nuestros setters con un poco de azúcar sintáctico por encima. Termina construyendo un elegante Interfaz fluida. En Baeldung hay un artículo que vale la pena echar un vistazo sobre la combinación del patrón Builder con Fluent Interfaces.

Debido al alcance que tiene Lombok en nuestras dependencias de Gradle, podemos acceder a los métodos proporcionados por el Builder durante el tiempo de desarrollo. Eche un vistazo al Prefijo Setter opción que agregamos a nuestro constructor.

En lugar de llamar a los creadores de vacíos tradicionales, podemos usar su correspondiente con [atributo] método en su lugar, y encadenarlos. Los setters de Builder no son nulos, por lo que pueden componerse y llamarse en cadena. Para obtener el objeto que se está creando, la llamada a la cadena de métodos debe terminar con .construir ()

Añada también un modelo UserInteraction. El propósito de esta clase es representar alguna interacción del usuario con un elemento DOM. Vamos a modelar esto como una clase contenedora para un campo DOMElement y un campo UserID, que es una cadena. Cree un archivo UserInteraction.java en modelos empaqueta y haz que se vea así:

no-line-numbers|dark|java@Data@Builder(setterPrefix = “with”)public class UserInteraction { private DomElement domElement; private UUID userId; private String interaction; private long unixTimestamp;}

Ahora podemos empezar con el servicio en sí. Determinaremos el origen de las interacciones de los usuarios (en este contexto, el controlador que las envía a la capa de servicio), codificando algunos Java Beans. Mantendremos las interacciones de los usuarios en una estructura de datos conveniente. Más adelante, integraremos la estructura en nuestra aplicación para poder sustituirla fácilmente por un controlador de API que pueda estar escuchando algún componente de la interfaz.

Sería interesante filtrar las interacciones de los usuarios. Por ejemplo, enumerar solo las interacciones de los usuarios que se produjeron entre dos marcas de tiempo o filtrarlas según el tipo de DOMElement. Diseñemos este comportamiento por contrato en nuestro servicio.

no-line-numbers|dark|javapublic interface UserInteractionService {List filterByDomElementType(String domElementType);List getUserMostRecentInteraction(UUID userUUID);}

Este es casi el camino a seguir. Nos falta el aspecto reactivo de las cosas. Para que este contrato sea reactivo, vamos a utilizar uno de los conceptos clave de Java Reactor: el Flux.

En el Documentación del proyecto Reactor Flux podemos encontrar la siguiente ilustración, que explica claramente cómo funciona Flux.

Flujo y Mono son las principales abstracciones de Reactive Streams utilizadas por Reactor. Mono es similar a Flux, pero acepta como máximo un elemento. La mayoría de los operadores Flux y Mono aceptan uno o más de este tipo y también devuelven uno de esos. Observe el siguiente ejemplo:

no-line-numbers|dark|javaFlux uppercaseCities = Flux.fromArray(new String[]{“boston”, “new york”, “seattle”}).map(city -> city.toUpperCase());

Este fragmento de código presenta una forma de crear un Flux a partir de una matriz y, a continuación, transforma todos los elementos inicialmente presentes en la matriz de cadenas en mayúsculas, lo que produce un nuevo Flux. Los flujos se pueden crear a partir de fuentes nativas de Reactor y de la mayoría de los iterables de Java. Tenga en cuenta que la mayoría de las operaciones de Flux se pueden encadenar de forma fluida, tal como definimos nuestro Builder anteriormente.

El contrato reactivo de nuestra clase UserInteractionService tendrá este aspecto:

no-line-numbers|dark|javapublic interface UserInteractionService {Flux filterByDomElementType(String domElementType);Mono getUserMostRecentInteraction(UUID userUUID);}

Vamos a crear una implementación para esta interfaz. Crearemos una implementación sencilla con datos iniciales y la refactorizaremos sobre la marcha para mantenerla ordenada, aprovechar las capacidades de Spring y fomentar la capacidad de mantenimiento. Empieza añadiendo algunos datos de prueba. ¡Te ayudaremos con eso!

no-line-numbers|dark|javaprivate List supplyData(){DomElement button = DomElement.builder() .withName(“register-button”) .withType(“button”) .build();DomElement contactUsLink = DomElement.builder() .withName(“contact-us-link”) .withType(“href”) .build();UserInteraction contactUsLinkHover = UserInteraction.builder() .withDomElement(contactUsLink) .withUserId(UUID.randomUUID()) .withInteraction(“hover”) .withUnixTimestamp(System.currentTimeMillis() / 1000L) .build();UserInteraction contactUsLinkClick = UserInteraction.builder() .withDomElement(contactUsLink) .withUserId(UUID.randomUUID()) .withInteraction(“click”) .withUnixTimestamp((System.currentTimeMillis() + 3000) / 1000L) .build();UserInteraction registerButtonClick = UserInteraction.builder() .withDomElement(button) .withUserId(UUID.randomUUID()) .withInteraction(“click”) .withUnixTimestamp(System.currentTimeMillis() – 50000 / 1000L) .build();return List.of(contactUsLinkHover, contactUsLinkClick, registerButtonClick);}

Estas son las interacciones de los usuarios auxiliares con las que funcionará nuestro sistema en esta PoC. Refactoriza eso empaquetando todos esos datos codificados de forma rígida en un método privado, de modo que nuestros métodos públicos no se vean inundados de inicializaciones de datos secundarios.

no-line-numbers|dark|java@Overridepublic Mono getUserMostRecentInteraction(UUID userUUID) { Flux userInteractions = Flux.fromIterable(this.supplyData()); userInteractions.subscribe(System.out::println); return userInteractions .filter(ui -> ui.getUserId().equals(userUUID)) .reduce((ui1, ui2) -> ui1.getUnixTimestamp() > ui2.getUnixTimestamp() ? ui1 : ui2);}

Están pasando muchas cosas ahí. Vamos a desglosarlo un poco.

  1. En primer lugar, estamos creando un flujo a partir de las interacciones de nuestros usuarios a partir de la lista de stubs que codificamos. Podemos crear flujos a partir de cualquier iterable o matriz en Java.
  2. En el primer método, solo aplicamos una operación de filtrado según el tipo de elemento dom de la interacción. Solo mantendremos aquellos que coincidan con el parámetro del método.
  3. En el segundo método, primero llamamos a un método de suscripción. El propósito de este método de suscripción es imprimir los elementos de flujo uno por uno. El método de suscripción permite aplicar efectos secundarios y también activa las operaciones de flujo. Los flujos son perezosos: no pasa nada hasta que nos suscribimos. Después de eso, invocamos otro operador de filtro seguido de un operador de reducción, que terminará devolviendo un solo elemento: la interacción de usuario más reciente para el parámetro user id.
  4. El:: se denomina operador de referencia de método. Permite pasar una función como argumento a otras funciones que requieren dicho argumento.

El oleoducto de operaciones de Flux está llamado a ser perezoso. Fluxes y Monos son perezosos. No se activará ningún cálculo hasta que nos suscribamos. El cliente de la canalización es responsable de activar el cálculo y es libre de elegir cuándo hacerlo suscribiéndose. Es realmente interesante cómo podemos pensar en dos etapas: el montaje y la ejecución. Podemos encadenar muchos complejos (y recuerda: ¡asincrónicos y no bloqueantes!) operaciones sin preocuparse por las etapas: todo el oleoducto se activará de una sola vez.

Hay muchas ventajas a la hora de elegir estructuras perezosas. Por ejemplo, si nuestras operaciones de filtrado fueran condicionales, es decir, hay ciertas situaciones en las que no se ejecutarán, nuestro código no desperdiciará ningún recurso al definir el ensamblaje de flujo. Además, podemos materializar nuestra consulta cuando queramos.

Antes de refactorizar nuestra aplicación, probémosla. Agregue el siguiente código a su método principal. No te preocupes por la línea SpringApplication por ahora.

no-line-numbers|dark|java@SpringBootApplicationpublic class ContentRecommenderApplication { public static void main(String[] args) { UserInteractionService service = new UserInteractionServiceImpl(); service.filterByDomElementType(“button”) .subscribe(ui -> System.out.println(ui.toString())); SpringApplication.run(UserInteractionApplication.class, args); }}

Y ejecútalo.

Refactorización

Nuestra PoC funciona, pero está lejos de ser aceptable. Teniendo en cuenta que planeamos escalar este servicio PoC hasta convertirlo en un microservicio reactivo real, además de WebFlux, podemos arreglar un poco las cosas. Además, podemos aprovechar las capacidades de Spring Framework, como la inyección automática de dependencias.

Queremos conectar automáticamente las dependencias para que, cuando agreguemos algunos controladores WebFlux, puedan localizar fácilmente el servicio con el que se comunicarán. Además, podemos aprovechar la división entre la interfaz y la implementación para garantizar el cumplimiento del principio de inversión de dependencias. Tenga en cuenta que, en aras de la simplicidad, nuestras interfaces e implementaciones residirán en el mismo componente físico. Podrían dividirse en dos JAR para fomentar la capacidad de mantenimiento y la modularidad.

Además, queremos borrar la inicialización de datos codificados de nuestro servicio. Automatizaremos la inicialización de los datos en un paquete de acceso a los datos para que se burle del comportamiento de acceso a la base de datos.

Hay un detalle que solemos pasar por alto cuando diseñamos software y es un buen diseño de paquetes/módulos. A menudo diseñamos en función de las clases y los objetos, pero tendemos a olvidarnos de los módulos. Dividiremos los paquetes para cumplir con el principio de dependencia estable (según la dirección de la estabilidad) y el principio de abstracción estable (un paquete debe ser abstracto ya que es estable). En nuestro caso, tenemos paquetes completamente abstractos y completamente volátiles, y nuestros paquetes volátiles dependen de los paquetes abstractos. Además, hemos definido interfaces para las reglas de negocio, por lo que tiene sentido suponer que esos son nuestros paquetes estables.

Nuestros paquetes tendrán el siguiente aspecto:

Y definiremos tres beans en una clase @Configuration llamada ServiceConfiguration:

no-line-numbers@Beanpublic List userInteractions(){ DomElement button = DomElement.builder() .withName(“register-button”) .withType(“button”) .build(); DomElement contactUsLink = DomElement.builder() .withName(“contact-us-link”) .withType(“href”) .build(); UserInteraction contactUsLinkHover = UserInteraction.builder() .withDomElement(contactUsLink) .withUserId(UUID.randomUUID()) .withInteraction(“hover”) .withUnixTimestamp(System.currentTimeMillis() / 1000L) .build(); UserInteraction contactUsLinkClick = UserInteraction.builder() .withDomElement(contactUsLink) .withUserId(UUID.randomUUID()) .withInteraction(“click”) .withUnixTimestamp((System.currentTimeMillis() + 3000) / 1000L) .build(); UserInteraction registerButtonClick = UserInteraction.builder() .withDomElement(button) .withUserId(UUID.randomUUID()) .withInteraction(“click”) .withUnixTimestamp(System.currentTimeMillis() – 50000 / 1000L) .build(); return List.of(contactUsLinkHover, contactUsLinkClick, registerButtonClick);}@Beanpublic UserInteractionRepository userInteractionRepository(){ return new UserInteractionRepositoryImpl();}@Beanpublic UserInteractionService userInteractionService(){ return new UserInteractionServiceImpl();}

Ese es nuestro método SupplyData convertido en Bean más la instanciación de nuestras interfaces de servicio y repositorio. Ahora nuestra clase Service tiene un aspecto limpio y ya no es responsable de crear datos secundarios. Nuestro último paso es configurar nuestro método principal. Esto no debería ser necesario si ya teníamos nuestros controladores WebFlux, pero por el momento, haz que tenga el siguiente aspecto:

no-line-numberspublic static void main(String[] args) {SpringApplication.run(UserInteractionApplication.class, args);ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfiguration.class);UserInteractionService userInteractionService = ctx.getBean(UserInteractionService.class);userInteractionService.filterByDomElementType(“button”) .map(UserInteraction::toString) .subscribe(System.out::println);}

Y así es como podemos hacer que nuestro servicio obtenga todas las interacciones de los usuarios para los botones.

Consulta nuestra ejemplo completo aquí.

Próximos pasos

Hemos desarrollado una capa de servicio para un microservicio reactivo. El paso futuro más interesante es añadir WebFlux para que nuestra aplicación esté expuesta en la web. Además, ¡algún mecanismo de persistencia sin bloqueo de alto rendimiento también sería bienvenido!

Manténgase a la vanguardia de las últimas tendencias y conocimientos sobre big data, aprendizaje automático e inteligencia artificial. ¡No se lo pierda y suscríbase a nuestro boletín de noticias!

La programación reactiva definitivamente se está haciendo un hueco. Desde RxJS hasta Akka Streams, surgieron bibliotecas reactivas en muchos lenguajes de programación para fomentar los principios del paradigma reactivo, tal como se establece en el Manifiesto reactivo.

La programación reactiva a menudo se explica en términos demasiado teóricos. Según el Manifiesto Reactivo, todos los sistemas responsivos, resilientes, elásticos y basados en mensajes son reactivos. La capacidad de respuesta tiene que ver con el tiempo de respuesta y el rendimiento. La resiliencia tiene que ver con la tolerancia a fallos y la disponibilidad. La elasticidad implica la capacidad de soportar diferentes cargas en diferentes momentos, lo que puede afectar al rendimiento, la disponibilidad y los gastos operativos. Además, la gestión de mensajes permite que el sistema se comporte de acuerdo con los mensajes que pasan de un componente a otro, estableciendo límites claros entre los componentes y manteniendo la transparencia de las interacciones entre ellos.

Nota sobre las transmisiones: las transmisiones son una abstracción de datos ilimitados. A menudo se comparan con lotes. Imagina que tenemos dos números enteros. Aplicar una función común a este tipo de números enteros, como el operador de suma (+), parece factible. Ahora, ¿qué pasa si los operandos no dejan de aparecer porque, por ejemplo, provienen de alguna entrada de usuario, que utiliza alguna aplicación en tiempo real? Entonces, la suma se convierte en un desafío. El uso de las transmisiones aprovecha un enfoque diferente para este problema. Profundizaremos en la composición de transmisiones más adelante.

Los flujos de eventos son particularmente eficaces en los lenguajes de programación que admiten la programación funcional hasta cierto punto. Los lenguajes multiparadigmáticos, como C# o Scala, tienen algunas abstracciones integradas para representar los flujos de datos. En este artículo, exploraremos una implementación particular de flujos reactivos utilizando Java moderno y Proyecto Reactor. Esto nos servirá como capa de servicio en una aplicación web. En futuros artículos analizaremos cómo esta capa de servicio podría interoperar con una WebFlux capa de acceso web. ¡Síguenos!

Lo que necesitarás

Para seguir esta guía, asegúrese de tener:

Basic knowledge of Java applications and functional programming

Experience developing some kind of asynchronous algorithms

Some development environment, such as Visual Studio Code or IntelliJ IDEA

Java 17 (which can be installed directly on IntelliJ)

Qué puede esperar

Después de completar esta guía, usted debería poder:

Identify basic use cases for reactive programming

Explain how to set up a basic Reactor application

Feel comfortable reading basic algorithms written in a reactive style

Recommend articles and blog posts written by experts to sustain decisions on architecture and design similar to the ones used in the example.

En esta guía utilizaremos Java, Gradle, Spring-Boot e IntelliJ CE.

La aplicación

Crearemos una PoC de microservicio de procesamiento de interacciones con el usuario. Nos centraremos en la capa de servicio y suprimiremos la capa de acceso web (que implementaremos en WebFlux en otro artículo) y la capa de acceso a los datos.

Creación de la aplicación

Para empezar, crea una aplicación básica de Spring con Gradle. Puedes hacerlo usando Spring Initializer, configurándolo manualmente o clonando nuestro proyecto inicial en GitHub. Maven también funciona, pero preferimos la sintaxis sucinta de Gradle en lugar de XML. Vamos a buscar nuestras dependencias de Repositorio Mvn. Vamos a presentar brevemente las principales dependencias utilizadas en nuestro proyecto:

Nuestro bloque de dependencias build.gradle tiene este aspecto:

no-line-numbers|dark|gradledependencies { implementation ‘org.springframework.boot:spring-boot-starter’ testImplementation ‘org.springframework.boot:spring-boot-starter-test’ compileOnly ‘org.projectlombok:lombok:1.18.28’ annotationProcessor ‘org.projectlombok:lombok:1.18.28’ testCompileOnly ‘org.projectlombok:lombok:1.18.28’ testAnnotationProcessor ‘org.projectlombok:lombok:1.18.28’ implementation ‘io.projectreactor:reactor-core:3.5.8’}

En primer lugar, veamos algunas de las características de Lombok. Una de nuestras funciones favoritas es el patrón de construcción incorporado y las clases de datos. Con esto, podemos escribir una clase de modelo simple en un archivo llamado DomElement.java dentro del paquete de modelos con este código:

no-line-numbers|dark|java@Data@Builder(setterPrefix = “with”)public class DomElement { private String name; private String type;}

El @Data La anotación lombok nos permite prescindir de los constructores, los descriptores de acceso, los modificadores, el comparador de igualdad y los métodos ToString. La función de datos de Lombok se comporta de forma similar a la de los registros de Java, aunque no son exactamente iguales. Las clases de @Data se pueden ajustar con precisión y la personalización se puede encontrar en la documentación de Lombok. Por ejemplo, los campos marcados como transitorio no se tendrá en cuenta para los comparadores de igualdad o los códigos hash.

La anotación @Builder nos permite evitar la creación de numerosos constructores sobrecargados o saturar nuestras clases con un exceso de setters. En vez de eso, nos permite usar el patrón Builder e invocar en cadena nuestros setters con un poco de azúcar sintáctico por encima. Termina construyendo un elegante Interfaz fluida. En Baeldung hay un artículo que vale la pena echar un vistazo sobre la combinación del patrón Builder con Fluent Interfaces.

Debido al alcance que tiene Lombok en nuestras dependencias de Gradle, podemos acceder a los métodos proporcionados por el Builder durante el tiempo de desarrollo. Eche un vistazo al Prefijo Setter opción que agregamos a nuestro constructor.

En lugar de llamar a los creadores de vacíos tradicionales, podemos usar su correspondiente con [atributo] método en su lugar, y encadenarlos. Los setters de Builder no son nulos, por lo que pueden componerse y llamarse en cadena. Para obtener el objeto que se está creando, la llamada a la cadena de métodos debe terminar con .construir ()

Añada también un modelo UserInteraction. El propósito de esta clase es representar alguna interacción del usuario con un elemento DOM. Vamos a modelar esto como una clase contenedora para un campo DOMElement y un campo UserID, que es una cadena. Cree un archivo UserInteraction.java en modelos empaqueta y haz que se vea así:

no-line-numbers|dark|java@Data@Builder(setterPrefix = “with”)public class UserInteraction { private DomElement domElement; private UUID userId; private String interaction; private long unixTimestamp;}

Ahora podemos empezar con el servicio en sí. Determinaremos el origen de las interacciones de los usuarios (en este contexto, el controlador que las envía a la capa de servicio), codificando algunos Java Beans. Mantendremos las interacciones de los usuarios en una estructura de datos conveniente. Más adelante, integraremos la estructura en nuestra aplicación para poder sustituirla fácilmente por un controlador de API que pueda estar escuchando algún componente de la interfaz.

Sería interesante filtrar las interacciones de los usuarios. Por ejemplo, enumerar solo las interacciones de los usuarios que se produjeron entre dos marcas de tiempo o filtrarlas según el tipo de DOMElement. Diseñemos este comportamiento por contrato en nuestro servicio.

no-line-numbers|dark|javapublic interface UserInteractionService {List filterByDomElementType(String domElementType);List getUserMostRecentInteraction(UUID userUUID);}

Este es casi el camino a seguir. Nos falta el aspecto reactivo de las cosas. Para que este contrato sea reactivo, vamos a utilizar uno de los conceptos clave de Java Reactor: el Flux.

En el Documentación del proyecto Reactor Flux podemos encontrar la siguiente ilustración, que explica claramente cómo funciona Flux.

Flujo y Mono son las principales abstracciones de Reactive Streams utilizadas por Reactor. Mono es similar a Flux, pero acepta como máximo un elemento. La mayoría de los operadores Flux y Mono aceptan uno o más de este tipo y también devuelven uno de esos. Observe el siguiente ejemplo:

no-line-numbers|dark|javaFlux uppercaseCities = Flux.fromArray(new String[]{“boston”, “new york”, “seattle”}).map(city -> city.toUpperCase());

Este fragmento de código presenta una forma de crear un Flux a partir de una matriz y, a continuación, transforma todos los elementos inicialmente presentes en la matriz de cadenas en mayúsculas, lo que produce un nuevo Flux. Los flujos se pueden crear a partir de fuentes nativas de Reactor y de la mayoría de los iterables de Java. Tenga en cuenta que la mayoría de las operaciones de Flux se pueden encadenar de forma fluida, tal como definimos nuestro Builder anteriormente.

El contrato reactivo de nuestra clase UserInteractionService tendrá este aspecto:

no-line-numbers|dark|javapublic interface UserInteractionService {Flux filterByDomElementType(String domElementType);Mono getUserMostRecentInteraction(UUID userUUID);}

Vamos a crear una implementación para esta interfaz. Crearemos una implementación sencilla con datos iniciales y la refactorizaremos sobre la marcha para mantenerla ordenada, aprovechar las capacidades de Spring y fomentar la capacidad de mantenimiento. Empieza añadiendo algunos datos de prueba. ¡Te ayudaremos con eso!

no-line-numbers|dark|javaprivate List supplyData(){DomElement button = DomElement.builder() .withName(“register-button”) .withType(“button”) .build();DomElement contactUsLink = DomElement.builder() .withName(“contact-us-link”) .withType(“href”) .build();UserInteraction contactUsLinkHover = UserInteraction.builder() .withDomElement(contactUsLink) .withUserId(UUID.randomUUID()) .withInteraction(“hover”) .withUnixTimestamp(System.currentTimeMillis() / 1000L) .build();UserInteraction contactUsLinkClick = UserInteraction.builder() .withDomElement(contactUsLink) .withUserId(UUID.randomUUID()) .withInteraction(“click”) .withUnixTimestamp((System.currentTimeMillis() + 3000) / 1000L) .build();UserInteraction registerButtonClick = UserInteraction.builder() .withDomElement(button) .withUserId(UUID.randomUUID()) .withInteraction(“click”) .withUnixTimestamp(System.currentTimeMillis() – 50000 / 1000L) .build();return List.of(contactUsLinkHover, contactUsLinkClick, registerButtonClick);}

Estas son las interacciones de los usuarios auxiliares con las que funcionará nuestro sistema en esta PoC. Refactoriza eso empaquetando todos esos datos codificados de forma rígida en un método privado, de modo que nuestros métodos públicos no se vean inundados de inicializaciones de datos secundarios.

no-line-numbers|dark|java@Overridepublic Mono getUserMostRecentInteraction(UUID userUUID) { Flux userInteractions = Flux.fromIterable(this.supplyData()); userInteractions.subscribe(System.out::println); return userInteractions .filter(ui -> ui.getUserId().equals(userUUID)) .reduce((ui1, ui2) -> ui1.getUnixTimestamp() > ui2.getUnixTimestamp() ? ui1 : ui2);}

Están pasando muchas cosas ahí. Vamos a desglosarlo un poco.

  1. En primer lugar, estamos creando un flujo a partir de las interacciones de nuestros usuarios a partir de la lista de stubs que codificamos. Podemos crear flujos a partir de cualquier iterable o matriz en Java.
  2. En el primer método, solo aplicamos una operación de filtrado según el tipo de elemento dom de la interacción. Solo mantendremos aquellos que coincidan con el parámetro del método.
  3. En el segundo método, primero llamamos a un método de suscripción. El propósito de este método de suscripción es imprimir los elementos de flujo uno por uno. El método de suscripción permite aplicar efectos secundarios y también activa las operaciones de flujo. Los flujos son perezosos: no pasa nada hasta que nos suscribimos. Después de eso, invocamos otro operador de filtro seguido de un operador de reducción, que terminará devolviendo un solo elemento: la interacción de usuario más reciente para el parámetro user id.
  4. El:: se denomina operador de referencia de método. Permite pasar una función como argumento a otras funciones que requieren dicho argumento.

El oleoducto de operaciones de Flux está llamado a ser perezoso. Fluxes y Monos son perezosos. No se activará ningún cálculo hasta que nos suscribamos. El cliente de la canalización es responsable de activar el cálculo y es libre de elegir cuándo hacerlo suscribiéndose. Es realmente interesante cómo podemos pensar en dos etapas: el montaje y la ejecución. Podemos encadenar muchos complejos (y recuerda: ¡asincrónicos y no bloqueantes!) operaciones sin preocuparse por las etapas: todo el oleoducto se activará de una sola vez.

Hay muchas ventajas a la hora de elegir estructuras perezosas. Por ejemplo, si nuestras operaciones de filtrado fueran condicionales, es decir, hay ciertas situaciones en las que no se ejecutarán, nuestro código no desperdiciará ningún recurso al definir el ensamblaje de flujo. Además, podemos materializar nuestra consulta cuando queramos.

Antes de refactorizar nuestra aplicación, probémosla. Agregue el siguiente código a su método principal. No te preocupes por la línea SpringApplication por ahora.

no-line-numbers|dark|java@SpringBootApplicationpublic class ContentRecommenderApplication { public static void main(String[] args) { UserInteractionService service = new UserInteractionServiceImpl(); service.filterByDomElementType(“button”) .subscribe(ui -> System.out.println(ui.toString())); SpringApplication.run(UserInteractionApplication.class, args); }}

Y ejecútalo.

Refactorización

Nuestra PoC funciona, pero está lejos de ser aceptable. Teniendo en cuenta que planeamos escalar este servicio PoC hasta convertirlo en un microservicio reactivo real, además de WebFlux, podemos arreglar un poco las cosas. Además, podemos aprovechar las capacidades de Spring Framework, como la inyección automática de dependencias.

Queremos conectar automáticamente las dependencias para que, cuando agreguemos algunos controladores WebFlux, puedan localizar fácilmente el servicio con el que se comunicarán. Además, podemos aprovechar la división entre la interfaz y la implementación para garantizar el cumplimiento del principio de inversión de dependencias. Tenga en cuenta que, en aras de la simplicidad, nuestras interfaces e implementaciones residirán en el mismo componente físico. Podrían dividirse en dos JAR para fomentar la capacidad de mantenimiento y la modularidad.

Además, queremos borrar la inicialización de datos codificados de nuestro servicio. Automatizaremos la inicialización de los datos en un paquete de acceso a los datos para que se burle del comportamiento de acceso a la base de datos.

Hay un detalle que solemos pasar por alto cuando diseñamos software y es un buen diseño de paquetes/módulos. A menudo diseñamos en función de las clases y los objetos, pero tendemos a olvidarnos de los módulos. Dividiremos los paquetes para cumplir con el principio de dependencia estable (según la dirección de la estabilidad) y el principio de abstracción estable (un paquete debe ser abstracto ya que es estable). En nuestro caso, tenemos paquetes completamente abstractos y completamente volátiles, y nuestros paquetes volátiles dependen de los paquetes abstractos. Además, hemos definido interfaces para las reglas de negocio, por lo que tiene sentido suponer que esos son nuestros paquetes estables.

Nuestros paquetes tendrán el siguiente aspecto:

Y definiremos tres beans en una clase @Configuration llamada ServiceConfiguration:

no-line-numbers@Beanpublic List userInteractions(){ DomElement button = DomElement.builder() .withName(“register-button”) .withType(“button”) .build(); DomElement contactUsLink = DomElement.builder() .withName(“contact-us-link”) .withType(“href”) .build(); UserInteraction contactUsLinkHover = UserInteraction.builder() .withDomElement(contactUsLink) .withUserId(UUID.randomUUID()) .withInteraction(“hover”) .withUnixTimestamp(System.currentTimeMillis() / 1000L) .build(); UserInteraction contactUsLinkClick = UserInteraction.builder() .withDomElement(contactUsLink) .withUserId(UUID.randomUUID()) .withInteraction(“click”) .withUnixTimestamp((System.currentTimeMillis() + 3000) / 1000L) .build(); UserInteraction registerButtonClick = UserInteraction.builder() .withDomElement(button) .withUserId(UUID.randomUUID()) .withInteraction(“click”) .withUnixTimestamp(System.currentTimeMillis() – 50000 / 1000L) .build(); return List.of(contactUsLinkHover, contactUsLinkClick, registerButtonClick);}@Beanpublic UserInteractionRepository userInteractionRepository(){ return new UserInteractionRepositoryImpl();}@Beanpublic UserInteractionService userInteractionService(){ return new UserInteractionServiceImpl();}

Ese es nuestro método SupplyData convertido en Bean más la instanciación de nuestras interfaces de servicio y repositorio. Ahora nuestra clase Service tiene un aspecto limpio y ya no es responsable de crear datos secundarios. Nuestro último paso es configurar nuestro método principal. Esto no debería ser necesario si ya teníamos nuestros controladores WebFlux, pero por el momento, haz que tenga el siguiente aspecto:

no-line-numberspublic static void main(String[] args) {SpringApplication.run(UserInteractionApplication.class, args);ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfiguration.class);UserInteractionService userInteractionService = ctx.getBean(UserInteractionService.class);userInteractionService.filterByDomElementType(“button”) .map(UserInteraction::toString) .subscribe(System.out::println);}

Y así es como podemos hacer que nuestro servicio obtenga todas las interacciones de los usuarios para los botones.

Consulta nuestra ejemplo completo aquí.

Próximos pasos

Hemos desarrollado una capa de servicio para un microservicio reactivo. El paso futuro más interesante es añadir WebFlux para que nuestra aplicación esté expuesta en la web. Además, ¡algún mecanismo de persistencia sin bloqueo de alto rendimiento también sería bienvenido!

Manténgase a la vanguardia de las últimas tendencias y conocimientos sobre big data, aprendizaje automático e inteligencia artificial. ¡No se lo pierda y suscríbase a nuestro boletín de noticias!