Inter-bean notifications using Spring Events

Java Spring
Michał Grabarczyk photo
Michał Grabarczyk
31 Mar 2020
4 min read

Since its early days Spring provided an easy way to enable publish-subscribe communication between beans leveraging Observer pattern. Still, the built-in messaging seems to be overlooked too often. Before jumping into Spring Integration project or other more advanced solution it’s worth evaluating whether this lightweight, built-in mechanism wouldn’t be just enough.

Use cases

Spring application events may be used to ensure loose coupling between components, remove circular dependencies between beans or as a way to propagate domain events.

Imagine a situation in which InvitationService calls createAccount of AccountService when a user accepts an invitation. Later on, when a user account is deleted, AccountService calls removeInvitations of InvitationService.

This leads to a circular dependency. One way to remove it is to use event-based notifications. In this case, we could introduce an InvitationAccepted domain event which would be published by InvitationService and consumed by AccountService.

Another motivation for using Spring Events is to be notified about Spring standard events like application context changes:

These can be used to perform initialization tasks that should take place after the application context is ready. One example is pre-filling a service-internal cache.

Publishing and consuming events

Defining events

Before Spring 4.2 it was necessary to extend the ApplicationEvent class to create your own event type. Since 4.2 it’s enough to create a POJO class with event definition:

@Value
class InvitationAcceptedEvent {
  String customerId;
}

Publishing events

Inject ApplicationEventPublisher into your event publishing component and call its publishEvent method with an instance of your event.

@Service
@RequiredArgsConstructor
class InvitationService {
  private final ApplicationEventPublisher eventPublisher;

  void accept(final String customerId) {
    eventPublisher.publishEvent(new InvitationAcceptedEvent(customerId));
  }
}

Subscribing to events

To subscribe to events you can use @EventListener annotation on a component’s method that will be processing them.

@Service
class AccountService {

  @EventListener
  void onInvitationAccepted(final InvitationAcceptedEvent event) {
    log.debug(“Invitation for customer id {}”, event.customerId());
  }
}

In the same way you can subscribe to standard Spring Events:

@EventListener
void onContextRefreshed(final ContextRefreshedEvent event) {
  log.info(
    "Context refreshed, number of beans: {}",
    event.getApplicationContext().getBeanDefinitionCount()
  );
}

You can narrow down events handling to the ones matching given SpEL expression:

@EventListener(condition = "#event.customerId == 'SPECIAL'")
void onInvitationAccepted(final InvitationAcceptedEvent event) {
  // ...
}

You can also handle multiple event types with a single listener:

@EventListener({InvitationAccepted.class, InvitationRejected.class})
void onInvitationEvents() {
  // ...
}

Returning new events

Methods marked with @EventListener may also return either a new event or a collection of events. Spring will propagate these new events further. However (as of Spring 5.2) this mustn’t be used for asynchronous events.

Ordering event listeners

In case multiple event listeners are created for the same event - you can define the explicit order in which event listeners should be triggered using the familiar @Order annotation:

@EventListener
@Order(10)
void onInvitationAccepted(final InvitationAcceptedEvent event)

Asynchronous Spring Events

By default all event processing is synchronous and it's run within the same thread in which the event was published.

In case you need asynchronous processing instead, you can let Spring know about this by annotating the processing method with @Async:

@EventListener
@Async
void onInvitationAccepted(final InvitationAcceptedEvent event)

Note that you can arbitrarily choose event listeners that should be run asynchronously while still keeping other listeners of the same event synchronous.

Remember that if asynchronous event processing fails, the exception is not propagated to the publisher. If you would still need to handle such exceptions check Exception Management with @Async.

Spring Events and transactions

As by default events are processed synchronously they operate within the transaction context of their publisher. The event is processed regardless of how the transaction ends as in the following example:

@Transactional
public void publishEventAndRollback() {
  eventPublisher.publishEvent(new Event());
  throw new RuntimeException("rollback");
}

@EventListener
void onEvent(final Event event) {
  // ...
}

When calling publishEventAndRollback the transaction gets rolled back. But it does not prevent onEvent from handling the fired event, which might seem counterintuitive.

If you expect the event to be processed only if a transaction is committed you should use @TransactionalEventListener annotation instead:

@TransactionalEventListener
void onEvent(final Event event)

If you need to process an event in a different phase than committed (e.g. only if the transaction is rolled back) you can specify the required transaction phase:

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
void onEvent(final Event event)

Other phases available are: AFTER_COMMIT (which is the default), BEFORE_COMMIT, AFTER_ROLLBACK and AFTER_COMPLETION (which is an alias for both AFTER_COMMIT and AFTER_ROLLBACK).

If no transaction is open the event won’t get processed by a transactional event listener (as the phase is unknown). However, you can use fallbackExecution attribute to change this behavior.

@TransactionalEventListener(fallbackExecution = true)
void onEvent(final Event event)

With this setting the event is processed even if it is published from a non-transactional context.

Generic Events

You can use generic events to group similar events. Consider the following generic event notifying about creation of an entity:

@Value
class EntityCreatedEvent<T>

together with these listeners:

@EventListener
void onInvitationCreated(final EntityCreatedEvent<Invitation> event) {
  // ..
}

@EventListener
void onPersonCreated(final EntityCreatedEvent<Person> event) {
  // ..
}

Unfortunately, due to generics’ type erasure you would be forced to create a set of dedicated event classes in order to receive an event for each entity type:

class InvitationCreatedEvent extends EntityCreatedEvent<Invitation> {}

class PersonCreatedEvent extends EntityCreatedEvent<Person> {}

Fortunately, Spring lets you include type information (that is otherwise lost in runtime) by implementing ResolvableTypeProvider:

@Value
class EntityCreatedEvent<T> implements ResolvableTypeProvider {
  T entity;

  @Override
  public ResolvableType getResolvableType() {
    return ResolvableType.forClassWithGenerics(
      getClass(),
      ResolvableType.forClass(entity.getClass())
    );
  }
}

This way you don’t have to create multiple classes for each entity type.

Conclusion

Spring Events are a simple, standard solution facilitating communication between beans within one application context. For most simple cases they might just be enough. Especially after changes made in Spring 4.2 (introducing annotation based configuration, better handling of transactional context and so forth) that made its use much more pleasant.

Still, there are some corner cases that you should be aware of, especially:

  • using event listeners within transactional context (@EventListener vs @TransactionalEventListener),
  • using asynchronous event listeners (e.g. they mustn't return any new events, exception handling),
  • coupling your events (which are often part of the domain) to Spring-related code.

All examples described in this article are presented in full in our git repository in case you want to play around with them.

More information

Build your backend with us

Your team of exceptional software engineers