Skip to content

Commit 81cdc78

Browse files
committed
GH-1654 - Fix caching of TransactionalEventListeners.
We now properly cache the TransactionalEventListeners instances *before* filtering them for matching listeners against the event instance as a second step.
1 parent 81d1452 commit 81cdc78

2 files changed

Lines changed: 82 additions & 33 deletions

File tree

spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.function.Consumer;
2626
import java.util.function.Predicate;
2727
import java.util.function.Supplier;
28+
import java.util.stream.Collectors;
2829
import java.util.stream.Stream;
2930

3031
import org.jspecify.annotations.Nullable;
@@ -76,7 +77,7 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
7677
static final String REPUBLISH_ON_RESTART = "spring.modulith.events.republish-outstanding-events-on-restart";
7778
static final String REPUBLISH_ON_RESTART_LEGACY = "spring.modulith.republish-outstanding-events-on-restart";
7879

79-
private final Map<ResolvableType, TransactionalEventListeners> listenerCache = new ConcurrentReferenceHashMap<>();
80+
private final Map<CacheKey, TransactionalEventListeners> cache = new ConcurrentReferenceHashMap<>();
8081
private final Supplier<EventPublicationRegistry> registry;
8182
private final Supplier<Environment> environment;
8283

@@ -114,35 +115,34 @@ public void multicastEvent(ApplicationEvent event) {
114115
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
115116

116117
var type = eventType == null ? ResolvableType.forInstance(event) : eventType;
117-
var listeners = getApplicationListeners(event, type);
118+
var candidates = super.getApplicationListeners(event, type);
118119

119-
if (listeners.isEmpty()) {
120+
if (candidates.isEmpty()) {
120121
return;
121122
}
122123

123-
listenerCache
124-
.computeIfAbsent(type, __ -> new TransactionalEventListeners(listeners, environment))
125-
.ifPresent(it -> storePublications(it, getEventToPersist(event)));
124+
var eventToPersist = getEventToPersist(event);
126125

127-
for (ApplicationListener listener : listeners) {
128-
listener.onApplicationEvent(event);
126+
// Find all listeners that will need to be invoked
127+
var matchingListeners = candidates.stream() //
128+
.filter(it -> matches(event, eventToPersist, it)) //
129+
.toList();
130+
131+
if (matchingListeners.isEmpty()) {
132+
return;
129133
}
130-
}
131134

132-
/*
133-
* (non-Javadoc)
134-
* @see org.springframework.context.event.AbstractApplicationEventMulticaster#getApplicationListeners(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)
135-
*/
136-
@Override
137-
protected Collection<ApplicationListener<?>> getApplicationListeners(ApplicationEvent event,
138-
ResolvableType eventType) {
135+
// From the candidates find the transactional ones and cache them by source and target type
136+
cache.computeIfAbsent(new CacheKey(type, getSourceType(event)),
137+
__ -> new TransactionalEventListeners(candidates, environment))
139138

140-
Object eventToPersist = getEventToPersist(event);
139+
// Make sure we honor the by-event instance evaluated conditions
140+
.filter(matchingListeners::contains)
141+
.ifPresent(stream -> storePublications(stream, eventToPersist));
141142

142-
return super.getApplicationListeners(event, eventType)
143-
.stream()
144-
.filter(it -> matches(event, eventToPersist, it))
145-
.toList();
143+
for (ApplicationListener listener : matchingListeners) {
144+
listener.onApplicationEvent(event);
145+
}
146146
}
147147

148148
/*
@@ -202,8 +202,7 @@ public void afterSingletonsInstantiated() {
202202

203203
private void invokeTargetListener(TargetEventPublication publication) {
204204

205-
var listeners = new TransactionalEventListeners(
206-
getApplicationListeners(), environment);
205+
var listeners = new TransactionalEventListeners(getApplicationListeners(), environment);
207206

208207
listeners.stream() //
209208
.filter(it -> publication.isIdentifiedBy(PublicationTargetIdentifier.of(it.getListenerId()))) //
@@ -251,6 +250,13 @@ private static Object getEventToPersist(ApplicationEvent event) {
251250
: event;
252251
}
253252

253+
private static @Nullable Class<?> getSourceType(ApplicationEvent event) {
254+
255+
var source = event.getSource();
256+
257+
return source != null ? source.getClass() : null;
258+
}
259+
254260
private static boolean matches(ApplicationEvent event, Object payload, ApplicationListener<?> listener) {
255261

256262
// Verify general listener matching by eagerly evaluating the condition
@@ -280,6 +286,13 @@ private static boolean invokeShouldHandle(ApplicationListener<?> candidate, Appl
280286
return true;
281287
}
282288

289+
private record CacheKey(ResolvableType eventType, @Nullable Class<?> sourceType) {
290+
291+
private CacheKey {
292+
Assert.notNull(eventType, "Event type must not be null");
293+
}
294+
}
295+
283296
/**
284297
* First-class collection to work with transactional event listeners, i.e. {@link ApplicationListener} instances that
285298
* implement {@link TransactionalApplicationListener}.
@@ -324,6 +337,10 @@ public TransactionalEventListeners(Collection<ApplicationListener<?>> listeners,
324337
.toList();
325338
}
326339

340+
private TransactionalEventListeners(List<TransactionalApplicationListener<ApplicationEvent>> listeners) {
341+
this.listeners = listeners;
342+
}
343+
327344
/**
328345
* Invokes the given {@link Consumer} for all transactional event listeners.
329346
*
@@ -350,6 +367,13 @@ public void ifPresent(Consumer<Stream<TransactionalApplicationListener<Applicati
350367
}
351368
}
352369

370+
public TransactionalEventListeners filter(
371+
Predicate<? super TransactionalApplicationListener<ApplicationEvent>> filter) {
372+
373+
return listeners.stream().filter(filter)
374+
.collect(Collectors.collectingAndThen(Collectors.toUnmodifiableList(), TransactionalEventListeners::new));
375+
}
376+
353377
/**
354378
* Returns all transactional event listeners.
355379
*

spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticasterUnitTests.java

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,28 @@
2222

2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.stream.Stream;
2526

2627
import org.junit.jupiter.api.BeforeEach;
2728
import org.junit.jupiter.api.Test;
29+
import org.mockito.ArgumentCaptor;
2830
import org.springframework.context.PayloadApplicationEvent;
2931
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3032
import org.springframework.context.event.ApplicationEventMulticaster;
3133
import org.springframework.context.event.EventListenerMethodProcessor;
32-
import org.springframework.core.ResolvableType;
3334
import org.springframework.core.env.MapPropertySource;
3435
import org.springframework.core.env.StandardEnvironment;
3536
import org.springframework.mock.env.MockEnvironment;
3637
import org.springframework.modulith.events.ApplicationModuleListener;
3738
import org.springframework.modulith.events.core.EventPublicationRegistry;
39+
import org.springframework.modulith.events.core.PublicationTargetIdentifier;
3840
import org.springframework.modulith.events.support.PersistentApplicationEventMulticaster.TransactionalEventListeners;
3941
import org.springframework.stereotype.Component;
4042
import org.springframework.transaction.event.TransactionPhase;
4143
import org.springframework.transaction.event.TransactionalApplicationListener;
4244
import org.springframework.transaction.event.TransactionalApplicationListenerMethodAdapter;
4345
import org.springframework.transaction.event.TransactionalEventListener;
46+
import org.springframework.transaction.event.TransactionalEventListenerFactory;
4447
import org.springframework.util.ReflectionUtils;
4548

4649
/**
@@ -92,21 +95,36 @@ void triggersRepublicationIfLegacyConfigExplicitlyEnabled() {
9295
verify(registry).processIncompletePublications(any(), any(), any());
9396
}
9497

95-
@Test // GH-277
98+
@Test // GH-277, GH-1654
9699
void honorsListenerCondition() throws Exception {
97100

98101
try (var ctx = new AnnotationConfigApplicationContext()) {
99102

100103
ctx.addBeanFactoryPostProcessor(new EventListenerMethodProcessor());
104+
ctx.registerBean(TransactionalEventListenerFactory.class, TransactionalEventListenerFactory::new);
101105
ctx.registerBean("applicationEventMulticaster", ApplicationEventMulticaster.class, () -> multicaster);
102-
ctx.registerBean("conditionalListener", ConditionalListener.class);
106+
ctx.registerBean("unconditionalListener", UnconditionalListener.class, UnconditionalListener::new);
107+
ctx.registerBean("conditionalListener", ConditionalListener.class, ConditionalListener::new);
103108
ctx.refresh();
104109

105-
assertListenerSelected(new SampleEvent(true), true);
106-
assertListenerSelected(new SampleEvent(false), false);
110+
multicast(new SampleEvent(false));
111+
multicast(new SampleEvent(true));
112+
113+
@SuppressWarnings("unchecked")
114+
ArgumentCaptor<Stream<PublicationTargetIdentifier>> captor = ArgumentCaptor.forClass(Stream.class);
115+
verify(registry, times(2)).store(any(), captor.capture());
116+
117+
var allValues = captor.getAllValues();
118+
119+
assertThat(allValues.get(0).count()).isEqualTo(1L);
120+
assertThat(allValues.get(1).count()).isEqualTo(2L);
107121
}
108122
}
109123

124+
private void multicast(Object event) {
125+
multicaster.multicastEvent(new PayloadApplicationEvent<>(this, event));
126+
}
127+
110128
@Test // GH-726
111129
void onlyConsidersAfterCommitListeners() {
112130

@@ -170,19 +188,26 @@ private static TransactionalApplicationListenerMethodAdapter getAdapter(Class<?>
170188
return new TransactionalApplicationListenerMethodAdapter(type.getName(), type, method);
171189
}
172190

173-
private void assertListenerSelected(SampleEvent event, boolean expected) {
191+
@Component
192+
static class UnconditionalListener {
174193

175-
var listeners = multicaster.getApplicationListeners(new PayloadApplicationEvent<>(this, event),
176-
ResolvableType.forClass(event.getClass()));
194+
boolean invoked = false;
177195

178-
assertThat(listeners).hasSize(expected ? 1 : 0);
196+
@TransactionalEventListener
197+
void on(SampleEvent event) {
198+
this.invoked = true;
199+
}
179200
}
180201

181202
@Component
182203
static class ConditionalListener {
183204

205+
boolean invoked = false;
206+
184207
@TransactionalEventListener(condition = "#event.supported")
185-
void on(SampleEvent event) {}
208+
void on(SampleEvent event) {
209+
this.invoked = true;
210+
}
186211
}
187212

188213
@Component

0 commit comments

Comments
 (0)