Spring Framework: event handling

logo spring

Event-driven systems allow multiple components to communicate with each other by sharing any information and, most importantly, without being directly related. The general idea is that certain components generate events or messages and others receive and process them, either synchronously or asynchronously.

>> Read this post in Spanish here <<

The Spring Framework provides a simple event system that is not very well known; a small treasure hidden in plain sight 💎. Even in the bibliography, it is usually mentioned casually and sometimes not even that. It uses the ApplicationContext (the Spring bean container) as a broker for sending and receiving events among the beans it contains.

Nothing to do with the possibilities offered by powerful (and complex) platforms such as Rabbit MQ or Apache Kafka: communication between different applications or microservices, queue management, support for high workloads… But, in some scenarios, the Spring Framework event system may be enough. It will save us from using oversized solutions for our needs.

In this post, we will take a look at this event system.

Contents

  1. Sample project
  2. Publishing and listening to events
    1. First steps
    2. Better with the @EventListener annotation
    3. Testing with JUnit 5 and Mockito
    4. Order
    5. Transactions
  3. Asynchronous listeners with @Async
  4. Source code

Sample project

I will use use a Maven sample project based on Spring Boot 3.0 (Java 17). The version is irrelevant: we will explore Spring core features that have been available for many years. So don’t worry if you are using older versions of Spring.

Let’s see pom file. In addition to the basic Spring Boot starter, I have added the spring-boot-starter-test to write some tests:

<?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>

The class with the main method that bootstraps 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);
	}

}

We use the previous class to run the project. It only starts and stops Spring. The code we write will be used in a test class that I will explain in time.

Publishing and listening to events

First steps

The management of an event in Spring requires the creation of three elements: the representation of the event with its associated data, its publishers, and the listeners that observe it.

The event is a Serializable class that specializes ApplicationEvent (we will see later that this is not always mandatory). The consequence is that it inherits a timestamp containing the instant of the event creation, as well as source, an object representing the publisher. This source object is provided to the constructor of ApplicationEvent and cannot be null. Of course, our event class can contain as many fields as we want.

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;
	}

}

The publisher is a Spring bean that receives via injection an instance of ApplicationEventPublisher (actually, the ApplicationContext). The publish method of this bean sends an event of any type.

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");
	}

}

All the beans that implement the ApplicationListener interface typed for MessageEvent will receive the event represented by the object 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());
    }

Better with the @EventListener annotation

That’s it! It was easy. But I see something I don’t like: this interface-based technique is limiting. What do we do if we want to handle more than one type of event in a class? It isn’t an exotic requirement, and yet it is impossible unless we come up with a fix.

Don’t!🛑 Fortunately, since Spring 4.2 (July 2015) it has been possible to mark any public method of a bean with @EventListener to receive an event. Thanks to this annotation, we can define all the @EventListener methods we want in a single class.

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());
	}

}

An additional benefit of getting rid of the ApplicationListener interface is that we are no longer forced to make the event inherit from ApplicationEvent. This requirement must only be fulfilled by the events used to type ApplicationListener.

Now, the parameter of the @EventListener method indicates the event type to handle. If we don’t want the object with the event, we have to specify its class in the annotation in this way:

@EventListener(MessageEvent.class)
public void listenMessageEvent() {
   ...
}

A method can receive many events. It is the case of the following example, which also demonstrates three events fired by Spring:

  • ContextStartedEvent. An event that happens when an ApplicationContext is started (ApplicationContext#start).
  • ContextRefreshedEvent. An event that happens when an ApplicationContext is started or refreshed (ApplicationContext#refresh).
  • ContextStoppedEvent. An event that is triggered when an ApplicationContext is stopped (ApplicationContext#stop).

Spring Boot provides these additional events.

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());
	}

}

A peculiar feature of @EventListener is that the method can fire an event by returning its object. In fact, it can return several in a Collection or an array, and each one will be published as a separate event.

The following UML diagram depicts the relationships between the classes we have written above. Note the absence of any relationship between the publisher and the listeners, other than the fact that they use MessageEvent.

Testing with JUnit 5 and Mockito

Let’s check that everything works as expected with a JUnit 5 test. It will invoke the DemoPublisherService#doSomething method to confirm that the listeners are executed.

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));
	}

}

There are several points to clarify about EventsTest. First, it is a class with tests that need the Spring context, hence the @SpringBootTest annotation. By default, those tests are written with Jupiter (JUnit 5).

Any Spring bean can be injected into EventsTest. The MessageAnnotatedListener and MessageListener injections are special because they use @SpyBean instead of @Autowired. I want versions of those beans converted to Mockito (*) Spy objects. This allows me to verify if their event-listening methods were executed exactly once (lines 33 and 34).

(*) Library already included in spring-boot-starter-test.

In the log, we can also check by eye the proper functioning of our event system:

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

It’s easy to spot that the MessageEvent listeners are executed synchronously in the same thread as the publisher. DemoPublisherService#doSomething fires the event and waits for all listeners to handle it. It will then continue with the next line of code. This implies that an exception in any of the listeners will propagate to the publisher.

Order

If necessary, you can set the listeners’ execution order by pairing @EventListener with @Order. The order is indicated by a number so that the sequence of execution goes from the lowest number (highest priority) to the largest.

The following configuration ensures that MessageAnnotatedListener will always be executed first. This is the opposite of what the previous log showed:

@EventListener
@Order(1)
public void listenMessageEvent(MessageEvent event) {
Transactions

Regarding transactionality, the listeners will take part in the (hypothetical) ongoing transaction in which the publisher participates, even if they do not have the @Transactional annotation. Not a surprise: it is the standard behavior.

@TransactionalEventListener is an @EventListener that enables you to configure at what point in the current transaction the listener will be executed. Analyzing this annotation is beyond the scope of this post, so I recommend you read this article. Anyway, if you use @TransactionalEventListener, keep in mind that the listener will only be invoked when the event is fired from a transactional publisher.

Asynchronous listeners with @Async

In practice, it will be usual that the listeners must be executed asynchronously, each one in its own execution thread. In this way, they will not block the execution of the method that publishes the event. It just fires it and continues with its job.

If you read other tutorials, you will see that some authors explain that this can be achieved by creating a bean of type ApplicationEventMulticaster that Spring will use to call the listeners.

@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
    SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
	eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
	return eventMulticaster;
}

SimpleApplicationEventMulticaster is an implementation of ApplicationEventMulticaster that invokes the listeners in the same thread in which the publisher method runs. That’s what we have done so far: the whole process is synchronous. But if you look at line 4, it is possible to specify an Executor with which the listeners will be called. It is an interface that executes a given method given as a Runnable. The selected implementation (SimpleAsyncTaskExecutor) will execute each call to TaskExecutor#execute made by ApplicationEventMulticaster in a new thread.

ApplicationEventMulticaster will distribute all the events we publish, which means that all the listeners will be executed asynchronously. What if we want some of them to be asynchronous and others not? We will have to find our way and create, for example, our own multicaster.

We have unnecessarily backed ourselves into a corner. We can forget about configuring a multicaster and turn an @EventListener method into an asynchronous task.

In a nutshell, it is enough to just pair @EventListener with @Async. I won’t go into details; check the following post:

Let’s add a new class to the project with an asynchronous listener for MessageEvent:

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());
	}

}

We see in action one of the great strengths of event-driven systems: adding this new listener for MessageEvent does not require messing with existing code. The open-closed principle at work.

In order for @Async to work, don’t forget to annotate with @EnableAsync a configuration class (one marked with @Configuration or @SpringBootApplication):

@SpringBootApplication
@EnableAsync
public class EventsApplication {

Mission accomplished! You can see in the log that when the asynchronous @EventListener writes its message, doSomething has already finished:

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

Asynchronous listeners cannot fire an event by returning the event object; they must call the ApplicationEventPublisher. Also, their exceptions do not affect the publisher, as both run in different threads. What is the same as in synchronous listeners is the use of @Order and @TransactionalEventListener.

Testing becomes more complicated as soon as it involves asynchronous code. The next "improvement" of the test will fail due to the last verify because listenAsyncMessageEvent has not yet been executed:

@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));
}

The solution is in the Mockito#timeout method. It allows setting a timeout in milliseconds for the conditions supported by 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));
}

Now the third assertion will be successful if listenAsyncMessageEvent is executed before one second passes from the instant in which the verification begins to be checked.

Source code

The project is available on GitHub.

Other posts in English

JSP and Spring Boot

Spring Framework: asynchronous methods with @Async, Future and TaskExecutor

Testing Spring Boot: Docker with Testcontainers and JUnit 5. MySQL and other images.

Deja una respuesta

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. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.