1. Preamble

The CTO of our company sent me an interesting case of a configuration, last night. He not fears coding something unusual, this is a good habit for a chief.

He wanted to make a one bean depends on another, and the first of them should created only if he set a certain property. Of course, in this case the second bean should not be created.

2. Let’s try to code this.

We need to define a two objects:

@Slf4j
@Getter
public class Foo {

    private String value = "ops";

    public Foo() {
        log.warn("new Foo()");
    }
}

@Slf4j
@Getter
public class Bar {

    private Foo foo;
    private String value = "dev";

    public Bar(Foo foo) {
        this.foo = foo;
        this.value += foo.getValue();
        log.warn("new Bar({})", foo);
    }
}

Then we make a first configuration file:

@Configuration
public class FooConfig {

    @Bean
    public Foo foo() {
        return new Foo();
    }
}

And a second config, which depends on the first:

@Configuration
@ConditionalOnBean(name = "foo")
public class BarConfig {

    @Bean
    public Bar bar(Foo foo) {
        return new Bar(foo);
    }
}

Write a simple test for this configuration:

@SpringBootTest
@RunWith(SpringRunner.class)
public class ConfigTest {

    @Autowired
    private Foo foo;

    @Autowired
    private Bar bar;

    @Test
    public void simpleTest() {
        Assertions.assertThat(foo).isNotNull();
        Assertions.assertThat(bar).isNotNull();
    }
}

And what we got as a result - No qualifying bean of type 'Bar'

Result of the first test

3. Read the documentation

It’s boring, but this is the simplest way to know how to do what you want. Open a javadoc about the ConditionalOnBean: https://docs.spring.io/ConditionalOnBean.html

And what we see?

The condition can only match the bean definitions that have been processed by the application context so far and, as such, it is strongly recommended to use this condition on auto-configuration classes only. If a candidate bean may be created by another auto-configuration, make sure that the one using this condition runs after.
— Phillip Webb

What does it mean for us:

  • We need to make our configuration classes as auto-configuration

  • The BarConfig must be processed after the FooConfig.

4. How to make auto-configuration class

Under the hood, auto-configuration files are the simple @Configuration classes that using conditional annotations such as @Conditional, @ConditionalOnClass, @ConditionalOnMissingBean, etc.

These annotations allow us to specify constraints and dependencies between the beans in the configuration.

In order for our configuration will be loaded by the Spring, we need to add a path to configuration classes into the spring.factories property file. This file must be located in the META-INF folder of the resources of your’s project.

This properties is processing by SpringFactoriesLoader which loads and instantiates factories of a given type from META-INF/spring.factories.

Be careful, it is dangerous to use extra spaces or line breaks in the contents of the file spring.factories. You must use a key-value format:

[interface | abstract class] = [comma-separated list of implementation or configuration class names]

For example:

spring.factories sample

5. How to change the order of processing a configurations

Now we have an ability to define a dependency between two configuration files. We can do this by using one of the following annotations:

  • @AutoConfigureAfter

  • @AutoConfigureBefore

For our task, we need to mark the BarConfig with the annotation AutoConfigureAfter and specify dependency on the FooConfig class:

@Configuration
@AutoConfigureAfter(FooConfig.class)
@ConditionalOnBean(name = "foo")
public class BarConfig {

    @Bean
    public Bar bar(Foo foo) {
        return new Bar(foo);
    }
}

For the testing a bean conditional we set a profile for the FooConfig:

@Profile("foo")
@Configuration
public class FooConfig {

    @Bean
    public Foo foo() {
        return new Foo();
    }
}

And modify test a bit:

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("foo")
public class ConfigTest {

    @Autowired
    private Foo foo;

    @Autowired
    private Bar bar;

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void contextLoads() {
        Assertions.assertThat(foo).isNotNull();
        Assertions.assertThat(bar).isNotNull();
    }
}

If you don’t set active profile to foo in test then you will not find both of the beans in the context:

@SpringBootTest
@RunWith(SpringRunner.class)
public class ConditionOnBeanTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void beanNotFoundTest() {
        Assertions.assertThat(applicationContext.containsBean("foo")).isFalse();
        Assertions.assertThat(applicationContext.containsBean("bar")).isFalse();
    }
}

All tests passed:

all test passed

6. Source code of this project on the github