Spring Application Events and @EventListener Example

Learn to use create and receive publisher-subscriber-style native events in a Spring application. Spring has built-in support for creating application events, publishing them, and then listening to them in event handlers.

This tutorial covers the following learning objectives:

  • Creating new application events by extending the ApplicationEvent class.
  • Using ApplicationEventPublisher to notify all matching listeners registered of a specific application event.
  • Registering the event listeners using the @EventListener annotation and by implementing the ApplicationListener interface.
  • If there are multiple listeners for a specific event then define the order using the @Order annotation.
  • Recording and matching all application events during unit testing using the @RecordApplicationEvents annotation.

1. What is an Application Event?

Sometimes in an application, we may want to add the capability for listening to specific application events and processing these events for various audit purposes. Examples of these events can be when a new record is added or deleted, or a certain kind of transaction is completed or rolled back.

We can always write the event processing code as another method within the existing application code, BUT then it will be tightly coupled with your existing code, and we will not have much handle to change it later (suppose you don’t want to process those events for a certain duration).

If you can configure event processors through the configuration then we need not change the application code as well as the event processor code in most circumstances. Any time we need to switch off event processing or add another event processor for that event, all you need to do is change your context file configuration, and that’s it. Sounds good !!

Let’s learn how we can achieve this by publishing and listening to events in a spring application.

2. Creating an Application Event

An application event in Spring is represented by created by extending the ApplicationEvent class which is an abstract class.

For example, if we want to notify the listeners whenever a new Employee is added then we can create a custom class AddEmployeeEvent as follows:

import com.howtodoinjava.demo.model.Employee;
import org.springframework.context.ApplicationEvent;

public class AddEmployeeEvent extends ApplicationEvent {

  public AddEmployeeEvent(Employee employee) {
    super(employee);
  }

  @Override
  public String toString() {
    return "ApplicationEvent: New Employee Saved :: " + this.getSource();
  }
}

With later versions of Spring, extending the ApplicationEvent is not mandatory.

import com.howtodoinjava.demo.model.Employee;

public class AddEmployeeEvent {

  private Employee employee;

  public AddEmployeeEvent(Employee employee) {
    this.employee = employee;
  }

  public Employee getEmployee() {
    return employee;
  }

  @Override
  public String toString() {
    return "ApplicationEvent: New Employee Saved :: " + this.employee;
  }
}

3. Publishing an Application Event

The ApplicationEventPublisher interface encapsulates the whole event publication functionality. To access the ApplicationEventPublisher in a class, we may, optionally, implement the ApplicationEventPublisherAware interface in that class; else the Spring injects it automatically in the runtime.

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

@Service
public class EmployeeService implements ApplicationEventPublisherAware {

  ApplicationEventPublisher applicationEventPublisher;

  @Override
  public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
    this.applicationEventPublisher = applicationEventPublisher;
  }

  //...
}

After we have access to ApplicationEventPublisher, we can use the applicationEventPublisher.publishEvent() method which accepts an ApplicationEvent type as a method argument.

import com.howtodoinjava.demo.dao.EmployeeRepository;
import com.howtodoinjava.demo.events.employeeMgmt.AddEmployeeEvent;
import com.howtodoinjava.demo.exception.ApplicationException;
import com.howtodoinjava.demo.model.Employee;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

@Service
public class EmployeeService implements ApplicationEventPublisherAware {

  EmployeeRepository repository;
  ApplicationEventPublisher applicationEventPublisher;

  @Override
  public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
    this.applicationEventPublisher = applicationEventPublisher;
  }

  public EmployeeService(EmployeeRepository repository) {
    this.repository = repository;
  }

  public Employee create(Employee employee) throws ApplicationException {
    Employee newEmployee = repository.save(employee);
    if (newEmployee != null) {
      applicationEventPublisher.publishEvent(new AddEmployeeEvent(newEmployee));   //Notify the listeners
      return newEmployee;
    }
    throw new ApplicationException("Employee could not be saved");
  }
}

4. Receiving an Application Event

We can listen to the application events in two ways i.e. using the ApplicationListener interface or using the @EventListener annotation.

4.1. Using ApplicationListener Interface

To listen to certain events, a bean must implement the ApplicationListener interface and handle the events in the onApplicationEvent() method. Actually, Spring will notify a listener of all events, so you must filter the events by yourself.

If we use generics, Spring will deliver only messages that match the generic type parameter. In this example, we are using generics code to listen only to the DeleteEmployeeEvent.

import com.howtodoinjava.demo.events.employeeMgmt.DeleteEmployeeEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class DeleteEmployeeEventListener implements ApplicationListener<DeleteEmployeeEvent> {

  @Override
  public void onApplicationEvent(DeleteEmployeeEvent event) {
    log.info(event.toString());
  }
}

4.2. Using @EventListener Annotation

Since Spring 4.1, we can annotate a method with @EventListener and register the event listener of the type of method argument.

import com.howtodoinjava.demo.events.employeeMgmt.AddEmployeeEvent;
import com.howtodoinjava.demo.events.employeeMgmt.DeleteEmployeeEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class EmployeeEventsListener {

  @EventListener
  void handleAddEmployeeEvent(AddEmployeeEvent event) {
    log.info(event.toString());
  }

  @EventListener
  void handleDeleteEmployeeEvent(DeleteEmployeeEvent event) {
    log.info(event.toString());
  }
}

Note that event type can be defined in the annotation itself. Also, we can register multiple events to a single method.

@EventListener({AddEmployeeEvent.class, DeleteEmployeeEvent.class})
void handleEmployeeEvents(EmployeeEvent event) {
  log.info(event.toString());
}

In most cases, the event handler method should be void. And if the method returns a value, the result of the method invocation is sent as a new event. Also, if the return type is either an array or a collection, each element is sent as a new individual event.

@EventListener({AddEmployeeEvent.class, DeleteEmployeeEvent.class})
RemoteAuditEvent handleEmployeeEvents(EmployeeEvent event) {

  log.info(event.toString());
  return new RemoteAuditEvent(event.toString());
}

5. Demo

In the following example, we are creating a new Employee object using the EmployeeService. Internally, the EmployeeService creates a new employee in the database and notifies the listeners by sending a new application event.

import com.howtodoinjava.demo.model.Employee;
import com.howtodoinjava.demo.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Slf4j
@SpringBootApplication
public class App implements CommandLineRunner {
  public static void main(String[] args) {
    SpringApplication.run(App.class);
  }

  @Autowired
  EmployeeService employeeService;

  @Override
  public void run(String... args) throws Exception {

    employeeService.create(new Employee("Lokesh Gupta"));
    log.info("New Employee Created...");
  }
}

Program output.

2023-12-04T23:01:06.524+05:30  INFO 5560 --- [           main] c.h.d.listners.EmployeeEventsListener    : 
ApplicationEvent: New Employee Saved :: Employee(id=1, name=Lokesh Gupta)
2023-12-04T23:01:06.524+05:30  INFO 5560 --- [           main] com.howtodoinjava.demo.App               : New Employee Created...

Great. We can listen to the events now and process them the way we want.

6. Using Testing with @RecordApplicationEvents

When performing the unit testing with Spring test module, we can annotate the test class with @RecordApplicationEvents annotation. This annotation instructs the Spring TestContext Framework to record all application events that are published in the ApplicationContext during the execution of a single test, either from the test thread or its descendants.

The recorded events can be accessed via the ApplicationEvents API within the tests.

Please note that application context itself also publish container events such as ContextClosedEvent, ContextRefreshedEvent, and RequestHandledEvent etc. If any of the beans want to be notified of these events, they can implement the ApplicationListener interface.

import com.howtodoinjava.demo.events.employeeMgmt.AddEmployeeEvent;
import com.howtodoinjava.demo.exception.ApplicationException;
import com.howtodoinjava.demo.model.Employee;
import com.howtodoinjava.demo.service.EmployeeService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;

@SpringBootTest
@RecordApplicationEvents
public class AppTest {

  @Autowired
  private ApplicationEvents applicationEvents;

  @Autowired
  private EmployeeService employeeService;

  @Test
  void employeeCreationShouldPublishEvent() throws ApplicationException {
    
    employeeService.create(new Employee("Alex"));

    Assertions.assertEquals(1, applicationEvents
        .stream(AddEmployeeEvent.class)
        .filter(event -> event.getEmployee().getName().equals("Alex"))
        .count());

    // There are multiple events recorded such as PrepareInstanceEvent,
    // BeforeTestMethodEvent, BeforeTestExecutionEvent, AddEmployeeEvent etc.
    applicationEvents.stream(AddEmployeeEvent.class).forEach(System.out::println);
  }
}

7. Conclusion

In this Spring tutorial, we discussed how to new application events by extending the ApplicationEvent class and using ApplicationEventPublisher to notify all matching listeners.

We registered the event listeners using the @EventListener annotation and printed the notified events in the demo application. We also learned to unit-test the whole feature using the @RecordApplicationEvents annotation.

Happy Learning !!

Source Code on Github

Comments

Subscribe
Notify of
guest
2 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.