Los sistemas basados en eventos permiten que múltiples componentes se comuniquen entre sí compartiendo cualquier tipo de información y, lo más importante, sin relacionarse directamente. La idea general es que ciertos componentes generen eventos o mensajes y otros los reciban y procesen, ya sea de forma síncrona o asíncrona.
>> Read this post in English here <<
El core de Spring Framework cuenta con un sencillo sistema de eventos poco conocido; un pequeño tesoro oculto a plena vista. Incluso en la bibliografía se suele citar de pasada, y a veces ni eso. Utiliza el ApplicationContext
(el contenedor de beans) como bróker para el envío y la recepción de eventos entre los beans que contiene.
Nada que ver con las posibilidades que ofrecen potentes ( y complejas) plataformas como Rabbit MQ o Apache Kafka: comunicación entre distintas aplicaciones o microservicios, gestión de colas, soporte de altas cargas de trabajo… Con todo, en algunos escenarios, el sistema de eventos de Spring puede ser suficiente. Nos evitará recurrir a soluciones sobredimensionadas para nuestras necesidades.
En el presente tutorial echaremos un vistazo a este sistema de eventos.
- Proyecto de ejemplo
- Envío y recepción de eventos
- Ejecución asíncrona del receptor con @Async
- Código de ejemplo
Nota. En el mundo Jakarta EE \ JEE existe un sistema de eventos parecido. Lo explico aquí.
Proyecto de ejemplo
Voy a utilizar un proyecto de ejemplo Maven basado en Spring Boot 3.0 (requiere Java 17). La versión es irrelevante; veremos funciones del core de Spring disponibles desde hace muchos años, así que no te preocupes si usas versiones antiguas de Spring.
Este es el pom
. Además del iniciador (starter) básico de Spring Boot, he añadido spring-boot-starter-test
para hacer una pequeña prueba.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.danielme.spring</groupId>
<artifactId>spring-events</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
La necesaria clase marcada con @SpringBootApplication
con el método main
que arranca Spring:
package com.danielme.spring.events;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EventsApplication {
public static void main(String[] args) {
SpringApplication.run(EventsApplication.class, args);
}
}
El proyecto se ejecuta con EventsApplication
. Solo arranca y detiene Spring. El código que escribamos lo usaremos en una prueba que explicaré a su debido tiempo.
Envío y recepción de eventos
Primeros pasos
La gestión de un evento en Spring precisa la creación de tres elementos: la representación del evento con sus datos asociados, sus emisores (publishers) y los receptores (listeners) que lo atienden.

El evento es una clase Serializable
que especializa a ApplicationEvent
(más adelante veremos que no siempre es obligatorio). La consecuencia es que hereda un sello de tiempo (timestamp
) que contiene el instante de la creación del evento, así como source
, objeto que representa al emisor. Este source
se proporciona al constructor de ApplicationEvent
y no puede ser null
. Por supuesto, nuestro evento puede contener todos los atributos que necesitemos. Aquí lo tienes:
package com.danielme.spring.events.model;
import org.springframework.context.ApplicationEvent;
public class MessageEvent extends ApplicationEvent {
private static final long serialVersionUID = -3762610544324295353L;
private final String message;
public MessageEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}

El emisor es un bean de Spring que recibe vía inyección un objeto de tipo ApplicationEventPublisher (en realidad, el ApplicationContext
). Su método publishEvent
envía un evento de cualquier tipo:
package com.danielme.spring.events.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import com.danielme.spring.events.model.MessageEvent;
@Service
public class DemoPublisherService {
private static final Logger logger = LoggerFactory.getLogger(DemoPublisherService.class);
private final ApplicationEventPublisher publisher;
public DemoPublisherService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void doSomething() {
logger.info("init doSomething method");
MessageEvent messageEvent = new MessageEvent(this, "Hello!!");
publisher.publishEvent(messageEvent);
logger.info("event was published");
}
}

Todos los beans que implementen la interfaz ApplicationListener
tipada para MessageEvent
recibirán el evento representado por messageEvent
.
package com.danielme.spring.events.listeners;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import com.danielme.spring.events.model.MessageEvent;
@Component
public class MessageListener implements ApplicationListener<MessageEvent> {
private static final Logger logger = LoggerFactory.getLogger(MessageListener.class);
public void onApplicationEvent(MessageEvent event) {
logger.info("Event received, content = " + event.getMessage() + ", timestamp " + event.getTimestamp());
}
Mejor con la anotación @EventListener
Eso es todo. Resultó fácil, pero veo algo que no me gusta: esta técnica basada en una interfaz es limitante. ¿Qué hacemos si queremos atender en una clase más de un tipo de evento? No es un requisito exótico y, sin embargo, resulta imposible, a menos que busquemos algún apaño.
¡No lo hagas! Por fortuna, desde Spring 4.2 (julio de 2015) es posible marcar cualquier método público de un bean con @EventListener para que reciba un evento. Gracias a esta anotación, en una misma clase definiremos todos los métodos @EventListener
que necesitemos.
package com.danielme.spring.events.listeners;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import com.danielme.spring.events.model.MessageEvent;
@Component
public class MessageAnnotatedListener {
private static final Logger logger = LoggerFactory.getLogger(MessageAnnotatedListener.class);
@EventListener
public void listenMessageEvent(MessageEvent event) {
logger.info("Event received, content = {}, timestamp {}", event.getMessage(), event.getTimestamp());
}
}
Un beneficio adicional de librarnos de la interfaz ApplicationListener
es que ya no estamos obligados a que el evento herede de ApplicationEvent
. Este requisito solo deben cumplirlo los eventos empleados para tipar ApplicationListener
.
Ahora, el parámetro del método @EventListener
indica el tipo del evento a procesar. Si no queremos el objeto con el evento, tendremos que especificar su clase en la anotación.
@EventListener(MessageEvent.class)
public void listenMessageEvent() {
...
}
Un método puede recibir varios eventos. Es el caso del próximo ejemplo, que además muestra tres eventos emitidos por Spring:
- ContextStartedEvent. Evento que se produce cuando se inicia un
ApplicationContext
(ApplicationContext#start
). - ContextRefreshedEvent. Evento que se produce cuando se inicia o actualiza un
ApplicationContext
(ApplicationContext#refresh
). - ContextStoppedEvent. Evento que se produce cuando se detiene un
ApplicationContext
(ApplicationContext#stop
).
Spring Boot aporta estos eventos adicionales.
package com.danielme.spring.events.listeners;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextStartedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextEventsListener {
private static final Logger logger = LoggerFactory.getLogger(ApplicationContextEventsListener.class);
@EventListener({ ContextStartedEvent.class, ContextRefreshedEvent.class, ContextStoppedEvent.class, })
public void listenContextEvents(ApplicationContextEvent event) {
logger.info("context event received: {}", event.getClass().getSimpleName());
}
}
Una capacidad curiosa de @EventListener
es que el método es capaz de emitir un evento con tan solo devolver su objeto. De hecho, puede retornar varios en una Collection
o array, y cada uno se enviará como un evento independiente.
El siguiente diagrama UML muestra las relaciones de las clases que hemos escrito. Fíjate en la ausencia de vínculo alguno entre el emisor y los receptores, más allá de que utilicen MessageEvent
.

Testing con JUnit 5
Comprobemos que todo está en orden con una prueba de JUnit 5. Invocará al método DemoPublisherService#doSomething
con el objetivo de chequear que los receptores son ejecutados.
package com.danielme.spring.events;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import com.danielme.spring.events.listeners.MessageAnnotatedListener;
import com.danielme.spring.events.listeners.MessageListener;
import com.danielme.spring.events.model.MessageEvent;
import com.danielme.spring.events.services.DemoPublisherService;
@SpringBootTest
class EventsTest {
@Autowired
DemoPublisherService publisher;
@SpyBean
MessageAnnotatedListener annotatedListener;
@SpyBean
MessageListener messageListener;
@Test
void testEventsWereListened() {
publisher.doSomething();
verify(annotatedListener, times(1)).listenMessageEvent(any(MessageEvent.class));
verify(messageListener, times(1)).onApplicationEvent(any(MessageEvent.class));
}
}
Hay varias cosas que aclarar sobre EventsTest
. En primer lugar, es una clase con pruebas que requieren el contexto de Spring, de ahí la anotación @SpringBootTest
. De forma predeterminada, esas pruebas se escriben con Jupiter (JUnit 5).
En EventsTest
se puede inyectar cualquier bean de Spring. Las inyecciones de MessageAnnotatedListener
y MessageListener
son especiales porque usan @SpyBean
en lugar de @Autowired
. Quiero versiones de esos beans convertidas en objetos de tipo Spy
de Mockito (librería incluida en spring-boot-starter-test
). Esto me permite verificar si sus métodos que escuchan a los eventos se ejecutaron una sola vez (líneas 33 y 34).
En la bitácora o log también podemos comprobar a ojo el correcto funcionamiento de los eventos.
2022-10-01 20:44:19.620 INFO 46136 --- [ main] com.danielme.spring.events.EventsTest : Started EventsTest in 2.03 seconds (JVM running for 3.527) 2022-10-01 20:44:19.670 INFO 46136 --- [ main] c.d.s.e.services.DemoPublisherService : init doSomething method 2022-10-01 20:44:19.685 INFO 46136 --- [ main] c.d.s.events.listeners.MessageListener : Event received, content = Hello!!, timestamp 1664649859670 2022-10-01 20:44:19.690 INFO 46136 --- [ main] c.d.s.e.l.MessageAnnotatedListener : Event received, content = Hello!!, timestamp 1664649859670 2022-10-01 20:44:19.691 INFO 46136 --- [ main] c.d.s.e.services.DemoPublisherService : event was published
Queda patente que los receptores de MessageEvent
se ejecutan de manera síncrona en el mismo hilo o hebra (thread) que el método emisor. DemoPublisherService#doSomething
lanza el evento y permanece a la espera de que todos los receptores lo procesen. Luego continuará con la próxima línea de código. Esto implica que una excepción en cualquiera de los receptores se propagará hacia el emisor.

Orden
Si fuera necesario, puedes establecer el orden de ejecución de los receptores acompañando @EventListener
con @Order
. Se indica con un número, de tal modo que la secuencia de ejecución va desde los valores menores (más prioritarios) a los mayores.
La siguiente configuración asegura que MessageAnnotatedListener
siempre se ejecutará primero. Es justo lo contrario de lo que mostró el log anterior.
@EventListener
@Order(1)
public void listenMessageEvent(MessageEvent event) {
Transacciones
Con respecto a la transaccionalidad, los métodos receptores participarán en la (hipotética) transacción en curso de la que forme parte el emisor aunque carezcan de la anotación @Transactional
. Ninguna sorpresa, pues es el comportamiento habitual, tal y como explico en este tutorial:
Te recomiendo la lectura de este artículo (en inglés) acerca de la anotación @TransactionalEventListener
. Es un @EventListener
que permite configurar en qué momento de la transacción en curso se ejecutará el receptor. Si lo usas, ten en cuenta que el receptor solo se invocará cuando el evento se lance desde un emisor transaccional.
Ejecución asíncrona del receptor con @Async
En la práctica, lo común será que los receptores deban ejecutarse de forma asíncrona cada uno en su propio hilo o thread. No bloquearán la ejecución del método que emite el evento: este lo lanza y continúa con sus asuntos.
Si consultas otros tutoriales, verás que algunos autores explican que esto se puede conseguir creando un bean de tipo ApplicationEventMulticaster
que Spring usará para llamar a los receptores:
@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
return eventMulticaster;
}
SimpleApplicationEventMulticaster
es una implementación de ApplicationEventMulticaster
que invoca a los receptores en el mismo hilo en el que corre el método emisor. Es decir, lo que hemos hecho hasta el momento: todo el proceso es síncrono. Pero si te fijas en la línea 4, es posible especificar un Executor
con el que se llamará a los receptores. Se trata de una interfaz que se limita a ejecutar un método dado en la forma de un Runnable
. La implementación elegida (SimpleAsyncTaskExecutor
) ejecutará cada llamada a TaskExecutor#execute
que realice ApplicationEventMulticaster
en un nuevo hilo.
ApplicationEventMulticaster
distribuirá todos los eventos que lancemos, por lo que todos los receptores serán asíncronos. ¿Y si queremos que algunos lo sean y otros no? Tendremos que buscarnos la vida y crear, por ejemplo, nuestro propio multicaster.
Nos hemos metido innecesariamente en un aprieto porque podemos olvidarnos de lo anterior y convertir un @EventListener
en una tarea asíncrona. No entraré en detalles; para eso está el siguiente tutorial.
En esencia, basta con acompañar @EventListener
con @Async
:
package com.danielme.spring.events.listeners;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.danielme.spring.events.model.MessageEvent;
@Component
public class MessageAsyncListener {
private static final Logger logger = LoggerFactory.getLogger(MessageAsyncListener.class);
@EventListener
@Async
public void processMessageEvent(MessageEvent event) {
logger.info("Procesing event asynchronous, content = {} , timestamp {}", event.getMessage(),
event.getTimestamp());
}
}
Vemos en acción uno de los puntos fuertes de los sistemas basados en eventos: la inclusión de este nuevo receptor para MessageEvent
no requiere tocar el código ya existente. Se respeta el célebre principio open-closed.
Para que @Async
funcione, hay que anotar con @EnableAsync
una clase de configuración (esto es, marcada con @Configuration
o @SpringBootApplication
).
@SpringBootApplication
@EnableAsync
public class EventsApplication {
Mision cumplida 💪. En el log vemos que cuando el @EventListener
asíncrono escribe su mensaje, doSomething
ya ha finalizado.
2022-10-01 22:09:31.050 INFO 49458 --- [ main] c.d.s.e.services.DemoPublisherService : init doSomething method 2022-10-01 22:09:31.063 INFO 49458 --- [ main] c.d.s.events.listeners.MessageListener : Event received, content = Hello!!, timestamp 1664654971050 2022-10-01 22:09:31.067 INFO 49458 --- [ main] c.d.s.e.l.MessageAnnotatedListener : Event received, content = Hello!!, timestamp 1664654971050 2022-10-01 22:09:31.130 INFO 49458 --- [ main] c.d.s.e.services.DemoPublisherService : event was published 2022-10-01 22:09:31.159 INFO 49458 --- [ task-1] c.d.s.e.listeners.MessageAsyncListener : Procesing event asynchronous, content = Hello!! , timestamp 1664654971050
Los receptores asíncronos no pueden lanzar un evento retornando su objeto: deben recurrir a ApplicationEventPublisher
. Asimismo, sus excepciones no afectan al emisor, pues ambos «corren» en hilos distintos. Lo que sí es igual que en los receptores síncronos es el empleo de @Order
y @TransactionalEventListener
.
Las pruebas se complican en el momento en que atañen a procesos asíncronos. La siguiente «mejora» del test fallará debido al último verify
porque todavía no se ha ejecutado listenAsyncMessageEvent
:
@Test
void testEventsWereListened() {
publisher.doSomething();
verify(annotatedListener, times(1)).listenMessageEvent(any(MessageEvent.class));
verify(messageListener, times(1)).onApplicationEvent(any(MessageEvent.class));
verify(asyncListener, times(1)).listenAsyncMessageEvent(any(MessageEvent.class));
}
La solución está en el método Mockito#timeout
. Permite definir un timeout en milisegundos para las condiciones admitidas por verify
.
@Test
void testEventsWereListened() {
publisher.doSomething();
verify(annotatedListener, times(1)).listenMessageEvent(any(MessageEvent.class));
verify(messageListener, times(1)).onApplicationEvent(any(MessageEvent.class));
verify(asyncListener, timeout(1000).times(1)).listenAsyncMessageEvent(any(MessageEvent.class));
}
Ahora la tercera aserción será exitosa cuando listenAsyncMessageEvent
sea ejecutado antes de que transcurra un segundo a partir del instante en el que la verificación comience a comprobarse.
Código de ejemplo
El proyecto se encuentra en GitHub. Para más información sobre GitHub, consultar este artículo.
Otros tutoriales relacionados con Spring
Testing en Spring Boot con JUnit 4-5. Mockito, MockMvc, REST Assured, bases de datos embebidas
Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA
Spring JDBC Template: simplificando el uso de SQL
Hola Daniel me parece un articulo fabuloso me gusto, pero sabes como podría hacer esto pero con Spring5 y no usar spring boot (esto ya lo hice y si funciona 🙂 )
El sistema de eventos está disponible de serie en el core de Spring y no hay que configurar nada. El ejemplo del tutorial no lleva Spring Boot.