Affects: 5.3.7
Imagine a configuration like that:
// module A depends on module B
@Configuration
@Import(ConfigB.class)
public class ConfigA {
@Bean
public SomeBean someBean() {
return new SomeBean();
}
}
// module B
@Configuration
public class ConfigB {
@Bean
@Conditional(ConfigB.SomeBeanExistsCondition.class)
public DependantBean dependantBean(SomeBean someBean) {
return new DependantBean(someBean);
}
public static class SomeBeanExistsCondition implements ConfigurationCondition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getBeanFactory().getBeanNamesForType(SomeBean.class).length != 0;
}
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
}
}
Now, if I write a test in module A with configuration for test ConfigA, then test context would not contain a bean of type DependantBean. It happens because context.getBeanFactory().getBeanNamesForType(SomeBean.class) search only for beans, that registered via @Component annotation, but not beans exposed via @Bean factory methods is counted in this process, nor imported directly via @Import(SomeBean.class)
Test examples
### Passing testpackage my.package;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.context.annotation.Import;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import my.package.impl.SomeBean;
@SpringJUnitConfig(TestSuccessDependencies.ConfigA.class)
public class TestSuccessDependencies {
private final BeanFactory beanFactory;
@Autowired
public TestSuccessDependencies(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Test
public void testThatAllBeansArePresent() {
Assertions.assertDoesNotThrow(() -> beanFactory.getBean(SomeBean.class));
Assertions.assertDoesNotThrow(() -> beanFactory.getBean(DependantBean.class));
}
@Configuration
@Import({ConfigB.class})
@ComponentScan("my.package.impl") // here placed the @Component public class SomeBean{}
public static class ConfigA {
}
@Configuration
public static class ConfigB {
@Bean
@Conditional(TestSuccessDependencies.ConfigB.Condition.class)
public DependantBean dependantBean(SomeBean someBean) {
return new DependantBean(someBean);
}
public static class Condition implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getBeanFactory().getBeanNamesForType(SomeBean.class).length != 0;
}
}
}
public static class DependantBean {
public DependantBean(SomeBean someBean) {
}
}
}
### Failing test
package my.package;
import javax.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.context.annotation.Import;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(TestFailureDependencies.ConfigA.class)
public class TestFailureDependencies {
private final BeanFactory beanFactory;
@Autowired
public TestFailureDependencies(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Test
public void testThatAllBeanIsPresent() {
Assertions.assertDoesNotThrow(() -> beanFactory.getBean(SomeBean.class));
Assertions.assertDoesNotThrow(() -> beanFactory.getBean(DependantBean.class));
}
@Configuration
public static class ConfigB {
@Bean
@Conditional(TestFailureDependencies.ConfigB.Condition.class)
public DependantBean dependantBean(SomeBean someBean) {
return new DependantBean(someBean);
}
public static class Condition implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getBeanFactory().getBeanNamesForType(SomeBean.class).length != 0;
}
}
}
@Configuration
@Import(ConfigB.class)
public static class ConfigA {
@Bean
public SomeBean someBean() {
return new SomeBean();
}
}
public static class DependantBean {
public DependantBean(SomeBean someBean) {
}
}
public static class SomeBean {
}
}
I think, that this is the bug. But maybe I'm wrong? Could anyone clarify?
Comment From: sbrannen
The behavior you have described is based on the order in which elements are processed in ConfigurationClassParser.doProcessConfigurationClass().
https://github.com/spring-projects/spring-framework/blob/7820804bf6d07635d6f28c607ecde9243db4628f/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java#L287-L328
The order is:
- Process
@ComponentScanannotations - Process
@Importannotations - Process
@ImportResourceannotations - Process
@Beanmethods - ...
Note that @ComponentScan processing results in eager bean definition registration for components detected via scanning, which explains why TestSuccessDependencies passes.
Comment From: gallyamb
@sbrannen The code snippet you provided is responsible for all @Configurations' classes parsing. The actual condition evaluation happens after the ConfigurationClassParser.parse at the this.reader.loadBeanDefinitions(configClasses);
https://github.com/spring-projects/spring-framework/blob/7820804bf6d07635d6f28c607ecde9243db4628f/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java#L350-L362
At this point all parsing already performed. But beanFactory still unable to find bean names for required type, if it's registered via @Bean annotated method. AFAIU, the beanFactory already knows about bean, but for some reason (that I recognise as a bug) it could not. Am I wrong?
Comment From: gallyamb
Could anyone verify my thoughts about this issue?
Comment From: gallyamb
@sbrannen sorry for tagging you, but I don't know who else could help me. What do you think about this comment?
Comment From: gallyamb
Hey, team! Who else can help me with initial discussion in this issue?
Comment From: gallyamb
Still pinging this issue in hope that anyone will take a look at this :)
Comment From: gallyamb
@sbrannen hi! Could you take a look at my answer above?
Comment From: snicoll
I am a bit confused because we mention component scanning and I don't see it in your example.
Unfortunately, re-implementing the condition that exists in Spring Boot may require more work than what you've done. If you can showcase that using the Spring Boot-based condition breaks as well, it might be a good opportunity to share a sample that we can run ourselves, rather than code in text.
Comment From: gallyamb
Hi, @snicoll! Thank you for your reply
I am a bit confused because we mention component scanning and I don't see it in your example
Issue description contains a "Test examples" section, where "Passing test" part contains and example, where component scanning is takes place
re-implementing the condition that exists in Spring Boot may require more work than what you've done
Of course, I understand that. But my goal is not to re-implement condition from Spring Boot, but to solve my certain case. And I've faced with a bug (IMO)
If you can showcase that using the Spring Boot-based condition breaks as well
The project I'm worked on used plain Spring, not Spring Boot (this is why I implement my own condition). And now I'm no longer work on that project (even don't use Spring now). So, unfortunately, I can't provide any further test case
But I can't understand, why already given test cases are not enough?
that we can run ourselves, rather than code in text
My bad, I didn't packed the test cases into a runnable example
Comment From: snicoll
Issue description contains a "Test examples" section, where "Passing test" part contains and example, where component scanning is takes place
Alright, it was hidden behind the section which is why I've missed it. Code in text like that are not a great candidate for a reproducer as we'd have to copy them over and miss a vital step in doing so. Also, I believe preparing a sample from a code snippet is not the best use of our time helping the community.
The component scanning is definitely not something you should be doing. If you want to pursue this, please share a small sample that reproduces the problem. You can do that by attaching a zip to this issue or pushing the code to a GitHub repository.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-projects-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.