From 17b20871980618246ca8c45f516585fc14642876 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 27 Feb 2024 22:33:18 +0100 Subject: [PATCH] Introduce background bootstrapping for individual singleton beans Closes gh-13410 Closes gh-19487 See gh-23501 --- .../BeanCurrentlyInCreationException.java | 6 +- .../config/ConfigurableBeanFactory.java | 20 ++- .../support/AbstractBeanDefinition.java | 35 ++++ .../factory/support/AbstractBeanFactory.java | 5 +- .../support/DefaultListableBeanFactory.java | 150 ++++++++++++++++-- .../support/DefaultSingletonBeanRegistry.java | 70 +++++--- .../ConfigurableApplicationContext.java | 13 +- .../context/annotation/Bean.java | 38 +++++ ...onfigurationClassBeanDefinitionReader.java | 5 + .../support/AbstractApplicationContext.java | 49 +++--- .../annotation/BackgroundBootstrapTests.java | 83 ++++++++++ 11 files changed, 410 insertions(+), 64 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java index 4c984fb12784..5f5fc7b99d30 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java @@ -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?"); } /** 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 737746018bad..1b7c0598ef17 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-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.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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 2add2e4672b5..450098ae7af2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -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 @@ protected AbstractBeanDefinition(BeanDefinition original) { 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 void overrideFrom(BeanDefinition other) { if (otherAbd.hasMethodOverrides()) { getMethodOverrides().addOverrides(otherAbd.getMethodOverrides()); } + setBackgroundInit(otherAbd.isBackgroundInit()); Boolean lazyInit = otherAbd.getLazyInit(); if (lazyInit != null) { setLazyInit(lazyInit); @@ -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. + *

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. + *

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} *

The default is {@code false}. 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 6e71eb5cd955..b10cb39ded83 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 @@ -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); } 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 51ee1a84845d..2c8d4dc794de 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 @@ -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; @@ -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; @@ -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; /** @@ -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 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 preInstantiationThread = + new NamedThreadLocal<>("Pre-instantiation thread marker"); + /** * Create a new DefaultListableBeanFactory. @@ -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 @@ -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()); @@ -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()) { @@ -965,24 +1014,34 @@ public void preInstantiateSingletons() throws BeansException { List 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> 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 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 @@ -2395,4 +2517,10 @@ public Object getOrderSource(Object obj) { } } + + private enum PreInstantiation { + + MAIN, BACKGROUND; + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 9e7b34dc11b7..66c0bb4a8c52 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -125,11 +125,6 @@ public void registerSingleton(String beanName, Object singletonObject) throws Il 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 void registerSingleton(String beanName, Object singletonObject) throws Il * @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 Object getSingleton(String beanName) { */ @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 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { 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 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { 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 Object getSingleton(String beanName, ObjectFactory singletonFactory) { } } + /** + * Determine whether the current thread is allowed to hold the singleton lock. + *

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. 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 662fdb2bfca1..1641d5fb920b 100644 --- a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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.context; import java.io.Closeable; +import java.util.concurrent.Executor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; @@ -53,6 +54,16 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life */ String CONFIG_LOCATION_DELIMITERS = ",; \t\n"; + /** + * The name of the {@link Executor bootstrap executor} bean in the context. + * If none is supplied, no background bootstrapping will be active. + * @since 6.2 + * @see java.util.concurrent.Executor + * @see org.springframework.core.task.TaskExecutor + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setBootstrapExecutor + */ + String BOOTSTRAP_EXECUTOR_BEAN_NAME = "bootstrapExecutor"; + /** * Name of the ConversionService bean in the factory. * If none is supplied, default conversion rules apply. diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java index a921a4bd2bc4..e83557c9cd44 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java @@ -260,6 +260,20 @@ */ boolean defaultCandidate() default true; + /** + * The bootstrap mode for this bean: default is the main pre-instantiation thread + * for non-lazy singleton beans and the caller thread for prototype beans. + *

Set {@link Bootstrap#BACKGROUND} 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 org.springframework.context.ConfigurableApplicationContext#refresh()}. + * 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. + * @since 6.2 + * @see Lazy + */ + Bootstrap bootstrap() default Bootstrap.DEFAULT; + /** * The optional name of a method to call on the bean instance during initialization. * Not commonly used, given that the method may be called programmatically directly @@ -299,4 +313,28 @@ */ String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; + + /** + * Local enumeration for the bootstrap mode. + * @since 6.2 + * @see #bootstrap() + */ + enum Bootstrap { + + /** + * Constant to indicate the main pre-instantiation thread for non-lazy + * singleton beans and the caller thread for prototype beans. + */ + DEFAULT, + + /** + * Allow for instantiating a bean on a background thread. + *

For a non-lazy singleton, a background pre-instantiation thread + * can be used while still enforcing the completion on context refresh. + * For a lazy singleton, a background pre-instantiation thread can be used + * with completion allowed at a later point (when actually accessed). + */ + BACKGROUND, + } + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index df5888469c20..c49e5736b106 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -251,6 +251,11 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { beanDef.setDefaultCandidate(false); } + Bean.Bootstrap instantiation = bean.getEnum("bootstrap"); + if (instantiation == Bean.Bootstrap.BACKGROUND) { + beanDef.setBackgroundInit(true); + } + String initMethodName = bean.getString("initMethod"); if (StringUtils.hasText(initMethodName)) { beanDef.setInitMethodName(initMethodName); 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 557c507f3f26..dc8ffe3d9396 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 @@ -26,6 +26,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -137,17 +138,6 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { - /** - * The name of the {@link LifecycleProcessor} bean in the context. - * If none is supplied, a {@link DefaultLifecycleProcessor} is used. - * @since 3.0 - * @see org.springframework.context.LifecycleProcessor - * @see org.springframework.context.support.DefaultLifecycleProcessor - * @see #start() - * @see #stop() - */ - public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; - /** * The name of the {@link MessageSource} bean in the context. * If none is supplied, message resolution is delegated to the parent. @@ -168,6 +158,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader */ public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; + /** + * The name of the {@link LifecycleProcessor} bean in the context. + * If none is supplied, a {@link DefaultLifecycleProcessor} is used. + * @since 3.0 + * @see org.springframework.context.LifecycleProcessor + * @see org.springframework.context.support.DefaultLifecycleProcessor + * @see #start() + * @see #stop() + */ + public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; + static { // Eagerly load the ContextClosedEvent class to avoid weird classloader issues @@ -806,8 +807,9 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa } /** - * Initialize the MessageSource. - * Use parent's if none defined in this context. + * Initialize the {@link MessageSource}. + *

Uses parent's {@code MessageSource} if none defined in this context. + * @see #MESSAGE_SOURCE_BEAN_NAME */ protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); @@ -837,8 +839,9 @@ protected void initMessageSource() { } /** - * Initialize the ApplicationEventMulticaster. - * Uses SimpleApplicationEventMulticaster if none defined in the context. + * Initialize the {@link ApplicationEventMulticaster}. + *

Uses {@link SimpleApplicationEventMulticaster} if none defined in the context. + * @see #APPLICATION_EVENT_MULTICASTER_BEAN_NAME * @see org.springframework.context.event.SimpleApplicationEventMulticaster */ protected void initApplicationEventMulticaster() { @@ -861,15 +864,16 @@ protected void initApplicationEventMulticaster() { } /** - * Initialize the LifecycleProcessor. - * Uses DefaultLifecycleProcessor if none defined in the context. + * Initialize the {@link LifecycleProcessor}. + *

Uses {@link DefaultLifecycleProcessor} if none defined in the context. + * @since 3.0 + * @see #LIFECYCLE_PROCESSOR_BEAN_NAME * @see org.springframework.context.support.DefaultLifecycleProcessor */ protected void initLifecycleProcessor() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) { - this.lifecycleProcessor = - beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); + this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); if (logger.isTraceEnabled()) { logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]"); } @@ -929,6 +933,13 @@ protected void registerListeners() { * initializing all remaining singleton beans. */ protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize bootstrap executor for this context. + if (beanFactory.containsBean(BOOTSTRAP_EXECUTOR_BEAN_NAME) && + beanFactory.isTypeMatch(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)) { + beanFactory.setBootstrapExecutor( + beanFactory.getBean(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)); + } + // Initialize conversion service for this context. if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) && beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java new file mode 100644 index 000000000000..8ea9ba0db194 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -0,0 +1,83 @@ +/* + * 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. + * 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.annotation; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Juergen Hoeller + * @since 6.2 + */ +class BackgroundBootstrapTests { + + @Test + @Timeout(5) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCustomExecutor() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.getBean("testBean3", TestBean.class); + ctx.close(); + } + + + @Configuration + static class CustomExecutorBeanConfig { + + @Bean + public ThreadPoolTaskExecutor bootstrapExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("Custom-"); + executor.setCorePoolSize(2); + executor.initialize(); + return executor; + } + + @Bean(bootstrap = BACKGROUND) @DependsOn("testBean3") + public TestBean testBean1(TestBean testBean3) throws InterruptedException{ + Thread.sleep(3000); + return new TestBean(); + } + + @Bean(bootstrap = BACKGROUND) @Lazy + public TestBean testBean2() throws InterruptedException { + Thread.sleep(3000); + return new TestBean(); + } + + @Bean @Lazy + public TestBean testBean3() { + return new TestBean(); + } + + @Bean + public String dependent(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) { + return ""; + } + } + +}