sábado, 18 de diciembre de 2021

Spring WebFlux: Iniciación

Este es un post que pensé hacer en 2019 cuando era algo más novedoso pero lo he ido retrasándolo. Y ahora vamos a hacer una pequeña introducción a esta tecnología con un ejemplo práctico. Es dos años después, pero más vale tarde que nunca. 

Spring Web Flux, es el módulo de Spring para la programación reactiva. Lo que nos permitirá básicamente realizar invocaciones asíncronas a micro servicios. Aquí podréis encontrar toda la información oficial. Lo que veremos en el ejemplo será la implementación que hace Spring de la especificación de Reactive Streams a través de la librería Reactor, más info aquí y aquí. Y tenemos que hacer uso de un contenedor de servlets 3.1+ como Netty, Tomcat, Jetty o Undertow.

Para empezar daremos una pequeña explicación sobre la programación reactiva, esta no es más que un nuevo paradigma de programación orientado a los flujos de datos (Streams) y a la propagación de eventos en contraposición al bloqueo en espera de respuesta. En esta programación reactiva hay un par de conceptos claves:
  • Publisher. Son aquellos que emiten el flujo de datos, es decir nuestros servicios.
  • Subscriber. Son aquellos que estan subscritos y reciben los datos asíncronamente, los clientes/paginas web. 
Como hemos dicho vamos a ver su funcionamiento con un ejemplo más o menos practico. Y se basará en la idea de que necesitamos implementar una página que muestre las incidencias en distintos servidores. Estos servidores cuentan con un micro servicio que muestra información de los mismos. Con el uso normal de Spring MVC, realizaríamos distintas llamadas de forma secuencial y tardaríamos la suma del tiempo asociado a cada una de las respuestas. Con Spring WebFlux tardaremos tanto como tarde la respuesta del servicio más lento. 

El ejemplo constará de dos proyectos, el primero será el micro servicio dummy que da la información sobre el sistema. Y el segundo será el cliente que reciba la información de los anteriores micro servicios, invocado varias veces pero con parámetros distintos. En ambos, para poder usar WebFlux y todas las clases asociadas como es normal en Spring simplemente tendremos que añadir una librería y nada de configuración. Esta es spring-boot-starter-webflux.

Al final, para poder comparar el rendimiento crearemos dos métodos distintos uno que hará uso de los stream reactivos y otro que no. Aunque en este post solo mostraremos el código asociado al módulo de Spring WebFlux. El primer micro servicio, el dummy que muestra la información, estará compuesto por un método que en vez de devolver un listado de objetos, incidencias en este caso, devolveremos un objeto de tipo Flux. 

¿Que es un objeto de tipo Flux? Reactor pone a disposición dos objetos que nos permiten el paso de otros objetos de forma asíncrona, estos son Mono y Flux. Mono nos permite enviar uno o ningún objeto y Flux  permite el envío desde ninguno a un listado indeterminado de objetos. Es decir que si queremos que responda un único objeto usaremos Mono y para un conjunto usaremos Flux. 

@GetMapping("/incidenceFlux/{env}/{server}")
public Flux<Incidence> findAllFlux(@PathVariable final String env, @PathVariable final String server) {
	return Flux.just(new Incidence(env, server, "info", "method called"),
		new Incidence(env, server, "warning", "incorrect data"),
		new Incidence(env, server, "info", "method ended"))
		.delaySequence(Duration.ofSeconds(3));
}

Flux.just es el típico método de utilidad para la creación de objetos. El método delaySequence nos permitirá añadir un poco de latencia a nuestro servicio y darle una apariencia más real. En el método sin uso de la API reactiva, utilizaremos un Thread.sleep(3000). De esta forma podremos medir la eficiencia en la comparación final. 

Ahora crearemos el cliente, compuesto por servicio y controlador. El servicio realizará las distintas llamadas a los micro servicios de cada uno de los servidores de los cuales queremos obtener información. Y el controlador recopilará esa información y se la pasará a la vista para que se la muestre al usuario. A continuación mostramos el servicio. 

public Flux<Incidence> findAllFlux() {
	Flux<Incidence> lstPre1 = WebClient.create("http://localhost:8085/incidenceFlux/pre/1").get().retrieve()
			.bodyToFlux(Incidence.class);
	Flux<Incidence> lstPre2 = WebClient.create("http://localhost:8085/incidenceFlux/pre/2").get().retrieve()
			.bodyToFlux(Incidence.class);
	Flux<Incidence> lstPro1 = WebClient.create("http://localhost:8085/incidenceFlux/pro/1").get().retrieve()
			.bodyToFlux(Incidence.class);
	Flux<Incidence> lstPro2 = WebClient.create("http://localhost:8085/incidenceFlux/pro/2").get().retrieve()
			.bodyToFlux(Incidence.class);
	return Flux.merge(lstPre1, lstPre2, lstPro1, lstPro2);
}

Para poder invocar de forma asíncrona un endpoint deberemos utilizar el objeto WebClient. Su uso es similar a los de otros clientes HTTP que hemos visto en otros posts, como puede ser Feign o HTTPClient. En este caso lo realizaremos de la siguiente forma:
  • get() nos permitirá hacer una invocación GET, pero podemos usar cualquier otro método HTTP. Todos nos darán acceso a un conjunto de métodos, por ejemplo cookie(), que nos permitirá modificar la invocación a realizar. 
  • retrieve() nos permitirá obtener la respuesta y también pondrá a disposición nuestra otro conjunto de métodos para manejar dicha respuesta. 
Por último con Flux.merge podremos unificar las respuestas de las distintas invocaciones. Algo similar a un addAll si estuviéramos utilizando objetos de tipo List e invocaciones bloqueantes. 

Ya solo nos quedará el controlador del cliente donde recibiremos el listado completo y mandaremos a la vista. En el también mostraremos cuanto hemos tardado en ejecutar el método. 

@RequestMapping("/incidenceListFlux")
public String incidenceListFlux(final Model modelo) {
	Long init = System.currentTimeMillis();
	modelo.addAttribute("incidences", service.findAllFlux()
            .collectList().block());
	Long end = System.currentTimeMillis();
	log.info("incidence ListFlux : " + (end - init) + " miliseconds");
	return "index";
}

Como puedes ver en el trozo de código anterior, la API nos provee de diferentes métodos que nos permitan tratar los objetos Mono y Flux. Con collectList podemos pasarlo a un objeto de tipo Mono<List<?>> y con block podemos hacer que se espere la invocación hasta que tengamos el listado completo y transformarlo finalmente a un listado de objetos. Pero hay muchos más métodos que nos permiten transformarlos a otros tipos de objetos, o tratar los datos sin necesidad de la confirmación de haber obtenidos todos los resultados. 

Por último una pequeña muestra de los tiempos de una ejecución bloqueante utilizando objetos List y otra ejecución asíncrona, utilizando objetos Flux. Y haciendo en ambos casos el mismo número de invocaciones:

e.h.e.a.controller.IncidenceController   : incidence ListFlux : 3392 miliseconds

e.h.e.a.controller.IncidenceController   : incidence List : 12088 miliseconds

Spring WebFlux abre un amplio abanico de posibilidades en todo el ámbito de la programación. Empezando por la programación web y aplicaciones front-end hasta la aplicación en sistemas de monitorización, en los cuales la mejora en los tiempos de respuesta en fundamental. Si deseas ver todo el código, lo tienes aquí

No hay comentarios:

Publicar un comentario