Skip to content

Recommend that bean definitions provide as much type information as possible #22925

@ttddyy

Description

@ttddyy

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions