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 07c33922a0..27ad39ee3a 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 @@ -24,6 +24,7 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -100,6 +101,15 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** Names of beans currently excluded from in creation checks. */ private final Set inCreationCheckExclusions = ConcurrentHashMap.newKeySet(16); + /** Specific lock for lenient creation tracking. */ + private final Lock lenientCreationLock = new ReentrantLock(); + + /** Specific lock condition for lenient creation tracking. */ + private final Condition lenientCreationFinished = this.lenientCreationLock.newCondition(); + + /** Names of beans that are currently in lenient creation. */ + private final Set singletonsInLenientCreation = new HashSet<>(); + /** Flag that indicates whether we're currently within destroySingletons. */ private volatile boolean singletonsCurrentlyInDestruction = false; @@ -243,6 +253,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock(); boolean acquireLock = !Boolean.FALSE.equals(lockFlag); boolean locked = (acquireLock && this.singletonLock.tryLock()); + boolean lenient = false; try { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { @@ -257,6 +268,14 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements Thread.currentThread().getName() + "\" while other thread holds " + "singleton lock for other beans " + this.singletonsCurrentlyInCreation); } + lenient = true; + this.lenientCreationLock.lock(); + try { + this.singletonsInLenientCreation.add(beanName); + } + finally { + this.lenientCreationLock.unlock(); + } } else { // No specific locking indication (outside a coordinated bootstrap) and @@ -285,7 +304,24 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements } catch (BeanCurrentlyInCreationException ex) { if (locked) { - throw ex; + this.lenientCreationLock.lock(); + try { + while ((singletonObject = this.singletonObjects.get(beanName)) == null) { + if (!this.singletonsInLenientCreation.contains(beanName)) { + throw ex; + } + try { + this.lenientCreationFinished.await(); + } + catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + return singletonObject; + } + finally { + this.lenientCreationLock.unlock(); + } } // Try late locking for waiting on specific bean to be finished. this.singletonLock.lock(); @@ -339,6 +375,16 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements if (locked) { this.singletonLock.unlock(); } + if (lenient) { + this.lenientCreationLock.lock(); + try { + this.singletonsInLenientCreation.remove(beanName); + this.lenientCreationFinished.signalAll(); + } + finally { + this.lenientCreationLock.unlock(); + } + } } } 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 index 3a0dc34c28..ec186e4ccd 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -19,14 +19,12 @@ package org.springframework.context.annotation; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.springframework.beans.factory.BeanCurrentlyInCreationException; import org.springframework.beans.factory.ObjectProvider; 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.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -42,8 +40,19 @@ class BackgroundBootstrapTests { void bootstrapWithUnmanagedThread() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class); ctx.getBean("testBean1", TestBean.class); - assertThatExceptionOfType(BeanCurrentlyInCreationException.class).isThrownBy( // late - not during refresh - () -> ctx.getBean("testBean2", TestBean.class)); + ctx.getBean("testBean2", TestBean.class); + ctx.close(); + } + + @Test + @Timeout(5) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithUnmanagedThreads() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.getBean("testBean3", TestBean.class); + ctx.getBean("testBean4", TestBean.class); ctx.close(); } @@ -55,6 +64,7 @@ class BackgroundBootstrapTests { ctx.getBean("testBean1", TestBean.class); ctx.getBean("testBean2", TestBean.class); ctx.getBean("testBean3", TestBean.class); + ctx.getBean("testBean4", TestBean.class); ctx.close(); } @@ -87,6 +97,45 @@ class BackgroundBootstrapTests { } + @Configuration(proxyBeanMethods = false) + static class UnmanagedThreadsBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean3, ObjectProvider testBean4) { + new Thread(testBean3::getObject).start(); + new Thread(testBean4::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(TestBean testBean4) { + return new TestBean(testBean4); + } + + @Bean + public TestBean testBean3(TestBean testBean4) { + return new TestBean(testBean4); + } + + @Bean + public TestBean testBean4() { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + } + + @Configuration(proxyBeanMethods = false) static class CustomExecutorBeanConfig { @@ -117,8 +166,8 @@ class BackgroundBootstrapTests { } @Bean - public String dependent(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) { - return ""; + public TestBean testBean4(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) { + return new TestBean(); } }