Skip to content

Commit 17b2087

Browse files
committed
Introduce background bootstrapping for individual singleton beans
Closes gh-13410 Closes gh-19487 See gh-23501
1 parent 9e7c642 commit 17b2087

File tree

11 files changed

+410
-64
lines changed

11 files changed

+410
-64
lines changed

Diff for: spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -32,8 +32,8 @@ public class BeanCurrentlyInCreationException extends BeanCreationException {
3232
* @param beanName the name of the bean requested
3333
*/
3434
public BeanCurrentlyInCreationException(String beanName) {
35-
super(beanName,
36-
"Requested bean is currently in creation: Is there an unresolvable circular reference?");
35+
super(beanName, "Requested bean is currently in creation: "+
36+
"Is there an unresolvable circular reference or an asynchronous initialization dependency?");
3737
}
3838

3939
/**

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

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -17,6 +17,7 @@
1717
package org.springframework.beans.factory.config;
1818

1919
import java.beans.PropertyEditor;
20+
import java.util.concurrent.Executor;
2021

2122
import org.springframework.beans.PropertyEditorRegistrar;
2223
import org.springframework.beans.PropertyEditorRegistry;
@@ -25,6 +26,7 @@
2526
import org.springframework.beans.factory.BeanFactory;
2627
import org.springframework.beans.factory.HierarchicalBeanFactory;
2728
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
29+
import org.springframework.beans.factory.support.AbstractBeanDefinition;
2830
import org.springframework.core.convert.ConversionService;
2931
import org.springframework.core.metrics.ApplicationStartup;
3032
import org.springframework.lang.Nullable;
@@ -146,6 +148,22 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single
146148
@Nullable
147149
BeanExpressionResolver getBeanExpressionResolver();
148150

151+
/**
152+
* Set the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor})
153+
* for background bootstrapping.
154+
* @since 6.2
155+
* @see AbstractBeanDefinition#setBackgroundInit
156+
*/
157+
void setBootstrapExecutor(@Nullable Executor executor);
158+
159+
/**
160+
* Return the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor})
161+
* for background bootstrapping, if any.
162+
* @since 6.2
163+
*/
164+
@Nullable
165+
Executor getBootstrapExecutor();
166+
149167
/**
150168
* Specify a {@link ConversionService} to use for converting
151169
* property values, as an alternative to JavaBeans PropertyEditors.

Diff for: spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java

+35
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
173173

174174
private boolean abstractFlag = false;
175175

176+
private boolean backgroundInit = false;
177+
176178
@Nullable
177179
private Boolean lazyInit;
178180

@@ -280,6 +282,7 @@ protected AbstractBeanDefinition(BeanDefinition original) {
280282
if (originalAbd.hasMethodOverrides()) {
281283
setMethodOverrides(new MethodOverrides(originalAbd.getMethodOverrides()));
282284
}
285+
setBackgroundInit(originalAbd.isBackgroundInit());
283286
Boolean lazyInit = originalAbd.getLazyInit();
284287
if (lazyInit != null) {
285288
setLazyInit(lazyInit);
@@ -358,6 +361,7 @@ public void overrideFrom(BeanDefinition other) {
358361
if (otherAbd.hasMethodOverrides()) {
359362
getMethodOverrides().addOverrides(otherAbd.getMethodOverrides());
360363
}
364+
setBackgroundInit(otherAbd.isBackgroundInit());
361365
Boolean lazyInit = otherAbd.getLazyInit();
362366
if (lazyInit != null) {
363367
setLazyInit(lazyInit);
@@ -572,6 +576,37 @@ public boolean isAbstract() {
572576
return this.abstractFlag;
573577
}
574578

579+
/**
580+
* Specify the bootstrap mode for this bean: default is {@code false} for using
581+
* the main pre-instantiation thread for non-lazy singleton beans and the caller
582+
* thread for prototype beans.
583+
* <p>Set this flag to {@code true} to allow for instantiating this bean on a
584+
* background thread. For a non-lazy singleton, a background pre-instantiation
585+
* thread can be used then, while still enforcing the completion at the end of
586+
* {@link DefaultListableBeanFactory#preInstantiateSingletons()}.
587+
* For a lazy singleton, a background pre-instantiation thread can be used as well
588+
* - with completion allowed at a later point, enforcing it when actually accessed.
589+
* <p>Note that this flag may be ignored by bean factories not set up for
590+
* background bootstrapping, always applying single-threaded bootstrapping
591+
* for non-lazy singleton beans.
592+
* @since 6.2
593+
* @see #setLazyInit
594+
* @see DefaultListableBeanFactory#setBootstrapExecutor
595+
*/
596+
public void setBackgroundInit(boolean backgroundInit) {
597+
this.backgroundInit = backgroundInit;
598+
}
599+
600+
/**
601+
* Return the bootstrap mode for this bean: default is {@code false} for using
602+
* the main pre-instantiation thread for non-lazy singleton beans and the caller
603+
* thread for prototype beans.
604+
* @since 6.2
605+
*/
606+
public boolean isBackgroundInit() {
607+
return this.backgroundInit;
608+
}
609+
575610
/**
576611
* {@inheritDoc}
577612
* <p>The default is {@code false}.

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

+1-4
Original file line numberDiff line numberDiff line change
@@ -1445,11 +1445,8 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous,
14451445
* @param mbd the merged bean definition to check
14461446
* @param beanName the name of the bean
14471447
* @param args the arguments for bean creation, if any
1448-
* @throws BeanDefinitionStoreException in case of validation failure
14491448
*/
1450-
protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args)
1451-
throws BeanDefinitionStoreException {
1452-
1449+
protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) {
14531450
if (mbd.isAbstract()) {
14541451
throw new BeanIsAbstractException(beanName);
14551452
}

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

+139-11
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737
import java.util.Map;
3838
import java.util.Optional;
3939
import java.util.Set;
40+
import java.util.concurrent.CompletableFuture;
41+
import java.util.concurrent.CompletionException;
4042
import java.util.concurrent.ConcurrentHashMap;
43+
import java.util.concurrent.Executor;
4144
import java.util.function.Consumer;
4245
import java.util.function.Predicate;
4346
import java.util.function.Supplier;
@@ -69,6 +72,7 @@
6972
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
7073
import org.springframework.beans.factory.config.DependencyDescriptor;
7174
import org.springframework.beans.factory.config.NamedBeanHolder;
75+
import org.springframework.core.NamedThreadLocal;
7276
import org.springframework.core.OrderComparator;
7377
import org.springframework.core.Ordered;
7478
import org.springframework.core.ResolvableType;
@@ -83,6 +87,7 @@
8387
import org.springframework.util.CollectionUtils;
8488
import org.springframework.util.CompositeIterator;
8589
import org.springframework.util.ObjectUtils;
90+
import org.springframework.util.ReflectionUtils;
8691
import org.springframework.util.StringUtils;
8792

8893
/**
@@ -151,6 +156,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
151156
/** Whether to allow eager class loading even for lazy-init beans. */
152157
private boolean allowEagerClassLoading = true;
153158

159+
@Nullable
160+
private Executor bootstrapExecutor;
161+
154162
/** Optional OrderComparator for dependency Lists and arrays. */
155163
@Nullable
156164
private Comparator<Object> dependencyComparator;
@@ -189,6 +197,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
189197
/** Whether bean definition metadata may be cached for all beans. */
190198
private volatile boolean configurationFrozen;
191199

200+
private final NamedThreadLocal<PreInstantiation> preInstantiationThread =
201+
new NamedThreadLocal<>("Pre-instantiation thread marker");
202+
192203

193204
/**
194205
* Create a new DefaultListableBeanFactory.
@@ -273,6 +284,17 @@ public boolean isAllowEagerClassLoading() {
273284
return this.allowEagerClassLoading;
274285
}
275286

287+
@Override
288+
public void setBootstrapExecutor(@Nullable Executor bootstrapExecutor) {
289+
this.bootstrapExecutor = bootstrapExecutor;
290+
}
291+
292+
@Override
293+
@Nullable
294+
public Executor getBootstrapExecutor() {
295+
return this.bootstrapExecutor;
296+
}
297+
276298
/**
277299
* Set a {@link java.util.Comparator} for dependency Lists and arrays.
278300
* @since 4.0
@@ -319,6 +341,7 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) {
319341
if (otherFactory instanceof DefaultListableBeanFactory otherListableFactory) {
320342
this.allowBeanDefinitionOverriding = otherListableFactory.allowBeanDefinitionOverriding;
321343
this.allowEagerClassLoading = otherListableFactory.allowEagerClassLoading;
344+
this.bootstrapExecutor = otherListableFactory.bootstrapExecutor;
322345
this.dependencyComparator = otherListableFactory.dependencyComparator;
323346
// A clone of the AutowireCandidateResolver since it is potentially BeanFactoryAware
324347
setAutowireCandidateResolver(otherListableFactory.getAutowireCandidateResolver().cloneIfNecessary());
@@ -954,6 +977,32 @@ protected Object obtainInstanceFromSupplier(Supplier<?> supplier, String beanNam
954977
return super.obtainInstanceFromSupplier(supplier, beanName, mbd);
955978
}
956979

980+
@Override
981+
protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) {
982+
super.checkMergedBeanDefinition(mbd, beanName, args);
983+
984+
if (mbd.isBackgroundInit()) {
985+
if (this.preInstantiationThread.get() == PreInstantiation.MAIN && getBootstrapExecutor() != null) {
986+
throw new BeanCurrentlyInCreationException(beanName, "Bean marked for background " +
987+
"initialization but requested in mainline thread - declare ObjectProvider " +
988+
"or lazy injection point in dependent mainline beans");
989+
}
990+
}
991+
else {
992+
// Bean intended to be initialized in main bootstrap thread
993+
if (this.preInstantiationThread.get() == PreInstantiation.BACKGROUND) {
994+
throw new BeanCurrentlyInCreationException(beanName, "Bean marked for mainline initialization " +
995+
"but requested in background thread - enforce early instantiation in mainline thread " +
996+
"through depends-on '" + beanName + "' declaration for dependent background beans");
997+
}
998+
}
999+
}
1000+
1001+
@Override
1002+
protected boolean isCurrentThreadAllowedToHoldSingletonLock() {
1003+
return (this.preInstantiationThread.get() != PreInstantiation.BACKGROUND);
1004+
}
1005+
9571006
@Override
9581007
public void preInstantiateSingletons() throws BeansException {
9591008
if (logger.isTraceEnabled()) {
@@ -965,24 +1014,34 @@ public void preInstantiateSingletons() throws BeansException {
9651014
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
9661015

9671016
// Trigger initialization of all non-lazy singleton beans...
968-
for (String beanName : beanNames) {
969-
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
970-
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
971-
if (isFactoryBean(beanName)) {
972-
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
973-
if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) {
974-
getBean(beanName);
1017+
List<CompletableFuture<?>> futures = new ArrayList<>();
1018+
this.preInstantiationThread.set(PreInstantiation.MAIN);
1019+
try {
1020+
for (String beanName : beanNames) {
1021+
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
1022+
if (!mbd.isAbstract() && mbd.isSingleton()) {
1023+
CompletableFuture<?> future = preInstantiateSingleton(beanName, mbd);
1024+
if (future != null) {
1025+
futures.add(future);
9751026
}
9761027
}
977-
else {
978-
getBean(beanName);
979-
}
1028+
}
1029+
}
1030+
finally {
1031+
this.preInstantiationThread.set(null);
1032+
}
1033+
if (!futures.isEmpty()) {
1034+
try {
1035+
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
1036+
}
1037+
catch (CompletionException ex) {
1038+
ReflectionUtils.rethrowRuntimeException(ex.getCause());
9801039
}
9811040
}
9821041

9831042
// Trigger post-initialization callback for all applicable beans...
9841043
for (String beanName : beanNames) {
985-
Object singletonInstance = getSingleton(beanName);
1044+
Object singletonInstance = getSingleton(beanName, false);
9861045
if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
9871046
StartupStep smartInitialize = getApplicationStartup().start("spring.beans.smart-initialize")
9881047
.tag("beanName", beanName);
@@ -992,6 +1051,69 @@ public void preInstantiateSingletons() throws BeansException {
9921051
}
9931052
}
9941053

1054+
@Nullable
1055+
private CompletableFuture<?> preInstantiateSingleton(String beanName, RootBeanDefinition mbd) {
1056+
if (mbd.isBackgroundInit()) {
1057+
Executor executor = getBootstrapExecutor();
1058+
if (executor != null) {
1059+
String[] dependsOn = mbd.getDependsOn();
1060+
if (dependsOn != null) {
1061+
for (String dep : dependsOn) {
1062+
getBean(dep);
1063+
}
1064+
}
1065+
CompletableFuture<?> future = CompletableFuture.runAsync(
1066+
() -> instantiateSingletonInBackgroundThread(beanName), executor);
1067+
addSingletonFactory(beanName, () -> {
1068+
try {
1069+
future.join();
1070+
}
1071+
catch (CompletionException ex) {
1072+
ReflectionUtils.rethrowRuntimeException(ex.getCause());
1073+
}
1074+
return future; // not to be exposed, just to lead to ClassCastException in case of mismatch
1075+
});
1076+
return (!mbd.isLazyInit() ? future : null);
1077+
}
1078+
else if (logger.isInfoEnabled()) {
1079+
logger.info("Bean '" + beanName + "' marked for background initialization " +
1080+
"without bootstrap executor configured - falling back to mainline initialization");
1081+
}
1082+
}
1083+
if (!mbd.isLazyInit()) {
1084+
instantiateSingleton(beanName);
1085+
}
1086+
return null;
1087+
}
1088+
1089+
private void instantiateSingletonInBackgroundThread(String beanName) {
1090+
this.preInstantiationThread.set(PreInstantiation.BACKGROUND);
1091+
try {
1092+
instantiateSingleton(beanName);
1093+
}
1094+
catch (RuntimeException | Error ex) {
1095+
if (logger.isWarnEnabled()) {
1096+
logger.warn("Failed to instantiate singleton bean '" + beanName + "' in background thread", ex);
1097+
}
1098+
throw ex;
1099+
}
1100+
finally {
1101+
this.preInstantiationThread.set(null);
1102+
}
1103+
}
1104+
1105+
private void instantiateSingleton(String beanName) {
1106+
if (isFactoryBean(beanName)) {
1107+
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
1108+
if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) {
1109+
getBean(beanName);
1110+
}
1111+
}
1112+
else {
1113+
getBean(beanName);
1114+
}
1115+
}
1116+
9951117

9961118
//---------------------------------------------------------------------
9971119
// Implementation of BeanDefinitionRegistry interface
@@ -2395,4 +2517,10 @@ public Object getOrderSource(Object obj) {
23952517
}
23962518
}
23972519

2520+
2521+
private enum PreInstantiation {
2522+
2523+
MAIN, BACKGROUND;
2524+
}
2525+
23982526
}

0 commit comments

Comments
 (0)