Sending Activity Notifications Based on REST API Calls with Aspects and Event Listeners
In this post I will show a basic implementation of sending email notifications to your users when certain actions are completed via REST calls, such as create, update, delete, etc. You can see the interaction between components in the following sequence diagram.
Let's say we have an entity Item and the ItemService with the following methods.
So we want to inform our users by sending emails when a user modifies the item table with create, update or delete actions. We will split the responsibilities of
- detecting an action took place
- handling the results of that action.
Detecting actions will be done in Aspects. Aspects will be placed between controller and service and act as a proxy to catch method calls. Once the action is detected, we need to handle it according to who is making the call, what type of action is it, what arguments are sent, what is the result sent back to caller. Handling the results of the action is done by the event listeners.
We can store the event specific details in enum variable.
Event Handling
When an action happens, for example a user creates a new item or deletes some items, we will publish events in order to pass the responsibility of handling it to other component. We should create an event listener that will subscribe to specific events for handling them, and event publisher that will publish the event to all of its listeners.
We can create a base event class that will represent the ItemEvent
EventListener should listen for these events. Once an event is received it should create the mail content and send it to mail subscribers for entity modification. Note that event listener is set as @Async
, so event is handled asynchronously and it will not block the caller.
This point is worth to pay attention, when the request goes all the way through controller -> service -> repository -> service -> aspect -> controller
, we will detect the action in aspect, and if we do not handle the action asynchronously then it will block this flow so frontend will wait until mail sending operation finishes. So we rather run it async so that the flow will complete immediately and sending email will be done on a separate thread. We also need to add @EnableAsync
annotation to be able to use @Async
annotation.
This example is using the event details to send email to users, but alternatively we could save the details of the action to database or publish to a message queue. For mail sending we have the helper classes: MailContentBuilder prepares the html content and MailSenderHelper sends the email. These component are not included in this post as it is out of context but if you would like to take a look at their implementation refer to: Sending HTML Emails with Thymeleaf in Spring applications
Event publisher is going to be pretty simple and its responsibility is to publish the given events type of ItemEvent.
Item Notification Aspect
We make use of AOP (Aspect Oriented Programming) to add a proxy between Controller
and Service
Layers. So I will write an Aspect
that separates the concern of saving the data and generating analytics from the details of action.
Defining the Pointcuts
as variables provides reusability. So we can define pointcuts for the package name and method names.
And then I can use combination of pointcuts inside the Advice
for matching the method calls. For the Save and Update actions I can use AfterReturning
advice as we will take action only after the successful responses.
@AfterReturning(
pointcut = "atServicePackage() && isSaveMethod()",
returning = "result"
)
public void handleSave(JoinPoint joinPoint, Item result){
publisher.publishItemEvent(createItemEvent(result, EventTypeSpecs.CREATE));
}
@AfterReturning(
pointcut = "atServicePackage() && isUpdateMethod()",
returning = "result"
)
public void handleUpdate(JoinPoint joinPoint, Item result){
publisher.publishItemEvent(createItemEvent(result, EventTypeSpecs.UPDATE));
}
This advices are catching the save(..)
and update(..)
method calls within the com.turkogluc.backend.service
package. As we are using AfterReturning
advice, handleSave
and handleUpdate
methods are going to be run after service methods finish their work and return successfully. So if there is an exception thrown response will not be caught by our aspect.
In our advices we can reach the return variable with returning = "result"
parameter at the annotation. And the arguments of the method with JoinPoint
, for instance first argument: joinPoint.getArgs()[0]
The advice takes the arguments and return variable of the method, sets the event type accordingly for instance EventTypeSpecs.CREATE
and creates and ItemEvent
. Event Handling is explained above, but here it is worth to note that Event Publisher publishes the created event. Here I use a helper method to create an event object:
It creates an ItemEvent with taking the current user from the Spring SecurityContextHolder
with getPrincipal()
, setting the user details, details of the returned item, and current date time. So using this method we publish the event via event publisher as follows:
publisher.publishItemEvent(createItemEvent(result, EventTypeSpecs.CREATE));
For the delete action we need to use Around
advice. Because the deleteById
method is void and does not return anything so before the method call we need to record which items are meant to be deleted and after the method returns we should verify that it was successful.
So we first get the idList from arguments by proceedingJoinPoint.getArgs()
. Until here it works as Before
Advice. And then we call proceedingJoinPoint.proceed()
method to go to service. After returning back from service we can publish the event unless there was an error at the service and exception thrown.
The published event will be caught by the event listener, and event listener asynchronously will prepare the email and sends it. As the event listener works concurrently, the response of API call will continue its flow and finish immediately, rather than waiting for the event handling to be finished.