-
Notifications
You must be signed in to change notification settings - Fork 41.3k
Description
One of the application team reported an issue and I found this slightly unintuitive behavior between condition evaluation of @ConditionalOn*Bean
and spring's autowiring resolution for method argument.
Sample Code:
public class AnotherApplication {
public static void main(String[] args) {
SpringApplication.run(Config.class, "--debug");
}
@SpringBootConfiguration(proxyBeanMethods = false)
static class Config {
@Bean
Parent foo() {
return new Child("FOO");
}
@Bean
Child bar() {
return new Child("BAR");
}
// @Bean
// Parent baz() {
// return new Child("BAZ");
// }
@Bean
@ConditionalOnSingleCandidate(Child.class) // <<<--
InitializingBean init(Child child) { // <<<--
return () -> {
System.out.println("initialized");
};
}
}
static class Parent {
String name;
public Parent(String name) {
this.name = name;
}
}
static class Child extends Parent {
public Child(String name) {
super(name);
}
}
}
Condition evaluation report:
Positive matches:
-----------------
AnotherApplication.Config#init matched:
- @ConditionalOnSingleCandidate (types: com.example.AnotherApplication$Child; SearchStrategy: all) found a primary bean from beans 'bar' (OnBeanCondition)
Failure:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of method init in com.example.AnotherApplication$Config required a single bean, but 2 were found:
- foo: defined by method 'foo' in com.example.AnotherApplication$Config
- bar: defined by method 'bar' in com.example.AnotherApplication$Config
Analysis
While performing OnBeanCondition
(@ConditionalOnSingleCandidate
), it compares the target type(Child
) against factory-method-return-type of each @Bean
- Parent
for bean foo
and Child
for bar
.
Therefore, Child
matches to bar
bean and satisfies the condition.
The details of this process is, in OnBeanCondition
, it calls beanFactory.getBeanNamesForType
to collect bean names for the specified type(Child
). In that, it calls AbstractBeanFactory#getSingleton
. At this time, spring did not create raw beans yet, thus, the beanInstance
becomes null
. Then, it goes to check the bean definition's factoryMethodReturnType
against specified condition type Child
.
On the other hand, when spring performs autowiring for method arguments, it also calls
beanFactory.getBeanNamesForType
. At this time, raw beans are available, and it finds the actual bean instances. Then, it proceeds to perform instanceof
check against the method argument type Child
.
This instanceof
matches to both foo
(Parent
) and bar
(Child
) beans. Therefore, it detects two beans and fails to resolve which one to apply for the method argument.
There is another not intuitive behavior.
In following situation, it will not match the condition because the return type of foo
is Parent
.
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
Parent foo() {
return new Child("FOO");
}
@Bean
@ConditionalOnBean(Child.class)
InitializingBean init(Child child) {
...
}
}
Negative matches:
-----------------
AnotherApplication.Config#init:
Did not match:
- @ConditionalOnBean (types: com.example.AnotherApplication$Child; SearchStrategy: all) did not find any beans of type com.example.AnotherApplication$Child (OnBeanCondition)
However, once I add Child bar()
bean, suddenly the condition matches but autoconfiguration for Child
fails with finding two beans - foo
and bar
.
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
Parent foo() {
return new Child("FOO");
}
@Bean
Child bar() {
return new Child("BAR");
}
@Bean
@ConditionalOnBean(Child.class)
InitializingBean init(Child child) {
...
}
}
Positive matches:
-----------------
AnotherApplication.Config#init matched:
- @ConditionalOnBean (types: com.example.AnotherApplication$Child; SearchStrategy: all) found bean 'bar' (OnBeanCondition)
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of method init in com.example.AnotherApplication$Config required a single bean, but 2 were found:
- foo: defined by method 'foo' in com.example.AnotherApplication$Config
- bar: defined by method 'bar' in com.example.AnotherApplication$Config
This suddenly matching multiple beans issue has reported in our application and I found this behavior.
I understand this is possibly working as designed.
The difference is due to the behavior of beanfactory.getBeanNamesForType
which changes slightly different based on the stage this method is called. And also due to that bean instances cannot be checked while processing conditions.
It might be a corner case, but may be good to document such behavior on @ConditonalOn*Bean
annotations.