Affects: 6.1.5
I was trying to override a @Bean
method from the parent class but with a different bean name, I expected the here would only be one bean generated by method in child, but the result was weird and has something to do whether you are using @Import
or @ComponentScan
.
I know this is not a practical usage, but it's still an interesting and intriguing problem.
Example
package com.ai.demo.configtest;
public class MyTestBean {
public MyTestBean(String msg) {
this.msg = msg;
}
String msg;
public String getMsg() {
return msg;
}
}
package com.ai.demo.configtest;
import org.springframework.context.annotation.Bean;
public class BeanInOverriddenParent {
@Bean("ParentBean")
MyTestBean getBeanOverridden() {
return new MyTestBean("parent");
}
}
package com.ai.demo.configtest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanOverridingChild extends BeanInOverriddenParent {
@Bean("ChildBean")
@Override
MyTestBean getBeanOverridden() {
return new MyTestBean("beanMethodOverriddenInChildBean");
}
}
package com.ai.demo.configtest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(BeanOverridingChild.class)
@ComponentScan(basePackageClasses = {TestMain.class})
public class TestMain {
@Autowired
MyTestBean[] myTestBeans;
public TestMain() {
}
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestMain.class);
TestMain bean = applicationContext.getBean(TestMain.class);
for (MyTestBean myTestBean : bean.myTestBeans) {
System.out.println(myTestBean.getMsg());
}
}
}
Case 1: only using @Import(BeanOverridingChild.class)
It produced two beans; the console output are like:
beanMethodOverriddenInChildBean
beanMethodOverriddenInChildBean
````
### Case 2: only using `@ComponentScan(basePackageClasses = {TestMain.class})`
It thrown an `BeanCreationException` in following output:
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'ParentBean' defined in class path resource [com/ai/demo/configtest/BeanOverridingChild.class]: No matching factory method found on class [com.ai.demo.configtest.BeanOverridingChild]: factory bean 'beanOverridingChild'; factory method 'getBeanOverridden()'. Check that a method with the specified name exists and that it is non-static.
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:616)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1335)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1165)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.
Comment From: Aimini
The case 1 isn't hard to understand since I found that the bean definitions is registered into DefaultListableBeanFactory.beanDefinitionMap
by name, And the ConfigurationCLassParser
generates two bean methods within ConfigurationClass
object, which has different name.
But the case 2 is more interesting since it prefer to throw exception and it actually says here is no proper 'getBeanOverridden()'. to construct it.
After some code-digging I found it's has something to do with the code below at line 460
https://github.com/spring-projects/spring-framework/blob/c04290e9ab0b587fe53dec0590aeb45fa55a870e/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java#L459-L461
In the first case it gets a Method
Object and it's com.ai.demo.configtest.MyTestBean com.ai.demo.configtest.BeanInOverriddenParent.getBeanOverridden()
,
In the second case it gets a null
, so the program continues to the
https://github.com/spring-projects/spring-framework/blob/c04290e9ab0b587fe53dec0590aeb45fa55a870e/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java#L466-L474
At below line, it gets all candidates to generate that bean but ignore the methods in super class if there already found a method with the same signature, so it'll only keeps class com.ai.demo.configtest.BeanOverridingChild
.
https://github.com/spring-projects/spring-framework/blob/c04290e9ab0b587fe53dec0590aeb45fa55a870e/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java#L468
But the mbd.isFactoryMethod
at below have to verify if the bean name is equal to the one from the method, and it fails.
https://github.com/spring-projects/spring-framework/blob/c04290e9ab0b587fe53dec0590aeb45fa55a870e/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java#L470
Then after various checks it decides to throw an BeanCreationException
.
https://github.com/spring-projects/spring-framework/blob/c04290e9ab0b587fe53dec0590aeb45fa55a870e/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java#L611
That mbd
object in code mbd.getResolvedFactoryMethod()
is getting factoryMethodToIntrospect
field inside itself, the difference is from
https://github.com/spring-projects/spring-framework/blob/c04290e9ab0b587fe53dec0590aeb45fa55a870e/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java#L232-L234
here can have SimpleAnnotationMetadata
when use ScanComponent
and StandardAnnotationComponent
when use Import
, that explains everything.
I personally think it is a problem and that only keeping bean in sub class is optimal. but on the opposite side I also think this can rarely become a realistic situation, so if it worthy to change the code takes me into dilemma.
Comment From: snicoll
Duplicate of #28286