I. Introduction
We know that Spring Boot does not allow two beans with the same name because suppose if there are two beans named myBean
how can Spring Boot know which bean to return when the following code is executed?
1 2 | MyBean myBean = context.getBean("myBean"); |
At this point Spring Boot will have to choose between the two and thus may lead to unexpected results. Therefore, from Spring Boot version 2.1
onwards, Spring Boot only allows each bean to have a unique name.
Of course, this is only on a theoretical level. Let’s look at the example below to understand better.
I have an entity AppBean
and two Configuration classes that respectively declare its 2 beans with the same name as appBean
as follows:
AppBean.java
1 2 3 4 5 6 7 8 9 10 11 12 | public class AppBean { private String message; public AppBean (String message) { this.message = message; } public String getMessage () { return message; } } |
MyConfig1.java
1 2 3 4 5 6 7 8 | @Configuration public class MyConfig1 { @Bean AppBean appBean() { return new AppBean("from config 1"); } } |
MyConfig2.java
1 2 3 4 5 6 7 8 | @Configuration public class MyConfig2 { @Bean AppBean appBean() { return new AppBean("from config 2"); } } |
Now I will write a Main
function to get the appBean
bean to use. Can you guess what will happen with the code below?
Main.java
1 2 3 4 5 6 7 8 9 10 11 12 | @SpringBootApplication public class Main { public static void main(String[] args) { System.out.println(SpringBootVersion.getVersion()); //2.7.2 > 2.1 ApplicationContext context = new AnnotationConfigApplicationContext(Main.class); AppBean bean = context.getBean(AppBean.class); System.out.println(bean.getMessage()); } } |
Are you expecting an error message like this?
A bean with that name has already been defined in class path ….
Unfortunately, what you may have been waiting for (including me) did not happen. The program still runs fine and silently prints the following in the console:
from config 2
Try to guess why not from config 1
but from config 2
, I will explain in detail in the next section.
II. Analyze Spring Boot’s source code
In the code in part I , because I declare the bean in @Configuration
class, to get the bean to use it, I need to go through AnnotationConfigApplicationContext
:
ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
Initializing the AnnotationConfigApplicationContext
instance should do the following things:
1. this()
Initialize reader and scanner
2. register(componentClasses);
Register componentClass will register bean with DefaultListableBeanFactory.
After we have registered the componentClass ( @SpringBootApplication
) Main , we already have a bean named main with the corresponding class.
Since the class marked as @SpringBootApplication
will scan the current package and sub-packages to find the bean, you must have figured out what Spring Boot will do next =)).
When @Configuration classes are provided as input, the @Configuration class itself is registered as a bean definition, and all declared @Bean methods within the class are also registered as bean definitions.
1 2 3 4 5 6 | ├── example2 │ ├── AppBean.java │ ├── Main.java │ ├── MyConfig1.java │ └── MyConfig2.java |
3. refresh();
In this function Spring Boot handles a lot of different things, but in the scope of this article what we need to care about most is the invokeBeanFactoryPostProcessors
method that will be used to load and initialize the bean.
To reduce rambling I will always go to the ConfigurationClassPostProcessor class to see how the flow goes. Callstack I will put at the end of the article.
The idea is basically to find the base packages of the component class and then scan @Configuration
class thui.
Callstack:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | doScan:292, ClassPathBeanDefinitionScanner (org.springframework.context.annotation) parse:128, ComponentScanAnnotationParser (org.springframework.context.annotation) doProcessConfigurationClass:296, ConfigurationClassParser (org.springframework.context.annotation) processConfigurationClass:250, ConfigurationClassParser (org.springframework.context.annotation) parse:207, ConfigurationClassParser (org.springframework.context.annotation) parse:175, ConfigurationClassParser (org.springframework.context.annotation) processConfigBeanDefinitions:331, ConfigurationClassPostProcessor (org.springframework.context.annotation) postProcessBeanDefinitionRegistry:247, ConfigurationClassPostProcessor (org.springframework.context.annotation) invokeBeanDefinitionRegistryPostProcessors:311, PostProcessorRegistrationDelegate (org.springframework.context.support) invokeBeanFactoryPostProcessors:112, PostProcessorRegistrationDelegate (org.springframework.context.support) invokeBeanFactoryPostProcessors:746, AbstractApplicationContext (org.springframework.context.support) refresh:564, AbstractApplicationContext (org.springframework.context.support) <init>:93, AnnotationConfigApplicationContext (org.springframework.context.annotation) main:27, Main (org.logbasex.service.import_selector_annotation.example2) |
After the scan is complete, we can add the following 2 configClasses:
Now comes the important stage of loading/registering BeanDefinitions from configClasses.
We can see that when registering the bean, the allowBeanDefinitionOverriding
variable of the DefaultListableBeanFactory class instance has the value = true. So in case the appBean
bean already exists, no Exception is thrown. And appBean
bean coming from the configuration class MyConfig2
will override the bean of the same name that comes from the class MyConfig1
. This is completely understandable because Spring bean default is singleton scope.
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
Callstack:
1 2 3 4 5 6 7 8 9 10 11 12 13 | registerBeanDefinition:1005, DefaultListableBeanFactory (org.springframework.beans.factory.support) loadBeanDefinitionsForBeanMethod:295, ConfigurationClassBeanDefinitionReader (org.springframework.context.annotation) loadBeanDefinitionsForConfigurationClass:153, ConfigurationClassBeanDefinitionReader (org.springframework.context.annotation) loadBeanDefinitions:129, ConfigurationClassBeanDefinitionReader (org.springframework.context.annotation) processConfigBeanDefinitions:343, ConfigurationClassPostProcessor (org.springframework.context.annotation) postProcessBeanDefinitionRegistry:247, ConfigurationClassPostProcessor (org.springframework.context.annotation) invokeBeanDefinitionRegistryPostProcessors:311, PostProcessorRegistrationDelegate (org.springframework.context.support) invokeBeanFactoryPostProcessors:112, PostProcessorRegistrationDelegate (org.springframework.context.support) invokeBeanFactoryPostProcessors:746, AbstractApplicationContext (org.springframework.context.support) refresh:564, AbstractApplicationContext (org.springframework.context.support) <init>:93, AnnotationConfigApplicationContext (org.springframework.context.annotation) main:27, Main (org.logbasex.service.import_selector_annotation.example2) |
III. Run with SpringApplication.run(Main.class, args)
From Spring boot 2.1 , bean overriding disabled by default, so if you execute the following code, from version 2.1 onward, you will get an error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @SpringBootApplication public class Main { public static void main(String[] args) { System.out.println(SpringBootVersion.getVersion()); // ApplicationContext context = new AnnotationConfigApplicationContext(Main.class); // isAllowBeanDefinitionOverriding() = true (vì khởi tạo context bằng từ khóa news) ApplicationContext context = new AnnotationConfigApplicationContext(Main2.class); AppBean2 bean = context.getBean(AppBean2.class); System.out.println(bean.getClass().getName()); // set isAllowBeanDefinitionOverriding() = false rồi mới khởi tạo. ConfigurableApplicationContext run = SpringApplication.run(Main.class, args); AppBean configurableBean = run.getBean(AppBean.class); System.out.println(configurableBean.getMessage()); } } |
Although disabled by default, we can still override beans by changing the settings in application.yml
1 2 3 4 5 | spring: main: allow-bean-definition-overriding: true |
IV. References
===
Thanks for reading.