I have a Spring Boot 1.5.x
project where some @Component
depends on other @Component
, and ultimately along the chain of dependencies, some @Component
can be enabled or disabled completely using @ConditionalOnProperty
.
I am using @ConditionalOnBean
to avoid instantiating @Component
that depends on other @Component
that have not been instantiated because of missing properties
.
However, it works only for direct dependencies, and not for transitive dependencies, but I can't understand why.
Let me try to explain with a simple example.
Considering MyServices.kt
:
private val logger = KotlinLogging.logger {}
class MyServices
@ConditionalOnProperty("service.a")
@Service
class ServiceA {
init {
logger.info { "A SERVICE" }
}
}
@ConditionalOnBean(ServiceA::class)
@ConditionalOnProperty("service.b")
@Service
class ServiceB(
private val serviceA: ServiceA
) {
init {
logger.info { "B SERVICE depends on $serviceA" }
}
}
@ConditionalOnBean(ServiceB::class)
@ConditionalOnProperty("service.c")
@Service
class ServiceC(
private val serviceB: ServiceB
) {
init {
logger.info { "C Service depends on $serviceB" }
}
}
With the following application.yml
:
service:
a: false
b: true
c: true
then Spring crashes at startup with the following:
**************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in org.gotson.transitivebeandependencies.ServiceC required a bean of type 'org.gotson.transitivebeandependencies.ServiceB' that could not be found.
Action:
Consider defining a bean of type 'org.gotson.transitivebeandependencies.ServiceB' in your configuration.
Here is the result of the autoconfiguration:
Positive matches:
ServiceC matched:
- @ConditionalOnProperty (service.c) matched (OnPropertyCondition)
- @ConditionalOnBean (types: org.gotson.transitivebeandependencies.ServiceB; SearchStrategy: all) found bean 'serviceB' (OnBeanCondition)
Negative matches:
ServiceA:
Did not match:
- @ConditionalOnProperty (service.a) found different value in property 'service.a' (OnPropertyCondition)
ServiceB:
Did not match:
- @ConditionalOnBean (types: org.gotson.transitivebeandependencies.ServiceA; SearchStrategy: all) did not find any beans (OnBeanCondition)
Matched:
- @ConditionalOnProperty (service.b) matched (OnPropertyCondition)
However, with the following application.yml
:
service:
a: true
b: false
c: true
then everything works fine, only an instance of ServiceA
gets instantiated, while no ServiceB
nor ServiceC
beans are created.
The same kind of behavior with @Bean
instead of @Component
works as expected.
MyBeans.kt
:
private val logger = KotlinLogging.logger {}
@Configuration
class MyBeans {
@ConditionalOnProperty("bean.a")
@Bean
fun beanA(): BeanA {
logger.info { "A BEAN" }
return BeanA("beanA")
}
@ConditionalOnBean(BeanA::class)
@ConditionalOnProperty("bean.b")
@Bean
fun beanB(beanA: BeanA): BeanB {
logger.info { "B BEAN depends on $beanA" }
return BeanB("beanB")
}
@ConditionalOnBean(BeanB::class)
@ConditionalOnProperty("bean.c")
@Bean
fun beanC(beanB: BeanB): BeanC {
logger.info { "C BEAN depends on $beanB" }
return BeanC("beanC")
}
}
data class BeanA(val name: String)
data class BeanB(val name: String)
data class BeanC(val name: String)
With application.yml
:
bean:
a: false
b: true
c: true
I get no beans of type BeanA
, BeanB
, or BeanC
instantiated.
Here is the result of the autoconfiguration:
Negative matches:
MyBeans#beanA:
Did not match:
- @ConditionalOnProperty (bean.a) found different value in property 'bean.a' (OnPropertyCondition)
MyBeans#beanB:
Did not match:
- @ConditionalOnBean (types: org.gotson.transitivebeandependencies.BeanA; SearchStrategy: all) did not find any beans (OnBeanCondition)
Matched:
- @ConditionalOnProperty (bean.b) matched (OnPropertyCondition)
MyBeans#beanC:
Did not match:
- @ConditionalOnBean (types: org.gotson.transitivebeandependencies.BeanB; SearchStrategy: all) did not find any beans (OnBeanCondition)
Matched:
- @ConditionalOnProperty (bean.c) matched (OnPropertyCondition)
I have setup a sample repo with tests to reproduce: https://github.com/gotson/spring-transitive
@ConditionalOnBean
is a bean registration phase check and, as such, needs to have an overview of what beans are effectivly available in the ApplicationContext
. Beans can be registered in a standard fashion with a regular @Bean
, exposing the same target type as the return type of the method. You may also have FactoryBean
with a more complex logic that can lead to exotic setup we have to handle.
Regardless, ordering is key. Configuration classes have to be processed in a given order if you want bean type matching to work properly. If you have a C1
configuration class that contributes a bean A
only if bean B
is available and said bean is contributed by C2
, C2
has to run first.
Spring Boot has a two steps parsing phase: first we parse all the user's configuration. Once that's done, we parse the bean definitions of auto-configurations. Auto-configurations themselves are ordered (using @AutoConfigureBefore
, @AutoConfigureAfter
). That way, we can guarantee that if you put @ConditionalOnBean
in an auto-configuration, it will be processed as expected with regards to user configuration. And if you rely on something contributed by another auto-configuration, you can easily order it using those annotations.
Your setup completely eludes ordering so it works if the ordering is right and doesn't if it isn't. The Javadoc of @ConditionalOnBean
clearly states that
it is strongly recommended to use this condition on auto-configuration classes only.
If you want to know more, there is a 3h university session available that covers, namely, this very topic on youtube.
The problem here is your definition of controls.
For the @Service example (false,true,true), your control property determines whether the Service is Defined. ServiceA is not defined nor constructed. ServiceB is defined, but not constructed because there is no ServiceA definition. ServiceC is defined and construction is attempted. As indicated, the construction of ServiceC fails due to the absence of an instance of ServiceB.
For the @Bean example (false,true,true), your control property determines whether the Bean is constructed. BeanA is not constructed, BeanB is not constructed because there is no BeanA, and BeanC is not constructed because there is no BeanB.
For the @Service example (true,false,true): ServiceA is defined and constructed, ServiceB is neither defined nor constructed, ServiceC is defined but no construction is attempted because ServiceB is not defined.
Methods decorated with @Bean produce a bean to be managed by the Spring container during configuration stage.
A Java class decorated with @Component (@Service) will be detected during component scan process and registered as a Spring bean.
Because the @Service annotation does not create and register the component in the same way that @Bean does, it is possible that using @ConditionalOnClass instead of @ConditionalOnBean may produce the results you want. There are some caveats regarding this annotation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With