Skip to content

Commit a9d100e

Browse files
committed
Support for always executing specific listeners in original thread
See gh-30244
1 parent dde8f44 commit a9d100e

File tree

4 files changed

+57
-6
lines changed

4 files changed

+57
-6
lines changed

spring-context/src/main/java/org/springframework/context/ApplicationListener.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ public interface ApplicationListener<E extends ApplicationEvent> extends EventLi
4848
*/
4949
void onApplicationEvent(E event);
5050

51+
/**
52+
* Return whether this listener supports asynchronous execution.
53+
* @return {@code true} if this listener instance can be executed asynchronously
54+
* depending on the multicaster configuration (the default), or {@code false} if it
55+
* needs to immediately run within the original thread which published the event
56+
* @since 6.1
57+
* @see org.springframework.context.event.SimpleApplicationEventMulticaster#setTaskExecutor
58+
*/
59+
default boolean supportsAsyncExecution() {
60+
return true;
61+
}
62+
5163

5264
/**
5365
* Create a new {@code ApplicationListener} for the given payload consumer.

spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -110,17 +110,23 @@ public interface ApplicationEventMulticaster {
110110
* Multicast the given application event to appropriate listeners.
111111
* <p>Consider using {@link #multicastEvent(ApplicationEvent, ResolvableType)}
112112
* if possible as it provides better support for generics-based events.
113+
* <p>If a matching {@code ApplicationListener} does not support asynchronous
114+
* execution, it must be run within the calling thread of this multicast call.
113115
* @param event the event to multicast
116+
* @see ApplicationListener#supportsAsyncExecution()
114117
*/
115118
void multicastEvent(ApplicationEvent event);
116119

117120
/**
118121
* Multicast the given application event to appropriate listeners.
119122
* <p>If the {@code eventType} is {@code null}, a default type is built
120123
* based on the {@code event} instance.
124+
* <p>If a matching {@code ApplicationListener} does not support asynchronous
125+
* execution, it must be run within the calling thread of this multicast call.
121126
* @param event the event to multicast
122127
* @param eventType the type of event (can be {@code null})
123128
* @since 4.2
129+
* @see ApplicationListener#supportsAsyncExecution()
124130
*/
125131
void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType);
126132

spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,15 @@ public SimpleApplicationEventMulticaster(BeanFactory beanFactory) {
7979
* to invoke each listener with.
8080
* <p>Default is equivalent to {@link org.springframework.core.task.SyncTaskExecutor},
8181
* executing all listeners synchronously in the calling thread.
82-
* <p>Consider specifying an asynchronous task executor here to not block the
83-
* caller until all listeners have been executed. However, note that asynchronous
84-
* execution will not participate in the caller's thread context (class loader,
85-
* transaction association) unless the TaskExecutor explicitly supports this.
82+
* <p>Consider specifying an asynchronous task executor here to not block the caller
83+
* until all listeners have been executed. However, note that asynchronous execution
84+
* will not participate in the caller's thread context (class loader, transaction context)
85+
* unless the TaskExecutor explicitly supports this.
86+
* <p>{@link ApplicationListener} instances which declare no support for asynchronous
87+
* execution ({@link ApplicationListener#supportsAsyncExecution()} always run within
88+
* the original thread which published the event, e.g. the transaction-synchronized
89+
* {@link org.springframework.transaction.event.TransactionalApplicationListener}.
90+
* @since 2.0
8691
* @see org.springframework.core.task.SyncTaskExecutor
8792
* @see org.springframework.core.task.SimpleAsyncTaskExecutor
8893
*/
@@ -92,6 +97,7 @@ public void setTaskExecutor(@Nullable Executor taskExecutor) {
9297

9398
/**
9499
* Return the current task executor for this multicaster.
100+
* @since 2.0
95101
*/
96102
@Nullable
97103
protected Executor getTaskExecutor() {
@@ -136,7 +142,7 @@ public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType even
136142
ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event));
137143
Executor executor = getTaskExecutor();
138144
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
139-
if (executor != null) {
145+
if (executor != null && listener.supportsAsyncExecution()) {
140146
executor.execute(() -> invokeListener(listener, event));
141147
}
142148
else {

spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.HashSet;
2121
import java.util.List;
2222
import java.util.Set;
23+
import java.util.concurrent.atomic.AtomicBoolean;
2324

2425
import org.aopalliance.intercept.MethodInvocation;
2526
import org.junit.jupiter.api.Test;
@@ -54,6 +55,7 @@
5455
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
5556
import static org.mockito.ArgumentMatchers.isA;
5657
import static org.mockito.BDDMockito.given;
58+
import static org.mockito.BDDMockito.willReturn;
5759
import static org.mockito.BDDMockito.willThrow;
5860
import static org.mockito.Mockito.mock;
5961
import static org.mockito.Mockito.times;
@@ -137,19 +139,44 @@ private void multicastEvent(boolean match, Class<?> listenerType, ApplicationEve
137139
public void simpleApplicationEventMulticasterWithTaskExecutor() {
138140
@SuppressWarnings("unchecked")
139141
ApplicationListener<ApplicationEvent> listener = mock();
142+
willReturn(true).given(listener).supportsAsyncExecution();
140143
ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext());
141144

142145
SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster();
146+
AtomicBoolean invoked = new AtomicBoolean();
143147
smc.setTaskExecutor(command -> {
148+
invoked.set(true);
144149
command.run();
145150
command.run();
146151
});
147152
smc.addApplicationListener(listener);
148153

149154
smc.multicastEvent(evt);
155+
assertThat(invoked.get()).isTrue();
150156
verify(listener, times(2)).onApplicationEvent(evt);
151157
}
152158

159+
@Test
160+
public void simpleApplicationEventMulticasterWithTaskExecutorAndNonAsyncListener() {
161+
@SuppressWarnings("unchecked")
162+
ApplicationListener<ApplicationEvent> listener = mock();
163+
willReturn(false).given(listener).supportsAsyncExecution();
164+
ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext());
165+
166+
SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster();
167+
AtomicBoolean invoked = new AtomicBoolean();
168+
smc.setTaskExecutor(command -> {
169+
invoked.set(true);
170+
command.run();
171+
command.run();
172+
});
173+
smc.addApplicationListener(listener);
174+
175+
smc.multicastEvent(evt);
176+
assertThat(invoked.get()).isFalse();
177+
verify(listener, times(1)).onApplicationEvent(evt);
178+
}
179+
153180
@Test
154181
public void simpleApplicationEventMulticasterWithException() {
155182
@SuppressWarnings("unchecked")

0 commit comments

Comments
 (0)