Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix publishing of PayloadApplicationEvents in parent context #30420

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -390,36 +390,54 @@ public void publishEvent(Object event) {
* @param eventType the resolved event type, if known
* @since 4.2
*/
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
protected void publishEvent(Object event, @Nullable final ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");

// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent applEvent) {
ResolvableType multicastType;
if (event instanceof PayloadApplicationEvent<?> payloadEvent) {
Assert.isTrue(eventType == null || eventType.equals(payloadEvent.getResolvableType()),
"Cannot publish a PayloadApplicationEvent with a non-matching eventType, got " + eventType);
applicationEvent = payloadEvent;
multicastType = payloadEvent.getResolvableType();
}
else if (event instanceof ApplicationEvent applEvent) {
applicationEvent = applEvent;
multicastType = eventType;
}
else {
applicationEvent = new PayloadApplicationEvent<>(this, event, eventType);
if (eventType == null) {
eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
}
//always derive the multicastType from the PayloadApplicationEvent, since it is guaranteed
//to have an inner payloadType after construction (covering eventType == null case)
multicastType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
}

// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
this.earlyApplicationEvents.add(applicationEvent); //we loose the eventType for applicationEvents at this point
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
getApplicationEventMulticaster().multicastEvent(applicationEvent, multicastType);
}

// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext abstractApplicationContext) {
abstractApplicationContext.publishEvent(event, eventType);
/*
If we just created a PayloadApplicationEvent out of an arbitrary object,
it wraps the event type (which could be important if the type has been provided
by the user and contains generics).
In that case, we can simply propagate only the payloadApplicationEvent. We can also do so
if the parent doesn't have a publishEvent method which accepts a ResolvableType.
The corner case is if the original event was an ApplicationEvent (not a PayloadApplicationEvent)
and an eventType was provided, in which case we must make an effort to propagate it as well.
*/
if (!(this.parent instanceof AbstractApplicationContext abstractApplicationContext)
|| event != applicationEvent) {
this.parent.publishEvent(applicationEvent);
}
else {
this.parent.publishEvent(event);
abstractApplicationContext.publishEvent(event, eventType);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.context.support;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import org.springframework.context.ApplicationEvent;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.event.AbstractApplicationEventMulticaster;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.springframework.context.support.AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME;

/**
* Tests for event publishing / listening support in {@code AbstractApplicationContext},
* including advanced scenarios: with {@code PayloadApplicationEvent} having a simple payload
* or a generified payload, with/without early firing of event (before a multicaster is ready),
* with/without a parent...
*/
class AbstractApplicationContextEventTests {

@Test
void cannotPublishPayloadEventWithInnerEventType() {
AbstractApplicationContext ctx = new StaticApplicationContext();
PayloadApplicationEvent<String> payloadApplicationEvent = new PayloadApplicationEvent<>(this, "message");

assertThatIllegalArgumentException().isThrownBy(() -> ctx.publishEvent(payloadApplicationEvent, ResolvableType.forClass(String.class)))
.withMessage("Cannot publish a PayloadApplicationEvent with a non-matching eventType, got java.lang.String");
}

@Test
void cannotPublishPayloadEventWithInconsistentEventType() {
AbstractApplicationContext ctx = new StaticApplicationContext();
PayloadApplicationEvent<String> payloadApplicationEvent = new PayloadApplicationEvent<>(this, "message");
ResolvableType inconsistentType = ResolvableType.forClassWithGenerics(PayloadApplicationEvent.class, Integer.class);

assertThatIllegalArgumentException().isThrownBy(() -> ctx.publishEvent(payloadApplicationEvent, inconsistentType))
.withMessage("Cannot publish a PayloadApplicationEvent with a non-matching eventType, got org.springframework.context.PayloadApplicationEvent<java.lang.Integer>");
}

@Nested
class NoParent {

@ParameterizedTest
@ValueSource(booleans = {true, false})
void simpleType(boolean earlyPublishing) {
String payload = "String event";
final String expectedResolvableType = "org.springframework.context.PayloadApplicationEvent<java.lang.String>";

assertPublishing(payload, expectedResolvableType, earlyPublishing, null);
}

@ParameterizedTest
@ValueSource(booleans = {true, false})
void generifiedTypeResolvedFromInstance(boolean earlyPublishing) {
List<String> payload = List.of("String", "List", "event");
final String expectedResolvableType = "org.springframework.context.PayloadApplicationEvent<java.util.ImmutableCollections$ListN<?>>";

assertPublishing(payload, expectedResolvableType, earlyPublishing, null);
}

@ParameterizedTest
@ValueSource(booleans = {true, false})
void generifiedTypeWithExplicitResolvableType(boolean earlyPublishing) {
List<String> payload = List.of("String", "List", "event");
final String expectedResolvableType = "org.springframework.context.PayloadApplicationEvent<java.util.List<java.lang.String>>";

assertPublishing(payload, expectedResolvableType, earlyPublishing, ResolvableType.forClassWithGenerics(List.class, String.class));
}

private <T> void assertPublishing(T payload, String expectedResolvableType, boolean earlyPublishing, @Nullable ResolvableType explicitEventType) {
AbstractApplicationContext ctx = new StaticApplicationContext();
TestMulticaster testMulticaster = new TestMulticaster();
ctx.getBeanFactory().registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, testMulticaster);

if (earlyPublishing) {
ctx.prepareRefresh();
assertThatIllegalStateException().as("before refresh").isThrownBy(ctx::getApplicationEventMulticaster);
}
else {
ctx.refresh();
assertThat(ctx.getApplicationEventMulticaster()).as("multicaster immediately ready")
.isSameAs(testMulticaster);
}

if (explicitEventType != null) {
ctx.publishEvent(payload, explicitEventType);
}
else {
ctx.publishEvent(payload);
}

if (earlyPublishing) {
//simulate the parts of refresh() that set up the multicaster and send early events
ctx.initApplicationEventMulticaster();
ctx.registerListeners();
assertThat(ctx.getApplicationEventMulticaster()).as("after refresh")
.isSameAs(testMulticaster);
}

assertThat(testMulticaster.events).singleElement()
.isInstanceOfSatisfying(PayloadApplicationEvent.class, pae -> {
assertThat(pae.getPayload()).isSameAs(payload);
assertThat(pae.getResolvableType()).hasToString(expectedResolvableType);
});
assertThat(testMulticaster.listenerLookupTypes).singleElement()
.hasToString(expectedResolvableType);
}
}

@Nested
class WithParent {

@ParameterizedTest
@ValueSource(booleans = {true, false})
void simpleType(boolean earlyPublishing) {
String payload = "String event";
final String expectedResolvableType = "org.springframework.context.PayloadApplicationEvent<java.lang.String>";

assertPublishing(payload, expectedResolvableType, earlyPublishing, null);
}

@ParameterizedTest
@ValueSource(booleans = {true, false})
void generifiedTypeResolvedFromInstance(boolean earlyPublishing) {
List<String> payload = List.of("String", "List", "event");
String expectedResolvableType = "org.springframework.context.PayloadApplicationEvent<java.util.ImmutableCollections$ListN<?>>";

assertPublishing(payload, expectedResolvableType, earlyPublishing, null);
}

@ParameterizedTest
@ValueSource(booleans = {true, false})
void generifiedTypeWithExplicitResolvableType(boolean earlyPublishing) {
List<String> payload = List.of("String", "List", "event");
final String expectedResolvableType = "org.springframework.context.PayloadApplicationEvent<java.util.List<java.lang.String>>";

assertPublishing(payload, expectedResolvableType, earlyPublishing, ResolvableType.forClassWithGenerics(List.class, String.class));
}

private <T> void assertPublishing(T payload, String expectedResolvableType, boolean earlyPublishing, @Nullable ResolvableType explicitEventType) {
AbstractApplicationContext parentCtx = new StaticApplicationContext();
TestMulticaster parentMulticaster = new TestMulticaster();
parentCtx.getBeanFactory().registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, parentMulticaster);
AbstractApplicationContext ctx = new StaticApplicationContext(parentCtx);
TestMulticaster childMulticaster = new TestMulticaster();
ctx.getBeanFactory().registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, childMulticaster);

assertThat(parentCtx.getBeanFactory()).as("parent and child beanFactories")
.isNotSameAs(ctx.getBeanFactory());
assertThat(parentCtx.getBeanFactory().getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME))
.as("parent and child multicasters")
.isNotSameAs(ctx.getBeanFactory().getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME));

if (earlyPublishing) {
parentCtx.prepareRefresh();
ctx.prepareRefresh();
assertThatIllegalStateException().as("before refresh").isThrownBy(ctx::getApplicationEventMulticaster);
}
else {
parentCtx.refresh();
ctx.refresh();
assertThat(ctx.getApplicationEventMulticaster()).as("multicaster immediately ready")
.isSameAs(childMulticaster)
.isNotSameAs(parentCtx.getApplicationEventMulticaster());
}

if (explicitEventType != null) {
ctx.publishEvent(payload, explicitEventType);
}
else {
ctx.publishEvent(payload);
}

if (earlyPublishing) {
//simulate the parts of refresh() that set up the multicaster and send early events
parentCtx.initApplicationEventMulticaster();
parentCtx.registerListeners();
ctx.initApplicationEventMulticaster();
ctx.registerListeners();
assertThat(ctx.getApplicationEventMulticaster()).as("after refresh")
.isSameAs(childMulticaster)
.isNotSameAs(parentCtx.getApplicationEventMulticaster());
}

assertThat(parentMulticaster.events).singleElement()
.isInstanceOfSatisfying(PayloadApplicationEvent.class, pae -> {
assertThat(pae.getPayload()).isSameAs(payload);
assertThat(pae.getResolvableType()).hasToString(expectedResolvableType);
});
assertThat(parentMulticaster.listenerLookupTypes).singleElement()
.hasToString(expectedResolvableType);

assertThat(childMulticaster.events).singleElement()
.isInstanceOfSatisfying(PayloadApplicationEvent.class, pae -> {
assertThat(pae.getPayload()).isSameAs(payload);
assertThat(pae.getResolvableType()).hasToString(expectedResolvableType);
});
assertThat(childMulticaster.listenerLookupTypes).singleElement()
.hasToString(expectedResolvableType);
}
}

private static class TestMulticaster extends AbstractApplicationEventMulticaster {

private List<ApplicationEvent> events = new ArrayList<>();
private List<ResolvableType> listenerLookupTypes = new ArrayList<>();

@Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, null);
}

@Override
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
//simulate the behavior of SimpleApplicationEventMulticaster
if (eventType == null) {
eventType = ResolvableType.forInstance(event);
}
if (event instanceof PayloadApplicationEvent<?>) {
this.events.add(event);
this.listenerLookupTypes.add(eventType);
} // ignore other "standard" ApplicationEvents
}
}

}