Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot transitive @Component dependencies with @ConditionalOnBean

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

like image 771
gotson Avatar asked May 11 '18 07:05

gotson


2 Answers

@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.

like image 132
Stephane Nicoll Avatar answered Sep 24 '22 14:09

Stephane Nicoll


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.

like image 27
OptimalChoice Avatar answered Sep 24 '22 14:09

OptimalChoice