Spring: Sistema de eventos

logo springLos 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.

Cursos de programación

  • 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

Testing Spring con JUnit 4

Ficheros .properties en Spring IoC

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.