Skip to content

Commit e228f4b

Browse files
committed
Consistent pre-resolution of event type vs payload type
Restores proper event type propagation to parent context. Selectively applies payload type to given payload object. Also reuses cached type for regular ApplicationEvent now. Closes gh-30360
1 parent c733ae0 commit e228f4b

File tree

4 files changed

+227
-31
lines changed

4 files changed

+227
-31
lines changed

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

Lines changed: 15 additions & 12 deletions
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.
@@ -41,27 +41,30 @@ public class PayloadApplicationEvent<T> extends ApplicationEvent implements Reso
4141

4242
private final ResolvableType payloadType;
4343

44+
4445
/**
45-
* Create a new PayloadApplicationEvent.
46+
* Create a new PayloadApplicationEvent, using the instance to infer its type.
4647
* @param source the object on which the event initially occurred (never {@code null})
4748
* @param payload the payload object (never {@code null})
48-
* @param payloadType the type object of payload object (can be {@code null})
49-
* @since 6.0
5049
*/
51-
public PayloadApplicationEvent(Object source, T payload, @Nullable ResolvableType payloadType) {
52-
super(source);
53-
Assert.notNull(payload, "Payload must not be null");
54-
this.payload = payload;
55-
this.payloadType = (payloadType != null) ? payloadType : ResolvableType.forInstance(payload);
50+
public PayloadApplicationEvent(Object source, T payload) {
51+
this(source, payload, null);
5652
}
5753

5854
/**
59-
* Create a new PayloadApplicationEvent, using the instance to infer its type.
55+
* Create a new PayloadApplicationEvent based on the provided payload type.
6056
* @param source the object on which the event initially occurred (never {@code null})
6157
* @param payload the payload object (never {@code null})
58+
* @param payloadType the type object of payload object (can be {@code null}).
59+
* Note that this is meant to indicate the payload type (e.g. {@code String}),
60+
* not the full event type (such as {@code PayloadApplicationEvent<&lt;String&gt;}).
61+
* @since 6.0
6262
*/
63-
public PayloadApplicationEvent(Object source, T payload) {
64-
this(source, payload, null);
63+
public PayloadApplicationEvent(Object source, T payload, @Nullable ResolvableType payloadType) {
64+
super(source);
65+
Assert.notNull(payload, "Payload must not be null");
66+
this.payload = payload;
67+
this.payloadType = (payloadType != null ? payloadType : ResolvableType.forInstance(payload));
6568
}
6669

6770

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ protected ErrorHandler getErrorHandler() {
128128

129129
@Override
130130
public void multicastEvent(ApplicationEvent event) {
131-
multicastEvent(event, resolveDefaultEventType(event));
131+
multicastEvent(event, null);
132132
}
133133

134134
@Override
135135
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
136-
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
136+
ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event));
137137
Executor executor = getTaskExecutor();
138138
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
139139
if (executor != null) {
@@ -145,10 +145,6 @@ public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType even
145145
}
146146
}
147147

148-
private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
149-
return ResolvableType.forInstance(event);
150-
}
151-
152148
/**
153149
* Invoke the given listener with the given event.
154150
* @param listener the ApplicationListener to invoke

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -385,23 +385,47 @@ public void publishEvent(Object event) {
385385

386386
/**
387387
* Publish the given event to all listeners.
388+
* <p>This is the internal delegate that all other {@code publishEvent}
389+
* methods refer to. It is not meant to be called directly but rather serves
390+
* as a propagation mechanism between application contexts in a hierarchy,
391+
* potentially overridden in subclasses for a custom propagation arrangement.
388392
* @param event the event to publish (may be an {@link ApplicationEvent}
389393
* or a payload object to be turned into a {@link PayloadApplicationEvent})
390-
* @param eventType the resolved event type, if known
394+
* @param typeHint the resolved event type, if known.
395+
* The implementation of this method also tolerates a payload type hint for
396+
* a payload object to be turned into a {@link PayloadApplicationEvent}.
397+
* However, the recommended way is to construct an actual event object via
398+
* {@link PayloadApplicationEvent#PayloadApplicationEvent(Object, Object, ResolvableType)}
399+
* instead for such scenarios.
391400
* @since 4.2
401+
* @see ApplicationEventMulticaster#multicastEvent(ApplicationEvent, ResolvableType)
392402
*/
393-
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
403+
protected void publishEvent(Object event, @Nullable ResolvableType typeHint) {
394404
Assert.notNull(event, "Event must not be null");
405+
ResolvableType eventType = null;
395406

396407
// Decorate event as an ApplicationEvent if necessary
397408
ApplicationEvent applicationEvent;
398409
if (event instanceof ApplicationEvent applEvent) {
399410
applicationEvent = applEvent;
411+
eventType = typeHint;
400412
}
401413
else {
402-
applicationEvent = new PayloadApplicationEvent<>(this, event, eventType);
403-
if (eventType == null) {
404-
eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
414+
ResolvableType payloadType = null;
415+
if (typeHint != null && ApplicationEvent.class.isAssignableFrom(typeHint.toClass())) {
416+
eventType = typeHint;
417+
}
418+
else {
419+
payloadType = typeHint;
420+
}
421+
applicationEvent = new PayloadApplicationEvent<>(this, event, payloadType);
422+
}
423+
424+
// Determine event type only once (for multicast and parent publish)
425+
if (eventType == null) {
426+
eventType = ResolvableType.forInstance(applicationEvent);
427+
if (typeHint == null) {
428+
typeHint = eventType;
405429
}
406430
}
407431

@@ -416,7 +440,7 @@ protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
416440
// Publish event via parent context as well...
417441
if (this.parent != null) {
418442
if (this.parent instanceof AbstractApplicationContext abstractApplicationContext) {
419-
abstractApplicationContext.publishEvent(event, eventType);
443+
abstractApplicationContext.publishEvent(event, typeHint);
420444
}
421445
else {
422446
this.parent.publishEvent(event);

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

Lines changed: 180 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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.
@@ -21,6 +21,7 @@
2121

2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.beans.BeansException;
2425
import org.springframework.context.ApplicationListener;
2526
import org.springframework.context.ConfigurableApplicationContext;
2627
import org.springframework.context.PayloadApplicationEvent;
@@ -68,16 +69,97 @@ void payloadApplicationEventWithType() {
6869
});
6970
}
7071

72+
@Test
73+
@SuppressWarnings("resource")
74+
void testEventClassWithPayloadType() {
75+
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(NumberHolderListener.class);
76+
77+
PayloadApplicationEvent<NumberHolder<Integer>> event = new PayloadApplicationEvent<>(this,
78+
new NumberHolder<>(42), ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class));
79+
ac.publishEvent(event);
80+
assertThat(ac.getBean(NumberHolderListener.class).events.contains(event.getPayload())).isTrue();
81+
ac.close();
82+
}
83+
84+
@Test
85+
@SuppressWarnings("resource")
86+
void testEventClassWithPayloadTypeOnParentContext() {
87+
ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(NumberHolderListener.class);
88+
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
89+
ac.refresh();
90+
91+
PayloadApplicationEvent<NumberHolder<Integer>> event = new PayloadApplicationEvent<>(this,
92+
new NumberHolder<>(42), ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class));
93+
ac.publishEvent(event);
94+
assertThat(parent.getBean(NumberHolderListener.class).events.contains(event.getPayload())).isTrue();
95+
ac.close();
96+
parent.close();
97+
}
98+
99+
@Test
100+
@SuppressWarnings("resource")
101+
void testPayloadObjectWithPayloadType() {
102+
final Object payload = new NumberHolder<>(42);
103+
104+
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(NumberHolderListener.class) {
105+
@Override
106+
protected void finishRefresh() throws BeansException {
107+
super.finishRefresh();
108+
// This is not recommended: use publishEvent(new PayloadApplicationEvent(...)) instead
109+
publishEvent(payload, ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class));
110+
}
111+
};
112+
113+
assertThat(ac.getBean(NumberHolderListener.class).events.contains(payload)).isTrue();
114+
ac.close();
115+
}
116+
117+
@Test
118+
@SuppressWarnings("resource")
119+
void testPayloadObjectWithPayloadTypeOnParentContext() {
120+
final Object payload = new NumberHolder<>(42);
121+
122+
ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(NumberHolderListener.class);
123+
ConfigurableApplicationContext ac = new GenericApplicationContext(parent) {
124+
@Override
125+
protected void finishRefresh() throws BeansException {
126+
super.finishRefresh();
127+
// This is not recommended: use publishEvent(new PayloadApplicationEvent(...)) instead
128+
publishEvent(payload, ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class));
129+
}
130+
};
131+
ac.refresh();
132+
133+
assertThat(parent.getBean(NumberHolderListener.class).events.contains(payload)).isTrue();
134+
ac.close();
135+
parent.close();
136+
}
137+
71138
@Test
72139
@SuppressWarnings("resource")
73140
void testEventClassWithInterface() {
74141
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(AuditableListener.class);
142+
75143
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
76144
ac.publishEvent(event);
77145
assertThat(ac.getBean(AuditableListener.class).events.contains(event)).isTrue();
78146
ac.close();
79147
}
80148

149+
@Test
150+
@SuppressWarnings("resource")
151+
void testEventClassWithInterfaceOnParentContext() {
152+
ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(AuditableListener.class);
153+
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
154+
ac.refresh();
155+
156+
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
157+
ac.publishEvent(event);
158+
assertThat(parent.getBean(AuditableListener.class).events.contains(event)).isTrue();
159+
ac.close();
160+
parent.close();
161+
}
162+
81163
@Test
82164
@SuppressWarnings("resource")
83165
void testProgrammaticEventListener() {
@@ -96,6 +178,27 @@ void testProgrammaticEventListener() {
96178
ac.close();
97179
}
98180

181+
@Test
182+
@SuppressWarnings("resource")
183+
void testProgrammaticEventListenerOnParentContext() {
184+
List<Auditable> events = new ArrayList<>();
185+
ApplicationListener<AuditablePayloadEvent<String>> listener = events::add;
186+
ApplicationListener<AuditablePayloadEvent<Integer>> mismatch = (event -> event.getPayload());
187+
188+
ConfigurableApplicationContext parent = new GenericApplicationContext();
189+
parent.addApplicationListener(listener);
190+
parent.addApplicationListener(mismatch);
191+
parent.refresh();
192+
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
193+
ac.refresh();
194+
195+
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
196+
ac.publishEvent(event);
197+
assertThat(events.contains(event)).isTrue();
198+
ac.close();
199+
parent.close();
200+
}
201+
99202
@Test
100203
@SuppressWarnings("resource")
101204
void testProgrammaticPayloadListener() {
@@ -108,10 +211,75 @@ void testProgrammaticPayloadListener() {
108211
ac.addApplicationListener(mismatch);
109212
ac.refresh();
110213

111-
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
112-
ac.publishEvent(event);
113-
assertThat(events.contains(event.getPayload())).isTrue();
214+
String payload = "xyz";
215+
ac.publishEvent(payload);
216+
assertThat(events.contains(payload)).isTrue();
217+
ac.close();
218+
}
219+
220+
@Test
221+
@SuppressWarnings("resource")
222+
void testProgrammaticPayloadListenerOnParentContext() {
223+
List<String> events = new ArrayList<>();
224+
ApplicationListener<PayloadApplicationEvent<String>> listener = ApplicationListener.forPayload(events::add);
225+
ApplicationListener<PayloadApplicationEvent<Integer>> mismatch = ApplicationListener.forPayload(Integer::intValue);
226+
227+
ConfigurableApplicationContext parent = new GenericApplicationContext();
228+
parent.addApplicationListener(listener);
229+
parent.addApplicationListener(mismatch);
230+
parent.refresh();
231+
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
232+
ac.refresh();
233+
234+
String payload = "xyz";
235+
ac.publishEvent(payload);
236+
assertThat(events.contains(payload)).isTrue();
237+
ac.close();
238+
parent.close();
239+
}
240+
241+
@Test
242+
@SuppressWarnings("resource")
243+
void testPlainPayloadListener() {
244+
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(PlainPayloadListener.class);
245+
246+
String payload = "xyz";
247+
ac.publishEvent(payload);
248+
assertThat(ac.getBean(PlainPayloadListener.class).events.contains(payload)).isTrue();
249+
ac.close();
250+
}
251+
252+
@Test
253+
@SuppressWarnings("resource")
254+
void testPlainPayloadListenerOnParentContext() {
255+
ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(PlainPayloadListener.class);
256+
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
257+
ac.refresh();
258+
259+
String payload = "xyz";
260+
ac.publishEvent(payload);
261+
assertThat(parent.getBean(PlainPayloadListener.class).events.contains(payload)).isTrue();
114262
ac.close();
263+
parent.close();
264+
}
265+
266+
267+
static class NumberHolder<T extends Number> {
268+
269+
public NumberHolder(T number) {
270+
}
271+
}
272+
273+
274+
@Component
275+
public static class NumberHolderListener {
276+
277+
public final List<NumberHolder<Integer>> events = new ArrayList<>();
278+
279+
@EventListener
280+
public void onEvent(NumberHolder<Integer> event) {
281+
events.add(event);
282+
}
115283
}
116284

117285

@@ -139,11 +307,16 @@ public void onEvent(Auditable event) {
139307
}
140308
}
141309

142-
static class NumberHolder<T extends Number> {
143310

144-
public NumberHolder(T number) {
145-
}
311+
@Component
312+
public static class PlainPayloadListener {
313+
314+
public final List<String> events = new ArrayList<>();
146315

316+
@EventListener
317+
public void onEvent(String event) {
318+
events.add(event);
319+
}
147320
}
148321

149322
}

0 commit comments

Comments
 (0)