Application events with Spring

· May 11, 2016

In this post I am looking into the awesome application event support provided by Spring Framework, which starting from version 4.2 got even better by introducing an annotation model to consume events, the possibility to publish any object as event not forcing to extend from ApplicationEvent.

Application events are less used in real applications, but internally in Spring framework (ContextRefreshedEvent, RequestHandledEvent, etc…), but also in Spring Boot (ApplicationStartedEvent, ApplicationEnvironmentPreparedEvent, etc …) are used intensively.

Basically the Spring ApplicationContext is capable to behave like an event bus which enables simple communication between Spring beans within the same ApplicationContext

This is how we could define a consumer of TodoCreatedEvents.

@Component
class TodoCreatedEventListener {

    @EventListener
    void handle(TodoCreatedEvent event) {
        ...
    }
}

@EventListener is a core annotation, no extra configuration is needed using Java config. Internally the EventListenerMethodProcessor registers an ApplicationListener instance with the event type inferred from the method signature.

The TodoCreatedEvent can be an ordinary POJO. Internally it will be wrapped into a PayloadApplicationEvent.

class TodoCreatedEvent {

  private String title;

  public TodoCreatedEvent(String title) {
     this.title = title;
  }

On the producer side the ApplicationEventPublisher was extended to publish any POJO as an event.

@Component
class TodoCreatedEventProducer {

    private final ApplicationEventPublisher publisher;

    @Autowired
    public TodoCreatedEventProducer(ApplicationEventPublisher publisher) { ... }

    public void createTodo(Todo todo) {
        publisher.publishEvent(new TodoCreatedEvent(todo.getTitle()));
    }

}

Generic events

It is possible to define the events using generics

@EventListener
public void onBidCeated(EntityCreatedEvent<Bid> event) {
    ...
}

When publishing the event in order for this to work we have two options. We could resolve the generic parameter, something like:

class BidCreatedEvent extends EntityCreatedEvent<Bid> { ... }

Or we could implement ResolvableTypeProvider to help Spring to figure out if the event instance matches a generic signature.

class EntityCreatedEvent<T> implements ResolvableTypeProvider {

    private T source;

    public EntityCreatedEvent(T source) {
        this.source = source;
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(),
                ResolvableType.forInstance(source));
    }
}

Listen and publish

You can publish an event as a result of handling another event by returning an instance of the new event or a collection of the new events in case you want to publish multiple events.

@EventListener
TaskModifiedEvent handleAssignedEvent1(TaskAssignedEvent event) {
    // handle TaskAssignedEvent
    return new TaskModifiedEvent();
}

Note, this functionality does not work for async events. In that case you can manually inject ApplicationEventPublisher in the listener and publish the events through this.

Async events

By default event listeners receive events synchronously, meaning that the publishing thread will block until all listeners have finished processing the event. The advantage of this is that if the publisher is running in a transactional context, the listener will receive the event within the same transactional context. However if processing events takes long time and scaling is important we can tell Spring to handle events asynchronously. In order to do this we need to redefine the ApplicationEventMulticaster bean with id applicationEventMulticaster configuring it with an asynchronous TaskExecutor.

@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
    SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
    eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
    eventMulticaster.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);
    return eventMulticaster;
}

Note that this change will be global to the ApplicationContext meaning that all methods annotated with @EventListener will be executed asynchronously. If we like to have some events delivered synchronously others asynchronously within the same ApplicationContext check out this blog post which details how it can be done with a custom annotation and a custom ApplicationEventMulticaster wrapping a synchronous and asynchronous ApplicationEventMulticaster instance.

A much easier way to handle some events asynchronously is to use the @Async annotation.

@Async
@EventListener
void handleAsync(MedicalRecordUpdatedEvent event) {
    // MedicalRecordUpdatedEvent is processed in a separate thread.
}

Filtering

It is possible to filter the events in the listener via the condition attribute. The following example shows that the event listener is called only if the bid is higher or equal than 100.

@EventListener(condition = "#bidCreatedEvent.amount >= 100")
public void handleHighBids(BidCreatedEvent bidCreatedEvent) {
    ...
}

Note that starting from Spring 4.3.0.RC1 we are able to specify the condition to refer to beans (e.g. @beanName.method()).

Transaction bound events

With synchronous event handling the listener can be bound to a phase of the transaction in which the publisher is running. The following example shows that the listener should only handle the TaskScheduledEvent once the transaction in which it was published committed successfully.

@TransactionalEventListener
public void handleAfterCommit(TaskScheduledEvent event)
    ...
}

If no transaction is running, the listener is not invoked at all, but we can override this with fallbackExecution attribute setting it to true. With the phase attribute we can bound to other phases of a transaction (BEFORE_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION, AFTER_COMMIT (default)). In the example below the listener method will be executed if the transaction in which the publisher is running was rolled back.

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback(TaskScheduledEvent event) {
    ...
}

Conclusion

As we have seen the support for application events in Spring is pretty comprehensive. It can be very helpful in event driven business applications. If you would like to try out these features have a look at this https://github.com/altfatterz/application-events-with-spring repository.

Twitter