spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.35k stars 40.51k forks source link

@ConditionalOnBean matches beans that are not autowire candidates resulting in UnsatisfiedDependencyException when an attempt is made to inject the bean #41526

Closed alexey-anufriev closed 2 weeks ago

alexey-anufriev commented 1 month ago

Assuming the following test:

@SpringBootTest
@EnableAutoConfiguration
public class TestAutowireCandidate {

    @Test
    void test() {
    }

    @SpringBootConfiguration
    static class TestConfig {

        @Bean(autowireCandidate = false)
        Executor executor() {
            return new ThreadPoolTaskExecutorBuilder().corePoolSize(1).maxPoolSize(2).build();
        }

    }

}

And having spring-boot-starter-actuator and micrometer-core in dependencies the test fails with:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration': 
Unsatisfied dependency expressed through method 'bindTaskExecutorsToRegistry' parameter 0: 
No qualifying bean of type 'java.util.Map<java.lang.String, java.util.concurrent.Executor>' available: 
expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

Reproducer: https://github.com/alexey-anufriev/autowire-candidate-error

wilkinsona commented 1 month ago

Thanks for the report.

This appears to be a more general problem and is not specific to TaskExecutorMetricsAutoConfiguration. For example, if you define a DataSource with autowireCandidate = false, JdbcTemplateAutoConfiguration will fail like this:

 org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jdbcTemplate' defined in org.springframework.boot.autoconfigure.jdbc.JdbcTemplateConfiguration: Unsatisfied dependency expressed through method 'jdbcTemplate' parameter 0: No qualifying bean of type 'javax.sql.DataSource' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:546)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1351)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:563)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:336)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:296)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:334)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.instantiateSingleton(DefaultListableBeanFactory.java:1115)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingleton(DefaultListableBeanFactory.java:1086)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1025)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:987)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:627)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.configureContext(AbstractApplicationContextRunner.java:428)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.createAndLoadContext(AbstractApplicationContextRunner.java:403)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.lambda$4(AbstractApplicationContextRunner.java:389)
    at org.springframework.boot.test.context.assertj.AssertProviderApplicationContextInvocationHandler.getContextOrStartupFailure(AssertProviderApplicationContextInvocationHandler.java:61)
    at org.springframework.boot.test.context.assertj.AssertProviderApplicationContextInvocationHandler.<init>(AssertProviderApplicationContextInvocationHandler.java:48)
    at org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider.get(ApplicationContextAssertProvider.java:113)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.createAssertableContext(AbstractApplicationContextRunner.java:389)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.consumeAssertableContext(AbstractApplicationContextRunner.java:362)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.lambda$1(AbstractApplicationContextRunner.java:341)
    at org.springframework.boot.test.util.TestPropertyValues.lambda$5(TestPropertyValues.java:174)
    at org.springframework.boot.test.util.TestPropertyValues.applyToSystemProperties(TestPropertyValues.java:188)
    at org.springframework.boot.test.util.TestPropertyValues.applyToSystemProperties(TestPropertyValues.java:173)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.lambda$0(AbstractApplicationContextRunner.java:341)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.withContextClassLoader(AbstractApplicationContextRunner.java:369)
    at org.springframework.boot.test.context.runner.AbstractApplicationContextRunner.run(AbstractApplicationContextRunner.java:340)
    at org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfigurationTests.testWithNonAutowireableDataSource(JdbcTemplateAutoConfigurationTests.java:211)

There's a mismatch between @ConditionalOnBean finding a matching bean in the bean factory and autowiring then being unable to use that bean as it's not an autowiring candidate.

At this point, Spring Boot's auto-configuration isn't compatible with autowireCandidate = false and I don't think there's a workaround. For now, if you want to use auto-configuration, you'll have to avoid defining beans that are not autowire candidates if those beans are the subject of @ConditionalOnBean.

wilkinsona commented 1 month ago

@alexey-anufriev what's your use case for an autowireCandidate = false bean in a Spring Boot app? If you're trying to hide something from other consumers, a common approach is to wrap it in a "container" type such as DefaultSockJsSchedulerContainer in Spring Framework.

alexey-anufriev commented 1 month ago

@alexey-anufriev what's your use case for an autowireCandidate = false bean in a Spring Boot app? If you're trying to hide something from other consumers, a common approach is to wrap it in a "container" type such as DefaultSockJsSchedulerContainer in Spring Framework.

My case is very close to my initially reported code, I have a ThreadPoolTaskExecutor dedicated to a concrete purpose. I could have wrapped it and hide from possible injections in other places in the code but ThreadPoolTaskExecutor itself has a pretty complex lifecycle and I would like Spring to manage it for me. Duplicating all the interfaces and delegating to the wrapped instance would lead to quite a lot of boilerplate code.

philwebb commented 1 month ago

We discussed this today and we consider this a bug, but one that is quite risky to fix before 3.4. We're considering adding some additional attribute to the ...OnBean condition annotations to allow autowireCandidate on the bean definition to be considered.

alexey-anufriev commented 1 month ago

Do you have in mind how will this work? Will the new default behavior be a breaking change? I mean, for now, non-candidates are skipped by default. Will I be required to consider those, or will I be required to exclude those?

wilkinsona commented 1 month ago

We're not yet certain and we'd like to do some experimentation. Right now, I think it's likely that it will be a breaking change and we'll start ignoring non-autowire candidates by default when looking for matching beans. We'll have to wait and see though.

quaff commented 2 weeks ago

It should be backported since Bean#autowireCandidate() is not new feature of Spring Framework, WDYT @wilkinsona

philwebb commented 1 week ago

We think it's a bit risky for a backport since it's a change in behavior.

quaff commented 1 week ago

We think it's a bit risky for a backport since it's a change in behavior.

It's a bug fix, I can't imagine which user case would rely the buggy behavior.

wilkinsona commented 1 week ago

It's also the risk of regression due to unexpected or accidental side-effects. Given that the problem has existed with XML config for Boot's entire life and with Java config since Boot 2.1 yet it was only reported for the first time last month, I see no need to back port the fix and risk destabilising maintenance branches.