Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
alexey-anufriev opened this issue Jul 16, 2024 · 10 comments
Assignees
Labels
type: bug A general bug
Milestone

Comments

@alexey-anufriev
Copy link

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

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jul 16, 2024
@wilkinsona
Copy link
Member

wilkinsona commented Jul 16, 2024

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 wilkinsona changed the title Exception in TaskExecutorMetricsAutoConfiguration for Executor marked as autowireCandidate = false @ConditionalOnBean matches beans that are not autowire candidates resulting in UnsatisfiedDependencyException when an attempt is made to inject the bean Jul 16, 2024
@wilkinsona wilkinsona added the for: team-meeting An issue we'd like to discuss as a team to make progress label Jul 16, 2024
@wilkinsona
Copy link
Member

@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
Copy link
Author

@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 philwebb added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged for: team-meeting An issue we'd like to discuss as a team to make progress labels Jul 22, 2024
@philwebb philwebb added this to the 3.4.x milestone Jul 22, 2024
@philwebb
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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
Copy link
Contributor

quaff commented Aug 21, 2024

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

@philwebb
Copy link
Member

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

@quaff
Copy link
Contributor

quaff commented Aug 21, 2024

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
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug A general bug
Projects
None yet
Development

No branches or pull requests

5 participants