Introduction:
In this article, you will learn about Custom Events in Spring. Like many capabilities provided by the Spring framework, events are one of the most useful features provided by ApplicationContext in Spring. Spring allows you to publish/subscribe events synchronously as well as asynchronously.
There are several standard Events in Spring as ContextRefreshedEvent
, ContextStartedEvent
, ContextStoppedEvent
, ContextClosedEvent
, RequestHandledEvent
and ServletRequestHandledEvent
. Spring also allows you to create your custom event classes based on your application need. We will first learn to create a custom event and then explore the list of standard ones in another article.
Custom events can be created synchronously as well as asynchronously. The asynchronously way of creation may look complicated but provides better performance due to the non-blocking nature of execution.
A. Synchronous custom Spring Event
There are just few simple rules that needs to be followed to pub/sub events synchronously.
- The event class should extend
ApplicationEvent
. - The publisher has to make use of
ApplicationEventPublisher
. - The event listener should implement the
ApplicationListener
.
1. Create the Custom event class
The custom event class has to extend the ApplicationEvent
abstract class. We will create a custom UserEvent class as below.
package com.jsbd.events;
import org.springframework.context.ApplicationEvent;
import java.util.StringJoiner;
public class UserEvent extends ApplicationEvent {
//Custom property
private String message;
//Custom property
private Integer eventId;
public UserEvent(Object source) {
super(source);
}
public UserEvent(Object source, String message,
Integer eventId) {
super(source);
this.message = message;
this.eventId = eventId;
}
public String getMessage() {
return this.message;
}
public Integer getEventId() {
return this.eventId;
}
@Override
public String toString() {
return new StringJoiner(", ", UserEvent.class.getSimpleName() + "[", "]")
.add("message='" + message + "'")
.add("source=" + source)
.add("eventId=" + eventId)
.toString();
}
}
2. Create an event Listener class
We will create an EventListener by implementing the ApplicationListener
interface and its method onApplicationEvent
. As you can see this method accepts UserEvent
as a parameter. Whenever there is an userEvent published, this method will get executed.
package com.jsbd.events;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class EventListener implements ApplicationListener<UserEvent> {
@Override
public void onApplicationEvent(UserEvent event) {
System.out.println("======= UserEvent Listener =======");
System.out.println(event);
}
}
3. Create an event Publisher class
The Spring framework already provides us ApplicationEventPublisher
, we will autowire this using @Autowired
inside our custom event publisher class.
@Component
public class EventPublisher {
@Autowired //Use setter based injection in real code
private ApplicationEventPublisher applicationEventPublisher;
public void publishEvent(String message, Integer eventId) {
applicationEventPublisher.publishEvent(
new UserEvent(this, message, eventId)
);
}
}
4. Publish an event and test the example
Now, we will write a simple JUnit test case to publish an event and check if the listener receives it. I have also added the configuration needed to run this example.
@Configuration
@ComponentScan("com.jsbd.events")
public class AppConfig {
}
@SpringJUnitConfig(AppConfig.class)
class EventPublisherTest {
@Autowired
EventPublisher eventPublisher;
@Test
public void sendSynchronousEvent() {
eventPublisher.publishEvent("User registered", 101);
}
Output:
======= UserEvent Listener ======= UserEvent[message='User registered', source=com.jsbd.events.EventPublisher@abf688e, eventId=101]
5. Use @EventListener to subscribe events
Spring 4.2 onwards, it is possible to just annotate a method with @EventListener
annotation and have the custom event as the method parameter. Spring will mark the method as a listener for the specified event UserEvent
.
@Component
public class AnnEventListener {
@EventListener
public void genericListener(UserEvent userEvent) {
System.out.println("\n===== General UserEvent Listener =====");
System.out.println(userEvent);
}
}
To test this method, we can just send an event using our EventPublisher
instance as shown before. The listener will work without any extra config.
B. Asynchronous event listener using @async
Synchronous publish-subscribe is blocking in nature, can have bad impact on application performance. To get rid of this, Spring provides asynchronous event listener using @async
annotation.
The first step is to enable the async processing by annotating the configuration with @EnableAsync
as below.
@Configuration
@ComponentScan("com.jsbd.events")
@EnableAsync
public class AppConfig {
//other configs if needed
}
Then, the second step is to use @async with the listener as shown below
@Component
public class AnnEventListener {
//Async Event listener
@EventListener
@Async
public void asyncListener(UserEvent userEvent) {
System.out.println("===== Async UserEvent Listener =====");
System.out.println(userEvent);
}
}
Optionally, if you want asynchronous message processing to be enabled globally for ApplicationContext, use the following configurations. You will not need to use @async annotation.
@Bean
@Scope("singleton")
ApplicationEventMulticaster eventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster
= new SimpleApplicationEventMulticaster();
eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
//optionally set the ErrorHandler
eventMulticaster.setErrorHandler(TaskUtils.LOG_AND_PROPAGATE_ERROR_HANDLER);
return eventMulticaster;
}
C. Runtime event Filtering using SpEL
Occasionally, you may want to filter out the events delivered to the listener. Spring allows you to define a SpEL based expression based on which the matched values are delivered to the event listener. Only the UserEvents with the eventId ==120
are delivered to this listener.
@Component
public class AnnEventListener {
//Event Listener with a Spring SpEL expression
@EventListener(condition = "#userEvent.eventId == 102")
public void processEvent(UserEvent userEvent) {
System.out.println("====== Conditional UserEvent Listener =====");
System.out.println(userEvent);
}
}
D. Ordering EventListeners using @Order
You can use the @Order
annotation when you want a specific eventListener to get invoked before another one. Remember, the annotation with the least value has the highest execution priority. So in the below example, listenerA
gets executed before listenerB
.
@Component
public class AnnEventListener {
@EventListener
@Order(1)
public void listenerA(UserEvent userEvent) {
System.out.println("\n===== UserEvent ListenerA =====");
System.out.println(userEvent);
}
@EventListener
@Order(5)
public void listenerB(UserEvent userEvent) {
System.out.println("\n===== UserEvent ListenerB =====");
System.out.println(userEvent);
}
}
E. Generic events
Spring also allows you to use a generic event class to publish any type of event. As shown in the below code, instead of creating too many classes for each event types, you can just create a GenericEvent
class with more properties to send any type of event.
public class GenericEvent<T> implements ResolvableTypeProvider {
private T message;
private Object source;
public GenericEvent(Object source, T message) {
this.source = source;
this.message = message;
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(
getClass(), ResolvableType.forInstance(getSource())
);
}
public T getMessage() {
return message;
}
public Object getSource() {
return source;
}
@Override
public String toString() {
return new StringJoiner(", ", GenericEvent.class.getSimpleName() + "[", "]")
.add("message=" + message)
.add("source=" + source)
.toString();
}
}
Once you have the GenericEvent class created, you can next create a listener by annotating the method with @EventListener
.
@Component
public class GenericEventListener {
@EventListener
public void listenEvent(GenericEvent event) {
System.out.println(event);
}
}
Now, let us publish few events and test.
@SpringJUnitConfig(AppConfig.class)
public class GenericEventTest {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Test
public void publishEvent() {
GenericEvent event1 = new GenericEvent<>(this, new Person("John"));
GenericEvent event2 = new GenericEvent<>(this, "Hello");
eventPublisher.publishEvent(event1);
eventPublisher.publishEvent(event2);
}
}
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return new StringJoiner(", ", Person.class.getSimpleName() + "[", "]")
.add("name='" + name + "'")
.toString();
}
}
- Spring’s eventing mechanism is designed for simple communication between Spring beans within the same application context.
- However, for more sophisticated needs you can explore the Spring Integration project.
Spring events offer a powerful mechanism for communication between Spring beans within an application. By leveraging the event publishing and handling infrastructure provided by Spring, developers can facilitate decoupled communication and enhance the modularity and flexibility of their Spring applications.
Through practical examples and code snippets, I demonstrated how to implement custom events and event listeners in a Spring application. By following these examples, readers can gain a deeper understanding of how Spring events work and how they can be effectively utilized in their own projects.
Overall, Spring events provide a flexible and robust solution for inter-bean communication in Spring applications. They promote loose coupling between components, enhance the maintainability and scalability of the codebase, and contribute to the overall robustness and reliability of the application architecture. Readers are encouraged to explore the provided examples and experiment with Spring events in their own applications to unlock the full potential of this powerful feature.
Join our gitter chat room and share your thoughts there.