Hi there,
I faced another behavioral difference between AutoConfigurationImportSelector and ApplicationContextRunner.
When @Conditional exists on auto configuration class, with ApplcicationContextRunner#withConfiguration using AutoConfigurations.of, the evaluation of Conditional happens early(at import time, not at processing auto configurations).
Here is the usecase and sudo code:
I am trying to control autoconfiguration (enable/disable) based on the annotation on user config.
// User Configuration
@Configuration
@EnableX // this enables MyAutoConfiguration
public static class MyUserConfig {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyImportSelector.class)
public @interface EnableX {
}
public static class MyImportSelector implements ImportSelector {
public static boolean enabled; // flag to enable/disable ConditionalOnX on MyAutoConfiguration
@Override
public String[] selectImports(AnnotationMetadata metadata) {
enabled = true;
return new String[0]; // return empty
}
}
// Auto Configuration
@Configuration
@ConditionalOnX
static class MyAutoConfiguration {
@Bean
public String foo() {
return "FOO";
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnXCondition.class)
public @interface ConditionalOnX {
}
public static class OnXCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
return MyImportSelector.enabled ? ConditionOutcome.match("enabled") : ConditionOutcome.noMatch("disabled");
}
}
@Test
void contextRunnerWithConditionOnAutoConfiguration() {
new ApplicationContextRunner()
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO))
.withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class))
.withUserConfiguration(MyUserConfig.class)
.run(context -> {
assertThat(context)
.hasNotFailed()
.hasBean("foo")
;
});
}
What I am trying here is based on the static boolean variable, MyImportSelector.enabled, activated via @EnableX annotation on user's config, decides whether to apply MyAutoConfiguration controlled by @ConditionalOnX annotation.
For normal case, since AutoConfigurationImportSelector defers the import of autoconfigurations, the evaludation of @Conditional happens at deferred import time. Thus, OnXCondition evaluation is guaranteed to be performed after normal configurations processing (MyUserConfig/@EnableX).
Therefore, it evaluates MyImportSelector first (where it sets boolean flag to true) triggered by @EnableX, then OnXCondition can read the updated value as part of processing autoconfiguration classes.
On the other hand, with ApplicationContextRunner, it processes OnXCondition then MyImportSelector. This is because in AbstractApplicationContextRunner#configureContext, it simply passes all configurations to context.register. The processing order of auto configurations is guaranteed by AutoConfigurations#getOrder but since it registers all configurations together, the evaluation of @Conditional happens at beginning(not deferred).
I have added some hacky change here: https://github.com/ttddyy/spring-boot/tree/context-runner-autoconfiguration commit: https://github.com/ttddyy/spring-boot/commit/fa16383b316dc7cf49cd7bf6499678f6e91f0925
With this change, the evaluation of auto configuration classes are deferred as well as evaluation of @Conditional on auto configurations.
The one missing part is identifying autoconfigurations in configurations of AbstractApplicationContextRunner since spring-boot-test module doesn't have dependency to spring-boot-autoconfiguration module where AutoConfigurations class is defined.
Relates to #17963
Comment From: wilkinsona
Thanks for the analysis. My initial reaction is that I am not sure that this is something that ApplicationContextRunner should support.
Arguably, your auto-configuration isn't really auto-configuration if an @Enable… annotation is required to enable it. What is the benefit of your current arrangement over having MyImportSelector import MyAutoConfiguration directly? Looking at your code snippets above, I can't see anything that would stop that from working.
Comment From: ttddyy
Hi @wilkinsona,
The reason I make my configuration as auto config is mainly for ordering as well as process it as part of other auto-configurations.
I am writing a common library that is used by several applications to integrate with our infrastructure.
So, I am in need for @ConditionalOnMissingBean as well as @AutoConfigure[Before|After] in my configuration classes. In order to use them properly, it needs to be auto configuration.
Also we require explicitness for enabling such configuration/feature; hence we write @Enable... annotation. Then, each application can choose which features(configurations) to use.
Aside from my usage of conditional on auto configuration, I think it is important to align the behavior between AutoConfigurationImportSelector and ApplicationContextRunner.
Especially context runner is used in test, having different behavior gives difficulty to developers.
Comment From: ttddyy
W.r.t Conditional evaluation on ApplicationContextRunner, another workaround I found is to implement ConfigurationCondition with returning ConfigurationPhase.REGISTER_BEAN in my condition class.
This allows conditional evaluation to happen only at bean registration time, not at import parsing.
Since AutoConfigurations has low priority order, in a way this guarantees my auto config to be processed in deferred fashion(after user configuration is parsed).
For configuration classes in my library, I also tried to create a custom deferred import selector that runs on same AutoConfigurationGroup in order to be processed as part of autoconfiguration semantics(to use @AutoConfigure[Before|After]). However, ImportAutoConfigurationImportSelector, AutoConfigurationImportSelector, and AutoConfigurationGroup are pretty much tied to auto configuration classes; so it is hard to reuse them.
Now, I started thinking to use @AutoConfigure[Before|After] in my library maybe too much to do. Instead, create a custom deferred import selector and gives lower/higher order priority than AutoConfigurationImportSelector in order to run before/after auto configurations. This way, at least my library configurations will run after user application's configurations (to use @ConditionalOn[Missing]Bean), then have control to run either before or after spring-boot autoconfigurations.
Comment From: philwebb
Closing following the discussion in https://github.com/spring-projects/spring-boot/pull/19400#issuecomment-579999264