Jim is a guy who never stops exploring. A couple of years ago, he dove into dependency injection. But now, all of a sudden, something else hit him. Event systems are pretty common these days. As a Spring guy, he’s been using events for ages, but now he feels the need to know how they actually work under the hood. So, how do they actually work?

IYKYK

I assume as a reader you are familiar with these event systems as well. But to be sure, let’s show a simple example. Spring automatically wires up an ApplicationEventPublisher bean, which you can use to send events:

@Service
@RequiredArgsConstructor
public class OrderService {
    private final ApplicationEventPublisher publisher;

    public void placeOrder(String orderId) {
        // business logic
        publisher.publishEvent(new OrderPlacedEvent(orderId));
    }
}

Listening to them is as easy as[1]:

@Component
public class OrderPlacedListener {
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        System.out.println("Order placed: " + event.orderId());
    }
}

Understanding requires insight. Insight must be anchored.

You can find this information in the documentation, of course. But the actual question remains: how does this work? If you think about it, it’s actually way easier than you might expect, because there are only two things going on:

  • Components can send events

  • Components can listen to events

To make it work, you need another component that bridges the two. Basically, a component where other components can register their methods as event listeners. Then, when an event is published, all matching listener methods are invoked.

The most basic implementation could look something like this:

@Component
public class EventBus {
   private final List<EventListenerInvoker> invokers = new ArrayList<>();

   public void register(Object bean, Method eventListener) {
       invokers.add(new EventListenerInvoker(bean, eventListener));
   }

   public void publish(Object event) {
       invokers.forEach(it -> it.invoke(event));
   }

   private record EventListenerInvoker(Object bean, Method eventListener) {
       @SneakyThrows
       void invoke(Object event) {
           // Check if the event type matches the first parameter of the listener method
           if (eventListener.getParameterTypes()[0].isAssignableFrom(event.getClass())) {
               eventListener.invoke(bean, event);
           }
       }
   }
}

And then you need a mechanism to loop over all your beans to register them as event listeners (reusing the initializedBeans from our previous DI exploration):

for (Object bean : initializedBeans.values()) {
    for (Method method : bean.getClass().getDeclaredMethods()) {
        if (method.isAnnotationPresent(EventListener.class)) {
            eventBus.register(bean, method);
        }
    }
}

That’s all there is to it. It’s that simple! Of course, you can improve this by supporting multiple event handlers per method, adding a Map to cache listeners by event type for better performance, or using an ExecutorService to make it asynchronous. If you want to see the full implementation, check out the updated example here!


1. By default, Spring events are synchronous. If you need asynchronous behavior, you need to @EnableAsync and use the @Async annotation next to the @EventListener annotation.
shadow-left