@EventListener and Synchronous Event Processing
@EventListener and Synchronous Event Processing
@EventListener is the annotation-based alternative to implementing ApplicationListener<T>. You annotate a method, Spring discovers it, wraps it, and registers it as a listener. The invocation is synchronous: the publisher thread calls your method directly, waits for it to return, then moves to the next listener.
This section covers the discovery mechanism, the adapter that bridges annotation to interface, type matching, ordering, and the return-value-as-event feature.
The Annotation
@Component
public class TenantAuditListener {
@EventListener
@Order(1)
public void onOrderCreated(OrderCreatedEvent event) {
auditLog.record(event.tenantId(), "ORDER_CREATED", event.orderId());
}
}
The method parameter OrderCreatedEvent declares the event type. @Order(1) sets priority. No interface to implement. No generic type parameter to specify. Spring handles the wiring.
The Mechanism: EventListenerMethodProcessor
Discovery
EventListenerMethodProcessor is a SmartInitializingSingleton and BeanFactoryPostProcessor registered automatically by Spring Boot’s auto-configuration. After all singleton beans are instantiated, its afterSingletonsInstantiated() method runs.
It iterates over every bean definition in the context. For each bean, it inspects every method for the @EventListener annotation using AnnotatedElementUtils.findMergedAnnotation(). This means meta-annotations work: you can create @AuditEvent that is annotated with @EventListener, and Spring will find it.
The processor uses EventListenerFactory instances to create the listener adapters. The default factory is DefaultEventListenerFactory. You can register custom factories if you need specialized wrapping logic, but this is rare.
ApplicationListenerMethodAdapter
Each discovered @EventListener method is wrapped in an ApplicationListenerMethodAdapter. This class implements ApplicationListener<ApplicationEvent> and GenericApplicationListener.
The adapter stores:
- The bean name (not the bean instance, to support prototype scope)
- The method reference
- The resolved event type(s)
- The
@Ordervalue, if present - A condition expression from
@EventListener(condition = "..."), if present
When the multicaster dispatches an event, it calls supportsEventType() on each listener. ApplicationListenerMethodAdapter checks whether the event class is assignable to the method parameter type. If generics are involved (e.g., EntityChangedEvent<Order>), the adapter resolves them using ResolvableType.
Invocation
When the event matches, ApplicationListenerMethodAdapter.onApplicationEvent() is called. It resolves the bean by name from the BeanFactory (supporting proxied beans and prototype scope), then invokes the method via reflection:
onApplicationEvent(event)
-> resolve bean from BeanFactory by name
-> method.invoke(bean, event)
For standard singleton beans, the bean resolution is fast: it is a map lookup. The reflective invocation adds minimal overhead. Spring does not generate bytecode for event listeners.
If the method throws a checked exception, the adapter wraps it in an UndeclaredThrowableException. Unchecked exceptions propagate directly. Either way, in synchronous mode, the exception reaches the publisher.
Event Type Matching
The method parameter determines which events the listener receives. Spring supports several patterns:
Single event type:
@EventListener
public void handle(OrderCreatedEvent event) {
// receives OrderCreatedEvent and subclasses
}
Multiple event types via annotation attribute:
@EventListener({OrderCreatedEvent.class, OrderCancelledEvent.class})
public void handle(Object event) {
// receives both types
}
Generic events:
@EventListener
public void handle(EntityChangedEvent<Order> event) {
// receives EntityChangedEvent<Order> but not EntityChangedEvent<Customer>
}
Generic resolution depends on ResolvableType. The event publisher must preserve the generic type. If you publish using new EntityChangedEvent<>(order) and the class declaration is EntityChangedEvent<T>, Spring resolves T from the actual type argument at the publication site. This works when the concrete type is known at compile time. It fails when you erase the type through raw usage.
Conditional listeners:
@EventListener(condition = "#event.amount > 1000")
public void handleLargeOrders(OrderCreatedEvent event) {
// SpEL condition evaluated before invocation
}
The condition is a SpEL expression evaluated against an EvaluationContext that includes the event as #event, the method arguments by name, and the bean as #root. If the condition evaluates to false, the listener is skipped.
Ordering with @Order
Listener ordering is controlled by @Order on the method or the class. The multicaster sorts listeners by order value before dispatching. Lower values run first.
@Component
public class OrderEventListeners {
@EventListener
@Order(1)
public void auditFirst(OrderCreatedEvent event) {
// runs first
auditRepository.record(event);
}
@EventListener
@Order(2)
public void analyticsSecond(OrderCreatedEvent event) {
// runs second
analyticsService.track(event);
}
@EventListener
@Order(3)
public void notifyLast(OrderCreatedEvent event) {
// runs third
notificationService.notify(event);
}
}
Without @Order, listeners have Ordered.LOWEST_PRECEDENCE by default. Among listeners with the same order value, the invocation order is not guaranteed by the framework. In practice, it follows bean registration order, which depends on classpath scanning order. Do not rely on this.
@Order applies independently to each method. If two methods are on the same class, each can have a different order value.
Return Value as New Event
If an @EventListener method returns a non-null value, Spring publishes that value as a new event. This creates an event chain.
@EventListener
public OrderAuditedEvent onOrderCreated(OrderCreatedEvent event) {
AuditRecord record = auditRepository.record(event.tenantId(), "ORDER_CREATED", event.orderId());
return new OrderAuditedEvent(event.tenantId(), event.orderId(), record.id());
}
@EventListener
public void onOrderAudited(OrderAuditedEvent event) {
log.info("Audit record {} created for order {}", event.auditId(), event.orderId());
}
The chain is synchronous. The multicaster dispatches the returned event before returning from the original listener. This means the execution flow is:
publishEvent(OrderCreatedEvent)called.onOrderCreated()runs, returnsOrderAuditedEvent.publishEvent(OrderAuditedEvent)called internally.onOrderAudited()runs.- Control returns to original publisher.
Returning a Collection publishes each element as a separate event. Returning void or null publishes nothing.
Be careful with circular chains. If listener A returns event B, and listener B returns event A, you get infinite recursion. Spring does not detect this. The stack overflows.
The Failure Mode
BROKEN: Slow Listener Blocks API Response
@RestController
public class OrderController {
private final OrderService orderService;
@PostMapping("/api/{tenantId}/orders")
public ResponseEntity<Order> createOrder(@PathVariable String tenantId,
@RequestBody CreateOrderRequest request) {
// BROKEN: this blocks until all synchronous listeners complete
Order order = orderService.createOrder(tenantId, request);
return ResponseEntity.ok(order);
}
}
@Component
public class ReportingListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// BROKEN: takes 3 seconds, blocks the HTTP response
reportingService.generateMonthlySnapshot(event.tenantId());
}
}
The createOrder() call publishes OrderCreatedEvent. The multicaster invokes ReportingListener.onOrderCreated() synchronously on the request thread. The HTTP response is delayed by 3 seconds because of a reporting snapshot that has no business blocking the API.
In a multi-tenant SaaS backend, this means one tenant’s order creation latency depends on the reporting system’s performance. That is a coupling violation.
The Correct Pattern
CORRECT: @Async on the Listener
@Component
public class ReportingListener {
@Async
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// CORRECT: runs on a separate thread, does not block the publisher
reportingService.generateMonthlySnapshot(event.tenantId());
}
}
With @Async, the multicaster still calls the adapter synchronously, but the adapter detects the @Async annotation and delegates to a TaskExecutor. The publisher thread returns immediately. The reporting logic runs on a pooled thread.
Exceptions in async listeners are handled by the AsyncUncaughtExceptionHandler. By default, they are logged and swallowed. Configure a custom handler if you need alerting:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, params) -> {
log.error("Async event listener failed: {} {}", method.getName(), throwable.getMessage(), throwable);
alertService.sendAlert("Event listener failure", throwable);
};
}
}
CORRECT: Multicaster-Level Executor
Alternatively, you can set an executor on the SimpleApplicationEventMulticaster itself. This makes all listeners async by default:
@Bean
public SimpleApplicationEventMulticaster applicationEventMulticaster(
@Qualifier("eventExecutor") TaskExecutor executor) {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
multicaster.setTaskExecutor(executor);
return multicaster;
}
This is a blunt instrument. Every listener, including framework-internal listeners that expect synchronous execution, will run asynchronously. Use this only when you understand the consequences. The @Async per-method approach is safer and more targeted.
Debugging Tips
Set a breakpoint in ApplicationListenerMethodAdapter.processEvent() to watch individual listener invocations. The this.method field shows which method is being called. The this.beanName field shows which bean owns it.
To see all registered listeners, inspect AbstractApplicationEventMulticaster.defaultRetriever.applicationListeners in the debugger. This Set<ApplicationListener<?>> contains every listener, both interface-based and annotation-based.
Enable DEBUG logging for org.springframework.context.event to see event publication and listener matching in the console output.