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