From 75e5a75da5bf95f333d359f6164f81e2f71bee5a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Mar 2025 20:46:09 +0100 Subject: [PATCH] Enforce circular reference exception within non-managed thread Closes gh-34672 --- .../support/DefaultSingletonBeanRegistry.java | 19 +++++++- .../annotation/BackgroundBootstrapTests.java | 47 ++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) 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 fd88d2c44c..eea12e5ab0 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 @@ -110,6 +110,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** Names of beans that are currently in lenient creation. */ private final Set singletonsInLenientCreation = new HashSet<>(); + /** Map from bean name to actual creation thread for leniently created beans. */ + private final Map lenientCreationThreads = new ConcurrentHashMap<>(); + /** Flag that indicates whether we're currently within destroySingletons. */ private volatile boolean singletonsCurrentlyInDestruction = false; @@ -307,6 +310,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements if (!this.singletonsInLenientCreation.contains(beanName)) { break; } + if (this.lenientCreationThreads.get(beanName) == Thread.currentThread()) { + throw ex; + } try { this.lenientCreationFinished.await(); } @@ -344,7 +350,18 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements // Leniently created singleton object could have appeared in the meantime. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { - singletonObject = singletonFactory.getObject(); + if (locked) { + singletonObject = singletonFactory.getObject(); + } + else { + this.lenientCreationThreads.put(beanName, Thread.currentThread()); + try { + singletonObject = singletonFactory.getObject(); + } + finally { + this.lenientCreationThreads.remove(beanName); + } + } newSingleton = true; } } 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 913cc863d0..bd2071f960 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,7 +19,9 @@ 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.factory.UnsatisfiedDependencyException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.testfixture.beans.TestBean; @@ -29,6 +31,7 @@ import org.springframework.core.testfixture.EnabledForTestGroups; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +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; @@ -85,6 +88,15 @@ class BackgroundBootstrapTests { ctx.close(); } + @Test + @Timeout(5) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceInSameThread() { + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInSameThreadBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + @Test @Timeout(5) @EnabledForTestGroups(LONG_RUNNING) @@ -179,7 +191,7 @@ class BackgroundBootstrapTests { catch (InterruptedException ex) { throw new RuntimeException(ex); } - return new TestBean(); + return new TestBean("testBean1"); } @Bean @@ -217,6 +229,39 @@ class BackgroundBootstrapTests { } + @Configuration(proxyBeanMethods = false) + static class CircularReferenceInSameThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(TestBean testBean3) { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + + @Bean + public TestBean testBean3(TestBean testBean2) { + return new TestBean(); + } + } + + @Configuration(proxyBeanMethods = false) static class CustomExecutorBeanConfig {