Skip to content

Commit 01e741d

Browse files
committed
Prohibit circular references by default
Closes gh-27652
1 parent 228e4e3 commit 01e741d

File tree

8 files changed

+193
-8
lines changed

8 files changed

+193
-8
lines changed

spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.beans.factory.config.BeanDefinition;
2727
import org.springframework.beans.factory.config.BeanDefinitionCustomizer;
2828
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
29+
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
2930
import org.springframework.beans.factory.support.BeanNameGenerator;
3031
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
3132
import org.springframework.boot.context.annotation.Configurations;
@@ -199,6 +200,17 @@ public SELF withAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverrid
199200
return newInstance(this.runnerConfiguration.withAllowBeanDefinitionOverriding(allowBeanDefinitionOverriding));
200201
}
201202

203+
/**
204+
* Specify if circular references between beans should be allowed.
205+
* @param allowCircularReferences if circular references between beans are allowed
206+
* @return a new instance with the updated circular references policy
207+
* @since 2.6.0
208+
* @see AbstractAutowireCapableBeanFactory#setAllowCircularReferences(boolean)
209+
*/
210+
public SELF withAllowCircularReferences(boolean allowCircularReferences) {
211+
return newInstance(this.runnerConfiguration.withAllowCircularReferences(allowCircularReferences));
212+
}
213+
202214
/**
203215
* Add an {@link ApplicationContextInitializer} to be called when the context is
204216
* created.
@@ -427,9 +439,13 @@ private A createAssertableContext() {
427439
private C createAndLoadContext() {
428440
C context = this.runnerConfiguration.contextFactory.get();
429441
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
430-
if (beanFactory instanceof DefaultListableBeanFactory) {
431-
((DefaultListableBeanFactory) beanFactory)
432-
.setAllowBeanDefinitionOverriding(this.runnerConfiguration.allowBeanDefinitionOverriding);
442+
if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
443+
((AbstractAutowireCapableBeanFactory) beanFactory)
444+
.setAllowCircularReferences(this.runnerConfiguration.allowCircularReferences);
445+
if (beanFactory instanceof DefaultListableBeanFactory) {
446+
((DefaultListableBeanFactory) beanFactory)
447+
.setAllowBeanDefinitionOverriding(this.runnerConfiguration.allowBeanDefinitionOverriding);
448+
}
433449
}
434450
try {
435451
configureContext(context);
@@ -504,6 +520,8 @@ protected static final class RunnerConfiguration<C extends ConfigurableApplicati
504520

505521
private boolean allowBeanDefinitionOverriding = false;
506522

523+
private boolean allowCircularReferences = false;
524+
507525
private List<ApplicationContextInitializer<? super C>> initializers = Collections.emptyList();
508526

509527
private TestPropertyValues environmentProperties = TestPropertyValues.empty();
@@ -525,6 +543,7 @@ private RunnerConfiguration(Supplier<C> contextFactory) {
525543
private RunnerConfiguration(RunnerConfiguration<C> source) {
526544
this.contextFactory = source.contextFactory;
527545
this.allowBeanDefinitionOverriding = source.allowBeanDefinitionOverriding;
546+
this.allowCircularReferences = source.allowCircularReferences;
528547
this.initializers = source.initializers;
529548
this.environmentProperties = source.environmentProperties;
530549
this.systemProperties = source.systemProperties;
@@ -540,6 +559,12 @@ private RunnerConfiguration<C> withAllowBeanDefinitionOverriding(boolean allowBe
540559
return config;
541560
}
542561

562+
private RunnerConfiguration<C> withAllowCircularReferences(boolean allowCircularReferences) {
563+
RunnerConfiguration<C> config = new RunnerConfiguration<>(this);
564+
config.allowCircularReferences = allowCircularReferences;
565+
return config;
566+
}
567+
543568
private RunnerConfiguration<C> withInitializer(ApplicationContextInitializer<? super C> initializer) {
544569
Assert.notNull(initializer, "Initializer must not be null");
545570
RunnerConfiguration<C> config = new RunnerConfiguration<>(this);

spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,10 @@
2323
import com.google.gson.Gson;
2424
import org.junit.jupiter.api.Test;
2525

26+
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
2627
import org.springframework.beans.factory.BeanDefinitionStoreException;
28+
import org.springframework.beans.factory.ObjectProvider;
29+
import org.springframework.beans.factory.annotation.Autowired;
2730
import org.springframework.boot.context.annotation.UserConfigurations;
2831
import org.springframework.boot.test.context.FilteredClassLoader;
2932
import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider;
@@ -181,6 +184,22 @@ void runDisablesBeanOverridingByDefault() {
181184
});
182185
}
183186

187+
@Test
188+
void runDisablesCircularReferencesByDefault() {
189+
get().withUserConfiguration(ExampleConsumerConfiguration.class, ExampleProducerConfiguration.class)
190+
.run((context) -> {
191+
assertThat(context).hasFailed();
192+
assertThat(context).getFailure().hasRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
193+
});
194+
}
195+
196+
@Test
197+
void circularReferencesCanBeAllowed() {
198+
get().withAllowCircularReferences(true)
199+
.withUserConfiguration(ExampleConsumerConfiguration.class, ExampleProducerConfiguration.class)
200+
.run((context) -> assertThat(context).hasNotFailed());
201+
}
202+
184203
@Test
185204
void runWithUserBeanShouldBeRegisteredInOrder() {
186205
get().withAllowBeanDefinitionOverriding(true).withBean(String.class, () -> "one")
@@ -250,4 +269,41 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)
250269

251270
}
252271

272+
static class Example {
273+
274+
}
275+
276+
@FunctionalInterface
277+
interface ExampleConfigurer {
278+
279+
void configure(Example example);
280+
281+
}
282+
283+
@Configuration(proxyBeanMethods = false)
284+
static class ExampleProducerConfiguration {
285+
286+
@Bean
287+
Example example(ObjectProvider<ExampleConfigurer> configurers) {
288+
Example example = new Example();
289+
configurers.orderedStream().forEach((configurer) -> configurer.configure(example));
290+
return example;
291+
}
292+
293+
}
294+
295+
@Configuration(proxyBeanMethods = false)
296+
static class ExampleConsumerConfiguration {
297+
298+
@Autowired
299+
Example example;
300+
301+
@Bean
302+
ExampleConfigurer configurer() {
303+
return (example) -> {
304+
};
305+
}
306+
307+
}
308+
253309
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.beans.factory.config.BeanDefinition;
3737
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3838
import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader;
39+
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
3940
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
4041
import org.springframework.beans.factory.support.BeanNameGenerator;
4142
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
@@ -212,6 +213,8 @@ public class SpringApplication {
212213

213214
private boolean allowBeanDefinitionOverriding;
214215

216+
private boolean allowCircularReferences;
217+
215218
private boolean isCustomEnvironment = false;
216219

217220
private boolean lazyInitialization = false;
@@ -374,9 +377,12 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab
374377
if (printedBanner != null) {
375378
beanFactory.registerSingleton("springBootBanner", printedBanner);
376379
}
377-
if (beanFactory instanceof DefaultListableBeanFactory) {
378-
((DefaultListableBeanFactory) beanFactory)
379-
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
380+
if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
381+
((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences);
382+
if (beanFactory instanceof DefaultListableBeanFactory) {
383+
((DefaultListableBeanFactory) beanFactory)
384+
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
385+
}
380386
}
381387
if (this.lazyInitialization) {
382388
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
@@ -918,6 +924,17 @@ public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverridi
918924
this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding;
919925
}
920926

927+
/**
928+
* Sets whether to allow circular references between beans and automatically try to
929+
* resolve them. Defaults to {@code false}.
930+
* @param allowCircularReferences if circular references are allowed
931+
* @since 2.6.0
932+
* @see AbstractAutowireCapableBeanFactory#setAllowCircularReferences(boolean)
933+
*/
934+
public void setAllowCircularReferences(boolean allowCircularReferences) {
935+
this.allowCircularReferences = allowCircularReferences;
936+
}
937+
921938
/**
922939
* Sets if beans should be initialized lazily. Defaults to {@code false}.
923940
* @param lazyInitialization if initialization should be lazy

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Set;
2828
import java.util.concurrent.atomic.AtomicBoolean;
2929

30+
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
3031
import org.springframework.beans.factory.support.BeanNameGenerator;
3132
import org.springframework.boot.ApplicationContextFactory;
3233
import org.springframework.boot.Banner;
@@ -600,4 +601,17 @@ public SpringApplicationBuilder applicationStartup(ApplicationStartup applicatio
600601
return this;
601602
}
602603

604+
/**
605+
* Whether to allow circular references between beans and automatically try to resolve
606+
* them.
607+
* @param allowCircularReferences whether circular references are allows
608+
* @return the current builder
609+
* @since 2.6.0
610+
* @see AbstractAutowireCapableBeanFactory#setAllowCircularReferences(boolean)
611+
*/
612+
public SpringApplicationBuilder allowCircularReferences(boolean allowCircularReferences) {
613+
this.application.setAllowCircularReferences(allowCircularReferences);
614+
return this;
615+
}
616+
603617
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ protected FailureAnalysis analyze(Throwable rootFailure, BeanCurrentlyInCreation
4141
if (dependencyCycle == null) {
4242
return null;
4343
}
44-
return new FailureAnalysis(buildMessage(dependencyCycle), null, cause);
44+
return new FailureAnalysis(buildMessage(dependencyCycle),
45+
"Relying upon circular references is discouraged and they are prohibited by default. "
46+
+ "Update your application to remove the dependency cycle between beans. "
47+
+ "As a last resort, it may be possible to break the cycle automatically be setting "
48+
+ "spring.main.allow-circular-references to true if you have not already done so.",
49+
cause);
4550
}
4651

4752
private DependencyCycle findCycle(Throwable rootFailure) {

spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,13 @@
807807
"description": "Whether bean definition overriding, by registering a definition with the same name as an existing definition, is allowed.",
808808
"defaultValue": false
809809
},
810+
{
811+
"name": "spring.main.allow-circular-references",
812+
"type": "java.lang.Boolean",
813+
"sourceType": "org.springframework.boot.SpringApplication",
814+
"description": "Whether to allow circular references between beans and automatically try to resolve them.",
815+
"defaultValue": false
816+
},
810817
{
811818
"name": "spring.main.banner-mode",
812819
"type": "org.springframework.boot.Banner$Mode",

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242

4343
import org.springframework.beans.CachedIntrospectionResults;
4444
import org.springframework.beans.factory.BeanCreationException;
45+
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
46+
import org.springframework.beans.factory.ObjectProvider;
47+
import org.springframework.beans.factory.UnsatisfiedDependencyException;
48+
import org.springframework.beans.factory.annotation.Autowired;
4549
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
4650
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
4751
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@@ -115,6 +119,7 @@
115119
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
116120
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
117121
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
122+
import static org.assertj.core.api.Assertions.assertThatNoException;
118123
import static org.mockito.ArgumentMatchers.any;
119124
import static org.mockito.ArgumentMatchers.anyString;
120125
import static org.mockito.ArgumentMatchers.argThat;
@@ -1099,6 +1104,21 @@ void beanDefinitionOverridingCanBeEnabled() {
10991104
.getBean("someBean")).isEqualTo("override");
11001105
}
11011106

1107+
@Test
1108+
void circularReferencesAreDisabledByDefault() {
1109+
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
1110+
.isThrownBy(() -> new SpringApplication(ExampleProducerConfiguration.class,
1111+
ExampleConsumerConfiguration.class).run("--spring.main.web-application-type=none"))
1112+
.withRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
1113+
}
1114+
1115+
@Test
1116+
void circularReferencesCanBeEnabled() {
1117+
assertThatNoException().isThrownBy(
1118+
() -> new SpringApplication(ExampleProducerConfiguration.class, ExampleConsumerConfiguration.class).run(
1119+
"--spring.main.web-application-type=none", "--spring.main.allow-circular-references=true"));
1120+
}
1121+
11021122
@Test
11031123
void relaxedBindingShouldWorkBeforeEnvironmentIsPrepared() {
11041124
SpringApplication application = new SpringApplication(ExampleConfig.class);
@@ -1698,4 +1718,41 @@ <E extends ApplicationEvent> E getEvent(Class<E> type) {
16981718

16991719
}
17001720

1721+
static class Example {
1722+
1723+
}
1724+
1725+
@FunctionalInterface
1726+
interface ExampleConfigurer {
1727+
1728+
void configure(Example example);
1729+
1730+
}
1731+
1732+
@Configuration(proxyBeanMethods = false)
1733+
static class ExampleProducerConfiguration {
1734+
1735+
@Bean
1736+
Example example(ObjectProvider<ExampleConfigurer> configurers) {
1737+
Example example = new Example();
1738+
configurers.orderedStream().forEach((configurer) -> configurer.configure(example));
1739+
return example;
1740+
}
1741+
1742+
}
1743+
1744+
@Configuration(proxyBeanMethods = false)
1745+
static class ExampleConsumerConfiguration {
1746+
1747+
@Autowired
1748+
Example example;
1749+
1750+
@Bean
1751+
ExampleConfigurer configurer() {
1752+
return (example) -> {
1753+
};
1754+
}
1755+
1756+
}
1757+
17011758
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzerTests.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ void cyclicBeanMethods() throws IOException {
6464
assertThat(lines.get(6)).isEqualTo("↑ ↓");
6565
assertThat(lines.get(7)).startsWith("| three defined in " + CyclicBeanMethodsConfiguration.class.getName());
6666
assertThat(lines.get(8)).isEqualTo("└─────┘");
67+
assertThat(analysis.getAction()).isNotNull();
6768
}
6869

6970
@Test
@@ -84,6 +85,7 @@ void cycleWithAutowiredFields() throws IOException {
8485
assertThat(lines.get(7)).startsWith(
8586
"| " + BeanTwoConfiguration.class.getName() + " (field private " + BeanThree.class.getName());
8687
assertThat(lines.get(8)).isEqualTo("└─────┘");
88+
assertThat(analysis.getAction()).isNotNull();
8789
}
8890

8991
@Test
@@ -107,6 +109,7 @@ void cycleReferencedViaOtherBeans() throws IOException {
107109
assertThat(lines.get(10))
108110
.startsWith("| three defined in " + CycleReferencedViaOtherBeansConfiguration.class.getName());
109111
assertThat(lines.get(11)).isEqualTo("└─────┘");
112+
assertThat(analysis.getAction()).isNotNull();
110113
}
111114

112115
@Test
@@ -120,6 +123,7 @@ void testSelfReferenceCycle() throws IOException {
120123
assertThat(lines.get(2)).isEqualTo("┌──->──┐");
121124
assertThat(lines.get(3)).startsWith("| bean defined in " + SelfReferenceBeanConfiguration.class.getName());
122125
assertThat(lines.get(4)).isEqualTo("└──<-──┘");
126+
assertThat(analysis.getAction()).isNotNull();
123127
}
124128

125129
@Test

0 commit comments

Comments
 (0)