noiaverbale
noiaverbale

Reputation: 1688

Why @ConditionalOnMissingBean is invoked before the bean it should check?

In my Spring Boot application I want beans to be created only if a module is not enabled.

In CoreConfig I've defined two beans (geocoder and travelDistanceCalculator) annotated with @ConditionalOnMissingBean referencing the interfaces those beans should initialize.

@Configuration
class CoreConfig {

    private val logger = loggerFor<CoreConfig>()

    @Bean
    @ConditionalOnMissingBean(Geocoder::class)
    fun geocoder(): Geocoder {
        logger.warn("no Geocoder bean instance has been provided. Instantiating default.")

        return object : Geocoder {
            override fun getGeocode(address: String) = Coordinates.Unavailable
        }
    }

    @Bean
    @ConditionalOnMissingBean(TravelDistanceCalculator::class)
    fun travelDistanceCalculator(): TravelDistanceCalculator {
        logger.warn("no TravelDistanceCalculator bean instance has been provided. Instantiating default.")

        return object : TravelDistanceCalculator {
            override fun getTravelDistanceInKm(origin: Coordinates, destination: Coordinates) = Double.NaN
        }
    }

    // other beans definitions...
}

Then GeolocationConfig defines a bean (HereApiClient) implementing both Geocoder and TravelDistanceCalculator interfaces.

@Configuration
@ConditionalOnProperty("app.geolocation.enable")
class GeolocationConfig {

    @Bean
    fun hereApiClient(
        geolocationProperties: GeolocationProperties,
        restTemplate: RestTemplate
    ): HereApiClient =
        HereApiClient(restTemplate, geolocationProperties)

}

app.geolocation.enable is defined as true in application.yml.

What happens here is that, on startup, geocoder and travelDistanceCalculator default beans defined in CoreConfig are initialized even if GeolocationConfig is enabled and HereApiClient is initialized shortly after them.

What am I missing here?

Upvotes: 5

Views: 11180

Answers (2)

Dirk Deyne
Dirk Deyne

Reputation: 6936

@ConditionalOn(Missing)Bean is intended to be used on auto-configuration classes. see javadoc If used in common configiruation-classes then the outcome depends on whichever configuration is loaded first. See andy-wilkinson answer

In your case you could easily use the same property (you are already providing) to configure the correct beans.

Use @ConditionalOnProperty(name="app.geolocation.enable") on GeolocationConfig

Use @ConditionalOnProperty(name="app.geolocation.enable",havingValue="false") on/in CoreConfig

EDIT

Use @ConditionalOnProperty(name="app.geolocation.enable",havingValue="false",matchIfMissing = true) if you want the CoreConfig to be used if the property is missing.

Upvotes: 3

Andy Wilkinson
Andy Wilkinson

Reputation: 116231

ConditionalOnMissingBean is dependent on the order in which @Configuration classes are processed and their beans are defined. In your example, I suspect that CoreConfig is being processed, its conditions evaluated, and its beans defined before your hereApiClient bean has been defined. As a result, when the missing bean conditions are evaluated, there is no matching bean found and the geocoder and travelDistanceCalculator beans are defined.

The javadoc for ConditionalOnMissingBean makes the following recommendation:

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.

You could follow this recommendation by using @Order on your configuration classes. Alternatively (and preferably, in my opinion) you could make CoreConfig an auto-configuration class by listing it in META-INF/spring.factories under the org.springframework.boot.autoconfigure.EnableAutoConfiguration key and moving it to a package where it will not be picked up by component scanning.

Upvotes: 4

Related Questions