diff --git a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java index d591cf7421f..5f3f0a6c959 100644 --- a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java +++ b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java @@ -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 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 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<<String>}). + * @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() { diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java index c84e201d2f2..daf6cd1e893 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -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 diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 347415b820e..871d166e512 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -385,23 +385,47 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader /** * Publish the given event to all listeners. + *

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); diff --git a/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java b/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java index 2280dddb659..4bb408101b5 100644 --- a/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java @@ -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> 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> 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 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 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 events = new ArrayList<>(); + ApplicationListener> listener = events::add; + ApplicationListener> mismatch = (event -> event.getPayload()); + + ConfigurableApplicationContext parent = new GenericApplicationContext(); + parent.addApplicationListener(listener); + parent.addApplicationListener(mismatch); + parent.refresh(); + ConfigurableApplicationContext ac = new GenericApplicationContext(parent); + ac.refresh(); + + AuditablePayloadEvent 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 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 events = new ArrayList<>(); + ApplicationListener> listener = ApplicationListener.forPayload(events::add); + ApplicationListener> 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 { + + public NumberHolder(T number) { + } + } + + + @Component + public static class NumberHolderListener { + + public final List> events = new ArrayList<>(); + + @EventListener + public void onEvent(NumberHolder event) { + events.add(event); + } + } + public interface Auditable { } @@ -139,11 +307,16 @@ class PayloadApplicationEventTests { } } - static class NumberHolder { - public NumberHolder(T number) { + @Component + public static class PlainPayloadListener { + + public final List events = new ArrayList<>(); + + @EventListener + public void onEvent(String event) { + events.add(event); } - } }