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 test
package 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 @ComponentScan annotations
  • Process @Import annotations
  • Process @ImportResource annotations
  • Process @Bean methods
  • ...

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.