Type something to search...
Spring WebFlux 2: Alta Concurrencia sin Más Hilos

Spring WebFlux 2: Alta Concurrencia sin Más Hilos

¡Bienvenido de nuevo a nuestra inmersión en Spring WebFlux! 👋

En la primera parte de esta serie, exploramos el “por qué” de la programación reactiva, entendiendo los problemas del bloqueo y descubriendo a Project Reactor como el motor que impulsa los flujos de datos asíncronos. Ahora que tenemos una base sólida sobre los principios reactivos y los tipos Mono/Flux, es momento de subir un nivel y entender cómo Spring WebFlux aplica estos conceptos para construir aplicaciones web eficientes y escalables.

En esta segunda entrega, nos centraremos en la arquitectura que diferencia a WebFlux de su predecesor, Spring MVC, y aprenderemos las dos formas principales de definir los endpoints de nuestra API reactiva.

3. Arquitectura de Spring WebFlux

Si Spring MVC se construyó sobre la API de Servlets (diseñada originalmente para un modelo síncrono de un hilo por petición), Spring WebFlux se construye sobre una pila completamente reactiva y no bloqueante. Esta diferencia fundamental es la clave de su capacidad para manejar alta concurrencia.

Teoría: Componentes Clave

La arquitectura de WebFlux se basa en:

  • Servidores No Bloqueantes: A diferencia de depender de un Contenedor de Servlets (como Tomcat, Jetty) configurado de forma tradicional, WebFlux utiliza servidores web diseñados para manejar I/O no bloqueante. El servidor por defecto integrado con Spring Boot WebFlux es Netty, un framework asíncrono basado en eventos muy popular en la industria por su rendimiento. Sin embargo, WebFlux es flexible y también soporta otros servidores reactivos como Undertow o incluso Servlets 3.1+ API en modo no bloqueante (aunque el uso de Netty o Undertow es más común y eficiente para aprovechar plenamente el potencial reactivo).
  • EventLoop: El corazón del procesamiento no bloqueante. En lugar de asignar un hilo por petición, WebFlux (y los servidores como Netty) utilizan un pequeño número de hilos llamados “Event Loop threads”. Estos hilos no realizan operaciones de I/O bloqueantes directamente. En cambio, delegan la operación al sistema operativo y quedan libres para procesar otras tareas o peticiones. Cuando la operación de I/O se completa (por ejemplo, llega la respuesta de una base de datos o un servicio externo), el sistema operativo notifica al Event Loop, que entonces toma el resultado y continúa el procesamiento del flujo reactivo asociado a esa petición.
  • Reactor Core: Como vimos en la Parte 1, Project Reactor proporciona los tipos Mono y Flux y los operadores para componer la lógica asíncrona. WebFlux se integra estrechamente con Reactor.
  • Spring Web Reactive Framework: Capas por encima de Reactor y el servidor para proporcionar la funcionalidad web: manejo de peticiones, ruteo, serialización/deserialización, manejo de errores, etc.

Cómo WebFlux Maneja las Peticiones (El Pipeline Reactivo)

Cuando una petición HTTP llega a un servidor WebFlux:

  1. Uno de los Event Loop threads del servidor la recibe.
  2. La petición pasa a través de la cadena de procesamiento de WebFlux (filtros, ruteo).
  3. La petición llega al Handler (controlador o función manejadora) correspondiente.
  4. El Handler ejecuta la lógica de negocio, que típicamente involucra operaciones que devuelven Mono o Flux (ej: llamar a un servicio, acceder a una base de datos reactiva).
  5. Estas operaciones, al ser reactivas y no bloqueantes, no detienen el Event Loop thread. El thread delega la tarea (ej: consulta a DB) y queda libre.
  6. Cuando la operación asíncrona finaliza (ej: la DB devuelve resultados), uno de los Event Loop threads recibe la notificación.
  7. Los resultados fluyen de vuelta a través de la cadena de operadores definida en el Mono/Flux.
  8. El resultado final del Mono/Flux se convierte en una respuesta HTTP y se envía de vuelta al cliente, de nuevo, utilizando los Event Loop threads de forma no bloqueante.

Todo el procesamiento, desde la recepción de la petición hasta el envío de la respuesta, se maneja sin bloquear los hilos principales, permitiendo que un pequeño número de hilos gestione una alta concurrencia.

Diferencias Arquitectónicas Fundamentales con Spring MVC

CaracterísticaSpring MVC (Tradicional)Spring WebFlux (Reactivo)
Modelo de HilosThread-per-request (Bloqueante)Event Loop (No Bloqueante)
Contenedor/ServidorBasado en Servlet API (Tomcat, Jetty, etc.)Basado en servidores reactivos (Netty, Undertow) o Servlet 3.1+ no bloqueante
Manejo de I/OBloqueante (por defecto)No Bloqueante
Dependencies Basespring-webmvcspring-webflux
Tipos de RetornoObjetos POJO, ResponseEntity, ModelAndView, etc.Mono<?>, Flux<?>, ResponseEntity<Mono<?>>, etc.
BackpressureNo aplica directamenteSoportado nativamente a través de Reactive Streams

¿Puedes usar Spring MVC y Spring WebFlux en el mismo proyecto?

Generalmente no. Aunque es técnicamente posible tener ambas dependencias en el classpath, Spring Boot configurará automáticamente solo una de las dos pilas web (MVC o WebFlux) basándose en la que encuentre primero o una configuración explícita. Son dos arquitecturas de manejo de peticiones fundamentalmente diferentes que no están diseñadas para coexistir y procesar la misma petición dentro del mismo contexto de aplicación Spring de forma híbrida y coherente. Debes elegir una u otra para tu aplicación web principal.

Casos Típicos/Práctica

  • Flujo de una Petición Típica en WebFlux:

    1. Llega petición HTTP a Netty (Event Loop thread A la recibe).
    2. WebFlux la rutea a un HandlerFunction (el mismo thread A).
    3. El Handler llama a un UserService.findById(id) que devuelve Mono<User>.
    4. UserService usa un ReactiveUserRepository.findById(id) (que usa un driver R2DBC no bloqueante).
    5. El Event Loop thread A delega la consulta a la DB y queda libre.
    6. Cuando la DB responde, otro Event Loop thread (B) recibe la notificación.
    7. El thread B retoma el flujo del Mono<User>.
    8. El resultado User fluye de regreso al Handler.
    9. El Handler devuelve el Mono<User>, que WebFlux serializa a JSON.
    10. El Event Loop thread B envía la respuesta HTTP de vuelta al cliente.
  • Modelo de Hilos de Spring MVC vs. WebFlux:

    • MVC: Un pico de 1000 peticiones concurrentes esperando por una DB lenta podría requerir 1000 hilos (o el tamaño máximo del pool), muchos de ellos inactivos.
    • WebFlux: Esas mismas 1000 peticiones podrían ser manejadas por 4-8 Event Loop threads, que nunca esperan, simplemente gestionan el estado de las operaciones asíncronas pendientes. Esto libera recursos para otras tareas.

4. Creación de Endpoints (Controladores y Endpoints Funcionales)

Spring WebFlux ofrece dos enfoques principales para definir los puntos finales de tu API: el modelo tradicional basado en anotaciones y un modelo más funcional.

Teoría: Dos Enfoques

  1. Basado en Anotaciones: Similar a Spring MVC, usas anotaciones como @RestController, @RequestMapping, @GetMapping, @PostMapping, @RequestBody, etc. La diferencia clave es que los métodos del controlador deben devolver tipos reactivos (Mono<?> o Flux<?>).
  2. Endpoints Funcionales: Un enfoque más funcional y declarativo. Defines las rutas usando RouterFunction y los manejadores de peticiones usando HandlerFunction. No hay anotaciones a nivel de método o clase; es todo código Java.

Uso de Anotaciones con Tipos Reactivos

Es el enfoque más familiar si vienes de Spring MVC. Simplemente creas clases con @RestController y métodos con anotaciones de mapeo HTTP. La diferencia crucial es el tipo de retorno:

  • Devuelve Mono<T> si esperas 0 o 1 objeto T en la respuesta.
  • Devuelve Flux<T> si esperas 0 a N objetos T en la respuesta (esto puede ser un array JSON o un stream de datos, por ejemplo, en Server-Sent Events).
  • Puedes envolver el tipo reactivo en ResponseEntity para tener control sobre el estado HTTP, cabeceras, etc.: Mono<ResponseEntity<T>> o ResponseEntity<Flux<T>>.

Recibir datos en el cuerpo de la petición también se hace reactivamente: usas @RequestBody con Mono<T>.

Uso de Endpoints Funcionales

Este enfoque desacopla completamente la definición de la ruta de la lógica de manejo de la petición.

  • RouterFunction<ServerResponse>: Define cómo las peticiones se rutean a los HandlerFunction basándose en predicados (métodos HTTP, rutas, cabeceras, etc.). Usas la clase RouterFunctions para construirlas (route(RequestPredicate, HandlerFunction)).
  • HandlerFunction<ServerResponse>: Contiene la lógica de negocio para manejar una petición. Recibe un ServerRequest como entrada y devuelve un Mono<ServerResponse>. La clase ServerResponse se usa para construir la respuesta (estado HTTP, cuerpo, cabeceras).

Ventajas del Enfoque Funcional:

  • Mayor separación de preocupaciones (ruteo vs. manejo).
  • Más fácil de testear unitariamente (HandlerFunction es solo una función pura).
  • Permite una construcción de rutas más programática y dinámica.
  • Evita el uso de reflexion asociado a las anotaciones (micro-optimización).

Desventajas del Enfoque Funcional:

  • Puede ser menos conciso y legible para APIs REST simples comparado con las anotaciones.
  • Menos familiar para desarrolladores acostumbrados al modelo de anotaciones.

Casos Típicos/Práctica

  • Endpoint GET que devuelva un Mono<MyObject> (Anotaciones):

    Asumiendo una clase MyObject { String message; }

    @RestController
    @RequestMapping("/api/greeting")
    public class GreetingController {
    
        @GetMapping("/{name}")
        public Mono<MyObject> getGreeting(@PathVariable String name) {
            // Simula una operación asíncrona que devuelve un solo objeto
            return Mono.just(new MyObject("Hello, " + name))
                       .delayElement(Duration.ofMillis(500)); // Simula latencia
        }
    }
  • Endpoint GET que devuelva un Flux<MyObject> (Stream de datos) (Anotaciones):

    @RestController
    @RequestMapping("/api/numbers")
    public class NumberStreamController {
    
        @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) // Importante: MediaType.TEXT_EVENT_STREAM_VALUE para SSE
        public Flux<String> streamNumbers() {
            // Emite un número cada segundo indefinidamente
            return Flux.interval(Duration.ofSeconds(1))
                       .map(sequence -> "Event: " + sequence);
        }
    
        @GetMapping("/list") // Devuelve como JSON array
        public Flux<MyObject> getObjectsList() {
            return Flux.just(new MyObject("one"), new MyObject("two"), new MyObject("three"))
                       .delayElements(Duration.ofMillis(100));
        }
    }
  • Endpoint POST que reciba un Mono<MyObject> en el body (Anotaciones):

    @RestController
    @RequestMapping("/api/objects")
    public class ObjectController {
    
        @PostMapping
        public Mono<String> createObject(@RequestBody Mono<MyObject> objectMono) {
            // Recibe un Mono<MyObject> del cuerpo de la petición
            // flatMap es necesario porque objectMono es un Publisher y save es otro Publisher
            return objectMono
                    .flatMap(obj -> {
                        System.out.println("Recibido objeto: " + obj.getMessage());
                        // Simula guardar el objeto asíncronamente y devolver un ID
                        return Mono.just("Object saved with ID: " + obj.getMessage().hashCode())
                                   .delayElement(Duration.ofMillis(300));
                    });
        }
    }
  • Definir una ruta y su manejador usando el enfoque funcional:

    Primero, el HandlerFunction:

    // En un archivo separado, por ejemplo, src/main/java/com/example/demo/handler/GreetingHandler.java
    @Component // Spring lo detecta como un Bean
    public class GreetingHandler {
    
        public Mono<ServerResponse> getGreeting(ServerRequest request) {
            String name = request.pathVariable("name");
            return Mono.just(new MyObject("Hello, " + name))
                       .delayElement(Duration.ofMillis(500)) // Simula latencia
                       .flatMap(obj -> ServerResponse.ok() // Construye la respuesta HTTP 200
                                                     .contentType(MediaType.APPLICATION_JSON) // Define el tipo de contenido
                                                     .bodyValue(obj)); // Pone el objeto en el cuerpo de la respuesta
        }
    
         public Mono<ServerResponse> createObject(ServerRequest request) {
             return request.bodyToMono(MyObject.class) // Extrae el cuerpo a un Mono<MyObject>
                    .flatMap(obj -> {
                        System.out.println("Recibido objeto (Funcional): " + obj.getMessage());
                         // Simula guardar
                        return Mono.just("Object saved (Funcional) with ID: " + obj.getMessage().hashCode())
                                   .delayElement(Duration.ofMillis(300));
                    })
                    .flatMap(responseString -> ServerResponse.status(HttpStatus.CREATED) // Construye respuesta 201 Created
                                                              .contentType(MediaType.TEXT_PLAIN)
                                                              .bodyValue(responseString));
         }
    }

    Luego, el RouterFunction (en una clase de configuración, por ejemplo):

    // En una clase de configuración, por ejemplo, src/main/java/com/example/demo/config/RoutingConfig.java
    @Configuration
    public class RoutingConfig {
    
        @Bean
        public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {
            return RouterFunctions.route(GET("/api/functional/greeting/{name}").and(accept(MediaType.APPLICATION_JSON)), greetingHandler::getGreeting)
                   .andRoute(POST("/api/functional/objects").and(contentType(MediaType.APPLICATION_JSON)), greetingHandler::createObject); // Combina con otras rutas
        }
    }
  • ¿Cuándo elegirías anotaciones vs. endpoints funcionales?

    • Anotaciones: Ideal para proyectos que migran de Spring MVC, equipos familiarizados con el modelo de anotaciones, o APIs REST con estructuras estándar. Es a menudo más rápido de implementar para casos simples o CRUDs.
    • Funcionales: Preferible para APIs con lógica de ruteo compleja o dinámica, si buscas una mayor separación de preocupaciones para facilitar el testing unitario de la lógica del manejador, o si simplemente prefieres un estilo más funcional y programático. Puede tener una curva de aprendizaje inicial si no estás acostumbrado.

Conclusión

En esta segunda entrega, hemos explorado la arquitectura fundamental de Spring WebFlux, entendiendo cómo su modelo no bloqueante basado en EventLoop y servidores como Netty le permite manejar eficientemente la alta concurrencia, a diferencia del modelo tradicional de Spring MVC. También hemos aprendido las dos vías principales para construir endpoints: el familiar enfoque basado en anotaciones (adaptado para devolver tipos reactivos) y el modelo más programático y funcional de RouterFunction y HandlerFunction, comprendiendo las fortalezas de cada uno y cuándo considerar usarlos.

Con la arquitectura y la creación de endpoints cubiertas, estamos listos para abordar la interacción de nuestra aplicación WebFlux con el mundo exterior y el manejo de datos y errores. En la próxima parte, nos sumergiremos en el uso de WebClient para consumir servicios externos reactivamente, la integración con bases de datos reactivas (R2DBC, drivers NoSQL) y las estrategias para gestionar errores en los flujos reactivos.

¡Hasta la próxima entrega de nuestra serie sobre WebFlux!

Related Posts

Cuándo Usar Colas de Mensajes en el Desarrollo de Software

Cuándo Usar Colas de Mensajes en el Desarrollo de Software

Las colas de mensajes son herramientas clave para construir sistemas distribuidos, escalables y tolerantes a fallos. En este artículo te comparto una guía con situaciones comunes donde su uso es altam

Leer más
RabbitMQ 1: Introducción a RabbitMQ, El Corazón de la Mensajería Asíncrona

RabbitMQ 1: Introducción a RabbitMQ, El Corazón de la Mensajería Asíncrona

En el mundo del desarrollo de software moderno, especialmente con el auge de los microservicios y los sistemas distribuidos, la forma en que las diferentes partes de una aplicación se comunican es fun

Leer más
RabbitMQ 3: Configuración y Gestión de Colas en RabbitMQ

RabbitMQ 3: Configuración y Gestión de Colas en RabbitMQ

Después de entender qué es RabbitMQ y cómo sus Exchanges y Bindings dirigen los mensajes, llegamos a la Cola. La cola es fundamentalmente un buffer confiable: es el lugar donde los mensajes esperan su

Leer más
RabbitMQ 4: Robustez y Seguridad en RabbitMQ

RabbitMQ 4: Robustez y Seguridad en RabbitMQ

Hemos recorrido el camino desde la introducción a RabbitMQ y su papel en la mensajería asíncrona, pasando por su arquitectura, componentes de enrutamiento (Exchanges y Bindings), y la gestión detallad

Leer más
RabbitMQ 2: Arquitectura y Enrutamiento Avanzado en RabbitMQ

RabbitMQ 2: Arquitectura y Enrutamiento Avanzado en RabbitMQ

En nuestro primer artículo, exploramos qué es RabbitMQ, por qué es fundamental para la comunicación asíncrona en sistemas distribuidos y cuáles son sus casos de uso típicos. Lo comparamos con una "ofi

Leer más
RabbitMQ 5: Consumo de Recursos, Latencia y Monitorización de RabbitMQ

RabbitMQ 5: Consumo de Recursos, Latencia y Monitorización de RabbitMQ

Hemos explorado la teoría detrás de RabbitMQ, su arquitectura, cómo enruta mensajes y cómo podemos construir sistemas robustos y seguros. Sin embargo, para operar RabbitMQ de manera efectiva en produc

Leer más
RabbitMQ 6: Alta Disponibilidad y Escalabilidad con Clustering en RabbitMQ

RabbitMQ 6: Alta Disponibilidad y Escalabilidad con Clustering en RabbitMQ

Hasta ahora, hemos hablado de cómo un nodo individual de RabbitMQ maneja mensajes, gestiona colas, y cómo monitorizar su rendimiento y seguridad. Sin embargo, para aplicaciones críticas que no pueden

Leer más
Kafka 1: Introducción a Apache Kafka, fundamentos y Casos de Uso

Kafka 1: Introducción a Apache Kafka, fundamentos y Casos de Uso

En el panorama tecnológico actual, los datos son el motor que impulsa la innovación. La capacidad de procesar, reaccionar y mover grandes volúmenes de datos en tiempo real se ha convertido en una nece

Leer más
Kafka 2: Arquitectura Profunda de Kafka, Topics, Particiones y Brokers

Kafka 2: Arquitectura Profunda de Kafka, Topics, Particiones y Brokers

En nuestro primer artículo, despegamos en el mundo de Apache Kafka, sentando las bases de lo que es esta potente plataforma de streaming de eventos y diferenciándola de los sistemas de mensajería trad

Leer más
Kafka 3: Productores y Consumidores, Configuración y Buenas Prácticas

Kafka 3: Productores y Consumidores, Configuración y Buenas Prácticas

Hemos navegado por los conceptos esenciales de Apache Kafka y desentrañado la arquitectura que reside bajo la superficie, comprendiendo cómo los Topics se dividen en Particiones distribuidas entre Bro

Leer más
Kafka 4: Procesamiento de Datos en Tiempo Real con Kafka Streams y ksqlDB

Kafka 4: Procesamiento de Datos en Tiempo Real con Kafka Streams y ksqlDB

En los artículos anteriores, hemos construido una sólida comprensión de Apache Kafka: qué es, por qué es una plataforma líder para streaming de eventos, cómo está estructurado internamente con Topic

Leer más
Spring WebFlux 1: Fundamentos Reactivos y el Corazón de Reactor

Spring WebFlux 1: Fundamentos Reactivos y el Corazón de Reactor

¡Hola, entusiasta del desarrollo moderno! 👋 En el vertiginoso mundo de las aplicaciones web, donde la escalabilidad y la eficiencia son reyes, ha surgido un paradigma que desafía el modelo tradicion

Leer más
Kafka 6: Despliegue, Seguridad y Optimización

Kafka 6: Despliegue, Seguridad y Optimización

Hemos explorado la arquitectura fundamental de Apache Kafka, la dinámica entre productores y consumidores, sus potentes capacidades para el procesamiento de flujos de datos y las herramientas que enri

Leer más
Spring WebFlux 3: Comunicación, Datos y Errores Reactivos

Spring WebFlux 3: Comunicación, Datos y Errores Reactivos

¡Continuemos nuestro viaje por el fascinante mundo de Spring WebFlux! En la Parte 1, sentamos las bases de la programación reactiva y exploramos Project Reactor, el corazón de WebFlux. En la **Pa

Leer más
Kafka 7: Patrones Avanzados y Anti-Patrones con Kafka

Kafka 7: Patrones Avanzados y Anti-Patrones con Kafka

Hemos recorrido un camino considerable en nuestra serie sobre Apache Kafka. Desde sus fundamentos y arquitectura interna hasta la interacción con productores y consumidores, las herramientas de proces

Leer más
Kafka 5: Más Allá del Core, Explorando el Ecosistema de Apache Kafka

Kafka 5: Más Allá del Core, Explorando el Ecosistema de Apache Kafka

Hemos navegado por las entrañas de Apache Kafka, comprendiendo su funcionamiento interno, la interacción entre productores y consumidores, e incluso cómo procesar datos en tiempo real con Kafka Stream

Leer más
Spring WebFlux 4: Comunicación Avanzada, Pruebas y Producción

Spring WebFlux 4: Comunicación Avanzada, Pruebas y Producción

La serie Spring WebFlux nos ha llevado a través de un viaje fascinante por el mundo de la programación reactiva, desde sus fundamentos y el poder de Project Reactor hasta la construcción de arquit

Leer más
Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

En el dinámico mundo del desarrollo de software, la complejidad es el enemigo silencioso. Las aplicaciones crecen, los requisitos cambian y, sin una guía clara, el código puede convertirse rápidamente

Leer más