From a051e30fe0ac00263ace6d97282a4268d35b83a2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2018 11:10:32 +0000 Subject: [PATCH] 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 --- .../main/asciidoc/spring-boot-features.adoc | 6 +- .../boot/SpringApplication.java | 11 +- .../boot/SpringApplicationRunListener.java | 31 +++- .../boot/SpringApplicationRunListeners.java | 22 ++- .../event/ApplicationStartedEvent.java | 57 ++++++ .../event/EventPublishingRunListener.java | 33 ++-- .../boot/SpringApplicationTests.java | 173 +++++++++++++----- 7 files changed, 253 insertions(+), 80 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 3e2fe15b93..20e29082b5 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -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 diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 5011f1b082..7766e454a3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -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); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java index a8f79fbf47..297b29879f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java @@ -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); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java index c744059479..233f1d0983 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java @@ -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) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java new file mode 100644 index 0000000000..7bb7fcc0f2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java index 1cd7c390e2..6468222163 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java @@ -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.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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index dba6c4d9e1..7b7d20a657 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -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 events = new ArrayList<>(); - class ApplicationRunningEventListener - implements ApplicationListener { - - @Override - public void onApplicationEvent(ApplicationEvent event) { - events.add((event)); - } - - } - application.addListeners(new ApplicationRunningEventListener()); + ApplicationListener 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 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 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 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 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 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 {