Skip to content

Commit 75e5a75

Browse files
committed
Enforce circular reference exception within non-managed thread
Closes gh-34672
1 parent 9bf01df commit 75e5a75

File tree

2 files changed

+64
-2
lines changed

2 files changed

+64
-2
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
110110
/** Names of beans that are currently in lenient creation. */
111111
private final Set<String> singletonsInLenientCreation = new HashSet<>();
112112

113+
/** Map from bean name to actual creation thread for leniently created beans. */
114+
private final Map<String, Thread> lenientCreationThreads = new ConcurrentHashMap<>();
115+
113116
/** Flag that indicates whether we're currently within destroySingletons. */
114117
private volatile boolean singletonsCurrentlyInDestruction = false;
115118

@@ -307,6 +310,9 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
307310
if (!this.singletonsInLenientCreation.contains(beanName)) {
308311
break;
309312
}
313+
if (this.lenientCreationThreads.get(beanName) == Thread.currentThread()) {
314+
throw ex;
315+
}
310316
try {
311317
this.lenientCreationFinished.await();
312318
}
@@ -344,7 +350,18 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
344350
// Leniently created singleton object could have appeared in the meantime.
345351
singletonObject = this.singletonObjects.get(beanName);
346352
if (singletonObject == null) {
347-
singletonObject = singletonFactory.getObject();
353+
if (locked) {
354+
singletonObject = singletonFactory.getObject();
355+
}
356+
else {
357+
this.lenientCreationThreads.put(beanName, Thread.currentThread());
358+
try {
359+
singletonObject = singletonFactory.getObject();
360+
}
361+
finally {
362+
this.lenientCreationThreads.remove(beanName);
363+
}
364+
}
348365
newSingleton = true;
349366
}
350367
}

spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java

+46-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import org.junit.jupiter.api.Test;
2020
import org.junit.jupiter.api.Timeout;
2121

22+
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
2223
import org.springframework.beans.factory.ObjectProvider;
24+
import org.springframework.beans.factory.UnsatisfiedDependencyException;
2325
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
2426
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
2527
import org.springframework.beans.testfixture.beans.TestBean;
@@ -29,6 +31,7 @@
2931
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
3032

3133
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3235
import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND;
3336
import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;
3437

@@ -85,6 +88,15 @@ void bootstrapWithCircularReference() {
8588
ctx.close();
8689
}
8790

91+
@Test
92+
@Timeout(5)
93+
@EnabledForTestGroups(LONG_RUNNING)
94+
void bootstrapWithCircularReferenceInSameThread() {
95+
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
96+
.isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInSameThreadBeanConfig.class))
97+
.withRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
98+
}
99+
88100
@Test
89101
@Timeout(5)
90102
@EnabledForTestGroups(LONG_RUNNING)
@@ -179,7 +191,7 @@ public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
179191
catch (InterruptedException ex) {
180192
throw new RuntimeException(ex);
181193
}
182-
return new TestBean();
194+
return new TestBean("testBean1");
183195
}
184196

185197
@Bean
@@ -217,6 +229,39 @@ public TestBean testBean2(TestBean testBean1) {
217229
}
218230

219231

232+
@Configuration(proxyBeanMethods = false)
233+
static class CircularReferenceInSameThreadBeanConfig {
234+
235+
@Bean
236+
public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
237+
new Thread(testBean2::getObject).start();
238+
try {
239+
Thread.sleep(1000);
240+
}
241+
catch (InterruptedException ex) {
242+
throw new RuntimeException(ex);
243+
}
244+
return new TestBean();
245+
}
246+
247+
@Bean
248+
public TestBean testBean2(TestBean testBean3) {
249+
try {
250+
Thread.sleep(2000);
251+
}
252+
catch (InterruptedException ex) {
253+
throw new RuntimeException(ex);
254+
}
255+
return new TestBean();
256+
}
257+
258+
@Bean
259+
public TestBean testBean3(TestBean testBean2) {
260+
return new TestBean();
261+
}
262+
}
263+
264+
220265
@Configuration(proxyBeanMethods = false)
221266
static class CustomExecutorBeanConfig {
222267

0 commit comments

Comments
 (0)