Reputation: 868
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
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
ImportSpecificConfig
an auto one. See How-to@ImportAutoConfiguration(ImportSpecificConfig.class)
instead of @Import(ImportSpecificConfig.class)
Upvotes: 0
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:
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
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