Eventos con Spring Framework

0
1323

Introducción

Una de las herramientas más potentes y que pasamos más por alto en Spring son los eventos. Gracias al uso de eventos podemos hacer que nuestros componentes compartan cualquier tipo de información a la vez que reducimos el acoplamiento entre ellos.

En una aplicación “típica” en la que hay llamadas directas entre componentes, lo más normal es que los componentes de una aplicación se llamen entre sí. Esto genera mucho acoplamiento entre ellos y hace que unos componentes tengan que tener conocimiento explícito de a qué componentes llamar cuando una acción se realiza y, por lo tanto, aumente la complejidad de los componentes y su mantenimiento.

Con una solución con eventos, podemos delegar en terceros la posibilidad de escuchar determinado evento para realizar cierta acción cuando algo ha ocurrido. De esta manera reducimos la complejidad de cada componente, el acoplamiento y reducimos el esfuerzo de mantenimiento

Pues bien, para solucionar todo esto, Spring Framework nos proporciona un sistema de eventos muy sencillo de usar.

Cabe destacar que esta solución es válida para componentes que comparten el mismo contexto de aplicación. Es decir, que si nuestro objetivo es hacer microservicios, necesitaremos de un broker de mensajes externo (Rabbit MQ, Kafka, etc.) que se encargue de publicar dichos eventos para terceros.

Si nuestra solución pasa por crear un monolito bien modularizado, Spring Events nos vale perfectamente para solucionar el problema.

Componentes

Para poder comenzar con Spring Events. Necesitamos conocer los componentes principales que compondrán la solución:

  • Evento: los eventos son clases serializables que contendrán la información que se va a intercambiar cuando un suceso ocurra. Véase como ejemplo: «se ha creado un usuario»
  • Publisher: será el encargado de publicar un evento, de un determinado tipo, en el contexto de la aplicación.
  • Listener: componente que estará escuchando si un evento ha ocurrido en el contexto de la aplicación. En el momento que dicho evento haya sucedido, el listener capturará dicho evento y realizará las operaciones que deba hacer en ese caso.

 

Manos a la obra!

Creando un evento de aplicación

Como hemos comentado anteriormente, un evento es una clase serializable que contendrá toda la información que se va a intercambiar entre los distintos componentes de mi aplicación. 

Para ello basta con crear una clase que extienda de ApplicationEvent. Así nuestro evento heredará el timestamp que contendrá el instante en el que el evento se creó y el source, que es el objeto que creó dicho evento.

Para este ejemplo vamos a crear un evento cuando un usuario ha sido creado:

public class UserCreatedEvent extends ApplicationEvent {

    private final String nif;
    private final String name;
    private final String surname;
    
    public UserCreatedEvent(Object source, String nif, String name, String surname) {
        super(source);
        this.nif = nif;
        this.name = name;
        this.surname = surname;
    }

    public String getNif() {
        return nif;
    }
    public String getName() {
        return name;
    }
    public String getSurname() {
        return surname;
    }
}

Desde Spring 4.2 se permite poder crear eventos sin necesidad de extender de ApplicationEvent, por lo que simplificamos aún más nuestra clase:

@Data
public class UserCreatedEvent {
    private final String nif;
    private final String name;
    private final String surname;
}

* Estoy usando Lombok para generar el constructor, getters y setters de la clase

Publicando mi evento

Una vez hemos creado el evento, necesitamos publicarlo en el contexto de la aplicación. Para ello necesitamos de un publisher que se encargue de ello.

Para poder publicar los eventos, nuestro componente usará el ApplicationEventPublisher el cual será el encargado de publicar dichos eventos en el contexto de nuestra aplicación.

@Component
public class UserCreatedEventPublisher{

    private final static Logger LOGGER = LoggerFactory.getLogger(UserCreatedEventPublisher.class);

    private final ApplicationEventPublisher eventPublisher;

    @Autowired
    public UserCreatedEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void publishUserCreatedEvent(String nif, String name, String surname){
        UserCreatedEvent event = new UserCreatedEvent(nif, name, surname);
        LOGGER.info("Event {} published", event);
        eventPublisher.publishEvent(event);
    }
}

Escuchando el evento

Una vez publicado el evento, lo siguiente será crear los componentes que van a estar escuchando a dicho evento. Para ello tenemos 2 opciones:

  • Implemente la interfaz ApplicationListener:
@Component
public class UserCreatedEventListener implements ApplicationListener<UserCreatedEvent> {

    private final static Logger LOGGER = LoggerFactory.getLogger(UserCreatedEventListener.class);

    @Override
    public void onApplicationEvent(UserCreatedEvent event) {
        //do stuff with repositories and other third parties
        LOGGER.info("Event {} received", event);
    }
}
  • Anotar el método de nuestro componente con la anotación @EventListener:
@Component
public class UserCreatedEventListener {

    private final static Logger LOGGER = LoggerFactory.getLogger(UserCreatedEventListener.class);

    @EventListener
    public void handleUserCreatedEvent(UserCreatedEvent event){
        //do stuff with repositories and other third parties
        LOGGER.info("Event {} received", event);
    }
}

Cualquiera de las dos opciones son perfectamente válidas aunque yo prefiero usar la segunda por sencillez.

Probando nuestra aplicación

Para probar la aplicación lanzaremos un test que lance un evento y veremos en el log la traza que dejará nuestra aplicación:

2023-01-03T11:53:28.110+01:00  INFO 15218 --- [main] c.s.e.p.UserCreatedEventPublisher        : Event UserCreatedEvent(nif=45673451A, name=Ismael, surname=Fernandez) published
2023-01-03T11:53:28.113+01:00  INFO 15218 --- [main] c.s.e.listener.UserCreatedEventListener  : Event UserCreatedEvent(nif=45673451A, name=Ismael, surname=Fernandez) received

Como podemos ver, el publisher publica un evento y el listener lo recibe correctamente. Pero hay algo aquí que debemos de tener en cuenta. La publicación del evento es síncrona, por lo tanto, todo el proceso de publicación/recepción del evento se realiza en el mismo hilo.

El principal objetivo de usar eventos es reducir el tiempo de respuesta de nuestro sistema y lo más lógico sería que el sistema funcionase de forma asíncrona. Para ello, crearemos otro Listener que funcione de forma asíncrona y así podremos comprobar que, efectivamente, los eventos podemos procesarlos de forma independiente para que el hilo principal pueda liberarse una vez un evento ha sido publicado.

Para poder escuchar los eventos de forma asíncrona, basta con usar la anotación @Async:

@Component
public class AsyncUserCreatedEventListener {

    private final static Logger LOGGER = LoggerFactory.getLogger(AsyncUserCreatedEventListener.class);

    @Async
    @EventListener
    public void handleUserCreatedEvent(UserCreatedEvent event){
        //do stuff with repositories and other third parties
        LOGGER.info("Event {} received", event);
    }
}

Volvemos a ejecutar nuestro test y ahora sí que podemos comprobar que el listener está escuchando los eventos en un hilo distinto al principal:

2023-01-03T11:58:09.421+01:00  INFO 15469 --- [           main] c.s.e.p.UserCreatedEventPublisher        : Event UserCreatedEvent(nif=45673451A, name=Ismael, surname=Fernandez) published
2023-01-03T11:58:09.424+01:00  INFO 15469 --- [           main] c.s.e.listener.UserCreatedEventListener  : Event UserCreatedEvent(nif=45673451A, name=Ismael, surname=Fernandez) received
2023-01-03T11:58:09.424+01:00  INFO 15469 --- [         task-1] c.s.e.l.AsyncUserCreatedEventListener    : Event UserCreatedEvent(nif=45673451A, name=Ismael, surname=Fernandez) received

Conclusiones

En este tutorial hemos visto cómo, de forma sencilla, podemos desacoplar nuestros componentes y aprovechar la potencia que nos dan los eventos para reducir el acoplamiento y el mantenimiento de nuestro sistema. En tutoriales posteriores veremos cómo podemos usar brokers externos y aplicar patrones de segregación de responsabilidad que harán de nuestra aplicación una solución más robusta, que maximizará el rendimiento, la escalabilidad y la seguridad.

Recursos

Puedes ver el código de la aplicación aquí

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad