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:

  1. ExampleBeanXmlConfiguration is processed
  2. Its @ImportResource is processed and a bean for ExampleBeanJavaConfiguration is defined
  3. ExampleBeanAutoConfiguration is processed. There is no ExampleBean so exampleBeanBar is defined
  4. ExampleBeanJavaConfiguration is processed and exampleBeanFoo is defined.

With the change to the XML described above, the sequence of events becomes the following:

  1. ExampleBeanXmlConfiguration is processed
  2. Its @ImportResource is processed and exampleBeanFromXml is defined
  3. ExampleBeanAutoConfiguration is processed. There is an ExampleBean so exampleBeanBar 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.