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 {