minioim
minioim

Reputation: 868

Spring @Import is ignoring @ConditionalOnBean defined on the same class

I am trying to set up a custom Spring auto-configuration.

The custom auto-configuration needs to import a regular Spring configuration but only if a Bean is present in context.

So far a simple @ConditionalOnBean on the configuration to be imported should do it as the doc clearly state it:

If a @Configuration class is marked with @Conditional, all of the @Bean methods, @Import annotations, and @ComponentScan annotations associated with that class will be subject to the conditions.

I don't want to set a @ConditionalOnBean on the regular Spring configuration (which is not in the auto-configuration module)

So I created a second Configuration class which is only used to conditionaly import my "old regular configuration"

Here is the relevant code.

package myproject.lib.autoconfigure

@Configuration
@AutoConfigureAfter(RabbitAutoConfiguration.class)
@Import(ImportSpecificConfig.class)
public class ObjectStorageFacadeAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(HttpClient.class)
    public HttpClient httpClient() {
        return HttpClient.newHttpClient();
    }
    
    //a few other beans...
}
package myproject.lib.autoconfigure

@ConditionalOnBean(ConnectionFactory.class)
@Configuration
@Import(SpecificConfig.class)
public class ImportSpecificConfig {
   @Bean
   public SomeBean aBean(){
     return new SomeBean();
   }
}
package other.lib

@Configuration
@Import(CommonConfiguration.class)
@EnableConfigurationProperties(ObjectStorageRabbitQueueProperties.class)
@ComponentScan
public class SpecificConfig {

}

The expected behaviour for me is to not process the @Import(SpecificConfig.class) in the ImportSpecificConfig.class when no ConnexionFactory bean is present. Also it shouldn't instantiate in context the SomeBean bean.

I then wrote a test:

class ObjectStorageFacadeAutoConfigurationTest {
    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(ObjectStorageFacadeAutoConfiguration.class));
    @Test
    void autoConfiguredCacheManagerIsInstrumented() {
        this.contextRunner
                .run((context) -> {
                    assertThat(context).hasSingleBean(HttpClient.class);
                });
    }
}

This test fails at starting the context. Arguing that I'm missing some beans in context required to instantiate a bean defined in... SpecificConfig package.

So my understanding (which I could confirm by using debuger) is that the @Import annotation in ImportSpecificConfig class is used even when the context does not have any bean matching with the @ConditionalOnBean condition. This trigger the @ComponentScan and @Import annotations from the old regular configuration, which fails the test because required beans are not present in context (which is intended).

By the way, the SomeBean is actually not instantiated, as expected.

The weird thing is that if I use the @ConditionalOnBean on the old regular SpecificConfig configuration class, the expected behaviour happens: the @Import(CommonConfiguration.class) is not activated, and then I don't have any problem with context initialization.

But of course it's not a way to do things correctly, since I'm not suppose to be able to change the lib source code.

tl;dr; the @Import in a @Configuration annotated class is evaluated, even if the @Configuration class has a @ConditionalOnBean which evaluates to false. And the bean defined in this @Configuration class are not instantiated, which confirms the fact that @ConditionalOnBean evaluates to false. It seems that the @ComponentScan is the real problem. I believe it shouldn't be evaluated, but it is.

Here is a simple reproductible example https://github.com/fdeguibert/sample

What don't I understand? Is there any trick about this @ConditionalOnBean that I don't see or is it a bad behaviour?

Upvotes: 3

Views: 2891

Answers (2)

Anderson
Anderson

Reputation: 2758

As it mentions in the official API doc,

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.

So the solution is

  1. Make ImportSpecificConfig an auto one. See How-to
  2. Use @ImportAutoConfiguration(ImportSpecificConfig.class) instead of @Import(ImportSpecificConfig.class)

Upvotes: 0

minioim
minioim

Reputation: 868

After more digging and thanks to @AndyWilkinson, I realized my mistake.

@ConditionalOnBean is effectively a REGISTER_BEAN phase condition. So the @Import ignore it, and obviously @ComponentScan ignore it as well since this annotation is needed during PARSE_CONFIGURATION phase.

So if my undestanding is correct, a @ConditionalOnBean applied at a @Configuration type level will actually be used by Beans defined in this @Configuration class and not by the class itself.

A good way to check it was to replace the @ConditionalOnBean with a @Conditional(FalseCondition.class)

And trying to define the FalseCondition in two ways:

  • first:
public class FalseCondition implements ConfigurationCondition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }
}

-> produce the same result as the @ConditionalOnBean : @Import is evaluated

  • second :
public class FalseCondition implements ConfigurationCondition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.PARSE_CONFIGURATION;
    }
}

-> @Import is not evaluated. since this condition applies during parse configuration phase.

Upvotes: 2

Related Questions