Los sistemas de eventos permiten que múltiples componentes intercambien información reduciendo el acoplamiento entre ellos suponiendo en la práctica una generalización más abstracta de los principios del patrón de diseño observador. El core de Spring cuenta con un sistema de eventos que quizás no sea demasiado conocido, de hecho en la bibliografía se suele citar de pasada. Este sistema de eventos de Spring es muy básico y utiliza el propio ApplicationContext como bróker para el envío de eventos entre los beans gestionados por el mismo. En este tutorial echaremos un vistazo a este sistema eventos.
Proyecto de ejemplo
Para hacer nuestras pruebas vamos a utilizar un proyecto Maven que sólo utiliza el módulo context de Spring 5. Usaremos log4j2 para mostrar y escribir todos los logs siempre a través de slf4j.
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.danielme.demo</groupId> <artifactId>spring-events</artifactId> <version>0.0.1</version> <name>spring-events</name> <description>Demo project for Spring Events</description> <url></url> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>5.1.7.RELEASE</spring.version> <log4j2.version>2.11.2</log4j2.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j2.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j2.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>${log4j2.version}</version> </dependency> </dependencies> </project>
La siguiente clase tiene un main y configura Spring.
package com.danielme.spring.events; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackageClasses = EventsApplication.class) public class EventsApplication { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext( EventsApplication.class); applicationContext.close(); } }
Envío y recepción de eventos
La gestión de un evento cuenta con tres elementos: la representación del evento propiamente dicho, su publicador y un oyente o listener que lo atiende. Veámoslo paso a paso con un ejemplo sencillo.
- El evento es una clase Serializable que hereda de ApplicationEvent y modela la información contenida por el evento, además de contener atributos heredados como por ejemplo un timestamp.
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; } }
Obsérvese que en el constructor tenemos que recibir el parámetro source para invocar al constructor de la clase padre. Este objeto es el que envía (publisher) el evento.
- Nuestro publicador instancia un MessageEvent y lo envía para que sea recibido por todos los beans gestionados por Spring que atiendan a dicho evento. Este envío es realizado mediante una instancia de ApplicationEventPublisher que ya existe en Spring y que podemos inyectar directamente por lo que cualquier bean puede realizar fácilmente envíos de eventos.
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"); } }
- El evento es atendido por todas los beans que implementen la interfaz ApplicationListener parametrizada para la clase que representa el evento.
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()); } }
- De forma alternativa (y recomendada), desde Spring 4.2 se puede anotar un método público con @EventListener para que reciba el evento. En una misma clase pueden definirse todos los @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 processMessageEvent(MessageEvent event) { logger.info("Event received, content = " + event.getMessage() + ", timestamp " + event.getTimestamp()); } }
Este método puede devolver uno varios eventos, en forma de array o colección, que son publicados de forma automática sin necesidad de utilizar de forma explícita ApplicationEventPublisher. Asimismo, el método puede atender de forma genérica más de un evento. El siguiente ejemplo atiende a varios eventos lanzados automáticamente por Spring.
package com.danielme.spring.events.listeners; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.ContextStartedEvent; 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, ContextClosedEvent.class, }) public void processContextStartedEvent() { logger.info("any context event received"); } }
El siguiente diagrama muestra cómo se relacionan las clases que se han creado, no hay ninguna relación entre publishers y listener más allá de la clase MessageEvent que modela los eventos.
Ahora probemos el sistema de eventos con la clase Main.
package com.danielme.spring.events; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import com.danielme.spring.events.services.DemoPublisherService; @Configuration @ComponentScan("com.danielme.spring.events") public class EventsApplication { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext( EventsApplication.class); DemoPublisherService demoPublisherService = applicationContext .getBean(DemoPublisherService.class); demoPublisherService.doSomething(); applicationContext.close(); } }
Y comprobamos el log.
11:50:26.969 [main] INFO com.danielme.spring.events.listeners.ApplicationContextEventsListener - any context event received 11:50:26.979 [main] INFO com.danielme.spring.events.services.DemoPublisherService - init doSomething method 11:50:26.982 [main] INFO com.danielme.spring.events.listeners.MessageAnnotatedListener - Event received, content = Hello!!, timestamp 1553338226979 11:50:26.982 [main] INFO com.danielme.spring.events.listeners.MessageListener - Event received, content = Hello!!, timestamp 1553338226979 11:50:26.982 [main] INFO com.danielme.spring.events.services.DemoPublisherService - event was published 11:50:26.982 [main] INFO com.danielme.spring.events.listeners.ApplicationContextEventsListener - any context event received
Tratamiento asíncrono
En el log anterior se puede comprobar el comportamiento síncrono de los eventos: el método «publicador» no continúa su ejecución hasta que los eventos que ha lanzado hayan sido procesado por todos los listeners. Esto permite que los métodos que reciban el evento formen parte del mismo proceso que lanza el evento y por tanto participen de la misma transacción si esta existe aunque con ciertas limitaciones tal y como comenta el siguiente artículo, pero no debería haber problemas si todas las operaciones con base de datos se realizan por ejemplo con Spring Data JPA.
En la práctica es muy habitual que los listeners deban ejecutarse de forma asíncrona en otro hilo de ejecución y no bloqueen la ejecución del método publisher. Este comportamiento puede conseguirse aplicando el sistema estándar de ejecución asíncrona de métodos de Spring anotando los métodos de tipo @EventListener con @Async siguiendo lo presentado en el tutorial Tips Spring : [CORE] Ejecución asíncrona de métodos
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 = " + event.getMessage() + ", timestamp " + event.getTimestamp()); } }
Activamos la ejecución asíncrona en Spring con @EnableAsync.
@Configuration @EnableAsync @ComponentScan(basePackageClasses = EventsApplication.class) public class EventsApplication {
Nota: probablemente queramos configurar un TaskExecutor, consultar el tutorial anteriormente mencionado.
Ahora en el log veremos que cuando el @EventListener asíncrono se ejecuta el método doSomething() ya ha finalizado su ejecución.
11:29:40.148 [main] INFO com.danielme.spring.events.listeners.ApplicationContextEventsListener - any context event received 11:29:40.157 [main] INFO com.danielme.spring.events.services.DemoPublisherService - init doSomething method 11:29:40.159 [main] INFO com.danielme.spring.events.listeners.MessageAnnotatedListener - Event received, content = Hello!!, timestamp 1553336980157 11:29:40.162 [main] INFO org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor - No task executor bean found for async processing: no bean of type TaskExecutor and no bean named 'taskExecutor' either 11:29:40.166 [main] INFO com.danielme.spring.events.listeners.MessageListener - Event received, content = Hello!!, timestamp 1553336980157 11:29:40.166 [main] INFO com.danielme.spring.events.services.DemoPublisherService - event was published 11:29:40.166 [main] INFO com.danielme.spring.events.listeners.ApplicationContextEventsListener - any context event received 11:29:40.180 [SimpleAsyncTaskExecutor-1] INFO com.danielme.spring.events.listeners.MessageAsyncListener - Procesing event asynchronous, content = Hello!!, timestamp 1553336980157
Código de ejemplo
El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar 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
Persistencia en BD con Spring Data JPA
Spring REST: Securización BASIC y JDBC con Spring Security
Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache
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.