Publish an event after refresh but before runners are called

This commit partially reverts the changes made in ec470fbe. While
the started message continues to be logged before any application and
command line runners are called, the publishing of
ApplicationReadyEvent now happens after the runners have been called.
Additionally, a new event, named ApplicationStartedEvent, has been
introduced. This new event is published after the context has been
refreshed but before any application and command line runners are
called.

Closes gh-11484

The reworking of the events described above also means that either
an ApplicationReadyEvent or an ApplicationFailedEvent will be
published and the latter should never be published once the former
has been published.

Closes gh-11485
pull/11598/merge
Andy Wilkinson 7 years ago
parent 4a9123d6e3
commit a051e30fe0

@ -235,8 +235,10 @@ except for the registration of listeners and initializers.
the context is known but before the context is created.
. An `ApplicationPreparedEvent` is sent just before the refresh is started but after bean
definitions have been loaded.
. An `ApplicationReadyEvent` is sent after the refresh and any related callbacks have
been processed, to indicate that the application is ready to service requests.
. An `ApplicationStartedEvent` is sent after the context has been refreshed but before any
application and command-line runners have been called.
. An `ApplicationReadyEvent` is sent after any application and command-line runners have
been called. It indicates that the application is ready to service requests.
. An `ApplicationFailedEvent` is sent if there is an exception on startup.
TIP: You often need not use application events, but it can be handy to know that they

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -326,19 +326,20 @@ public class SpringApplication {
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, exceptionReporters, ex);
throw new IllegalStateException(ex);
}
listeners.running(context);
return context;
}
private ConfigurableEnvironment prepareEnvironment(
@ -800,7 +801,7 @@ public class SpringApplication {
try {
try {
handleExitCode(context, exception);
listeners.finished(context, exception);
listeners.failed(context, exception);
}
finally {
reportFailure(exceptionReporters, exception);
@ -829,7 +830,7 @@ public class SpringApplication {
// Continue with normal handling of the original failure
}
if (logger.isErrorEnabled()) {
logger.error("Application startup failed", failure);
logger.error("Application run failed", failure);
registerLoggedException(failure);
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -30,6 +30,7 @@ import org.springframework.core.io.support.SpringFactoriesLoader;
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
*/
public interface SpringApplicationRunListener {
@ -61,11 +62,29 @@ public interface SpringApplicationRunListener {
void contextLoaded(ConfigurableApplicationContext context);
/**
* Called immediately before the run method finishes.
* @param context the application context or null if a failure occurred before the
* context was created
* @param exception any run exception or null if run completed successfully.
* The context has been refreshed and the application has started but
* {@link CommandLineRunner CommandLineRunners} and {@link ApplicationRunner
* ApplicationRunners} have not been called.
* @param context the application context.
*/
void finished(ConfigurableApplicationContext context, Throwable exception);
void started(ConfigurableApplicationContext context);
/**
* Called immediately before the run method finishes, when the application context has
* been refreshed and all {@link CommandLineRunner CommandLineRunners} and
* {@link ApplicationRunner ApplicationRunners} have been called.
* @param context the application context.
* @since 2.0.0
*/
void running(ConfigurableApplicationContext context);
/**
* Called when a failure occurs when running the application.
* @param context the application context or {@code null} if a failure occurred before
* the context was created
* @param exception the failure
* @since 2.0.0
*/
void failed(ConfigurableApplicationContext context, Throwable exception);
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -67,16 +67,28 @@ class SpringApplicationRunListeners {
}
}
public void finished(ConfigurableApplicationContext context, Throwable exception) {
public void started(ConfigurableApplicationContext context) {
for (SpringApplicationRunListener listener : this.listeners) {
callFinishedListener(listener, context, exception);
listener.started(context);
}
}
private void callFinishedListener(SpringApplicationRunListener listener,
public void running(ConfigurableApplicationContext context) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.running(context);
}
}
public void failed(ConfigurableApplicationContext context, Throwable exception) {
for (SpringApplicationRunListener listener : this.listeners) {
callFailedListener(listener, context, exception);
}
}
private void callFailedListener(SpringApplicationRunListener listener,
ConfigurableApplicationContext context, Throwable exception) {
try {
listener.finished(context, exception);
listener.failed(context, exception);
}
catch (Throwable ex) {
if (exception == null) {

@ -0,0 +1,57 @@
/*
* Copyright 2012-2018 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
*
* http://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.boot.context.event;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Event published once the application context has been refreshed but before any
* {@link ApplicationRunner application} and {@link CommandLineRunner command line}
* runners have been called.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@SuppressWarnings("serial")
public class ApplicationStartedEvent extends SpringApplicationEvent {
private final ConfigurableApplicationContext context;
/**
* Create a new {@link ApplicationStartedEvent} instance.
* @param application the current application
* @param args the arguments the application is running with
* @param context the context that was being created
*/
public ApplicationStartedEvent(SpringApplication application, String[] args,
ConfigurableApplicationContext context) {
super(application, args);
this.context = context;
}
/**
* Return the application context.
* @return the context
*/
public ConfigurableApplicationContext getApplicationContext() {
return this.context;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -39,6 +39,7 @@ import org.springframework.util.ErrorHandler;
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author Andy Wilkinson
*/
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
@ -92,8 +93,21 @@ public class EventPublishingRunListener implements SpringApplicationRunListener,
}
@Override
public void finished(ConfigurableApplicationContext context, Throwable exception) {
SpringApplicationEvent event = getFinishedEvent(context, exception);
public void started(ConfigurableApplicationContext context) {
context.publishEvent(
new ApplicationStartedEvent(this.application, this.args, context));
}
@Override
public void running(ConfigurableApplicationContext context) {
context.publishEvent(
new ApplicationReadyEvent(this.application, this.args, context));
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
ApplicationFailedEvent event = new ApplicationFailedEvent(this.application,
this.args, context, exception);
if (context != null && context.isActive()) {
// Listeners have been registered to the application context so we should
// use it at this point if we can
@ -108,22 +122,11 @@ public class EventPublishingRunListener implements SpringApplicationRunListener,
this.initialMulticaster.addApplicationListener(listener);
}
}
if (event instanceof ApplicationFailedEvent) {
this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
}
this.initialMulticaster.multicastEvent(event);
}
}
private SpringApplicationEvent getFinishedEvent(
ConfigurableApplicationContext context, Throwable exception) {
if (exception != null) {
return new ApplicationFailedEvent(this.application, this.args, context,
exception);
}
return new ApplicationReadyEvent(this.application, this.args, context);
}
private static class LoggingErrorHandler implements ErrorHandler {
private static Log logger = LogFactory.getLog(EventPublishingRunListener.class);

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,13 +17,11 @@
package org.springframework.boot;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@ -46,6 +44,7 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.BeansException;
import org.springframework.beans.CachedIntrospectionResults;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
@ -53,7 +52,9 @@ import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEven
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.boot.testsupport.rule.OutputCapture;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
@ -97,12 +98,15 @@ import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.StandardServletEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@ -365,27 +369,22 @@ public class SpringApplicationTests {
}
@Test
public void eventsOrder() {
@SuppressWarnings("unchecked")
public void eventsArePublishedInExpectedOrder() {
SpringApplication application = new SpringApplication(ExampleConfig.class);
application.setWebApplicationType(WebApplicationType.NONE);
final List<ApplicationEvent> events = new ArrayList<>();
class ApplicationRunningEventListener
implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent event) {
events.add((event));
}
}
application.addListeners(new ApplicationRunningEventListener());
ApplicationListener<ApplicationEvent> listener = mock(ApplicationListener.class);
application.addListeners(listener);
this.context = application.run();
assertThat(events).hasSize(5);
assertThat(events.get(0)).isInstanceOf(ApplicationStartingEvent.class);
assertThat(events.get(1)).isInstanceOf(ApplicationEnvironmentPreparedEvent.class);
assertThat(events.get(2)).isInstanceOf(ApplicationPreparedEvent.class);
assertThat(events.get(3)).isInstanceOf(ContextRefreshedEvent.class);
assertThat(events.get(4)).isInstanceOf(ApplicationReadyEvent.class);
InOrder inOrder = Mockito.inOrder(listener);
inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartingEvent.class));
inOrder.verify(listener)
.onApplicationEvent(isA(ApplicationEnvironmentPreparedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationPreparedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ContextRefreshedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationReadyEvent.class));
inOrder.verifyNoMoreInteractions();
}
@Test
@ -589,29 +588,124 @@ public class SpringApplicationTests {
@Test
@SuppressWarnings("unchecked")
public void runnersAreCalledAfterApplicationReadyEventIsPublished() throws Exception {
SpringApplication application = new SpringApplication(
MockRunnerConfiguration.class);
public void runnersAreCalledAfterStartedIsLoggedAndBeforeApplicationReadyEventIsPublished()
throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class);
ApplicationRunner applicationRunner = mock(ApplicationRunner.class);
CommandLineRunner commandLineRunner = mock(CommandLineRunner.class);
application.addInitializers((context) -> {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("commandLineRunner", new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
assertThat(SpringApplicationTests.this.output.toString())
.contains("Started");
commandLineRunner.run(args);
}
});
beanFactory.registerSingleton("applicationRunner", new ApplicationRunner() {
@Override
public void run(ApplicationArguments args) throws Exception {
assertThat(SpringApplicationTests.this.output.toString())
.contains("Started");
applicationRunner.run(args);
}
});
});
application.setWebApplicationType(WebApplicationType.NONE);
ApplicationListener<ApplicationReadyEvent> eventListener = mock(
ApplicationListener.class);
application.addListeners(eventListener);
this.context = application.run();
ApplicationRunner applicationRunner = this.context
.getBean(ApplicationRunner.class);
CommandLineRunner commandLineRunner = this.context
.getBean(CommandLineRunner.class);
InOrder applicationRunnerOrder = Mockito.inOrder(eventListener,
applicationRunner);
applicationRunnerOrder.verify(eventListener)
.onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class));
applicationRunnerOrder.verify(applicationRunner)
.run(ArgumentMatchers.any(ApplicationArguments.class));
applicationRunnerOrder.verify(eventListener)
.onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class));
InOrder commandLineRunnerOrder = Mockito.inOrder(eventListener,
commandLineRunner);
commandLineRunnerOrder.verify(commandLineRunner).run();
commandLineRunnerOrder.verify(eventListener)
.onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class));
commandLineRunnerOrder.verify(commandLineRunner).run();
}
@Test
public void applicationRunnerFailureCausesApplicationFailedEventToBePublished()
throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class);
application.setWebApplicationType(WebApplicationType.NONE);
@SuppressWarnings("unchecked")
ApplicationListener<SpringApplicationEvent> listener = mock(
ApplicationListener.class);
application.addListeners(listener);
ApplicationRunner runner = mock(ApplicationRunner.class);
Exception failure = new Exception();
willThrow(failure).given(runner).run(isA(ApplicationArguments.class));
application.addInitializers((context) -> context.getBeanFactory()
.registerSingleton("runner", runner));
this.thrown.expectCause(equalTo(failure));
try {
application.run();
}
finally {
verify(listener).onApplicationEvent(isA(ApplicationStartedEvent.class));
verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class));
verify(listener, times(0))
.onApplicationEvent(isA(ApplicationReadyEvent.class));
}
}
@Test
public void commandLineRunnerFailureCausesApplicationFailedEventToBePublished()
throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class);
application.setWebApplicationType(WebApplicationType.NONE);
@SuppressWarnings("unchecked")
ApplicationListener<SpringApplicationEvent> listener = mock(
ApplicationListener.class);
application.addListeners(listener);
CommandLineRunner runner = mock(CommandLineRunner.class);
Exception failure = new Exception();
willThrow(failure).given(runner).run();
application.addInitializers((context) -> context.getBeanFactory()
.registerSingleton("runner", runner));
this.thrown.expectCause(equalTo(failure));
try {
application.run();
}
finally {
verify(listener).onApplicationEvent(isA(ApplicationStartedEvent.class));
verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class));
verify(listener, times(0))
.onApplicationEvent(isA(ApplicationReadyEvent.class));
}
}
@Test
public void failureInReadyEventListenerDoesNotCausePublicationOfFailedEvent() {
SpringApplication application = new SpringApplication(ExampleConfig.class);
application.setWebApplicationType(WebApplicationType.NONE);
@SuppressWarnings("unchecked")
ApplicationListener<SpringApplicationEvent> listener = mock(
ApplicationListener.class);
application.addListeners(listener);
RuntimeException failure = new RuntimeException();
willThrow(failure).given(listener)
.onApplicationEvent(isA(ApplicationReadyEvent.class));
this.thrown.expect(equalTo(failure));
try {
application.run();
}
finally {
verify(listener).onApplicationEvent(isA(ApplicationReadyEvent.class));
verify(listener, times(0))
.onApplicationEvent(isA(ApplicationFailedEvent.class));
}
}
@Test
@ -811,7 +905,7 @@ public class SpringApplicationTests {
ApplicationListener<ApplicationEvent> listener = this.context
.getBean("testApplicationListener", ApplicationListener.class);
verifyListenerEvents(listener, ContextRefreshedEvent.class,
ApplicationReadyEvent.class);
ApplicationStartedEvent.class, ApplicationReadyEvent.class);
}
@SuppressWarnings("unchecked")
@ -1249,21 +1343,6 @@ public class SpringApplicationTests {
}
@Configuration
static class MockRunnerConfiguration {
@Bean
public CommandLineRunner commandLineRunner() {
return mock(CommandLineRunner.class);
}
@Bean
public ApplicationRunner applicationRunner() {
return mock(ApplicationRunner.class);
}
}
static class ExitStatusException extends RuntimeException
implements ExitCodeGenerator {

Loading…
Cancel
Save