Skip to content

Commit

Permalink
Introduce background bootstrapping for individual singleton beans
Browse files Browse the repository at this point in the history
Closes gh-13410
Closes gh-19487
See gh-23501
  • Loading branch information
jhoeller committed Feb 27, 2024
1 parent 9e7c642 commit 17b2087
Show file tree
Hide file tree
Showing 11 changed files with 410 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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?");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -25,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.factory.support.AbstractBeanDefinition;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.lang.Nullable;
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess

private boolean abstractFlag = false;

private boolean backgroundInit = false;

@Nullable
private Boolean lazyInit;

Expand Down Expand Up @@ -280,6 +282,7 @@ protected AbstractBeanDefinition(BeanDefinition original) {
if (originalAbd.hasMethodOverrides()) {
setMethodOverrides(new MethodOverrides(originalAbd.getMethodOverrides()));
}
setBackgroundInit(originalAbd.isBackgroundInit());
Boolean lazyInit = originalAbd.getLazyInit();
if (lazyInit != null) {
setLazyInit(lazyInit);
Expand Down Expand Up @@ -358,6 +361,7 @@ public void overrideFrom(BeanDefinition other) {
if (otherAbd.hasMethodOverrides()) {
getMethodOverrides().addOverrides(otherAbd.getMethodOverrides());
}
setBackgroundInit(otherAbd.isBackgroundInit());
Boolean lazyInit = otherAbd.getLazyInit();
if (lazyInit != null) {
setLazyInit(lazyInit);
Expand Down Expand Up @@ -572,6 +576,37 @@ public boolean isAbstract() {
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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1445,11 +1445,8 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous,
* @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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
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;
Expand Down Expand Up @@ -69,6 +72,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.core.NamedThreadLocal;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
Expand All @@ -83,6 +87,7 @@
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;

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -273,6 +284,17 @@ public boolean isAllowEagerClassLoading() {
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
Expand Down Expand Up @@ -319,6 +341,7 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) {
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());
Expand Down Expand Up @@ -954,6 +977,32 @@ protected Object obtainInstanceFromSupplier(Supplier<?> supplier, String beanNam
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()) {
Expand All @@ -965,24 +1014,34 @@ public void preInstantiateSingletons() throws BeansException {
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);
Expand All @@ -992,6 +1051,69 @@ public void preInstantiateSingletons() throws BeansException {
}
}

@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
Expand Down Expand Up @@ -2395,4 +2517,10 @@ public Object getOrderSource(Object obj) {
}
}


private enum PreInstantiation {

MAIN, BACKGROUND;
}

}
Loading

0 comments on commit 17b2087

Please sign in to comment.