Spring Boot version: 2.1.5.RELEASE
I have a @Bean
definition in Java configuration class which is loading to application context via xml. I also have an auto-configuration with @ConditionalOnMissingBean
condition.
Expected result: - Single bean is created
Actual result: - Two beans were created
If I load Java configuration using @Import
annotation or if I add it by sources()
method of SpringApplicationBuilder
it works as expected.
Test case:
package com.example;
import org.junit.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.annotation.UserConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import static org.assertj.core.api.Assertions.assertThat;
public class ConditionalOnMissingBeanTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
@Test
public void testOnMissingBeanConditionWithConfigurationBeanInXml() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(ExampleBeanAutoConfiguration.class))
.withConfiguration(UserConfigurations.of(ExampleBeanXmlConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(ExampleBean.class); // <-- FAILING HERE
assertThat(context.getBean(ExampleBean.class).value).isEqualTo("foo");
});
}
@Test
public void testOnMissingBeanConditionWithConfigurationBeanInJava() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(ExampleBeanAutoConfiguration.class))
.withConfiguration(UserConfigurations.of(ExampleBeanJavaConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(ExampleBean.class);
assertThat(context.getBean(ExampleBean.class).value).isEqualTo("foo");
});
}
@Test
public void testOnMissingBeanConditionWithAutoconfiguration() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(ExampleBeanAutoConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(ExampleBean.class);
assertThat(context.getBean(ExampleBean.class).value).isEqualTo("bar");
});
}
}
class ExampleBean {
final String value;
ExampleBean(String value) {
this.value = value;
}
}
@Configuration
class ExampleBeanJavaConfiguration {
@Bean
public ExampleBean exampleBeanFoo() {
return new ExampleBean("foo");
}
}
@Configuration
@ImportResource(locations = "classpath:/com/example/config.xml")
class ExampleBeanXmlConfiguration {
}
@Configuration
@ConditionalOnMissingBean(ExampleBean.class)
class ExampleBeanAutoConfiguration {
@Bean
public ExampleBean exampleBeanBar() {
return new ExampleBean("bar");
}
}
Xml configuration:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="exampleBeanJavaConfiguration" class="com.example.ExampleBeanJavaConfiguration"/>
</beans>
I debugged code, and found that in testOnMissingBeanConditionWithConfigurationBeanInXml
test case exampleBeanBar
(xml) bean definition is loaded after exampleBeanFoo
(auto-config).
Comment From: wilkinsona
Thanks for the detailed tests that reproduce the problem. They've made triaging this much easier than it might have been.
The problem is caused by going from Java configuration out to XML configuration and then back in to Java configuration. If you change the XML to define ExampleBean
directly, the problem does not occur:
<bean id="exampleBeanFromXml" class="com.example.ExampleBean">
<constructor-arg>
<value>foo</value>
</constructor-arg>
</bean>
With the indirection of going from Java to XML and back to Java again, the problem occurs because configuration class processing does not immediately process the beans registered via @ImportResource
as possible @Configuration
classes. Instead it continues processing the other already-known classes, before looping round and processing any new classes added in the previous pass.
The result of this breadth-first rather than depth-first processing is the following sequence of events:
ExampleBeanXmlConfiguration
is processed- Its
@ImportResource
is processed and a bean forExampleBeanJavaConfiguration
is defined ExampleBeanAutoConfiguration
is processed. There is noExampleBean
soexampleBeanBar
is definedExampleBeanJavaConfiguration
is processed andexampleBeanFoo
is defined.
With the change to the XML described above, the sequence of events becomes the following:
ExampleBeanXmlConfiguration
is processed- Its
@ImportResource
is processed andexampleBeanFromXml
is defined ExampleBeanAutoConfiguration
is processed. There is anExampleBean
soexampleBeanBar
is not defined
Changing the behaviour here will require a change in Spring Framework and I'm not sure it's a change that can be made. Let's transfer this to Framework and see what the team says. It may be that we just need to document a limitation in this section of the docs.
Comment From: snicoll
Sorry this took so long. I confirm we don't intend to invest more time in mixed XML/Java config scenario like this one.