diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java index 5d88fc7c0a..073e109a8b 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java @@ -26,6 +26,7 @@ import java.util.function.Supplier; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionCustomizer; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.context.annotation.Configurations; @@ -199,6 +200,17 @@ public abstract class AbstractApplicationContextRunner> initializers = Collections.emptyList(); private TestPropertyValues environmentProperties = TestPropertyValues.empty(); @@ -525,6 +543,7 @@ public abstract class AbstractApplicationContextRunner source) { this.contextFactory = source.contextFactory; this.allowBeanDefinitionOverriding = source.allowBeanDefinitionOverriding; + this.allowCircularReferences = source.allowCircularReferences; this.initializers = source.initializers; this.environmentProperties = source.environmentProperties; this.systemProperties = source.systemProperties; @@ -540,6 +559,12 @@ public abstract class AbstractApplicationContextRunner withAllowCircularReferences(boolean allowCircularReferences) { + RunnerConfiguration config = new RunnerConfiguration<>(this); + config.allowCircularReferences = allowCircularReferences; + return config; + } + private RunnerConfiguration withInitializer(ApplicationContextInitializer initializer) { Assert.notNull(initializer, "Initializer must not be null"); RunnerConfiguration config = new RunnerConfiguration<>(this); diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java index 4a12319aa6..f0fb920872 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import com.google.gson.Gson; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.annotation.UserConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider; @@ -181,6 +184,22 @@ abstract class AbstractApplicationContextRunnerTests { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + }); + } + + @Test + void circularReferencesCanBeAllowed() { + get().withAllowCircularReferences(true) + .withUserConfiguration(ExampleConsumerConfiguration.class, ExampleProducerConfiguration.class) + .run((context) -> assertThat(context).hasNotFailed()); + } + @Test void runWithUserBeanShouldBeRegisteredInOrder() { get().withAllowBeanDefinitionOverriding(true).withBean(String.class, () -> "one") @@ -250,4 +269,41 @@ abstract class AbstractApplicationContextRunnerTests configurers) { + Example example = new Example(); + configurers.orderedStream().forEach((configurer) -> configurer.configure(example)); + return example; + } + + } + + @Configuration(proxyBeanMethods = false) + static class ExampleConsumerConfiguration { + + @Autowired + Example example; + + @Bean + ExampleConfigurer configurer() { + return (example) -> { + }; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index e7196b4e08..e01c000fa4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -36,6 +36,7 @@ import org.springframework.beans.CachedIntrospectionResults; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -212,6 +213,8 @@ public class SpringApplication { private boolean allowBeanDefinitionOverriding; + private boolean allowCircularReferences; + private boolean isCustomEnvironment = false; private boolean lazyInitialization = false; @@ -374,9 +377,12 @@ public class SpringApplication { if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } - if (beanFactory instanceof DefaultListableBeanFactory) { - ((DefaultListableBeanFactory) beanFactory) - .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); + if (beanFactory instanceof AbstractAutowireCapableBeanFactory) { + ((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences); + if (beanFactory instanceof DefaultListableBeanFactory) { + ((DefaultListableBeanFactory) beanFactory) + .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); + } } if (this.lazyInitialization) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); @@ -918,6 +924,17 @@ public class SpringApplication { this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding; } + /** + * Sets whether to allow circular references between beans and automatically try to + * resolve them. Defaults to {@code false}. + * @param allowCircularReferences if circular references are allowed + * @since 2.6.0 + * @see AbstractAutowireCapableBeanFactory#setAllowCircularReferences(boolean) + */ + public void setAllowCircularReferences(boolean allowCircularReferences) { + this.allowCircularReferences = allowCircularReferences; + } + /** * Sets if beans should be initialized lazily. Defaults to {@code false}. * @param lazyInitialization if initialization should be lazy diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java index 684c19d118..515fe29cfc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java @@ -27,6 +27,7 @@ import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.boot.ApplicationContextFactory; import org.springframework.boot.Banner; @@ -600,4 +601,17 @@ public class SpringApplicationBuilder { return this; } + /** + * Whether to allow circular references between beans and automatically try to resolve + * them. + * @param allowCircularReferences whether circular references are allows + * @return the current builder + * @since 2.6.0 + * @see AbstractAutowireCapableBeanFactory#setAllowCircularReferences(boolean) + */ + public SpringApplicationBuilder allowCircularReferences(boolean allowCircularReferences) { + this.application.setAllowCircularReferences(allowCircularReferences); + return this; + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java index a9c58a5039..e51c8f6d32 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java @@ -41,7 +41,12 @@ class BeanCurrentlyInCreationFailureAnalyzer extends AbstractFailureAnalyzer new SpringApplication(ExampleProducerConfiguration.class, + ExampleConsumerConfiguration.class).run("--spring.main.web-application-type=none")) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + void circularReferencesCanBeEnabled() { + assertThatNoException().isThrownBy( + () -> new SpringApplication(ExampleProducerConfiguration.class, ExampleConsumerConfiguration.class).run( + "--spring.main.web-application-type=none", "--spring.main.allow-circular-references=true")); + } + @Test void relaxedBindingShouldWorkBeforeEnvironmentIsPrepared() { SpringApplication application = new SpringApplication(ExampleConfig.class); @@ -1698,4 +1718,41 @@ class SpringApplicationTests { } + static class Example { + + } + + @FunctionalInterface + interface ExampleConfigurer { + + void configure(Example example); + + } + + @Configuration(proxyBeanMethods = false) + static class ExampleProducerConfiguration { + + @Bean + Example example(ObjectProvider configurers) { + Example example = new Example(); + configurers.orderedStream().forEach((configurer) -> configurer.configure(example)); + return example; + } + + } + + @Configuration(proxyBeanMethods = false) + static class ExampleConsumerConfiguration { + + @Autowired + Example example; + + @Bean + ExampleConfigurer configurer() { + return (example) -> { + }; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzerTests.java index 7a828feadc..b7b34bcbcb 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzerTests.java @@ -64,6 +64,7 @@ class BeanCurrentlyInCreationFailureAnalyzerTests { assertThat(lines.get(6)).isEqualTo("↑ ↓"); assertThat(lines.get(7)).startsWith("| three defined in " + CyclicBeanMethodsConfiguration.class.getName()); assertThat(lines.get(8)).isEqualTo("└─────┘"); + assertThat(analysis.getAction()).isNotNull(); } @Test @@ -84,6 +85,7 @@ class BeanCurrentlyInCreationFailureAnalyzerTests { assertThat(lines.get(7)).startsWith( "| " + BeanTwoConfiguration.class.getName() + " (field private " + BeanThree.class.getName()); assertThat(lines.get(8)).isEqualTo("└─────┘"); + assertThat(analysis.getAction()).isNotNull(); } @Test @@ -107,6 +109,7 @@ class BeanCurrentlyInCreationFailureAnalyzerTests { assertThat(lines.get(10)) .startsWith("| three defined in " + CycleReferencedViaOtherBeansConfiguration.class.getName()); assertThat(lines.get(11)).isEqualTo("└─────┘"); + assertThat(analysis.getAction()).isNotNull(); } @Test @@ -120,6 +123,7 @@ class BeanCurrentlyInCreationFailureAnalyzerTests { assertThat(lines.get(2)).isEqualTo("┌──->──┐"); assertThat(lines.get(3)).startsWith("| bean defined in " + SelfReferenceBeanConfiguration.class.getName()); assertThat(lines.get(4)).isEqualTo("└──<-──┘"); + assertThat(analysis.getAction()).isNotNull(); } @Test