Skip to content

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

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

Closed
ttddyy opened this issue Aug 13, 2020 · 1 comment
Closed
Labels
type: documentation A documentation update
Milestone

Comments

@ttddyy
Copy link
Contributor

ttddyy commented Aug 13, 2020

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.

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

As you have surmised, this is working as designed. Condition evaluation can only use the type information that's available in the bean factory. It cannot use any information that's only available once bean creation has begun as doing so would trigger unwanted bean initialization. It's also worth noting that things can behave in a similar way during autowiring if the type information to identify a match isn't available until a bean has been created. In Spring in general it's a good idea for your bean definitions to provide as much type information as possible, but certainly more so when bean-based conditions come into play. We had an issue in the past to provide more information in Boot's own configuration. Some updates to the documentation sound like a good idea to me. Thanks for the suggestion.

@wilkinsona wilkinsona changed the title Different bean resolution in @ConditionalOn*Bean vs autowiring Recommend that bean definitions provide as much type information as possible Aug 13, 2020
@wilkinsona wilkinsona added type: documentation A documentation update and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 13, 2020
@wilkinsona wilkinsona added this to the 2.2.x milestone Aug 13, 2020
@wilkinsona wilkinsona modified the milestones: 2.2.x, 2.2.12 Nov 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: documentation A documentation update
Projects
None yet
Development

No branches or pull requests

3 participants