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