Add application startup metrics support

This commit adds a new `StartupStep` interface and its factory
`ApplicationStartup`. Such steps are created, tagged with metadata and
thir execution time can be recorded - in order to collect metrics about
the application startup.

The default implementation is a "no-op" variant and has no side-effect.
Other implementations can record and collect events in a dedicated
metrics system or profiling tools. We provide here an implementation for
recording and storing steps with Java Flight Recorder.

This commit also instruments the Spring application context to gather
metrics about various phases of the application context, such as:

* context refresh phase
* bean definition registry post-processing
* bean factory post-processing
* beans instantiation and post-processing

Third part libraries involved in the Spring application context can
reuse the same infrastructure to record similar metrics.

Closes gh-24878
This commit is contained in:
Brian Clozel
2020-07-27 15:08:01 +02:00
parent 4252b7fd7d
commit 9301d7a294
22 changed files with 890 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-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.
@@ -26,6 +26,7 @@ import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.StringValueResolver;
@@ -276,6 +277,20 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single
@Nullable
Scope getRegisteredScope(String scopeName);
/**
* Set the {@code ApplicationStartup} for this bean factory.
* <p>This allows the application context to record metrics during application startup.
* @param applicationStartup the new application startup
* @since 5.3.0
*/
void setApplicationStartup(ApplicationStartup applicationStartup);
/**
* Return the {@code ApplicationStartup} for this bean factory.
* @since 5.3.0
*/
ApplicationStartup getApplicationStartup();
/**
* Provides a security access control context relevant to this factory.
* @return the applicable AccessControlContext (never {@code null})

View File

@@ -69,6 +69,8 @@ import org.springframework.beans.factory.config.DestructionAwareBeanPostProcesso
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.core.AttributeAccessor;
import org.springframework.core.DecoratingClassLoader;
import org.springframework.core.NamedThreadLocal;
@@ -178,6 +180,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
new NamedThreadLocal<>("Prototype beans currently in creation");
/** Application startup metrics. **/
private ApplicationStartup applicationStartup = ApplicationStartup.getDefault();
/**
* Create a new AbstractBeanFactory.
@@ -297,6 +301,11 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
}
try {
StartupStep beanCreation = this.applicationStartup.start("spring.beans.instantiate")
.tag("beanName", name);
if (requiredType != null) {
beanCreation.tag("beanType", requiredType::toString);
}
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);
@@ -374,6 +383,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
throw new ScopeNotActiveException(beanName, scopeName, ex);
}
}
beanCreation.end();
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
@@ -1044,6 +1054,17 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
this.securityContextProvider = securityProvider;
}
@Override
public void setApplicationStartup(ApplicationStartup applicationStartup) {
Assert.notNull(applicationStartup, "applicationStartup should not be null");
this.applicationStartup = applicationStartup;
}
@Override
public ApplicationStartup getApplicationStartup() {
return this.applicationStartup;
}
/**
* Delegate the creation of the access control context to the
* {@link #setSecurityContextProvider SecurityContextProvider}.
@@ -1380,7 +1401,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
else {
throw new NoSuchBeanDefinitionException(parentBeanName,
"Parent name '" + parentBeanName + "' is equal to bean name '" + beanName +
"': cannot be resolved without a ConfigurableBeanFactory parent");
"': cannot be resolved without a ConfigurableBeanFactory parent");
}
}
}
@@ -2068,7 +2089,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
super.replaceAll(operator);
beanPostProcessorCache = null;
}
};
}
/**

View File

@@ -71,6 +71,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.NamedBeanHolder;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.core.OrderComparator;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
@@ -564,7 +565,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
}
}
else {
else {
if (includeNonSingletons || isNonLazyDecorated ||
(allowFactoryBeanInit && isSingleton(beanName, mbd, dbd))) {
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
@@ -937,6 +938,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize")
.tag("beanName", beanName);
SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
@@ -947,6 +950,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
else {
smartSingleton.afterSingletonsInstantiated();
}
smartInitialize.end();
}
}
}
@@ -1672,7 +1676,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
if (candidatePriority.equals(highestPriority)) {
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
"Multiple beans found with the same priority ('" + highestPriority +
"') among candidates: " + candidates.keySet());
"') among candidates: " + candidates.keySet());
}
else if (candidatePriority < highestPriority) {
highestPriorityBeanName = candidateBeanName;
@@ -1758,7 +1762,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
throw new NoSuchBeanDefinitionException(resolvableType,
"expected at least 1 bean which qualifies as autowire candidate. " +
"Dependency annotations: " + ObjectUtils.nullSafeToString(descriptor.getAnnotations()));
"Dependency annotations: " + ObjectUtils.nullSafeToString(descriptor.getAnnotations()));
}
/**
@@ -1803,6 +1807,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
public boolean isRequired() {
return false;
}
@Override
public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory) {
return (!ObjectUtils.isEmpty(args) ? beanFactory.getBean(beanName, args) :
@@ -2019,6 +2024,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
public boolean isRequired() {
return false;
}
@Override
@Nullable
public Object resolveNotUnique(ResolvableType type, Map<String, Object> matchingBeans) {

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2002-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.
* You may obtain a copy of the License at
*
* https://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.beans.metrics;
/**
* Instruments the application startup phase using {@link StartupStep steps}.
* <p>The core container and its infrastructure components can use the {@code ApplicationStartup}
* to mark steps during the application startup and collect data about the execution context
* or their processing time.
*
* @author Brian Clozel
* @since 5.3.0
*/
public interface ApplicationStartup {
/**
* Return a default "no op" {@code ApplicationStartup} implementation.
* <p>This variant is designed for minimal overhead and does not record data.
*/
static ApplicationStartup getDefault() {
return new DefaultApplicationStartup();
}
/**
* Create a new step and marks its beginning.
* <p>A step name describes the current action or phase. This technical
* name should be "." namespaced and can be reused to describe other instances of
* the same step during application startup.
* @param name the step name
*/
StartupStep start(String name);
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2002-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.
* You may obtain a copy of the License at
*
* https://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.beans.metrics;
import java.util.Collections;
import java.util.Iterator;
import java.util.function.Supplier;
/**
* Default "no op" {@code ApplicationStartup} implementation.
* <p>This variant is designed for minimal overhead and does not record events.
*
* @author Brian Clozel
*/
class DefaultApplicationStartup implements ApplicationStartup {
@Override
public DefaultStartupStep start(String name) {
return new DefaultStartupStep();
}
static class DefaultStartupStep implements StartupStep {
boolean recorded = false;
private final DefaultTags TAGS = new DefaultTags();
@Override
public String getName() {
return "default";
}
@Override
public long getId() {
return 0L;
}
@Override
public Long getParentId() {
return null;
}
@Override
public Tags tags() {
return this.TAGS;
}
@Override
public StartupStep tag(String key, String value) {
if (this.recorded) {
throw new IllegalArgumentException();
}
return this;
}
@Override
public StartupStep tag(String key, Supplier<String> value) {
if (this.recorded) {
throw new IllegalArgumentException();
}
return this;
}
@Override
public void end() {
this.recorded = true;
}
static class DefaultTags implements StartupStep.Tags {
@Override
public Iterator<StartupStep.Tag> iterator() {
return Collections.emptyIterator();
}
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2002-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.
* You may obtain a copy of the License at
*
* https://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.beans.metrics;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
/**
* Step recording metrics about a particular phase or action happening during the {@link ApplicationStartup}.
* <p>The lifecycle of a {@code StartupStep} goes as follows:
* <ol>
* <li>the step is created and starts by calling {@link ApplicationStartup#start(String) the application startup}
* and is assigned a unique {@link StartupStep#getId() id}.
* <li>we can then attach information with {@link Tags} during processing
* <li>we then need to mark the {@link #end()} of the step
* </ol>
* <p>Implementations can track the "execution time" or other metrics for steps.
*
* @author Brian Clozel
* @since 5.3.0
*/
public interface StartupStep {
/**
* Return the name of the startup step.
* <p>A step name describes the current action or phase. This technical
* name should be "." namespaced and can be reused to describe other instances of
* similar steps during application startup.
*/
String getName();
/**
* Return the unique id for this step within the application startup.
*/
long getId();
/**
* Return, if available, the id of the parent step.
* <p>The parent step is the step that was started the most recently when the current step was created.
*/
@Nullable
Long getParentId();
/**
* Add a {@link Tag} to the step.
* @param key tag key
* @param value tag value
*/
StartupStep tag(String key, String value);
/**
* Add a {@link Tag} to the step.
* @param key tag key
* @param value {@link Supplier} for the tag value
*/
StartupStep tag(String key, Supplier<String> value);
/**
* Return the {@link Tag} collection for this step.
*/
Tags tags();
/**
* Record the state of the step and possibly other metrics like execution time.
* <p>Once ended, changes on the step state are not allowed.
*/
void end();
/**
* Mutable collection of {@link Tag}.
*/
interface Tags extends Iterable<Tag> {
}
/**
* Simple key/value association for storing step metadata.
*/
interface Tag {
/**
* Return the {@code Tag} name.
*/
String getKey();
/**
* Return the {@code Tag} value.
*/
String getValue();
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2002-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.
* You may obtain a copy of the License at
*
* https://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.beans.metrics.jfr;
import java.util.ArrayDeque;
import java.util.Deque;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.beans.metrics.StartupStep;
/**
* {@link ApplicationStartup} implementation for the Java Flight Recorder.
* <p>This variant records {@link StartupStep} as Flight Recorder events; because such events
* only support base types, the {@link StartupStep.Tags} are serialized as a single String attribute.
* <p>Once this is configured on the application context, you can record data by launching the application
* with recording enabled: {@code java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar app.jar}.
*
* @author Brian Clozel
* @since 5.3
*/
public class FlightRecorderApplicationStartup implements ApplicationStartup {
private long currentSequenceId;
private final Deque<Long> currentSteps;
public FlightRecorderApplicationStartup() {
this.currentSequenceId = 0;
this.currentSteps = new ArrayDeque<>();
this.currentSteps.offerFirst(0L);
}
@Override
public StartupStep start(String name) {
FlightRecorderStartupStep step = new FlightRecorderStartupStep(++this.currentSequenceId, name,
this.currentSteps.peekFirst(), committedStep -> this.currentSteps.removeFirst());
this.currentSteps.offerFirst(this.currentSequenceId);
return step;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2002-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.
* You may obtain a copy of the License at
*
* https://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.beans.metrics.jfr;
import jdk.jfr.Category;
import jdk.jfr.Description;
import jdk.jfr.Event;
import jdk.jfr.Label;
/**
* {@link Event} extension for recording {@link FlightRecorderStartupStep}
* in Java Flight Recorder.
* <p>{@link org.springframework.beans.metrics.StartupStep.Tags} are serialized as a single {@code String},
* since Flight Recorder events do not support complex types.
*
* @author Brian Clozel
*/
@Category("Spring Application")
@Label("Startup Step")
@Description("Spring Application Startup")
class FlightRecorderStartupEvent extends Event {
public final long eventId;
public final long parentId;
@Label("Name")
public final String name;
@Label("Tags")
String tags = "";
public FlightRecorderStartupEvent(long eventId, String name, long parentId) {
this.name = name;
this.eventId = eventId;
this.parentId = parentId;
}
public void setTags(String tags) {
this.tags = tags;
}
}

View File

@@ -0,0 +1,166 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://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.beans.metrics.jfr;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.metrics.StartupStep;
/**
* {@link StartupStep} implementation for the Java Flight Recorder.
* <p>This variant delegates to a {@link FlightRecorderStartupEvent JFR event extension}
* to collect and record data in Java Flight Recorder.
*
* @author Brian Clozel
*/
class FlightRecorderStartupStep implements StartupStep {
private final FlightRecorderStartupEvent event;
private final FlightRecorderTags tags = new FlightRecorderTags();
private final Consumer<FlightRecorderStartupStep> recordingCallback;
public FlightRecorderStartupStep(long id, String name, long parentId,
Consumer<FlightRecorderStartupStep> recordingCallback) {
this.event = new FlightRecorderStartupEvent(id, name, parentId);
this.event.begin();
this.recordingCallback = recordingCallback;
}
@Override
public String getName() {
return this.event.name;
}
@Override
public long getId() {
return this.event.eventId;
}
@Override
public Long getParentId() {
return this.event.parentId;
}
@Override
public StartupStep tag(String key, String value) {
this.tags.add(key, value);
return this;
}
@Override
public StartupStep tag(String key, Supplier<String> value) {
this.tags.add(key, value.get());
return this;
}
@Override
public Tags tags() {
return this.tags;
}
@Override
public void end() {
this.event.end();
if (this.event.shouldCommit()) {
StringBuilder builder = new StringBuilder();
this.tags.forEach(tag ->
builder.append(tag.getKey()).append('=').append(tag.getValue()).append(',')
);
this.event.setTags(builder.toString());
}
this.event.commit();
this.recordingCallback.accept(this);
}
protected FlightRecorderStartupEvent getEvent() {
return this.event;
}
static class FlightRecorderTags implements Tags {
private Tag[] tags = new Tag[0];
public void add(String key, String value) {
Tag[] newTags = new Tag[this.tags.length + 1];
System.arraycopy(this.tags, 0, newTags, 0, this.tags.length);
newTags[newTags.length - 1] = new FlightRecorderTag(key, value);
this.tags = newTags;
}
public void add(String key, Supplier<String> value) {
add(key, value.get());
}
@NotNull
@Override
public Iterator<Tag> iterator() {
return new TagsIterator();
}
private class TagsIterator implements Iterator<Tag> {
private int idx = 0;
@Override
public boolean hasNext() {
return this.idx < tags.length;
}
@Override
public Tag next() {
return tags[this.idx++];
}
@Override
public void remove() {
throw new UnsupportedOperationException("tags are append only");
}
}
}
static class FlightRecorderTag implements Tag {
private final String key;
private final String value;
public FlightRecorderTag(String key, String value) {
this.key = key;
this.value = value;
}
@Override
public String getKey() {
return this.key;
}
@Override
public String getValue() {
return this.value;
}
}
}

View File

@@ -0,0 +1,9 @@
/**
* Support package for recording startup metrics using Java Flight Recorder.
*/
@NonNullApi
@NonNullFields
package org.springframework.beans.metrics.jfr;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -0,0 +1,9 @@
/**
* Support package for recording metrics during application startup.
*/
@NonNullApi
@NonNullFields
package org.springframework.beans.metrics;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2002-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.
* You may obtain a copy of the License at
*
* https://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.context;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.metrics.ApplicationStartup;
/**
* Interface to be implemented by any object that wishes to be notified
* of the {@link ApplicationStartup} that it runs with.
*
* @author Brian Clozel
* @since 5.3.0
* @see ApplicationContextAware
*/
public interface ApplicationStartupAware extends Aware {
/**
* Set the ApplicationStartup that this object runs with.
* <p>Invoked after population of normal bean properties but before an init
* callback like InitializingBean's afterPropertiesSet or a custom init-method.
* Invoked before ApplicationContextAware's setApplicationContext.
* @param applicationStartup application startup to be used by this object
*/
void setApplicationStartup(ApplicationStartup applicationStartup);
}

View File

@@ -21,6 +21,7 @@ import java.io.Closeable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ProtocolResolver;
@@ -87,6 +88,12 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life
*/
String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment";
/**
* Name of the {@link ApplicationStartup} bean in the factory.
* @since 5.3.0
*/
String APPLICATION_STARTUP_BEAN_NAME = "applicationStartup";
/**
* {@link Thread#getName() Name} of the {@linkplain #registerShutdownHook()
* shutdown hook} thread: {@value}.
@@ -127,6 +134,21 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life
@Override
ConfigurableEnvironment getEnvironment();
/**
* Set the {@link ApplicationStartup} for this application context.
* <p>This allows the application context to record metrics
* during startup.
* @param applicationStartup the new context event factory
* @since 5.3.0
*/
void setApplicationStartup(ApplicationStartup applicationStartup);
/**
* Return the {@link ApplicationStartup} for this application context.
* @since 5.3.0
*/
ApplicationStartup getApplicationStartup();
/**
* Add a new BeanFactoryPostProcessor that will get applied to the internal
* bean factory of this application context on refresh, before any of the

View File

@@ -16,11 +16,13 @@
package org.springframework.context.annotation;
import java.util.Arrays;
import java.util.function.Supplier;
import org.springframework.beans.factory.config.BeanDefinitionCustomizer;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.lang.Nullable;
@@ -63,7 +65,9 @@ public class AnnotationConfigApplicationContext extends GenericApplicationContex
* through {@link #register} calls and then manually {@linkplain #refresh refreshed}.
*/
public AnnotationConfigApplicationContext() {
StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create");
this.reader = new AnnotatedBeanDefinitionReader(this);
createAnnotatedBeanDefReader.end();
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
@@ -159,7 +163,10 @@ public class AnnotationConfigApplicationContext extends GenericApplicationContex
@Override
public void register(Class<?>... componentClasses) {
Assert.notEmpty(componentClasses, "At least one component class must be specified");
StartupStep registerComponentClass = this.getApplicationStartup().start("spring.context.component-classes.register")
.tag("classes", () -> Arrays.toString(componentClasses));
this.reader.register(componentClasses);
registerComponentClass.end();
}
/**
@@ -173,7 +180,10 @@ public class AnnotationConfigApplicationContext extends GenericApplicationContex
@Override
public void scan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan")
.tag("packages", () -> Arrays.toString(basePackages));
this.scanner.scan(basePackages);
scanPackages.end();
}

View File

@@ -48,6 +48,9 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.context.ApplicationStartupAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ConfigurationClassEnhancer.EnhancedConfiguration;
@@ -84,7 +87,7 @@ import org.springframework.util.ClassUtils;
* @since 3.0
*/
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
PriorityOrdered, ResourceLoaderAware, ApplicationStartupAware, BeanClassLoaderAware, EnvironmentAware {
/**
* A {@code BeanNameGenerator} using fully qualified class names as default bean names.
@@ -142,6 +145,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
/* Using fully qualified class names as default bean names by default. */
private BeanNameGenerator importBeanNameGenerator = IMPORT_BEAN_NAME_GENERATOR;
private ApplicationStartup applicationStartup = ApplicationStartup.getDefault();
@Override
public int getOrder() {
@@ -223,6 +228,10 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
}
}
@Override
public void setApplicationStartup(ApplicationStartup applicationStartup) {
this.applicationStartup = applicationStartup;
}
/**
* Derive further bean definitions from the configuration classes in the registry.
@@ -323,6 +332,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
parser.parse(candidates);
parser.validate();
@@ -337,6 +347,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();
candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
@@ -379,6 +390,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
* @see ConfigurationClassEnhancer
*/
public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
StartupStep enhanceConfigClasses = this.applicationStartup.start("spring.context.config-classes.enhance");
Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
@@ -417,6 +429,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
}
if (configBeanDefs.isEmpty()) {
// nothing to enhance -> return immediately
enhanceConfigClasses.end();
return;
}
if (IN_NATIVE_IMAGE) {
@@ -439,6 +452,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
beanDef.setBeanClass(enhancedClass);
}
}
enhanceConfigClasses.tag("classCount", () -> String.valueOf(configBeanDefs.keySet().size())).end();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-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.
@@ -22,6 +22,8 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.ResolvableType;
@@ -44,6 +46,7 @@ import org.springframework.util.ErrorHandler;
* @author Rod Johnson
* @author Juergen Hoeller
* @author Stephane Nicoll
* @author Brian Clozel
* @see #setTaskExecutor
*/
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
@@ -54,6 +57,9 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM
@Nullable
private ErrorHandler errorHandler;
@Nullable
private ApplicationStartup applicationStartup;
/**
* Create a new SimpleApplicationEventMulticaster.
@@ -121,6 +127,21 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM
return this.errorHandler;
}
/**
* Set the {@link ApplicationStartup} to track event listener invocations during startup.
* @since 5.3
*/
public void setApplicationStartup(@Nullable ApplicationStartup applicationStartup) {
this.applicationStartup = applicationStartup;
}
/**
* Return the current application startup for this multicaster.
*/
@Nullable
public ApplicationStartup getApplicationStartup() {
return this.applicationStartup;
}
@Override
public void multicastEvent(ApplicationEvent event) {
@@ -135,6 +156,16 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else if (this.applicationStartup != null) {
StartupStep invocationStep = this.applicationStartup.start("spring.event.invoke-listener");
invokeListener(listener, event);
invocationStep.tag("event", event::toString);
if (eventType != null) {
invocationStep.tag("eventType", eventType::toString);
}
invocationStep.tag("listener", listener::toString);
invocationStep.end();
}
else {
invokeListener(listener, event);
}

View File

@@ -39,6 +39,8 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.beans.support.ResourceEditorRegistrar;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -119,6 +121,7 @@ import org.springframework.util.ReflectionUtils;
* @author Stephane Nicoll
* @author Sam Brannen
* @author Sebastien Deleuze
* @author Brian Clozel
* @since January 21, 2001
* @see #refreshBeanFactory
* @see #getBeanFactory
@@ -227,6 +230,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
@Nullable
private ApplicationEventMulticaster applicationEventMulticaster;
/** Application startup metrics. **/
private ApplicationStartup applicationStartup = ApplicationStartup.getDefault();
/** Statically specified listeners. */
private final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();
@@ -444,6 +450,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
return this.applicationEventMulticaster;
}
@Override
public void setApplicationStartup(ApplicationStartup applicationStartup) {
Assert.notNull(applicationStartup, "applicationStartup should not be null");
this.applicationStartup = applicationStartup;
}
@Override
public ApplicationStartup getApplicationStartup() {
return this.applicationStartup;
}
/**
* Return the internal LifecycleProcessor used by the context.
* @return the internal LifecycleProcessor (never {@code null})
@@ -532,6 +549,8 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
@@ -545,11 +564,13 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
@@ -590,6 +611,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
contextRefresh.end();
}
}
}
@@ -676,6 +698,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
beanFactory.ignoreDependencyInterface(ApplicationStartup.class);
// BeanFactory interface not registered as resolvable type in a plain factory.
// MessageSource registered (and found for autowiring) as a bean.
@@ -704,6 +727,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
if (!beanFactory.containsLocalBean(APPLICATION_STARTUP_BEAN_NAME)) {
beanFactory.registerSingleton(APPLICATION_STARTUP_BEAN_NAME, getApplicationStartup());
}
}
/**
@@ -789,7 +815,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
simpleApplicationEventMulticaster.setApplicationStartup(getApplicationStartup());
this.applicationEventMulticaster = simpleApplicationEventMulticaster;
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isTraceEnabled()) {
logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-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.
@@ -25,6 +25,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.EmbeddedValueResolver;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ApplicationStartupAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.EnvironmentAware;
@@ -80,7 +81,8 @@ class ApplicationContextAwareProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)){
bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware ||
bean instanceof ApplicationStartupAware)) {
return bean;
}
@@ -119,6 +121,9 @@ class ApplicationContextAwareProcessor implements BeanPostProcessor {
if (bean instanceof MessageSourceAware) {
((MessageSourceAware) bean).setMessageSource(this.applicationContext);
}
if (bean instanceof ApplicationStartupAware) {
((ApplicationStartupAware) bean).setApplicationStartup(this.applicationContext.getApplicationStartup());
}
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-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.
@@ -266,6 +266,7 @@ public class GenericApplicationContext extends AbstractApplicationContext implem
throw new IllegalStateException(
"GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
}
this.beanFactory.setApplicationStartup(this.getApplicationStartup());
this.beanFactory.setSerializationId(getId());
}

View File

@@ -36,6 +36,8 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProce
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
@@ -94,7 +96,7 @@ final class PostProcessorRegistrationDelegate {
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
@@ -107,7 +109,7 @@ final class PostProcessorRegistrationDelegate {
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
@@ -124,7 +126,7 @@ final class PostProcessorRegistrationDelegate {
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
}
@@ -275,10 +277,13 @@ final class PostProcessorRegistrationDelegate {
* Invoke the given BeanDefinitionRegistryPostProcessor beans.
*/
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) {
for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
StartupStep postProcessBeanDefRegistry = applicationStartup.start("spring.context.beandef-registry.post-process")
.tag("postProcessor", postProcessor::toString);
postProcessor.postProcessBeanDefinitionRegistry(registry);
postProcessBeanDefRegistry.end();
}
}
@@ -289,7 +294,10 @@ final class PostProcessorRegistrationDelegate {
Collection<? extends BeanFactoryPostProcessor> postProcessors, ConfigurableListableBeanFactory beanFactory) {
for (BeanFactoryPostProcessor postProcessor : postProcessors) {
StartupStep postProcessBeanFactory = beanFactory.getApplicationStartup().start("spring.context.bean-factory.post-process")
.tag("postProcessor", postProcessor::toString);
postProcessor.postProcessBeanFactory(beanFactory);
postProcessBeanFactory.end();
}
}

View File

@@ -1610,3 +1610,66 @@ http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd
----
[[application-startup-steps]]
== Application Startup Steps
This part of the appendix lists the existing `StartupSteps` that the core container is instrumented with.
WARNING: The name and detailed information about each startup step is not part of the public contract and
is subject to change; this is considered as an implementation detail of the core container and will follow
its behavior changes.
.Application startup steps defined in the core container
|===
| Name| Description| Tags
| `spring.beans.instantiate`
| Instantiation of a bean and its dependencies.
| `beanName` the name of the bean, `beanType` the type required at the injection point.
| `spring.beans.smart-initialize`
| Initialization of `SmartInitializingSingleton` beans.
| `beanName` the name of the bean.
| `spring.context.annotated-bean-reader.create`
| Creation of the `AnnotatedBeanDefinitionReader`.
|
| `spring.context.base-packages.scan`
| Scanning of base packages.
| `packages` array of base packages for scanning.
| `spring.context.beans.post-process`
| Beans post-processing phase.
|
| `spring.context.bean-factory.post-process`
| Invocation of the `BeanFactoryPostProcessor` beans.
| `postProcessor` the current post-processor.
| `spring.context.beandef-registry.post-process`
| Invocation of the `BeanDefinitionRegistryPostProcessor` beans.
| `postProcessor` the current post-processor.
| `spring.context.component-classes.register`
| Registration of component classes through `AnnotationConfigApplicationContext#register`.
| `classes` array of given classes for registration.
| `spring.context.config-classes.enhance`
| Enhancement of configuration classes with CGLIB proxies.
| `classCount` count of enhanced classes.
| `spring.context.config-classes.parse`
| Configuration classes parsing phase with the `ConfigurationClassPostProcessor`.
| `classCount` count of processed classes.
| `spring.context.refresh`
| Application context refresh phase.
|
| `spring.event.invoke-listener`
| Invocation of event listeners, if done in the main thread.
| `event` the current application event, `eventType` its type and `listener` the listener processing this event.
|===

View File

@@ -11027,7 +11027,75 @@ location path as a classpath location. You can also use location paths (resource
with special prefixes to force loading of definitions from the classpath or a URL,
regardless of the actual context type.
[[context-functionality-startup]]
=== Application Startup tracking
The `ApplicationContext` manages the lifecycle of Spring applications and provides a rich
programming model around components. As a result, complex applications can have equally
complex component graphs and startup phases.
Tracking the application startup steps with specific metrics can help understand where
time is being spent during the startup phase, but it can also be used as a way to better
understand the context lifecycle as a whole.
The `AbstractApplicationContext` (and its subclasses) is instrumented with an
`ApplicationStartup`, which collects `StartupStep` data about various startup phases:
* application context lifecycle (base packages scanning, config classes management)
* beans lifecycle (instantiation, smart initialization, post processing)
* application events processing
Here is an example of instrumentation in the `AnnotationConfigApplicationContext`:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
// create a startup step and start recording
val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();
----
The application context is already instrumented with multiple steps.
Once recorded, these startup steps can be collected, displayed and analyzed with specific tools.
For a complete list of existing startup steps, you can check out the
<<application-startup-steps, dedicated appendix section>>.
The default `ApplicationStartup` implementation is a no-op variant, for minimal overhead.
This means no metrics will be collected during application startup by default.
Spring Framework ships with an implementation for tracking startup steps with Java Flight Recorder:
`FlightRecorderApplicationStartup`. To use this variant, you must configure an instance of it
to the `ApplicationContext` as soon as it's been created.
Developers can also use the `ApplicationStartup` infrastructure if they're providing their own
`AbstractApplicationContext` subclass, or if they wish to collect more precise data.
WARNING: `ApplicationStartup` is meant to be only used during application startup and for
the core container; this is by no means a replacement for Java profilers or
metrics libraries like https://micrometer.io[Micrometer].
To start collecting custom `StartupStep`, components can either get the `ApplicationStartup`
instance from the application context directly, make their component implement `ApplicationStartupAware`,
or ask for the `ApplicationStartup` type on any injection point.
NOTE: Developers should not use the `"spring.*"` namespace when creating custom startup steps.
This namespace is reserved for internal Spring usage and is subject to change.
[[context-create]]
=== Convenient ApplicationContext Instantiation for Web Applications