Introduce background bootstrapping for individual singleton beans

Closes gh-13410
Closes gh-19487
See gh-23501
This commit is contained in:
Juergen Hoeller
2024-02-27 22:33:18 +01:00
parent 9e7c64270b
commit 17b2087198
11 changed files with 410 additions and 64 deletions

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -17,6 +17,7 @@
package org.springframework.beans.factory.config;
import java.beans.PropertyEditor;
import java.util.concurrent.Executor;
import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;
@@ -25,6 +26,7 @@ import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.lang.Nullable;
@@ -146,6 +148,22 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single
@Nullable
BeanExpressionResolver getBeanExpressionResolver();
/**
* Set the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor})
* for background bootstrapping.
* @since 6.2
* @see AbstractBeanDefinition#setBackgroundInit
*/
void setBootstrapExecutor(@Nullable Executor executor);
/**
* Return the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor})
* for background bootstrapping, if any.
* @since 6.2
*/
@Nullable
Executor getBootstrapExecutor();
/**
* Specify a {@link ConversionService} to use for converting
* property values, as an alternative to JavaBeans PropertyEditors.

View File

@@ -173,6 +173,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
private boolean abstractFlag = false;
private boolean backgroundInit = false;
@Nullable
private Boolean lazyInit;
@@ -280,6 +282,7 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
if (originalAbd.hasMethodOverrides()) {
setMethodOverrides(new MethodOverrides(originalAbd.getMethodOverrides()));
}
setBackgroundInit(originalAbd.isBackgroundInit());
Boolean lazyInit = originalAbd.getLazyInit();
if (lazyInit != null) {
setLazyInit(lazyInit);
@@ -358,6 +361,7 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
if (otherAbd.hasMethodOverrides()) {
getMethodOverrides().addOverrides(otherAbd.getMethodOverrides());
}
setBackgroundInit(otherAbd.isBackgroundInit());
Boolean lazyInit = otherAbd.getLazyInit();
if (lazyInit != null) {
setLazyInit(lazyInit);
@@ -572,6 +576,37 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
return this.abstractFlag;
}
/**
* Specify the bootstrap mode for this bean: default is {@code false} for using
* the main pre-instantiation thread for non-lazy singleton beans and the caller
* thread for prototype beans.
* <p>Set this flag to {@code true} to allow for instantiating this bean on a
* background thread. For a non-lazy singleton, a background pre-instantiation
* thread can be used then, while still enforcing the completion at the end of
* {@link DefaultListableBeanFactory#preInstantiateSingletons()}.
* For a lazy singleton, a background pre-instantiation thread can be used as well
* - with completion allowed at a later point, enforcing it when actually accessed.
* <p>Note that this flag may be ignored by bean factories not set up for
* background bootstrapping, always applying single-threaded bootstrapping
* for non-lazy singleton beans.
* @since 6.2
* @see #setLazyInit
* @see DefaultListableBeanFactory#setBootstrapExecutor
*/
public void setBackgroundInit(boolean backgroundInit) {
this.backgroundInit = backgroundInit;
}
/**
* Return the bootstrap mode for this bean: default is {@code false} for using
* the main pre-instantiation thread for non-lazy singleton beans and the caller
* thread for prototype beans.
* @since 6.2
*/
public boolean isBackgroundInit() {
return this.backgroundInit;
}
/**
* {@inheritDoc}
* <p>The default is {@code false}.

View File

@@ -1445,11 +1445,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
* @param mbd the merged bean definition to check
* @param beanName the name of the bean
* @param args the arguments for bean creation, if any
* @throws BeanDefinitionStoreException in case of validation failure
*/
protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args)
throws BeanDefinitionStoreException {
protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) {
if (mbd.isAbstract()) {
throw new BeanIsAbstractException(beanName);
}

View File

@@ -37,7 +37,10 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -69,6 +72,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.NamedBeanHolder;
import org.springframework.core.NamedThreadLocal;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
@@ -83,6 +87,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.CompositeIterator;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -151,6 +156,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
/** Whether to allow eager class loading even for lazy-init beans. */
private boolean allowEagerClassLoading = true;
@Nullable
private Executor bootstrapExecutor;
/** Optional OrderComparator for dependency Lists and arrays. */
@Nullable
private Comparator<Object> dependencyComparator;
@@ -189,6 +197,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
/** Whether bean definition metadata may be cached for all beans. */
private volatile boolean configurationFrozen;
private final NamedThreadLocal<PreInstantiation> preInstantiationThread =
new NamedThreadLocal<>("Pre-instantiation thread marker");
/**
* Create a new DefaultListableBeanFactory.
@@ -273,6 +284,17 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
return this.allowEagerClassLoading;
}
@Override
public void setBootstrapExecutor(@Nullable Executor bootstrapExecutor) {
this.bootstrapExecutor = bootstrapExecutor;
}
@Override
@Nullable
public Executor getBootstrapExecutor() {
return this.bootstrapExecutor;
}
/**
* Set a {@link java.util.Comparator} for dependency Lists and arrays.
* @since 4.0
@@ -319,6 +341,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
if (otherFactory instanceof DefaultListableBeanFactory otherListableFactory) {
this.allowBeanDefinitionOverriding = otherListableFactory.allowBeanDefinitionOverriding;
this.allowEagerClassLoading = otherListableFactory.allowEagerClassLoading;
this.bootstrapExecutor = otherListableFactory.bootstrapExecutor;
this.dependencyComparator = otherListableFactory.dependencyComparator;
// A clone of the AutowireCandidateResolver since it is potentially BeanFactoryAware
setAutowireCandidateResolver(otherListableFactory.getAutowireCandidateResolver().cloneIfNecessary());
@@ -954,6 +977,32 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
return super.obtainInstanceFromSupplier(supplier, beanName, mbd);
}
@Override
protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) {
super.checkMergedBeanDefinition(mbd, beanName, args);
if (mbd.isBackgroundInit()) {
if (this.preInstantiationThread.get() == PreInstantiation.MAIN && getBootstrapExecutor() != null) {
throw new BeanCurrentlyInCreationException(beanName, "Bean marked for background " +
"initialization but requested in mainline thread - declare ObjectProvider " +
"or lazy injection point in dependent mainline beans");
}
}
else {
// Bean intended to be initialized in main bootstrap thread
if (this.preInstantiationThread.get() == PreInstantiation.BACKGROUND) {
throw new BeanCurrentlyInCreationException(beanName, "Bean marked for mainline initialization " +
"but requested in background thread - enforce early instantiation in mainline thread " +
"through depends-on '" + beanName + "' declaration for dependent background beans");
}
}
}
@Override
protected boolean isCurrentThreadAllowedToHoldSingletonLock() {
return (this.preInstantiationThread.get() != PreInstantiation.BACKGROUND);
}
@Override
public void preInstantiateSingletons() throws BeansException {
if (logger.isTraceEnabled()) {
@@ -965,24 +1014,34 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
// Trigger initialization of all non-lazy singleton beans...
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) {
getBean(beanName);
List<CompletableFuture<?>> futures = new ArrayList<>();
this.preInstantiationThread.set(PreInstantiation.MAIN);
try {
for (String beanName : beanNames) {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
if (!mbd.isAbstract() && mbd.isSingleton()) {
CompletableFuture<?> future = preInstantiateSingleton(beanName, mbd);
if (future != null) {
futures.add(future);
}
}
else {
getBean(beanName);
}
}
}
finally {
this.preInstantiationThread.set(null);
}
if (!futures.isEmpty()) {
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
}
catch (CompletionException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getCause());
}
}
// Trigger post-initialization callback for all applicable beans...
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
Object singletonInstance = getSingleton(beanName, false);
if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
StartupStep smartInitialize = getApplicationStartup().start("spring.beans.smart-initialize")
.tag("beanName", beanName);
@@ -992,6 +1051,69 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
@Nullable
private CompletableFuture<?> preInstantiateSingleton(String beanName, RootBeanDefinition mbd) {
if (mbd.isBackgroundInit()) {
Executor executor = getBootstrapExecutor();
if (executor != null) {
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
getBean(dep);
}
}
CompletableFuture<?> future = CompletableFuture.runAsync(
() -> instantiateSingletonInBackgroundThread(beanName), executor);
addSingletonFactory(beanName, () -> {
try {
future.join();
}
catch (CompletionException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getCause());
}
return future; // not to be exposed, just to lead to ClassCastException in case of mismatch
});
return (!mbd.isLazyInit() ? future : null);
}
else if (logger.isInfoEnabled()) {
logger.info("Bean '" + beanName + "' marked for background initialization " +
"without bootstrap executor configured - falling back to mainline initialization");
}
}
if (!mbd.isLazyInit()) {
instantiateSingleton(beanName);
}
return null;
}
private void instantiateSingletonInBackgroundThread(String beanName) {
this.preInstantiationThread.set(PreInstantiation.BACKGROUND);
try {
instantiateSingleton(beanName);
}
catch (RuntimeException | Error ex) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to instantiate singleton bean '" + beanName + "' in background thread", ex);
}
throw ex;
}
finally {
this.preInstantiationThread.set(null);
}
}
private void instantiateSingleton(String beanName) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) {
getBean(beanName);
}
}
else {
getBean(beanName);
}
}
//---------------------------------------------------------------------
// Implementation of BeanDefinitionRegistry interface
@@ -2395,4 +2517,10 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
private enum PreInstantiation {
MAIN, BACKGROUND;
}
}

View File

@@ -125,11 +125,6 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
Assert.notNull(singletonObject, "Singleton object must not be null");
this.singletonLock.lock();
try {
Object oldObject = this.singletonObjects.get(beanName);
if (oldObject != null) {
throw new IllegalStateException("Could not register object [" + singletonObject +
"] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound");
}
addSingleton(beanName, singletonObject);
}
finally {
@@ -144,7 +139,11 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
* @param singletonObject the singleton object
*/
protected void addSingleton(String beanName, Object singletonObject) {
this.singletonObjects.put(beanName, singletonObject);
Object oldObject = this.singletonObjects.putIfAbsent(beanName, singletonObject);
if (oldObject != null) {
throw new IllegalStateException("Could not register object [" + singletonObject +
"] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound");
}
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
@@ -181,17 +180,17 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
*/
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
// Quick check for existing instance without full singleton lock.
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
if (!this.singletonLock.tryLock()) {
// Avoid early singleton inference outside of original creation thread
// Avoid early singleton inference outside of original creation thread.
return null;
}
try {
// Consistent creation of early reference within full singleton lock
// Consistent creation of early reference within full singleton lock.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
@@ -199,8 +198,13 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
// Singleton could have been added or removed in the meantime.
if (this.singletonFactories.remove(beanName) != null) {
this.earlySingletonObjects.put(beanName, singletonObject);
}
else {
singletonObject = this.singletonObjects.get(beanName);
}
}
}
}
@@ -224,30 +228,36 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
boolean locked = this.singletonLock.tryLock();
boolean acquireLock = isCurrentThreadAllowedToHoldSingletonLock();
boolean locked = (acquireLock && this.singletonLock.tryLock());
try {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
if (locked) {
this.singletonCreationThread = Thread.currentThread();
}
else {
Thread otherThread = this.singletonCreationThread;
if (otherThread != null) {
if (acquireLock) {
if (locked) {
this.singletonCreationThread = Thread.currentThread();
}
else {
Thread threadWithLock = this.singletonCreationThread;
// Another thread is busy in a singleton factory callback, potentially blocked.
// Fallback as of 6.2: process given singleton bean outside of singleton lock.
// Thread-safe exposure is still guaranteed, there is just a risk of collisions
// when triggering creation of other beans as dependencies of the current bean.
if (logger.isInfoEnabled()) {
if (threadWithLock != null && logger.isInfoEnabled()) {
logger.info("Creating singleton bean '" + beanName + "' in thread \"" +
Thread.currentThread().getName() + "\" while thread \"" + otherThread.getName() +
Thread.currentThread().getName() + "\" while thread \"" + threadWithLock.getName() +
"\" holds singleton lock for other beans " + this.singletonsCurrentlyInCreation);
}
}
else {
// Singleton lock currently held by some other registration method -> wait.
this.singletonLock.lock();
locked = true;
else {
// Singleton lock currently held by some other registration method -> wait.
this.singletonLock.lock();
locked = true;
// Singleton object might have possibly appeared in the meantime.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject != null) {
return singletonObject;
}
}
}
}
if (this.singletonsCurrentlyInDestruction) {
@@ -305,6 +315,16 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
}
}
/**
* Determine whether the current thread is allowed to hold the singleton lock.
* <p>By default, any thread may acquire and hold the singleton lock, except
* background threads from {@link DefaultListableBeanFactory#setBootstrapExecutor}.
* @since 6.2
*/
protected boolean isCurrentThreadAllowedToHoldSingletonLock() {
return true;
}
/**
* Register an exception that happened to get suppressed during the creation of a
* singleton bean instance, e.g. a temporary circular reference resolution problem.