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