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
This commit is contained in:
Juergen Hoeller 2023-05-10 17:17:48 +02:00
parent c733ae0f22
commit e228f4ba64
4 changed files with 229 additions and 33 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -41,19 +41,6 @@ public class PayloadApplicationEvent<T> extends ApplicationEvent implements Reso
private final ResolvableType payloadType;
/**
* Create a new PayloadApplicationEvent.
* @param source the object on which the event initially occurred (never {@code null})
* @param payload the payload object (never {@code null})
* @param payloadType the type object of payload object (can be {@code null})
* @since 6.0
*/
public PayloadApplicationEvent(Object source, T payload, @Nullable ResolvableType payloadType) {
super(source);
Assert.notNull(payload, "Payload must not be null");
this.payload = payload;
this.payloadType = (payloadType != null) ? payloadType : ResolvableType.forInstance(payload);
}
/**
* Create a new PayloadApplicationEvent, using the instance to infer its type.
@ -64,6 +51,22 @@ public class PayloadApplicationEvent<T> extends ApplicationEvent implements Reso
this(source, payload, null);
}
/**
* Create a new PayloadApplicationEvent based on the provided payload type.
* @param source the object on which the event initially occurred (never {@code null})
* @param payload the payload object (never {@code null})
* @param payloadType the type object of payload object (can be {@code null}).
* Note that this is meant to indicate the payload type (e.g. {@code String}),
* not the full event type (such as {@code PayloadApplicationEvent<&lt;String&gt;}).
* @since 6.0
*/
public PayloadApplicationEvent(Object source, T payload, @Nullable ResolvableType payloadType) {
super(source);
Assert.notNull(payload, "Payload must not be null");
this.payload = payload;
this.payloadType = (payloadType != null ? payloadType : ResolvableType.forInstance(payload));
}
@Override
public ResolvableType getResolvableType() {

View File

@ -128,12 +128,12 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM
@Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, resolveDefaultEventType(event));
multicastEvent(event, null);
}
@Override
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
@ -145,10 +145,6 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM
}
}
private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
return ResolvableType.forInstance(event);
}
/**
* Invoke the given listener with the given event.
* @param listener the ApplicationListener to invoke

View File

@ -385,23 +385,47 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
/**
* Publish the given event to all listeners.
* <p>This is the internal delegate that all other {@code publishEvent}
* methods refer to. It is not meant to be called directly but rather serves
* as a propagation mechanism between application contexts in a hierarchy,
* potentially overridden in subclasses for a custom propagation arrangement.
* @param event the event to publish (may be an {@link ApplicationEvent}
* or a payload object to be turned into a {@link PayloadApplicationEvent})
* @param eventType the resolved event type, if known
* @param typeHint the resolved event type, if known.
* The implementation of this method also tolerates a payload type hint for
* a payload object to be turned into a {@link PayloadApplicationEvent}.
* However, the recommended way is to construct an actual event object via
* {@link PayloadApplicationEvent#PayloadApplicationEvent(Object, Object, ResolvableType)}
* instead for such scenarios.
* @since 4.2
* @see ApplicationEventMulticaster#multicastEvent(ApplicationEvent, ResolvableType)
*/
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
protected void publishEvent(Object event, @Nullable ResolvableType typeHint) {
Assert.notNull(event, "Event must not be null");
ResolvableType eventType = null;
// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent applEvent) {
applicationEvent = applEvent;
eventType = typeHint;
}
else {
applicationEvent = new PayloadApplicationEvent<>(this, event, eventType);
if (eventType == null) {
eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
ResolvableType payloadType = null;
if (typeHint != null && ApplicationEvent.class.isAssignableFrom(typeHint.toClass())) {
eventType = typeHint;
}
else {
payloadType = typeHint;
}
applicationEvent = new PayloadApplicationEvent<>(this, event, payloadType);
}
// Determine event type only once (for multicast and parent publish)
if (eventType == null) {
eventType = ResolvableType.forInstance(applicationEvent);
if (typeHint == null) {
typeHint = eventType;
}
}
@ -416,7 +440,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext abstractApplicationContext) {
abstractApplicationContext.publishEvent(event, eventType);
abstractApplicationContext.publishEvent(event, typeHint);
}
else {
this.parent.publishEvent(event);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.PayloadApplicationEvent;
@ -68,16 +69,97 @@ class PayloadApplicationEventTests {
});
}
@Test
@SuppressWarnings("resource")
void testEventClassWithPayloadType() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(NumberHolderListener.class);
PayloadApplicationEvent<NumberHolder<Integer>> event = new PayloadApplicationEvent<>(this,
new NumberHolder<>(42), ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class));
ac.publishEvent(event);
assertThat(ac.getBean(NumberHolderListener.class).events.contains(event.getPayload())).isTrue();
ac.close();
}
@Test
@SuppressWarnings("resource")
void testEventClassWithPayloadTypeOnParentContext() {
ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(NumberHolderListener.class);
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
ac.refresh();
PayloadApplicationEvent<NumberHolder<Integer>> event = new PayloadApplicationEvent<>(this,
new NumberHolder<>(42), ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class));
ac.publishEvent(event);
assertThat(parent.getBean(NumberHolderListener.class).events.contains(event.getPayload())).isTrue();
ac.close();
parent.close();
}
@Test
@SuppressWarnings("resource")
void testPayloadObjectWithPayloadType() {
final Object payload = new NumberHolder<>(42);
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(NumberHolderListener.class) {
@Override
protected void finishRefresh() throws BeansException {
super.finishRefresh();
// This is not recommended: use publishEvent(new PayloadApplicationEvent(...)) instead
publishEvent(payload, ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class));
}
};
assertThat(ac.getBean(NumberHolderListener.class).events.contains(payload)).isTrue();
ac.close();
}
@Test
@SuppressWarnings("resource")
void testPayloadObjectWithPayloadTypeOnParentContext() {
final Object payload = new NumberHolder<>(42);
ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(NumberHolderListener.class);
ConfigurableApplicationContext ac = new GenericApplicationContext(parent) {
@Override
protected void finishRefresh() throws BeansException {
super.finishRefresh();
// This is not recommended: use publishEvent(new PayloadApplicationEvent(...)) instead
publishEvent(payload, ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class));
}
};
ac.refresh();
assertThat(parent.getBean(NumberHolderListener.class).events.contains(payload)).isTrue();
ac.close();
parent.close();
}
@Test
@SuppressWarnings("resource")
void testEventClassWithInterface() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(AuditableListener.class);
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
ac.publishEvent(event);
assertThat(ac.getBean(AuditableListener.class).events.contains(event)).isTrue();
ac.close();
}
@Test
@SuppressWarnings("resource")
void testEventClassWithInterfaceOnParentContext() {
ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(AuditableListener.class);
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
ac.refresh();
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
ac.publishEvent(event);
assertThat(parent.getBean(AuditableListener.class).events.contains(event)).isTrue();
ac.close();
parent.close();
}
@Test
@SuppressWarnings("resource")
void testProgrammaticEventListener() {
@ -96,6 +178,27 @@ class PayloadApplicationEventTests {
ac.close();
}
@Test
@SuppressWarnings("resource")
void testProgrammaticEventListenerOnParentContext() {
List<Auditable> events = new ArrayList<>();
ApplicationListener<AuditablePayloadEvent<String>> listener = events::add;
ApplicationListener<AuditablePayloadEvent<Integer>> mismatch = (event -> event.getPayload());
ConfigurableApplicationContext parent = new GenericApplicationContext();
parent.addApplicationListener(listener);
parent.addApplicationListener(mismatch);
parent.refresh();
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
ac.refresh();
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
ac.publishEvent(event);
assertThat(events.contains(event)).isTrue();
ac.close();
parent.close();
}
@Test
@SuppressWarnings("resource")
void testProgrammaticPayloadListener() {
@ -108,12 +211,77 @@ class PayloadApplicationEventTests {
ac.addApplicationListener(mismatch);
ac.refresh();
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
ac.publishEvent(event);
assertThat(events.contains(event.getPayload())).isTrue();
String payload = "xyz";
ac.publishEvent(payload);
assertThat(events.contains(payload)).isTrue();
ac.close();
}
@Test
@SuppressWarnings("resource")
void testProgrammaticPayloadListenerOnParentContext() {
List<String> events = new ArrayList<>();
ApplicationListener<PayloadApplicationEvent<String>> listener = ApplicationListener.forPayload(events::add);
ApplicationListener<PayloadApplicationEvent<Integer>> mismatch = ApplicationListener.forPayload(Integer::intValue);
ConfigurableApplicationContext parent = new GenericApplicationContext();
parent.addApplicationListener(listener);
parent.addApplicationListener(mismatch);
parent.refresh();
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
ac.refresh();
String payload = "xyz";
ac.publishEvent(payload);
assertThat(events.contains(payload)).isTrue();
ac.close();
parent.close();
}
@Test
@SuppressWarnings("resource")
void testPlainPayloadListener() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(PlainPayloadListener.class);
String payload = "xyz";
ac.publishEvent(payload);
assertThat(ac.getBean(PlainPayloadListener.class).events.contains(payload)).isTrue();
ac.close();
}
@Test
@SuppressWarnings("resource")
void testPlainPayloadListenerOnParentContext() {
ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(PlainPayloadListener.class);
ConfigurableApplicationContext ac = new GenericApplicationContext(parent);
ac.refresh();
String payload = "xyz";
ac.publishEvent(payload);
assertThat(parent.getBean(PlainPayloadListener.class).events.contains(payload)).isTrue();
ac.close();
parent.close();
}
static class NumberHolder<T extends Number> {
public NumberHolder(T number) {
}
}
@Component
public static class NumberHolderListener {
public final List<NumberHolder<Integer>> events = new ArrayList<>();
@EventListener
public void onEvent(NumberHolder<Integer> event) {
events.add(event);
}
}
public interface Auditable {
}
@ -139,11 +307,16 @@ class PayloadApplicationEventTests {
}
}
static class NumberHolder<T extends Number> {
public NumberHolder(T number) {
@Component
public static class PlainPayloadListener {
public final List<String> events = new ArrayList<>();
@EventListener
public void onEvent(String event) {
events.add(event);
}
}
}