In this post I will show how to use Spring events, which are events that can be published and listened to within a Spring application context running in one JVM.
Background
I was looking for a way to decouple different parts of an application I am working on. While I have read about event sourcing, I do not need to store the events and I also do not want to introduce an entire framework or programming paradigm to solve this problem. The good old observer design pattern came to my rescue!
It turns out that the Spring framework version which I am already using in my application, has implemented the observer design pattern with event listeners and publishers within a Spring application context.
Being true to my habit, I like to try out unfamiliar technologies etc in a small example application before I apply it to the problem I have at hand.
Example Application
The example application presented in this article is a Spring Boot application with no additional dependencies running on Spring 4.3.2 and Java 8.
The complete project can be found on GitHub and so I will omit parts that are not closely related to Spring events.
Note that the annotation-based configuration etc for Spring application events are not available in versions prior to version 4.2 of the Spring framework.
Main Application Starter Class
The main application starter class is implemented as follows:
package se.ivankrizsan.spring; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; /** * Main entry-point for the Spring events demo application. * * @author Ivan Krizsan */ @SpringBootApplication @EnableScheduling public class SpringEventsApplication { public static void main(String[] args) { SpringApplication.run(SpringEventsApplication.class, args); /* Wait in order for events to be published and received. */ try { Thread.sleep(10000L); } catch (final InterruptedException theException) { } } }
The main part of this class was generated by Spring Boot, I have added two things:
- The @EnableScheduling annotation.
Enables scheduling using the @Scheduled Spring annotation on methods that are to be invoked periodically.
I use Spring scheduling to publish events in the example application. - Thread.sleep
If the application would be to exit immediately, no events would be published.
Event Classes
Event listeners can select which type(s) of event(s) to listen to by specifying the event parameter type of a listener method or by using the classes attribute of the @EventListener annotation on a listener method. In the example I have created three different event classes, but I can imagine that an application can contain many event classes that are crafted with a level of attention similar to that given the domain model.
Instances of all the event classes in this example are immutable.
BarEvent
The BarEvent class just contain a message string.
package se.ivankrizsan.spring.events; import org.springframework.context.ApplicationEvent; /** * Bar type event. * Instances of the event-type are immutable. * * @author Ivan Krizsan */ public class BarEvent extends ApplicationEvent { protected String mBarMessage; /** * Creates a new event instance. * * @param inSource Object from which event originates. * @param inBarMessage Message string to be passed on in the event. */ public BarEvent(final Object inSource, final String inBarMessage) { super(inSource); mBarMessage = inBarMessage; } public String getBarMessage() { return mBarMessage; } }
FooEvent
The FooEvent class is identical to the BarEvent class, except for some of the names:
package se.ivankrizsan.spring.events; import org.springframework.context.ApplicationEvent; /** * Foo type event. * Instances of the event-type are immutable. * * @author Ivan Krizsan */ public class FooEvent extends ApplicationEvent { protected String mFooMessage; /** * Creates a new event instance. * * @param inSource Object from which event originates. * @param inFooMessage Message string to be passed on in the event. */ public FooEvent(final Object inSource, final String inFooMessage) { super(inSource); mFooMessage = inFooMessage; } public String getFooMessage() { return mFooMessage; } }
MultiLevelEvent
The MultiLevelEvent class contains a message string and additionally also contain a severity level raining from 1 to 10.
package se.ivankrizsan.spring.events; import org.springframework.context.ApplicationEvent; /** * An event type that contains a message and has a severity-level 1 to 10. * Instances of the event-type are immutable. * * @author Ivan Krizsan */ public class MultiLevelEvent extends ApplicationEvent { protected String mMessage; protected int mSeverityLevel; /** * Creates a new event instance. * * @param inSource Object from which event originates. * @param inMessage Message string to be passed on in the event. * @param inSeverityLevel Severity level, 1 to 10, to be set on the event. */ public MultiLevelEvent(final Object inSource, final String inMessage, final int inSeverityLevel) { super(inSource); if (inSeverityLevel < 1 || inSeverityLevel > 10) { throw new IllegalArgumentException(("Severity level must be 1 to 10 inclusive")); } mMessage = inMessage; mSeverityLevel = inSeverityLevel; } public String getMessage() { return mMessage; } public int getSeverityLevel() { return mSeverityLevel; } }
Event Publisher
There is just one single event publisher in this example but it publishes all three types of events according to different schedules:
package se.ivankrizsan.spring.eventpublishers; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import se.ivankrizsan.spring.events.BarEvent; import se.ivankrizsan.spring.events.FooEvent; import se.ivankrizsan.spring.events.MultiLevelEvent; import java.util.Date; /** * Spring component that publishes different types of events at different intervals. * Note that the events are published synchronously and the call to the publishEvent method * will not return until the event has been processed by all listeners. * * @author Ivan Krizsan */ @Component public class MyEventPublisher implements ApplicationEventPublisherAware { protected ApplicationEventPublisher mApplicationEventPublisher; /** * Publishes {@link FooEvent}s at regular interval. */ @Scheduled(fixedRate = 4000L) public void publishFooEvents() { final Date theDate = new Date(); final FooEvent theFooEvent = new FooEvent(this, "Foo event sent at: " + theDate.toString()); System.out.println("MyEventPublisher - Before publishing FooEvent"); mApplicationEventPublisher.publishEvent(theFooEvent); System.out.println("MyEventPublisher - After publishing FooEvent"); } /** * Publishes {@link BarEvent}s at regular interval. */ @Scheduled(fixedRate = 3000L) public void publishBarEvents() { final Date theDate = new Date(); final BarEvent theBarEvent = new BarEvent(this, "Bar event sent at: " + theDate.toString()); System.out.println("MyEventPublisher - Before publishing BarEvent"); mApplicationEventPublisher.publishEvent(theBarEvent); System.out.println("MyEventPublisher - After publishing BarEvent"); } /** * Publishes {@link MultiLevelEvent}s with random severity levels at regular interval. */ @Scheduled(fixedRate = 2000L) public void sendMultiLevelEvents() { final Date theDate = new Date(); final int theSeverityLevel = (int) (Math.random() * 10 + 1); final MultiLevelEvent theMultiLevelEvent = new MultiLevelEvent(this, "Multi-level event at: " + theDate.toString(), theSeverityLevel); System.out.println("MyEventPublisher - Before publishing MultiLevelEvent"); mApplicationEventPublisher.publishEvent(theMultiLevelEvent); System.out.println("MyEventPublisher - After publishing MultiLevelEvent"); } @Override public void setApplicationEventPublisher(final ApplicationEventPublisher inApplicationEventPublisher) { mApplicationEventPublisher = inApplicationEventPublisher; } }
Note that:
- The event publishing class implements the ApplicationEventPublisherAware interface.
This interface allows for the event publishing class to obtain an ApplicationEventPublisher object which it can use to publish application events with.
The method setApplicationEventPublisher method is from the ApplicationEventPublisherAware interface. - Foo events are published each 4th second from the publishFooEvents method.
The message enclosed with an event is given a timestamp. - Bar events are published each 3rd second from the publishBarEvents method.
- MultiLevel events are published each other second from the publishMultiLevelEvents method.
In addition to a message string with a time stamp, each event is also given a random severity level from 1 to 10 (inclusive). - Each of the event publishing methods prints a message before and after the publishing of an event.
This will be used to verify that Spring application events are published and processed synchronously.
Event Listeners
The example program contains four event listener classes. They show different aspects of event listening, such as filtering, listening to multiple event types, multiple event listeners receiving one and the same event etc.
AllEventsListener
The AllEventsListener class listens to all events within the Spring application. This includes, as we will see, some events generated by the Spring framework itself.
The main purpose of this class is to show that one single event listener can listen to many (all) types of events.
package se.ivankrizsan.spring.listeners; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; /** * Event listener that listens to all events within the application. * * @author Ivan Krizsan */ @Component public class AllEventsListener { /** * Handles received event. * * @param inEvent Received event. */ @EventListener public void handleEvent(final Object inEvent) { System.out.println("- AllEventsListener received event: " + inEvent); } }
Note that:
- The event listener class is annotated with the @Component annotation.
Each event listener is to be a Spring bean, in order for Spring to be able to pick up and register the listener methods in the listener.
Using Java or XML Spring configuration to define listener beans work equally well. - The method handleEvent is annotated with the @EventListener annotation.
This annotation tells Spring that this is a method that is to be registered as an event listener. - The type of the inEvent parameter of the handleEvent method is Object.
This will match all event types and filter out no event types. - Handling of the event consists of printing out a message.
FooBarEventListener
The FooBarEventListener contain two event listener methods; one that handles Foo events and another that handles Bar events.
package se.ivankrizsan.spring.listeners; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import se.ivankrizsan.spring.events.BarEvent; import se.ivankrizsan.spring.events.FooEvent; /** * Event listener that listens to {@code FooEvent}s and {@code BarEvent}s. * * @author Ivan Krizsan */ @Component public class FooBarEventListener { /** * Handles the received {@link FooEvent}. * * @param inFooEvent Received event. */ @EventListener public void handleFooEvents(final FooEvent inFooEvent) { System.out.println("FooBarEventListener received a FooEvent with the message: " + inFooEvent.getFooMessage()); } /** * Handles the received {@link BarEvent}. * * @param inBarEvent Received event. */ @EventListener({BarEvent.class}) public void handleBarEvents(final BarEvent inBarEvent) { System.out.println("FooBarEventListener received a BarEvent with the message: " + inBarEvent.getBarMessage()); } }
Note that:
- The event listener class is annotated with the @Component annotation.
The motivation is the same as for the AllEventsListener event listener class discussed earlier. - The method handleFooEvents is annotated with the @EventListener annotation.
This registers the method with Spring as an event listener method. - The parameter-type of the inFooEvent parameter of the handleFooEvents method is FooEvent.
This will cause Spring to deliver only events of the type FooEvent to the event listener method.
If there is an event type that, directly or indirectly, inherits from FooEvent then events of this type will also be delivered to this listener method. - The method handleBarEvents is annotated with the @EventListener annotation.
As before, this registers the method with Spring as an event listener method. - The @EventListener annotation on the handleBarEvents method contain BarEvent.class.
This is another way to filter events that are to be delivered to the event listener method, which allows one and the same event listener method to handle multiple types of events that are not related to each other in the class hierarchy. - Event processing in the different listener method contains of printing a message with the message string from the event being processed appended.
FooEventListener
The FooEventListener class was created to show that multiple listeners can receive one and the same event, provided that the event matches the conditions of the listeners.
package se.ivankrizsan.spring.listeners; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import se.ivankrizsan.spring.events.FooEvent; /** * Event listener that listens to {@code FooEvent}s. * * @author Ivan Krizsan */ @Component public class FooEventListener { /** * Handles the received {@link FooEvent}. * * @param inFooEvent Received event. */ @EventListener public void handleFooEvents(final FooEvent inFooEvent) { System.out.println("FooEventListener received a FooEvent with the message: " + inFooEvent.getFooMessage()); } }
Nothing new in this class, so on to the next.
MultiLevelEventListener
The MultiLevelEventListener event listener shows that it is possible to filter events in a more fine-grained fashion.
package se.ivankrizsan.spring.listeners; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import se.ivankrizsan.spring.events.MultiLevelEvent; /** * Event listener that listens to {@code MultiLevelEvent}s and handles separates events of low, mid and high * severity. * * @author Ivan Krizsan */ @Component public class MultiLevelEventListener { /** * Handles the received {@link MultiLevelEvent} that has a severity level of 3 or less. * * @param inMultiLevelEvent Received event. */ @EventListener(condition = "#inMultiLevelEvent.severityLevel <= 3") public void handleLowSeverityEvents(final MultiLevelEvent inMultiLevelEvent) { System.out.println("* A low-severity event was received: " + inMultiLevelEvent.getMessage() + ", severity level: " + inMultiLevelEvent.getSeverityLevel()); } /** * Handles the received {@link MultiLevelEvent} that has a severity level in the range 3 to 6 inclusive. * * @param inMultiLevelEvent Received event. */ @EventListener(condition = "#inMultiLevelEvent.severityLevel > 3 && #inMultiLevelEvent.severityLevel <= 6") public void handleMidSeverityEvents(final MultiLevelEvent inMultiLevelEvent) { System.out.println("** A mid-severity event was received: " + inMultiLevelEvent.getMessage() + ", severity level: " + inMultiLevelEvent.getSeverityLevel()); } /** * Handles the received {@link MultiLevelEvent} that has a severity level above 6. * * @param inMultiLevelEvent Received event. */ @EventListener(condition = "#inMultiLevelEvent.severityLevel > 6") public void handleHighSeverityEvents(final MultiLevelEvent inMultiLevelEvent) { System.out.println("*** A high-severity event was received: " + inMultiLevelEvent.getMessage() + ", severity level: " + inMultiLevelEvent.getSeverityLevel()); } }
As before, the even listener class is annotated with @Component and all the event listener methods are annotated with @EventListener.
Note that:
- The parameter type of all the three event listener methods are the same – MultiLevelEvent.
Thus only MultiLevelEvent events will be considered for these three listener methods. - Each of the three event listener methods contain a condition attribute on the @EventListener annotation.
This attribute contain an expression in the Spring Expression Language that will be used to further filter the events considered for the event listener methods.
Thus the handleLowSeverityEvents method will only handle events of the type MultiLevelEvent which has a severityLevel less than, or equal to, three.
The handleMidSeverityEvents method handles MultiLevelEvent events with a severityLevel in the range from 3 to 7 (non-inclusive).
Finally, the handleHighSeverityEvents method handles events of the type MultiLevelEvent with a severityLevel larger than six.
Running the Example Program
If we now run the example program, we will see output similar to this in the console:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.4.0.RELEASE) 2016-09-20 22:12:44.356 INFO 2055 --- [ main] s.i.spring.SpringEventsApplication : Starting SpringEventsApplication 2016-09-20 22:12:44.360 INFO 2055 --- [ main] s.i.spring.SpringEventsApplication : No active profile set, falling back to default profiles: default 2016-09-20 22:12:44.410 INFO 2055 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@543788f3: startup date [Tue Sep 20 22:12:44 CEST 2016]; root of context hierarchy 2016-09-20 22:12:44.999 INFO 2055 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup - AllEventsListener received event: org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@543788f3: startup date [Tue Sep 20 22:12:44 CEST 2016]; root of context hierarchy] 2016-09-20 22:12:45.007 INFO 2055 --- [ main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing MyEventPublisher - Before publishing FooEvent - AllEventsListener received event: se.ivankrizsan.spring.events.FooEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] FooBarEventListener received a FooEvent with the message: Foo event sent at: Tue Sep 20 22:12:45 CEST 2016 FooEventListener received a FooEvent with the message: Foo event sent at: Tue Sep 20 22:12:45 CEST 2016 MyEventPublisher - After publishing FooEvent MyEventPublisher - Before publishing BarEvent - AllEventsListener received event: se.ivankrizsan.spring.events.BarEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] FooBarEventListener received a BarEvent with the message: Bar event sent at: Tue Sep 20 22:12:45 CEST 2016 MyEventPublisher - After publishing BarEvent MyEventPublisher - Before publishing MultiLevelEvent - AllEventsListener received event: se.ivankrizsan.spring.events.MultiLevelEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] - AllEventsListener received event: org.springframework.boot.context.event.ApplicationReadyEvent[source=org.springframework.boot.SpringApplication@6f3187b0] 2016-09-20 22:12:45.017 INFO 2055 --- [ main] s.i.spring.SpringEventsApplication : Started SpringEventsApplication in 0.872 seconds (JVM running for 1.149) * A low-severity event was received: Multi-level event at: Tue Sep 20 22:12:45 CEST 2016, severity level: 1 MyEventPublisher - After publishing MultiLevelEvent MyEventPublisher - Before publishing MultiLevelEvent - AllEventsListener received event: se.ivankrizsan.spring.events.MultiLevelEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] * A low-severity event was received: Multi-level event at: Tue Sep 20 22:12:47 CEST 2016, severity level: 1 MyEventPublisher - After publishing MultiLevelEvent MyEventPublisher - Before publishing BarEvent - AllEventsListener received event: se.ivankrizsan.spring.events.BarEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] FooBarEventListener received a BarEvent with the message: Bar event sent at: Tue Sep 20 22:12:48 CEST 2016 MyEventPublisher - After publishing BarEvent MyEventPublisher - Before publishing FooEvent - AllEventsListener received event: se.ivankrizsan.spring.events.FooEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] FooBarEventListener received a FooEvent with the message: Foo event sent at: Tue Sep 20 22:12:49 CEST 2016 FooEventListener received a FooEvent with the message: Foo event sent at: Tue Sep 20 22:12:49 CEST 2016 MyEventPublisher - After publishing FooEvent MyEventPublisher - Before publishing MultiLevelEvent - AllEventsListener received event: se.ivankrizsan.spring.events.MultiLevelEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] *** A high-severity event was received: Multi-level event at: Tue Sep 20 22:12:49 CEST 2016, severity level: 7 MyEventPublisher - After publishing MultiLevelEvent MyEventPublisher - Before publishing BarEvent - AllEventsListener received event: se.ivankrizsan.spring.events.BarEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] FooBarEventListener received a BarEvent with the message: Bar event sent at: Tue Sep 20 22:12:51 CEST 2016 MyEventPublisher - After publishing BarEvent MyEventPublisher - Before publishing MultiLevelEvent - AllEventsListener received event: se.ivankrizsan.spring.events.MultiLevelEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] *** A high-severity event was received: Multi-level event at: Tue Sep 20 22:12:51 CEST 2016, severity level: 10 MyEventPublisher - After publishing MultiLevelEvent MyEventPublisher - Before publishing FooEvent - AllEventsListener received event: se.ivankrizsan.spring.events.FooEvent[source=se.ivankrizsan.spring.eventpublishers.MyEventPublisher@4722ef0c] FooBarEventListener received a FooEvent with the message: Foo event sent at: Tue Sep 20 22:12:53 CEST 2016 FooEventListener received a FooEvent with the message: Foo event sent at: Tue Sep 20 22:12:53 CEST 2016 MyEventPublisher - After publishing FooEvent
Note that:
- On row 13, the AllEventsListener receives a ContextRefreshedEvent.
This event is sent by the Spring framework when the Spring application context is initialized or refreshed. - On rows 15-19 we can follow the publishing and handling of a FooEvent.
- The FooEvent is handled by three different event listeners; the AllEventsListener, the FooBarEventListener and the FooEventListener.
- The event publishing and handling indeed seem synchronous, since the message “After publishing FooEvent” is printed only after all the event listeners have finished handling the event.
- On rows 28, 32, 45 and 53 we can see that MultiLevelEvent-s with different severity levels are indeed handled by different event listener methods.
This concludes this very brief introduction to Spring application events.
Happy coding!