From 9bf01df2302b05f9b31cdcc759c58e6c31bc49fa Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Mar 2025 20:45:06 +0100 Subject: [PATCH 1/2] Evaluate lenientLockingAllowed flag per DefaultListableBeanFactory instance See gh-34303 --- .../beans/factory/support/DefaultListableBeanFactory.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 95b3a058ff..cf395ec5c8 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 @@ -138,8 +138,6 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto */ public static final String STRICT_LOCKING_PROPERTY_NAME = "spring.locking.strict"; - private static final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); - @Nullable private static Class jakartaInjectProviderClass; @@ -159,6 +157,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private static final Map> serializableFactories = new ConcurrentHashMap<>(8); + /** Whether lenient locking is allowed in this factory. */ + private final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); + /** Optional id for this factory, for serialization purposes. */ @Nullable private String serializationId; @@ -1051,7 +1052,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @Override @Nullable protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (lenientLockingAllowed && this.preInstantiationPhase ? + return (this.lenientLockingAllowed && this.preInstantiationPhase ? this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); } From 75e5a75da5bf95f333d359f6164f81e2f71bee5a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Mar 2025 20:46:09 +0100 Subject: [PATCH 2/2] 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 {