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

Viewed 964

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.

  • Custom Autoconfiguration class
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...
}
  • Importer configuration:
package myproject.lib.autoconfigure

@ConditionalOnBean(ConnectionFactory.class)
@Configuration
@Import(SpecificConfig.class)
public class ImportSpecificConfig {
   @Bean
   public SomeBean aBean(){
     return new SomeBean();
   }
}
  • Old regular configuration (which also imports a bunch of things)
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?

1 Answers

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.

Related