Allow additional listeners registration in early events

Update `EventPublishingRunListener` so that event listeners may add
additional listeners to the `SpringApplication` during early events.

Prior to this commit, the listeners were collected only once which
meant that if a listener for an `ApplicationStartingEvent` called
`application.addListener(...)`, it would be ignored.

Closes gh-32300
This commit is contained in:
Phillip Webb 2022-09-12 13:56:31 -07:00
parent 6955ed9dcf
commit 88913b11ce
2 changed files with 59 additions and 47 deletions

View File

@ -28,6 +28,7 @@ import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.LivenessState;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ApplicationEventMulticaster;
@ -62,9 +63,7 @@ class EventPublishingRunListener implements SpringApplicationRunListener, Ordere
this.application = application;
this.args = args;
this.initialMulticaster = new SimpleApplicationEventMulticaster();
for (ApplicationListener<?> listener : application.getListeners()) {
this.initialMulticaster.addApplicationListener(listener);
}
refreshApplicationListeners();
}
@Override
@ -74,21 +73,19 @@ class EventPublishingRunListener implements SpringApplicationRunListener, Ordere
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
this.initialMulticaster
.multicastEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
multicastInitialEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment) {
this.initialMulticaster.multicastEvent(
multicastInitialEvent(
new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment));
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
this.initialMulticaster
.multicastEvent(new ApplicationContextInitializedEvent(this.application, this.args, context));
multicastInitialEvent(new ApplicationContextInitializedEvent(this.application, this.args, context));
}
@Override
@ -99,7 +96,7 @@ class EventPublishingRunListener implements SpringApplicationRunListener, Ordere
}
context.addApplicationListener(listener);
}
this.initialMulticaster.multicastEvent(new ApplicationPreparedEvent(this.application, this.args, context));
multicastInitialEvent(new ApplicationPreparedEvent(this.application, this.args, context));
}
@Override
@ -135,6 +132,15 @@ class EventPublishingRunListener implements SpringApplicationRunListener, Ordere
}
}
private void multicastInitialEvent(ApplicationEvent event) {
this.initialMulticaster.multicastEvent(event);
refreshApplicationListeners();
}
private void refreshApplicationListeners() {
this.application.getListeners().forEach(this.initialMulticaster::addApplicationListener);
}
private static class LoggingErrorHandler implements ErrorHandler {
private static final Log logger = LogFactory.getLog(EventPublishingRunListener.class);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 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.
@ -22,7 +22,6 @@ import java.util.Collections;
import java.util.Deque;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.DefaultBootstrapContext;
@ -31,6 +30,8 @@ import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ -40,46 +41,47 @@ import static org.mockito.Mockito.mock;
* Tests for {@link EventPublishingRunListener}
*
* @author Brian Clozel
* @author Phillip Webb
*/
class EventPublishingRunListenerTests {
private DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
private SpringApplication application;
private EventPublishingRunListener runListener;
private TestApplicationListener eventListener;
@BeforeEach
void setup() {
this.eventListener = new TestApplicationListener();
this.application = mock(SpringApplication.class);
given(this.application.getListeners()).willReturn(Collections.singleton(this.eventListener));
this.runListener = new EventPublishingRunListener(this.application, null);
@Test
void shouldPublishLifecycleEvents() {
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
StaticApplicationContext context = new StaticApplicationContext();
TestApplicationListener applicationListener = new TestApplicationListener();
SpringApplication application = mock(SpringApplication.class);
given(application.getListeners()).willReturn(Collections.singleton(applicationListener));
EventPublishingRunListener publishingListener = new EventPublishingRunListener(application, null);
applicationListener.assertReceivedNoEvents();
publishingListener.starting(bootstrapContext);
applicationListener.assertReceivedEvent(ApplicationStartingEvent.class);
publishingListener.environmentPrepared(bootstrapContext, null);
applicationListener.assertReceivedEvent(ApplicationEnvironmentPreparedEvent.class);
publishingListener.contextPrepared(context);
applicationListener.assertReceivedEvent(ApplicationContextInitializedEvent.class);
publishingListener.contextLoaded(context);
applicationListener.assertReceivedEvent(ApplicationPreparedEvent.class);
context.refresh();
publishingListener.started(context, null);
applicationListener.assertReceivedEvent(ApplicationStartedEvent.class, AvailabilityChangeEvent.class);
publishingListener.ready(context, null);
applicationListener.assertReceivedEvent(ApplicationReadyEvent.class, AvailabilityChangeEvent.class);
}
@Test
void shouldPublishLifecycleEvents() {
StaticApplicationContext context = new StaticApplicationContext();
assertThat(this.eventListener.receivedEvents()).isEmpty();
this.runListener.starting(this.bootstrapContext);
checkApplicationEvents(ApplicationStartingEvent.class);
this.runListener.environmentPrepared(this.bootstrapContext, null);
checkApplicationEvents(ApplicationEnvironmentPreparedEvent.class);
this.runListener.contextPrepared(context);
checkApplicationEvents(ApplicationContextInitializedEvent.class);
this.runListener.contextLoaded(context);
checkApplicationEvents(ApplicationPreparedEvent.class);
context.refresh();
this.runListener.started(context, null);
checkApplicationEvents(ApplicationStartedEvent.class, AvailabilityChangeEvent.class);
this.runListener.ready(context, null);
checkApplicationEvents(ApplicationReadyEvent.class, AvailabilityChangeEvent.class);
}
void checkApplicationEvents(Class<?>... eventClasses) {
assertThat(this.eventListener.receivedEvents()).extracting("class").contains((Object[]) eventClasses);
void initialEventListenerCanAddAdditionalListenersToApplication() {
SpringApplication application = new SpringApplication();
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
ConfigurableEnvironment environment = new StandardEnvironment();
TestApplicationListener lateAddedApplicationListener = new TestApplicationListener();
ApplicationListener<ApplicationStartingEvent> listener = (event) -> event.getSpringApplication()
.addListeners(lateAddedApplicationListener);
application.addListeners(listener);
EventPublishingRunListener runListener = new EventPublishingRunListener(application, null);
runListener.starting(bootstrapContext);
runListener.environmentPrepared(bootstrapContext, environment);
lateAddedApplicationListener.assertReceivedEvent(ApplicationEnvironmentPreparedEvent.class);
}
static class TestApplicationListener implements ApplicationListener<ApplicationEvent> {
@ -91,12 +93,16 @@ class EventPublishingRunListenerTests {
this.events.add(event);
}
List<ApplicationEvent> receivedEvents() {
void assertReceivedNoEvents() {
assertThat(this.events).isEmpty();
}
void assertReceivedEvent(Class<?>... eventClasses) {
List<ApplicationEvent> receivedEvents = new ArrayList<>();
while (!this.events.isEmpty()) {
receivedEvents.add(this.events.pollFirst());
}
return receivedEvents;
assertThat(receivedEvents).extracting("class").contains((Object[]) eventClasses);
}
}