diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java
index 166fec179239..413202337aa4 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java
@@ -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.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.
+ *
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})
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java
index 0689905bd538..cb35af57760b 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java
@@ -69,6 +69,8 @@
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 prototypesCurrentlyInCreation =
new NamedThreadLocal<>("Prototype beans currently in creation");
+ /** Application startup metrics. **/
+ private ApplicationStartup applicationStartup = ApplicationStartup.getDefault();
/**
* Create a new AbstractBeanFactory.
@@ -297,6 +301,11 @@ else if (requiredType != null) {
}
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 @@ else if (mbd.isPrototype()) {
throw new ScopeNotActiveException(beanName, scopeName, ex);
}
}
+ beanCreation.end();
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
@@ -1044,6 +1054,17 @@ public void setSecurityContextProvider(SecurityContextProvider securityProvider)
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 @@ protected RootBeanDefinition getMergedBeanDefinition(
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 void replaceAll(UnaryOperator operator) {
super.replaceAll(operator);
beanPostProcessorCache = null;
}
- };
+ }
/**
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java
index 178cb2a0f67c..2b74a9439eae 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java
@@ -71,6 +71,7 @@
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 @@ private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSi
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 void preInstantiateSingletons() throws BeansException {
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) () -> {
@@ -947,6 +950,7 @@ public void preInstantiateSingletons() throws BeansException {
else {
smartSingleton.afterSingletonsInstantiated();
}
+ smartInitialize.end();
}
}
}
@@ -1672,7 +1676,7 @@ protected String determineHighestPriorityCandidate(Map candidate
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 @@ private void raiseNoMatchingBeanFound(
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 @@ private Optional> createOptionalDependency(
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 Object getIfUnique() throws BeansException {
public boolean isRequired() {
return false;
}
+
@Override
@Nullable
public Object resolveNotUnique(ResolvableType type, Map matchingBeans) {
diff --git a/spring-beans/src/main/java/org/springframework/beans/metrics/ApplicationStartup.java b/spring-beans/src/main/java/org/springframework/beans/metrics/ApplicationStartup.java
new file mode 100644
index 000000000000..ff09c5f07cac
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/metrics/ApplicationStartup.java
@@ -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}.
+ * 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.
+ *
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.
+ *
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);
+
+}
diff --git a/spring-beans/src/main/java/org/springframework/beans/metrics/DefaultApplicationStartup.java b/spring-beans/src/main/java/org/springframework/beans/metrics/DefaultApplicationStartup.java
new file mode 100644
index 000000000000..62d66c243425
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/metrics/DefaultApplicationStartup.java
@@ -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.
+ *
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 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 iterator() {
+ return Collections.emptyIterator();
+ }
+ }
+ }
+
+}
diff --git a/spring-beans/src/main/java/org/springframework/beans/metrics/StartupStep.java b/spring-beans/src/main/java/org/springframework/beans/metrics/StartupStep.java
new file mode 100644
index 000000000000..d782c605c300
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/metrics/StartupStep.java
@@ -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}.
+ * The lifecycle of a {@code StartupStep} goes as follows:
+ *
+ * the step is created and starts by calling {@link ApplicationStartup#start(String) the application startup}
+ * and is assigned a unique {@link StartupStep#getId() id}.
+ * we can then attach information with {@link Tags} during processing
+ * we then need to mark the {@link #end()} of the step
+ *
+ * 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.
+ *
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.
+ *
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 value);
+
+ /**
+ * Return the {@link Tag} collection for this step.
+ */
+ Tags tags();
+
+ /**
+ * Record the state of the step and possibly other metrics like execution time.
+ * Once ended, changes on the step state are not allowed.
+ */
+ void end();
+
+ /**
+ * Mutable collection of {@link Tag}.
+ */
+ interface Tags extends Iterable {
+
+ }
+
+ /**
+ * Simple key/value association for storing step metadata.
+ */
+ interface Tag {
+
+ /**
+ * Return the {@code Tag} name.
+ */
+ String getKey();
+
+ /**
+ * Return the {@code Tag} value.
+ */
+ String getValue();
+ }
+}
diff --git a/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderApplicationStartup.java b/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderApplicationStartup.java
new file mode 100644
index 000000000000..9567ae808821
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderApplicationStartup.java
@@ -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.
+ * 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.
+ *
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 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;
+ }
+
+}
diff --git a/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderStartupEvent.java b/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderStartupEvent.java
new file mode 100644
index 000000000000..e9f38e2c209e
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderStartupEvent.java
@@ -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.
+ * {@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;
+ }
+
+}
diff --git a/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderStartupStep.java b/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderStartupStep.java
new file mode 100644
index 000000000000..6a4ff6e3d747
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderStartupStep.java
@@ -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.
+ *
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 recordingCallback;
+
+ public FlightRecorderStartupStep(long id, String name, long parentId,
+ Consumer 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 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 value) {
+ add(key, value.get());
+ }
+
+ @NotNull
+ @Override
+ public Iterator iterator() {
+ return new TagsIterator();
+ }
+
+ private class TagsIterator implements Iterator {
+
+ 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;
+ }
+
+ }
+
+}
diff --git a/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/package-info.java b/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/package-info.java
new file mode 100644
index 000000000000..abe273433226
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/metrics/jfr/package-info.java
@@ -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;
diff --git a/spring-beans/src/main/java/org/springframework/beans/metrics/package-info.java b/spring-beans/src/main/java/org/springframework/beans/metrics/package-info.java
new file mode 100644
index 000000000000..14241d656cb8
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/metrics/package-info.java
@@ -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;
diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationStartupAware.java b/spring-context/src/main/java/org/springframework/context/ApplicationStartupAware.java
new file mode 100644
index 000000000000..ac0db3513647
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/context/ApplicationStartupAware.java
@@ -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.
+ * 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);
+
+}
diff --git a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java
index ded4db3ad276..28f300caf054 100644
--- a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java
+++ b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java
@@ -21,6 +21,7 @@
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.
+ *
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
diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java
index 696655ad0d7f..44f31572220d 100644
--- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java
+++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java
@@ -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 void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver
@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 void register(Class>... componentClasses) {
@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();
}
diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java
index 1865ec2095dc..d29f0a4863f7 100644
--- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java
+++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java
@@ -48,6 +48,9 @@
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 @@
* @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 void setBeanClassLoader(ClassLoader beanClassLoader) {
}
}
+ @Override
+ public void setApplicationStartup(ApplicationStartup applicationStartup) {
+ this.applicationStartup = applicationStartup;
+ }
/**
* Derive further bean definitions from the configuration classes in the registry.
@@ -323,6 +332,7 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.
Set candidates = new LinkedHashSet<>(configCandidates);
Set 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 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.
}
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 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.
* @see ConfigurationClassEnhancer
*/
public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
+ StartupStep enhanceConfigClasses = this.applicationStartup.start("spring.context.config-classes.enhance");
Map configBeanDefs = new LinkedHashMap<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
@@ -417,6 +429,7 @@ else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) {
}
if (configBeanDefs.isEmpty()) {
// nothing to enhance -> return immediately
+ enhanceConfigClasses.end();
return;
}
if (IN_NATIVE_IMAGE) {
@@ -439,6 +452,7 @@ else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) {
beanDef.setBeanClass(enhancedClass);
}
}
+ enhanceConfigClasses.tag("classCount", () -> String.valueOf(configBeanDefs.keySet().size())).end();
}
diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java
index cb70ac4ea0c2..4e89c6bd8d7a 100644
--- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java
+++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java
@@ -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.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 @@
* @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 @@ protected ErrorHandler getErrorHandler() {
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 void multicastEvent(final ApplicationEvent event, @Nullable ResolvableTyp
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);
}
diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java
index 7f20cf9e4f2d..34c620bf1b2a 100644
--- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java
+++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java
@@ -39,6 +39,8 @@
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 @@
* @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> applicationListeners = new LinkedHashSet<>();
@@ -444,6 +450,17 @@ ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalState
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 Collection> getApplicationListeners() {
@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 void refresh() throws BeansException, IllegalStateException {
// 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 void refresh() throws BeansException, IllegalStateException {
// 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 @@ protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
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 @@ protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
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 @@ protected void initApplicationEventMulticaster() {
}
}
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 " +
diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java
index 1a5a6ecf69b6..a58bb6303470 100644
--- a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java
+++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java
@@ -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.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 @@ public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicati
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 @@ private void invokeAwareInterfaces(Object bean) {
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);
}
diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java
index b1105ccaaebf..29f51e40e065 100644
--- a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java
+++ b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java
@@ -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 @@ protected final void refreshBeanFactory() throws IllegalStateException {
throw new IllegalStateException(
"GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
}
+ this.beanFactory.setApplicationStartup(this.getApplicationStartup());
this.beanFactory.setSerializationId(getId());
}
diff --git a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java
index 0b070e4d72a5..80c5ad354ab0 100644
--- a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java
+++ b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java
@@ -36,6 +36,8 @@
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 @@ public static void invokeBeanFactoryPostProcessors(
}
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 @@ public static void invokeBeanFactoryPostProcessors(
}
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 @@ public static void invokeBeanFactoryPostProcessors(
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
- invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
+ invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
}
@@ -275,10 +277,13 @@ private static void sortPostProcessors(List> postProcessors, ConfigurableLista
* 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 @@ private static void invokeBeanFactoryPostProcessors(
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();
}
}
diff --git a/src/docs/asciidoc/core/core-appendix.adoc b/src/docs/asciidoc/core/core-appendix.adoc
index ee20da30b48d..9a2a250301e0 100644
--- a/src/docs/asciidoc/core/core-appendix.adoc
+++ b/src/docs/asciidoc/core/core-appendix.adoc
@@ -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.
+|===
\ No newline at end of file
diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc
index 59018d6c259e..e4dc8308f9c8 100644
--- a/src/docs/asciidoc/core/core-beans.adoc
+++ b/src/docs/asciidoc/core/core-beans.adoc
@@ -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
+<>.
+
+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