
A couple of months ago, I had no idea about Events and how they can be used in our projects.
Before diving into them, let's answer this question: How can we establish communication between components without being coupled, in other words, invoking a method without a direct call?
But first would be great if we define what Events are:
Events are represented as some notifications that are meant for sharing data between loosely coupled components, and each side can grow independently without affecting the other.
To make things easier, let's stick to this event for the rest of the article:
After a client's registration, we send a confirmation email to activate his account.
We can imagine this event like this:
void register(Client client) {
registerClient(client);
sendConfirmationEmail(client);
}
Based on the Observer design pattern, Spring framework provides an ApplicationEvents which ensures the indirect communication between components (publish/subscribe).
By projecting this on our example, we first need to create our components: ClientService
and MailService
.
Let's get our hands dirty πͺπ»
@Service
public class ClientService {
public void register(Client client){
// some business logic
// send welcome mail to registered client
}
}
@Service
@Slf4j
public class MailService {
public void send(String to, String subject, String content){
log.info("Sending mail with subject '{}' to '{}' and content '{}'", subject, to, content);
// sending mail implementation
}
}
After creating our service, we have to create an Event.
public class ClientRegistrationEvent extends ApplicationEvent {
private String to;
private String subject;
private String content;
public ClientRegistrationEvent(Object source, String to, String subject, String content) {
super(source);
this.to = to;
this.subject = subject;
this.content = content;
}
}
As we can see ClientRegistrationEvent extends from ApplicationEvent so that we have to create a new constructor that match super class constructor to pass an Object that reference to the source from where this event was published.
Since Spring 4.2 there is no need to extend from ApplicationEvent to publish an event.
So our ClientRegistrationEvent will look much cleaner:
@AllArgsConstructor
public class ClientRegistrationEvent {
private String to;
private String subject;
private String content;
}
Once the event is created, we have to add a Listener (subscriber) to catch our ClientRegistrationEvent.
@Component
@AllArgsConstructor
public class MailEventListener implements ApplicationListener<ClientRegistrationEvent> {
@Override
public void onApplicationEvent(ClientRegistrationEvent clientRegistrationEvent) {}
}
Also starting from Spring 4.2, listening on Events can be done via Annotation-Driven @EventListener
@Component
@AllArgsConstructor
public class MailEventListener {
private final MailService mailService;
@EventListener
public void onApplicationEvent(ClientRegistrationEvent clientRegistrationEvent) {}
}
Spring events are executed synchronously.
Last but not least, the relevant part of this example is to publish that created event.
Publishing events can be done by autowiring ApplicationEventPublisher and invoking the publishEvent method.
Let's complete the example:
@AllArgsConstructor
public class ClientRegistrationEvent {
private String to;
private String subject;
private String content;
}
@Component
@AllArgsConstructor
public class MailEventListener {
private final MailService mailService;
@EventListener
public void onApplicationEvent(ClientRegistrationEvent clientRegistrationEvent) {
mailService.send(clientRegistrationEvent.getTo(), clientRegistrationEvent.getSubject(), clientRegistrationEvent.getContent());
}
}
@Slf4j
@Service
@AllArgsConstructor
public class ClientService {
private final ApplicationEventPublisher eventPublisher;
public void register(Client client){
// some business logic
log.info("Start sending registration mail to newly created client '{}'", client.getFullName());
ClientRegistrationEvent clientRegistrationEvent = new ClientRegistrationEvent(client.getMail(), "Client registration", "Welcome aboard ".concat(client.getFullName()));
eventPublisher.publishEvent(clientRegistrationEvent);
log.info("Successfully registering client '{}'", client.getFullName());
}
}
@Slf4j
@Service
public class MailService {
public void send(String to, String subject, String content){
log.info("Sending mail with subject '{}' to '{}' and content '{}'", subject, to, content);
// sending mail implementation
}
}
Result Output:
INFO 8197 --- [ main] io.xhub.springevent.service.ClientService : Start sending registration mail to newly created client 'John Doe'
INFO 8197 --- [ main] io.xhub.springevent.service.MailService : Sending mail with subject 'Client registration' to 'test@example.com' and content 'Welcome aboard John Doe'
INFO 8197 --- [ main] io.xhub.springevent.service.ClientService : Successfully registering client 'John Doe'
Until now, we know how to use Spring Application Events, but how we can benefit from Events within @Transactional boundaries ?
No worry, Spring came with another annotation @TransactionalEventListener, which is a simple EventListener get invoked according to TransactionPhase.
This TransactionalEventListener can be executed during an active Transaction or if the fallbackExecution = true, otherwise it will work as `EventListener`.
TransactionPhase are represented as follow:
To work with @TransactionalEventListener you have to add this dependency
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
Let's see an example of AFTER_COMMIT the default TransactionPhase
@Transactional
public void register(){
// some business logic
ClientRegistrationEvent applicationEvent = new ClientRegistrationEvent("test@example.com", "Client registration", "Welcome aboard John Doe");
eventPublisher.publishEvent(applicationEvent);
log.info("Successfully registering client '{}'", "John Doe");
}
After adding @Transactional on top of our register method, it will invoke the event once the Transaction is committed
@TransactionalEventListener
public void on(ClientRegistrationEvent clientRegistrationEvent) {
log.info("{} get published", clientRegistrationEvent.getClass().getSimpleName());
mailService.send(clientRegistrationEvent.getTo(), clientRegistrationEvent.getSubject(), clientRegistrationEvent.getContent());
// Add some business logic after the commit is set
}
At this point, the transaction have been committed.
As mentioned in the Spring documentation, after the transaction has been committed it is still active and accessible, but you can't perform any commit. To prevent this from happening, you have to run it into a new Transaction.
Wrap up:
Start your next project by adding events, and you will get a more flexible and scalable way to communicate between components and especially the way you can use it inside a transaction boundary.
Resources:
https://www.baeldung.com/spring-events