From 9301d7a29451c9458a070bdfa7b0f466859282ac Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 27 Jul 2020 15:08:01 +0200 Subject: [PATCH] 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 --- .../config/ConfigurableBeanFactory.java | 17 +- .../factory/support/AbstractBeanFactory.java | 25 ++- .../support/DefaultListableBeanFactory.java | 12 +- .../beans/metrics/ApplicationStartup.java | 47 +++++ .../metrics/DefaultApplicationStartup.java | 92 ++++++++++ .../beans/metrics/StartupStep.java | 106 +++++++++++ .../jfr/FlightRecorderApplicationStartup.java | 55 ++++++ .../jfr/FlightRecorderStartupEvent.java | 57 ++++++ .../jfr/FlightRecorderStartupStep.java | 166 ++++++++++++++++++ .../beans/metrics/jfr/package-info.java | 9 + .../beans/metrics/package-info.java | 9 + .../context/ApplicationStartupAware.java | 41 +++++ .../ConfigurableApplicationContext.java | 22 +++ .../AnnotationConfigApplicationContext.java | 10 ++ .../ConfigurationClassPostProcessor.java | 16 +- .../SimpleApplicationEventMulticaster.java | 33 +++- .../support/AbstractApplicationContext.java | 30 +++- .../ApplicationContextAwareProcessor.java | 9 +- .../support/GenericApplicationContext.java | 3 +- .../PostProcessorRegistrationDelegate.java | 16 +- src/docs/asciidoc/core/core-appendix.adoc | 63 +++++++ src/docs/asciidoc/core/core-beans.adoc | 68 +++++++ 22 files changed, 890 insertions(+), 16 deletions(-) create mode 100644 spring-beans/src/main/java/org/springframework/beans/metrics/ApplicationStartup.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/metrics/DefaultApplicationStartup.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/metrics/StartupStep.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderApplicationStartup.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderStartupEvent.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/metrics/jfr/FlightRecorderStartupStep.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/metrics/jfr/package-info.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/metrics/package-info.java create mode 100644 spring-context/src/main/java/org/springframework/context/ApplicationStartupAware.java 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: + *

    + *
  1. the step is created and starts by calling {@link ApplicationStartup#start(String) the application startup} + * and is assigned a unique {@link StartupStep#getId() id}. + *
  2. we can then attach information with {@link Tags} during processing + *
  3. 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 postProcessors, BeanDefinitionRegistry registry) { + Collection 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 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