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'
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.
What does it mean for us:
-
We need to make our configuration classes as auto-configuration
-
The
BarConfig
must be processed after theFooConfig
.
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:
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: