Andy Wilkinson opened SPR-16217 and commented
ConfigurationClassParser populates its knownSuperclasses map during the configuration phase of condition evaluation. This means that a subclass that has a register bean phase condition can cause the superclass to be skipped even if another superclass that's encountered later would have included it.
The above is perhaps best illustrated by some tests:
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.context.annotation.Import;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class KnownSuperclassesBug {
@Test
public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInRegisterBeanPhase() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
RegisterBeanPhaseImportingConfiguration.class)) {
context.getBean("someBean");
}
}
@Test
public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInParseConfigurationPhase() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
ParseConfigurationPhaseImportingConfiguration.class)) {
context.getBean("someBean");
}
}
public static class RegisterBeanPhaseCondition implements ConfigurationCondition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
}
public static class ParseConfigurationPhaseCondition
implements ConfigurationCondition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.PARSE_CONFIGURATION;
}
}
@Import({ RegisterBeanPhaseConditionConfiguration.class, BarConfiguration.class })
public static class RegisterBeanPhaseImportingConfiguration {
}
@Import({ ParseConfigurationPhaseConditionConfiguration.class,
BarConfiguration.class })
public static class ParseConfigurationPhaseImportingConfiguration {
}
public static class BaseConfiguration {
@Bean
public String someBean() {
return "foo";
}
}
@Conditional(RegisterBeanPhaseCondition.class)
public static class RegisterBeanPhaseConditionConfiguration
extends BaseConfiguration {
}
@Conditional(ParseConfigurationPhaseCondition.class)
public static class ParseConfigurationPhaseConditionConfiguration
extends BaseConfiguration {
}
public static class BarConfiguration extends BaseConfiguration {
}
}
My expectation is that both tests will pass. As things stand baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInRegisterBeanPhase fails and baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInParseConfigurationPhase() passes.
The first test fails because RegisterBeanPhaseConditionConfiguration is processed and BaseConfiguration is added to the knownSuperclasses map. When BarConfiguration is processed BaseConfiguration is already in the knownSuperclasses map so its "import" via BarConfiguration is lost. When the register bean phase condition on RegisterBeanPhaseConditionConfiguration is evaluated it doesn't match so it's skipped along with BaseConfiguration that it imports.
Affects: 4.3.12, 5.0.1
Reference URL: https://github.com/spring-projects/spring-boot/pull/11063
Issue Links: - #19538 ImportAware.setImportMetadata not invoked if import inherited from superclass with negative condition - #21690 TrackedConditionEvaluator skips loading bean definitions for configuration classes that should not be skipped
Referenced from: commits https://github.com/spring-projects/spring-framework/commit/47383fce97e8748481e7ffe0896b08366d058ddb, https://github.com/spring-projects/spring-framework/commit/08c95fbcb38645486309074eaef7fcf42b846e38
1 votes, 5 watchers
Comment From: spring-projects-issues
Andy Wilkinson commented
I've lowered the priority to minor. I marked it as major as I thought it was blocking the merge of the referenced Spring Boot pull request. I've just figured out that's not the case.
Comment From: spring-projects-issues
Juergen Hoeller commented
This turns out to be surprisingly non-trivial: The knownSuperclasses handling in ConfigurationClassParser has a deep assumption about locally managing included/excluded superclasses. In order for that to be overridable in the REGISTER_BEAN phase, we'd have to export some common state to ConfigurationClassBeanDefinitionReader... or get rid of the special superclass handling to begin with (but that would change the existing semantics, letting later occurences of the superclass override earlier bean definitions and point to the later configuration class instance).
I'll revisit this for 5.0.3.
Comment From: jhoeller
There is a lot of complexity in the condition evaluation for configuration classes, and I'm inclined to accept the present quirks instead of making it even more complex. In practice, I'd rather recommend against scenarios with such complex assumptions to begin with.
Comment From: jhoeller
The above said, I am going to look at the specifically narrowed #28676 which effectively supersedes this issue.