Instrument SpringBootApplication for ApplicationStartup
This commit allows the configuration of a custom `ApplicationStartup` implementation on the `SpringApplication` and `SpringApplicationBuilder` for collecting `StartupStep` metrics. This also instruments Spring Boot run listeners and server-specific application context implementations for collecting Spring Boot application events during startup. Closes gh-22600
This commit is contained in:
@@ -75,6 +75,7 @@ import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.core.metrics.ApplicationStartup;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -246,6 +247,8 @@ public class SpringApplication {
|
||||
|
||||
private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT;
|
||||
|
||||
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
|
||||
|
||||
/**
|
||||
* Create a new {@link SpringApplication} instance. The application context will load
|
||||
* beans from the specified primary sources (see {@link SpringApplication class-level}
|
||||
@@ -316,6 +319,7 @@ public class SpringApplication {
|
||||
configureIgnoreBeanInfo(environment);
|
||||
Banner printedBanner = printBanner(environment);
|
||||
context = createApplicationContext();
|
||||
context.setApplicationStartup(this.applicationStartup);
|
||||
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
|
||||
new Class<?>[] { ConfigurableApplicationContext.class }, context);
|
||||
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
|
||||
@@ -422,7 +426,8 @@ public class SpringApplication {
|
||||
private SpringApplicationRunListeners getRunListeners(String[] args) {
|
||||
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
|
||||
return new SpringApplicationRunListeners(logger,
|
||||
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
|
||||
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
|
||||
this.applicationStartup);
|
||||
}
|
||||
|
||||
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
|
||||
@@ -1237,6 +1242,22 @@ public class SpringApplication {
|
||||
return asUnmodifiableOrderedSet(this.listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ApplicationStartup} to use for collecting startup metrics.
|
||||
* @param applicationStartup the application startup to use
|
||||
*/
|
||||
public void setApplicationStartup(ApplicationStartup applicationStartup) {
|
||||
this.applicationStartup = (applicationStartup != null) ? applicationStartup : ApplicationStartup.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ApplicationStartup} used for collecting startup metrics.
|
||||
* @return the application startup
|
||||
*/
|
||||
public ApplicationStartup getApplicationStartup() {
|
||||
return this.applicationStartup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper that can be used to run a {@link SpringApplication} from the
|
||||
* specified source using default settings.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2020 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.
|
||||
@@ -24,6 +24,8 @@ import org.apache.commons.logging.Log;
|
||||
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.metrics.ApplicationStartup;
|
||||
import org.springframework.core.metrics.StartupStep;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
@@ -37,51 +39,69 @@ class SpringApplicationRunListeners {
|
||||
|
||||
private final List<SpringApplicationRunListener> listeners;
|
||||
|
||||
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
|
||||
private final ApplicationStartup applicationStartup;
|
||||
|
||||
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners,
|
||||
ApplicationStartup applicationStartup) {
|
||||
this.log = log;
|
||||
this.listeners = new ArrayList<>(listeners);
|
||||
this.applicationStartup = applicationStartup;
|
||||
}
|
||||
|
||||
void starting() {
|
||||
StartupStep starting = this.applicationStartup.start("spring.boot.application.starting");
|
||||
for (SpringApplicationRunListener listener : this.listeners) {
|
||||
listener.starting();
|
||||
}
|
||||
starting.end();
|
||||
}
|
||||
|
||||
void environmentPrepared(ConfigurableEnvironment environment) {
|
||||
StartupStep environmentPrepared = this.applicationStartup.start("spring.boot.application.environment-prepared");
|
||||
for (SpringApplicationRunListener listener : this.listeners) {
|
||||
listener.environmentPrepared(environment);
|
||||
}
|
||||
environmentPrepared.end();
|
||||
}
|
||||
|
||||
void contextPrepared(ConfigurableApplicationContext context) {
|
||||
StartupStep contextPrepared = this.applicationStartup.start("spring.boot.application.context-prepared");
|
||||
for (SpringApplicationRunListener listener : this.listeners) {
|
||||
listener.contextPrepared(context);
|
||||
}
|
||||
contextPrepared.end();
|
||||
}
|
||||
|
||||
void contextLoaded(ConfigurableApplicationContext context) {
|
||||
StartupStep contextLoaded = this.applicationStartup.start("spring.boot.application.context-loaded");
|
||||
for (SpringApplicationRunListener listener : this.listeners) {
|
||||
listener.contextLoaded(context);
|
||||
}
|
||||
contextLoaded.end();
|
||||
}
|
||||
|
||||
void started(ConfigurableApplicationContext context) {
|
||||
StartupStep started = this.applicationStartup.start("spring.boot.application.started");
|
||||
for (SpringApplicationRunListener listener : this.listeners) {
|
||||
listener.started(context);
|
||||
}
|
||||
started.end();
|
||||
}
|
||||
|
||||
void running(ConfigurableApplicationContext context) {
|
||||
StartupStep running = this.applicationStartup.start("spring.boot.application.running");
|
||||
for (SpringApplicationRunListener listener : this.listeners) {
|
||||
listener.running(context);
|
||||
}
|
||||
running.end();
|
||||
}
|
||||
|
||||
void failed(ConfigurableApplicationContext context, Throwable exception) {
|
||||
StartupStep failed = this.applicationStartup.start("spring.boot.application.failed");
|
||||
for (SpringApplicationRunListener listener : this.listeners) {
|
||||
callFailedListener(listener, context, exception);
|
||||
}
|
||||
failed.tag("exception", exception.getClass().toString()).tag("message", exception.getMessage()).end();
|
||||
}
|
||||
|
||||
private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context,
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.metrics.ApplicationStartup;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -548,4 +549,16 @@ public class SpringApplicationBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ApplicationStartup} to be used with the
|
||||
* {@link ApplicationContext} for collecting startup metrics.
|
||||
* @param applicationStartup the application startup to use
|
||||
* @return the current builder
|
||||
* @since 2.4.0
|
||||
*/
|
||||
public SpringApplicationBuilder applicationStartup(ApplicationStartup applicationStartup) {
|
||||
this.application.setApplicationStartup(applicationStartup);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.boot.web.context.ConfigurableWebServerApplicationCont
|
||||
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.core.metrics.StartupStep;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -84,14 +85,17 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli
|
||||
private void createWebServer() {
|
||||
WebServerManager serverManager = this.serverManager;
|
||||
if (serverManager == null) {
|
||||
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
|
||||
String webServerFactoryBeanName = getWebServerFactoryBeanName();
|
||||
ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName);
|
||||
createWebServer.tag("factory", webServerFactory.getClass().toString());
|
||||
boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
|
||||
this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);
|
||||
getBeanFactory().registerSingleton("webServerGracefulShutdown",
|
||||
new WebServerGracefulShutdownLifecycle(this.serverManager));
|
||||
getBeanFactory().registerSingleton("webServerStartStop",
|
||||
new WebServerStartStopLifecycle(this.serverManager));
|
||||
createWebServer.end();
|
||||
}
|
||||
initPropertySources();
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.metrics.StartupStep;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.ContextLoaderListener;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
@@ -174,8 +175,11 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon
|
||||
WebServer webServer = this.webServer;
|
||||
ServletContext servletContext = getServletContext();
|
||||
if (webServer == null && servletContext == null) {
|
||||
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
|
||||
ServletWebServerFactory factory = getWebServerFactory();
|
||||
createWebServer.tag("factory", factory.getClass().toString());
|
||||
this.webServer = factory.getWebServer(getSelfInitializer());
|
||||
createWebServer.end();
|
||||
getBeanFactory().registerSingleton("webServerGracefulShutdown",
|
||||
new WebServerGracefulShutdownLifecycle(this.webServer));
|
||||
getBeanFactory().registerSingleton("webServerStartStop",
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
@@ -100,6 +101,8 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.metrics.ApplicationStartup;
|
||||
import org.springframework.core.metrics.StartupStep;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.test.context.support.TestPropertySourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -111,8 +114,11 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -679,13 +685,11 @@ class SpringApplicationTests {
|
||||
application.addListeners(eventListener);
|
||||
this.context = application.run();
|
||||
InOrder applicationRunnerOrder = Mockito.inOrder(eventListener, applicationRunner);
|
||||
applicationRunnerOrder.verify(applicationRunner).run(ArgumentMatchers.any(ApplicationArguments.class));
|
||||
applicationRunnerOrder.verify(eventListener)
|
||||
.onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class));
|
||||
applicationRunnerOrder.verify(applicationRunner).run(any(ApplicationArguments.class));
|
||||
applicationRunnerOrder.verify(eventListener).onApplicationEvent(any(ApplicationReadyEvent.class));
|
||||
InOrder commandLineRunnerOrder = Mockito.inOrder(eventListener, commandLineRunner);
|
||||
commandLineRunnerOrder.verify(commandLineRunner).run();
|
||||
commandLineRunnerOrder.verify(eventListener)
|
||||
.onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class));
|
||||
commandLineRunnerOrder.verify(eventListener).onApplicationEvent(any(ApplicationReadyEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1149,6 +1153,28 @@ class SpringApplicationTests {
|
||||
.getBean(AtomicInteger.class)).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customApplicationStartupPublishStartupSteps() {
|
||||
ApplicationStartup applicationStartup = mock(ApplicationStartup.class);
|
||||
StartupStep startupStep = mock(StartupStep.class);
|
||||
given(applicationStartup.start(anyString())).willReturn(startupStep);
|
||||
given(startupStep.tag(anyString(), anyString())).willReturn(startupStep);
|
||||
given(startupStep.tag(anyString(), ArgumentMatchers.<Supplier<String>>any())).willReturn(startupStep);
|
||||
|
||||
SpringApplication application = new SpringApplication(ExampleConfig.class);
|
||||
application.setWebApplicationType(WebApplicationType.NONE);
|
||||
application.setApplicationStartup(applicationStartup);
|
||||
this.context = application.run();
|
||||
|
||||
assertThat(this.context.getBean(ApplicationStartup.class)).isEqualTo(applicationStartup);
|
||||
verify(applicationStartup).start("spring.boot.application.starting");
|
||||
verify(applicationStartup).start("spring.boot.application.environment-prepared");
|
||||
verify(applicationStartup).start("spring.boot.application.context-prepared");
|
||||
verify(applicationStartup).start("spring.boot.application.context-loaded");
|
||||
verify(applicationStartup).start("spring.boot.application.started");
|
||||
verify(applicationStartup).start("spring.boot.application.running");
|
||||
}
|
||||
|
||||
private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
|
||||
S state) {
|
||||
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)
|
||||
|
||||
Reference in New Issue
Block a user