Introducción
Los eventos nos permiten hacer que nuestra aplicación sea más flexible y desacoplada que si usaramos invocaciones a métodos directamente. Esto se hace registrando oyentes para los distintos eventos y, además, puede haber múltiples oyentes por evento.
Springboot ofrece una serie de herramientas para usar el patrón Observer sin tener que hacer ninguna configuración específica y a través de anotaciones.
Como ejemplo vamos a gestionar eventos con Springboot en la que se creen cursos. Cuando un curso sea creado se va a crear un evento y un consumidor de ese evento va a mostrar por consola el título del curso.
Indice
- Definiendo los eventos
- Publicando
- Escuchando
- Probando el ejemplo
- Soporte a transacciones
- Métodos asíncronos
- Referencias
Definiendo los eventos
Los eventos deben extender la clase ApplicationEvent.
public class Course extends ApplicationEvent { }
Ahora bien, para hacernos las cosas más fáciles, si no extendemos de esta clase Spring de forma automática envolverá nuestro objeto en un PayloadApplicationEvent. Esta es la forma que se va a usar en este tutorial.
Publicando eventos
Para publicar eventos se emplea la interfaz ApplicationEventPublisher, que se introduce en un campo de la clase encargada de publicar los eventos. Spring se va a encargar de inyectar esta dependencia al instanciar CoursePublisher.
@Component public class CoursePublisher { private final ApplicationEventPublisher publisher; public CoursePublisher(ApplicationEventPublisher publisher){ this.publisher = publisher; } public void publish(){ System.out.println("PUBLISHER: Producing course"); publisher.publishEvent(new Course(generateRandomString())); } private String generateRandomString(){ return RandomStringUtils.randomNumeric(5); } }
El método publish es el encargado de publicar el evento del curso.
Escuchando eventos
Un evento puede tener varios oyentes en distintos puntos de la aplicación, en función de la lógica de negocio. Es Spring el que se encarga de registrar los oyentes al iniciar la aplicación.
Hay dos formas de escuchar un evento: mediante anotaciones con @EventListener o implementando la clase ApplicationListener nosotros mismos. ApplicationListener define un único método, onApplicationEvent, que se llama cuando un evento es lanzado.
@Component public class CourseConsumer { @EventListener public void createCourse(Course course) { System.out.println("CONSUMER: A course has been created with title: " + course.getTitle()); } }
Usando la anotación @EventListener hace que sea Spring el que registre un ApplicationListener para el evento concreto. La anotación de @Component está para que Spring lo detecte como Bean para luego inyectarlo donde sea necesario.
Probando el ejemplo
Para probar todo esto se puede crear un pequeño test que levante el contexto de Spring y ejecutarlo. Es necesario levantar el contexto para que se inyecten las dependencias de los @Autowired.
@RunWith(SpringRunner.class) @SpringBootTest(classes = App.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class CourseConsumerTest { @Autowired private CourseConsumer courseConsumer; @Autowired private CoursePublisher coursePublisher; @Test public void testConsumerProducer(){ courseProducer.publish(); courseProducer.publish(); courseProducer.publish(); } }
Las anotaciones usadas sobre la clase son: @RunWith que proporciona funcionalidad para los tests y @SpringBootTest que levanta el contexto de Spring. Al correr el test se observará en la consola el mensaje que CourseConsumer tiene definido.
PUBLISHER: Producing course CONSUMER: A course has been created with title: 28435 PUBLISHER: Producing course CONSUMER: A course has been created with title: 08887 PUBLISHER: Producing course CONSUMER: A course has been created with title: 80268
Soporte a transacciones
Mediante el uso de la anotación @TransactionalEventListener se consigue un oyente que va a tener en cuenta si el evento se encuentra en una transacción. ¿Cuándo se va a ejecutar el método del oyente? Se puede especificar la fase en la que se quiere invocar utilizando el argumento phase. Existen cuatro opciones:
- AFTER_COMMIT es la opción por defecto. Se invoca una vez el commit ha finalizado de forma exitosa.
- AFTER_COMPLETION se invoca cuando la transacción se ha completado.
- AFTER_ROLLBACK se invoca solo si la transacción ha terminado en un roll back.
- BEFORE_COMMIT se invoca antes de que la transacción haga el commit.
Métodos asíncronos
Spring permite crear y publicar eventos de forma síncrona por defecto. Esto tiene la ventaja de permitir al oyente participar en el contexto de la transacción del evento.
Si queremos un comportamiento asíncrono lo que hay que hacer es anotar el método del Bean que queremos que sea asíncrono con @Async. De este modo ese método se ejecutará en otro hilo y el que llama al método no esperará a recibir una respuesta para seguir con la ejecución. También hay que añadir en la configuración la anotación @EnableAsync.
@SpringBootApplication @EnableAsync public class App { private static ApplicationContext applicationContext; public static void main(String[] args) { applicationContext = SpringApplication.run(App.class, args); } }
Vamos a anotar el método de publish como asíncrono. Además se añade una espera de 10 segundos antes de publicar el evento.
@Component public class CoursePublisher { private final ApplicationEventPublisher publisher; public CoursePublisher(ApplicationEventPublisher publisher){ this.publisher = publisher; } @Async public void publish() throws InterruptedException { System.out.println("PUBLISHER: Producing course"); Thread.sleep(10000); publisher.publishEvent(new Course(generateRandomString())); } private String generateRandomString(){ return RandomStringUtils.randomNumeric(5); } }
En el test hay que incluir otro tiempo (de 11 segundos) de espera para que no se cierre la terminal.
@Test public void testConsumerProducer() throws InterruptedException { courseProducer.publish(); courseProducer.publish(); courseProducer.publish(); Thread.sleep(11000); }
El resultado en la consola es:
PUBLISHER: Producing course PUBLISHER: Producing course PUBLISHER: Producing course CONSUMER: A course has been created with title: 71685 CONSUMER: A course has been created with title: 81937 CONSUMER: A course has been created with title: 44439
No se ha esperado a que el primer publisher termine para llamar al siguiente, si no que ha tenido un comportamiento asíncrono. Como se ha visto, configurar los eventos con Springboot para que sean asíncronos es sencillo.