Skip to content

Commit 9301d7a

Browse files
committed
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
1 parent 4252b7f commit 9301d7a

22 files changed

+890
-16
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import org.springframework.beans.factory.BeanFactory;
2727
import org.springframework.beans.factory.HierarchicalBeanFactory;
2828
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
29+
import org.springframework.beans.metrics.ApplicationStartup;
2930
import org.springframework.core.convert.ConversionService;
3031
import org.springframework.lang.Nullable;
3132
import org.springframework.util.StringValueResolver;
@@ -276,6 +277,20 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single
276277
@Nullable
277278
Scope getRegisteredScope(String scopeName);
278279

280+
/**
281+
* Set the {@code ApplicationStartup} for this bean factory.
282+
* <p>This allows the application context to record metrics during application startup.
283+
* @param applicationStartup the new application startup
284+
* @since 5.3.0
285+
*/
286+
void setApplicationStartup(ApplicationStartup applicationStartup);
287+
288+
/**
289+
* Return the {@code ApplicationStartup} for this bean factory.
290+
* @since 5.3.0
291+
*/
292+
ApplicationStartup getApplicationStartup();
293+
279294
/**
280295
* Provides a security access control context relevant to this factory.
281296
* @return the applicable AccessControlContext (never {@code null})

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
7070
import org.springframework.beans.factory.config.Scope;
7171
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
72+
import org.springframework.beans.metrics.ApplicationStartup;
73+
import org.springframework.beans.metrics.StartupStep;
7274
import org.springframework.core.AttributeAccessor;
7375
import org.springframework.core.DecoratingClassLoader;
7476
import org.springframework.core.NamedThreadLocal;
@@ -178,6 +180,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
178180
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
179181
new NamedThreadLocal<>("Prototype beans currently in creation");
180182

183+
/** Application startup metrics. **/
184+
private ApplicationStartup applicationStartup = ApplicationStartup.getDefault();
181185

182186
/**
183187
* Create a new AbstractBeanFactory.
@@ -297,6 +301,11 @@ else if (requiredType != null) {
297301
}
298302

299303
try {
304+
StartupStep beanCreation = this.applicationStartup.start("spring.beans.instantiate")
305+
.tag("beanName", name);
306+
if (requiredType != null) {
307+
beanCreation.tag("beanType", requiredType::toString);
308+
}
300309
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
301310
checkMergedBeanDefinition(mbd, beanName, args);
302311

@@ -374,6 +383,7 @@ else if (mbd.isPrototype()) {
374383
throw new ScopeNotActiveException(beanName, scopeName, ex);
375384
}
376385
}
386+
beanCreation.end();
377387
}
378388
catch (BeansException ex) {
379389
cleanupAfterBeanCreationFailure(beanName);
@@ -1044,6 +1054,17 @@ public void setSecurityContextProvider(SecurityContextProvider securityProvider)
10441054
this.securityContextProvider = securityProvider;
10451055
}
10461056

1057+
@Override
1058+
public void setApplicationStartup(ApplicationStartup applicationStartup) {
1059+
Assert.notNull(applicationStartup, "applicationStartup should not be null");
1060+
this.applicationStartup = applicationStartup;
1061+
}
1062+
1063+
@Override
1064+
public ApplicationStartup getApplicationStartup() {
1065+
return this.applicationStartup;
1066+
}
1067+
10471068
/**
10481069
* Delegate the creation of the access control context to the
10491070
* {@link #setSecurityContextProvider SecurityContextProvider}.
@@ -1380,7 +1401,7 @@ protected RootBeanDefinition getMergedBeanDefinition(
13801401
else {
13811402
throw new NoSuchBeanDefinitionException(parentBeanName,
13821403
"Parent name '" + parentBeanName + "' is equal to bean name '" + beanName +
1383-
"': cannot be resolved without a ConfigurableBeanFactory parent");
1404+
"': cannot be resolved without a ConfigurableBeanFactory parent");
13841405
}
13851406
}
13861407
}
@@ -2068,7 +2089,7 @@ public void replaceAll(UnaryOperator<BeanPostProcessor> operator) {
20682089
super.replaceAll(operator);
20692090
beanPostProcessorCache = null;
20702091
}
2071-
};
2092+
}
20722093

20732094

20742095
/**

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
7272
import org.springframework.beans.factory.config.DependencyDescriptor;
7373
import org.springframework.beans.factory.config.NamedBeanHolder;
74+
import org.springframework.beans.metrics.StartupStep;
7475
import org.springframework.core.OrderComparator;
7576
import org.springframework.core.ResolvableType;
7677
import org.springframework.core.annotation.MergedAnnotation;
@@ -564,7 +565,7 @@ private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSi
564565
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
565566
}
566567
}
567-
else {
568+
else {
568569
if (includeNonSingletons || isNonLazyDecorated ||
569570
(allowFactoryBeanInit && isSingleton(beanName, mbd, dbd))) {
570571
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
@@ -937,6 +938,8 @@ public void preInstantiateSingletons() throws BeansException {
937938
for (String beanName : beanNames) {
938939
Object singletonInstance = getSingleton(beanName);
939940
if (singletonInstance instanceof SmartInitializingSingleton) {
941+
StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize")
942+
.tag("beanName", beanName);
940943
SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
941944
if (System.getSecurityManager() != null) {
942945
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
@@ -947,6 +950,7 @@ public void preInstantiateSingletons() throws BeansException {
947950
else {
948951
smartSingleton.afterSingletonsInstantiated();
949952
}
953+
smartInitialize.end();
950954
}
951955
}
952956
}
@@ -1672,7 +1676,7 @@ protected String determineHighestPriorityCandidate(Map<String, Object> candidate
16721676
if (candidatePriority.equals(highestPriority)) {
16731677
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
16741678
"Multiple beans found with the same priority ('" + highestPriority +
1675-
"') among candidates: " + candidates.keySet());
1679+
"') among candidates: " + candidates.keySet());
16761680
}
16771681
else if (candidatePriority < highestPriority) {
16781682
highestPriorityBeanName = candidateBeanName;
@@ -1758,7 +1762,7 @@ private void raiseNoMatchingBeanFound(
17581762

17591763
throw new NoSuchBeanDefinitionException(resolvableType,
17601764
"expected at least 1 bean which qualifies as autowire candidate. " +
1761-
"Dependency annotations: " + ObjectUtils.nullSafeToString(descriptor.getAnnotations()));
1765+
"Dependency annotations: " + ObjectUtils.nullSafeToString(descriptor.getAnnotations()));
17621766
}
17631767

17641768
/**
@@ -1803,6 +1807,7 @@ private Optional<?> createOptionalDependency(
18031807
public boolean isRequired() {
18041808
return false;
18051809
}
1810+
18061811
@Override
18071812
public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory) {
18081813
return (!ObjectUtils.isEmpty(args) ? beanFactory.getBean(beanName, args) :
@@ -2019,6 +2024,7 @@ public Object getIfUnique() throws BeansException {
20192024
public boolean isRequired() {
20202025
return false;
20212026
}
2027+
20222028
@Override
20232029
@Nullable
20242030
public Object resolveNotUnique(ResolvableType type, Map<String, Object> matchingBeans) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans.metrics;
18+
19+
/**
20+
* Instruments the application startup phase using {@link StartupStep steps}.
21+
* <p>The core container and its infrastructure components can use the {@code ApplicationStartup}
22+
* to mark steps during the application startup and collect data about the execution context
23+
* or their processing time.
24+
*
25+
* @author Brian Clozel
26+
* @since 5.3.0
27+
*/
28+
public interface ApplicationStartup {
29+
30+
/**
31+
* Return a default "no op" {@code ApplicationStartup} implementation.
32+
* <p>This variant is designed for minimal overhead and does not record data.
33+
*/
34+
static ApplicationStartup getDefault() {
35+
return new DefaultApplicationStartup();
36+
}
37+
38+
/**
39+
* Create a new step and marks its beginning.
40+
* <p>A step name describes the current action or phase. This technical
41+
* name should be "." namespaced and can be reused to describe other instances of
42+
* the same step during application startup.
43+
* @param name the step name
44+
*/
45+
StartupStep start(String name);
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans.metrics;
18+
19+
import java.util.Collections;
20+
import java.util.Iterator;
21+
import java.util.function.Supplier;
22+
23+
/**
24+
* Default "no op" {@code ApplicationStartup} implementation.
25+
* <p>This variant is designed for minimal overhead and does not record events.
26+
*
27+
* @author Brian Clozel
28+
*/
29+
class DefaultApplicationStartup implements ApplicationStartup {
30+
31+
@Override
32+
public DefaultStartupStep start(String name) {
33+
return new DefaultStartupStep();
34+
}
35+
36+
static class DefaultStartupStep implements StartupStep {
37+
38+
boolean recorded = false;
39+
40+
private final DefaultTags TAGS = new DefaultTags();
41+
42+
@Override
43+
public String getName() {
44+
return "default";
45+
}
46+
47+
@Override
48+
public long getId() {
49+
return 0L;
50+
}
51+
52+
@Override
53+
public Long getParentId() {
54+
return null;
55+
}
56+
57+
@Override
58+
public Tags tags() {
59+
return this.TAGS;
60+
}
61+
62+
@Override
63+
public StartupStep tag(String key, String value) {
64+
if (this.recorded) {
65+
throw new IllegalArgumentException();
66+
}
67+
return this;
68+
}
69+
70+
@Override
71+
public StartupStep tag(String key, Supplier<String> value) {
72+
if (this.recorded) {
73+
throw new IllegalArgumentException();
74+
}
75+
return this;
76+
}
77+
78+
@Override
79+
public void end() {
80+
this.recorded = true;
81+
}
82+
83+
static class DefaultTags implements StartupStep.Tags {
84+
85+
@Override
86+
public Iterator<StartupStep.Tag> iterator() {
87+
return Collections.emptyIterator();
88+
}
89+
}
90+
}
91+
92+
}

0 commit comments

Comments
 (0)