Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Event-Driven Programming in Spring Boot with @EventListener

Tech 2

Event-driven design lets components react to facts instead of polling or tightly coupling modules. After some action completes, you publish an event; interested listeners consume it and run their own logic. In classic patterns, this aligns with publish–subscribe and the observer pattern.

Core pieces in Spring’s event model:

  • Event payload: the data carried from the producer to consumers
  • Listener: code that handles a specific event type
  • Publisher: the component that fires events into the ApplicationContext

Listening with ApplicationListener

Spring exposes a generic listener interface. Implementing it registers a listener for a specific event type.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class BootEventLogger implements ApplicationListener<ApplicationEvent> {
    private static final Logger log = LoggerFactory.getLogger(BootEventLogger.class);

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        log.info("Observed: {}", event.getClass().getSimpleName());
    }
}

Bootstrapping a standard Spring Boot application is unchanged:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class EventDemoApp {
    public static void main(String[] args) {
        SpringApplication.run(EventDemoApp.class, args);
    }
}

When the app starts, you’ll see lifecycle events such as ContextRefreshedEvent and ApplicationReadyEvent being logged.

Custom domain event and interface-based listener

Define a custom event. You can publish any object as an event in modern Spring, but extending ApplicationEvent is still common and explicit.

import org.springframework.context.ApplicationEvent;

public class NoticeEvent extends ApplicationEvent {
    private final String text;

    public NoticeEvent(Object source, String text) {
        super(source);
        this.text = text;
    }

    public String getText() {
        return text;
    }
}

Create a listener that targets this type:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class InterfaceBasedNoticeListener implements ApplicationListener<NoticeEvent> {
    private static final Logger log = LoggerFactory.getLogger(InterfaceBasedNoticeListener.class);

    @Override
    public void onApplicationEvent(NoticeEvent event) {
        log.info("Interface-based handler got: {}", event.getText());
    }
}

Publish the event through ApplicationEventPublisher:

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class DomainEventPublisher {
    private final ApplicationEventPublisher publisher;

    public DomainEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void publishNotice(String message) {
        publisher.publishEvent(new NoticeEvent(this, message));
    }
}

Trigger publication via a simple HTTP endpoint:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/events")
public class EventSampleController {
    private final DomainEventPublisher events;

    public EventSampleController(DomainEventPublisher events) {
        this.events = events;
    }

    @PostMapping("/notice")
    public void raise(@RequestParam String msg) {
        events.publishNotice(msg);
    }
}

Issuing a POST to /events/notice?msg=hello will trigger both default framework listeners and the NoticeEvent listener.

Annotation-driven listeners with @EventListener

Instead of implementing an interface, you can annotate any bean method with @EventListener. This is method-level and allows several handlers in one class.

Log any ApplicationEvent:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GlobalEventLogger {
    private static final Logger log = LoggerFactory.getLogger(GlobalEventLogger.class);

    @EventListener(classes = ApplicationEvent.class)
    public void logLifecycle(ApplicationEvent event) {
        log.info("Lifecycle observed: {}", event.getClass().getName());
    }
}

Handle the custom event with a method listener:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
public class AnnotationBasedNoticeListener {
    private static final Logger log = LoggerFactory.getLogger(AnnotationBasedNoticeListener.class);

    @EventListener
    @Order(0)
    public void whenNoticeArrives(NoticeEvent event) {
        log.info("Annotation-based handler got: {}", event.getText());
    }
}

Interface-based and annotation-based listeners can coexist. If you need a specific call order among listeners, add @Order on the methods or implement Ordered.

How @EventListener works under the hood

When a Spring Boot application creates its context (for a servlet app, an AnnotationConfigServletWebServerApplicationContext), it registers a set of annotation processors. One of them wires up method-level event listeners.

The key registration happens via AnnotatedBeanDefinitionReader, which calls:

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment env) {
    // ...
    AnnotationConfigUtils.registerAnnotationConfigProcessors(registry);
}

Among those processors is EventListenerMethodProcessor, a BeanFactory post-processor. After signletons are ready, it scans beans for methods annotated with @EventListener, adapts them to ApplicationListener instances, and registers them with the context:

@Override
public void afterSingletonsInstantiated() {
    for (String name : beanFactory.getBeanNamesForType(Object.class)) {
        Class<?> type = AutoProxyUtils.determineTargetClass(beanFactory, name);
        if (type != null) {
            scanAndRegister(name, type);
        }
    }
}

private void scanAndRegister(String beanName, Class<?> type) {
    Map<Method, EventListener> methods = MethodIntrospector.selectMethods(
        type, m -> AnnotatedElementUtils.findMergedAnnotation(m, EventListener.class));

    for (Method method : methods.keySet()) {
        ApplicationListener<?> listener = factory.createApplicationListener(beanName, type, method);
        if (listener instanceof ApplicationListenerMethodAdapter adapter) {
            adapter.init(applicationContext, evaluator);
        }
        applicationContext.addApplicationListener(listener);
    }
}

The factory variable above represents available EventListenerFactory implementations. Spring provides one for regular @EventListener and another for @TransactionalEventListener.

Transaction-aware listeners

To receive events only in relation to transaction boundaries, use @TransactionalEventListener. For example, process an event after a successful commit:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.event.TransactionPhase;

@Component
public class TxAwareNoticeListener {
    private static final Logger log = LoggerFactory.getLogger(TxAwareNoticeListener.class);

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onCommitted(NoticeEvent event) {
        log.info("Delivered after commit: {}", event.getText());
    }
}

This handler is invoked only when the event is published within an active transaction and that transaction commits successfully.

Tags: spring

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.