Spring Event-Driven Architecture: Interview Self-Test with Observer Pattern and Decoupling Design
This article provides a comprehensive interview self‑test on Spring's event‑driven mechanism, covering the three core components, synchronous vs asynchronous handling, the role and default implementation of ApplicationEventMulticaster, adapter flow, async configuration options, @TransactionalEventListener phases, exception propagation, @Order ordering, SpEL condition filtering, a comparison with MQ, POJO events, and the listenerCache optimization.
Interview Self‑Test
Q1: What are the three core components of Spring event‑driven architecture?
• ApplicationEvent – the event object; since Spring 5.x any POJO can be used as an event. • ApplicationEventPublisher – the publisher; the publishEvent() method is defined on ApplicationContext . • ApplicationListener – the listener; can be registered via the @EventListener annotation or by implementing the interface. Analogy: broadcast system – news release (ApplicationEvent), transmitter (ApplicationEventPublisher), radio (ApplicationListener). <code>// 1. Event object – any POJO public class OrderCreatedEvent { private final int orderId; public OrderCreatedEvent(int orderId) { this.orderId = orderId; } public int getOrderId() { return orderId; } } // 2. Publish the event @Service public class OrderService { private final ApplicationEventPublisher publisher; public void createOrder() { publisher.publishEvent(new OrderCreatedEvent(1001)); } } // 3. Listen to the event @Component public class OrderListener { @EventListener public void onOrderCreated(OrderCreatedEvent event) { System.out.println("Received order: " + event.getOrderId()); } } </code> Follow‑up: publishEvent() returns void . The publishing thread does not wait for listeners, but in synchronous mode any exception thrown by a listener propagates back to the publisher. Q2: Is @EventListener synchronous or asynchronous by default? It is synchronous. The annotated method runs in the publisher’s thread, blocking the publisher until the listener finishes. <code>@EventListener public void handle(OrderCreatedEvent event) { // This code runs synchronously in the publisher thread! // A slow operation (e.g., HTTP call) will block the publisher. } </code> Developers sometimes mistakenly assume it is asynchronous, leading to latency issues when listeners perform heavy work. Verification: print the current thread name inside the listener and compare it with the publisher’s thread. To make listeners asynchronous there are two approaches (see Q5): Configure a SimpleApplicationEventMulticaster with a taskExecutor (global async). Invoke an @Async bean method from the listener (fine‑grained, recommended). Note: Adding @Async directly on an @EventListener method often has no effect because SimpleApplicationEventMulticaster calls the listener via ApplicationListenerMethodAdapter , bypassing Spring AOP proxies. Q3: What is the role of ApplicationEventMulticaster and its default implementation? The multicaster is the dispatch hub for Spring events. Its core responsibilities are: Manage the listener registry ( addApplicationListener() / removeApplicationListener() ). Distribute events to all matching listeners via multicastEvent() . Control synchronous or asynchronous execution based on whether a taskExecutor is configured. The default implementation is SimpleApplicationEventMulticaster . <code>public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) { ResolvableType type = eventType != null ? eventType : ResolvableType.forClass(event.getClass()); for (ApplicationListener<?> listener : getApplicationListeners(event, type)) { Executor executor = getTaskExecutor(); if (executor != null) { executor.execute(() -> invokeListener(listener, event)); // async } else { invokeListener(listener, event); // sync (default) } } } </code> Bean name requirement: the bean must be named applicationEventMulticaster ; otherwise Spring creates a default SimpleApplicationEventMulticaster instance. Q4: How does an @EventListener method get adapted? During container startup, EventListenerMethodProcessor (which implements SmartInitializingSingleton ) scans all singleton beans for @EventListener methods. For each method it calls EventListenerFactory.createApplicationListener() , which creates a GenericApplicationListenerMethodAdapter and registers it with the ApplicationEventMulticaster . EventListenerMethodProcessor – scans beans after singleton instantiation. EventListenerFactory – factory that creates ApplicationListener instances. GenericApplicationListenerMethodAdapter – adapter implementing ApplicationListener , GenericApplicationListener and SmartApplicationListener . The adapter is needed because @EventListener marks a plain bean method, while the multicaster only manages ApplicationListener instances. The adapter pattern bridges this gap. Implements ApplicationListener – can receive events. Implements GenericApplicationListener – supports generic type matching via supportsResolvableType . Implements SmartApplicationListener – provides ordering via getOrder() (used by @Order ). Q5: How to configure asynchronous events? Two ways, each with pros and cons: Global async: Define a SimpleApplicationEventMulticaster bean and inject a ThreadPoolTaskExecutor . <code>@Configuration public class AsyncEventConfig { @Bean public ApplicationEventMulticaster applicationEventMulticaster(ThreadPoolTaskExecutor taskExecutor) { SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster(); multicaster.setTaskExecutor(taskExecutor); multicaster.setErrorHandler(e -> System.err.println("Async event error: " + e.getMessage())); return multicaster; } @Bean public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-event-"); executor.initialize(); return executor; } } </code> Drawback: all listeners become asynchronous, which may break transactional listeners. Fine‑grained async (recommended): Call an @Async bean method from the listener. <code>@Component public class AsyncOrderProcessor { @Async("eventAsyncExecutor") public CompletableFuture<String> processOrderAsync(OrderCreatedEvent event) { // async processing return CompletableFuture.completedFuture("done"); } } @EventListener public void onOrderCreated(OrderCreatedEvent event) { asyncOrderProcessor.processOrderAsync(event); // async execution } </code> Allows precise control over which listeners run asynchronously. Q6: What are the four transaction phases of @TransactionalEventListener ? BEFORE_COMMIT – triggered before transaction commit (e.g., final validation). AFTER_COMMIT – triggered after successful commit (e.g., sending notifications). AFTER_ROLLBACK – triggered after rollback (e.g., logging rollback, compensation). AFTER_COMPLETION – triggered after transaction completion regardless of outcome (e.g., cleaning temporary resources). <code>@Component public class TransactionalOrderListener { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCreatedAfterCommit(OrderCreatedEvent event) { emailService.send(event); } @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) public void onOrderRolledBack(OrderCreatedEvent event) { log.warn("Order rolled back: {}", event.getOrderId()); } } </code> Spring registers callbacks with TransactionSynchronizationManager . When the transaction commits or rolls back, the manager invokes afterCommit() or afterCompletion() on the listener. Follow‑up: If there is no active transaction, the listener falls back to synchronous execution (default fallbackProcessing=true ). Setting fallbackProcessing=false skips execution. Q7: Does an exception in a synchronous listener propagate back to the publisher? Yes. In the default synchronous mode, an exception thrown by a listener aborts the publisher’s subsequent logic. <code>public void createOrder() { orderRepository.save(order); eventPublisher.publishEvent(new OrderCreatedEvent(order)); // This line is not reached if a listener throws an exception. sendWelcomeMessage(order); } @EventListener public void handle(OrderCreatedEvent event) { throw new RuntimeException("Email send failed!"); } </code> Lesson: listeners should catch their own exceptions to avoid breaking the publishing flow. <code>@EventListener public void handle(OrderCreatedEvent event) { try { emailService.send(event); } catch (Exception e) { log.error("Failed to send email", e); // Do not rethrow; publisher continues. } } </code> Q8: How does @Order control listener execution order? When multiple listeners handle the same event, the @Order value (smaller = higher priority) determines the order. <code>@EventListener @Order(1) public void validateOrder(OrderCreatedEvent event) { System.out.println("[1] Validate order"); } @EventListener @Order(2) public void sendEmail(OrderCreatedEvent event) { System.out.println("[2] Send email"); } @EventListener @Order(3) public void updateInventory(OrderCreatedEvent event) { System.out.println("[3] Update inventory"); } </code> The sorting is performed by GenericApplicationListenerMethodAdapter (which implements SmartApplicationListener.getOrder() ) and AbstractEventRegistry.sortListeners() before dispatch. Note: @Order only guarantees order for synchronous execution; in asynchronous mode the thread pool decides the actual order. Q9: How does SpEL condition filtering work? The condition attribute of @EventListener accepts a SpEL expression evaluated just before invoking the listener. The listener runs only if the expression returns true . <code>// Only events with orderId > 100 trigger @EventListener(condition = "#event.orderId > 100") public void onLargeOrder(OrderCreatedEvent event) { /* ... */ } // Only events from admin user trigger @EventListener(condition = "#event.userName == 'admin'") public void onAdminOrder(OrderCreatedEvent event) { /* ... */ } </code> SpEL context variables: #event or #args[0] – the event object. #root – the root object (usually the publisher). #args – method argument array. Implementation uses GenericApplicationListenerMethodAdapter with StandardBeanExpressionEvaluator to evaluate the expression. If it evaluates to false , the listener is skipped. When is it executed? After the multicaster has retrieved all candidate listeners ( getApplicationListeners() ) and before invokeListener() is called. Q10: Spring events vs. MQ – comparison Communication scope: Spring events – in‑process (same JVM); MQ – inter‑process, cross‑service, cross‑machine. Reliability: Spring events – low (lost if JVM crashes); MQ – high (persistent, ACK). Performance: Spring events – nanosecond‑level method call; MQ – millisecond‑level network I/O. Message persistence: none for Spring events; provided by MQ. Retry mechanism: manual for Spring events; built‑in (dead‑letter, retry) for MQ. Monitoring: weak (custom logs) for Spring events; strong (management UI) for MQ. Deployment cost: zero (built into Spring) vs. high (MQ cluster). Typical use‑cases: module decoupling and local notifications for Spring events; cross‑service communication, asynchronous processing, event sourcing for MQ. Selection guidance: Same JVM module decoupling → use Spring events. Cross‑service reliability required → use MQ. Common pattern → combine both: local Spring events for intra‑module communication and MQ for inter‑service communication. <code>@Service public class OrderService { @Autowired ApplicationEventPublisher eventPublisher; @Autowired RabbitTemplate rabbitTemplate; public void createOrder(Order order) { orderRepository.save(order); // 1. Local event (same JVM) eventPublisher.publishEvent(new OrderCreatedEvent(order)); // 2. Cross‑service notification rabbitTemplate.convertAndSend("order.exchange", "order.created", order); } } </code> Q11: Can any POJO be used as an event object? Yes. Since Spring 4.2 (and thus Spring 5.x) any object can be published as an event; extending ApplicationEvent is no longer required. <code>// New style – any POJO public class OrderEvent { private final int orderId; public OrderEvent(int orderId) { this.orderId = orderId; } } // Publishing is identical eventPublisher.publishEvent(new OrderEvent(123)); </code> Internally, Spring wraps a non‑ ApplicationEvent object in a PayloadApplicationEvent . The GenericApplicationListenerMethodAdapter matches the payload type via ResolvableType , allowing listeners to declare the original POJO type directly. Q12: What is the purpose of listenerCache ? listenerCache is a performance optimization inside AbstractApplicationEventMulticaster . It caches the mapping "event type → matching listeners" to avoid repeated scanning. First time an event type is seen, the multicaster iterates all ApplicationListener instances and selects those that supportsEvent() . The result is stored in listenerCache keyed by the event class or ResolvableType . Subsequent events of the same type read the listener list directly from the cache, eliminating the traversal. When listeners are added or removed ( addApplicationListener() , removeApplicationListener() , etc.), the cache is cleared. The cache greatly improves throughput when an application has dozens or hundreds of listeners and events are published frequently. Cache key: typically the event’s Class or ResolvableType ; the value is the list of matching ApplicationListener instances. References Java Development Guide – Spring Event‑Driven Full Analysis (source code tracing, solution comparison, pitfalls self‑check). Spring Framework Official Documentation – Event and Listeners. Spring Framework source code: ApplicationEventMulticaster , SimpleApplicationEventMulticaster , EventListenerMethodProcessor , TransactionSynchronizationManager , TransactionalApplicationListenerAdapter . 《Spring 源码深度解析》第 5 章 – 事件与监听。
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
CodeSmart Hoops
A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
