Problem Description
In a recent project, I encountered a problem with annotations, as follows Spring
:@ConfigurationProperties
@ConfigurationProperties
An annotation is definedUser Bean
.
@ConfigurationProperties(prefix = "my.user")
@Component
@Data
public class User {
private String userName;
}
- By
@Autowired
usingUser
Bean, there is no problem.
@RestController
@RequestMapping("/config")
@EnableConfigurationProperties(User.class)
public class UserConfigController {
@Autowired
private User user;
@GetMapping("/username1")
public String username1() {
return user.getUserName();
}
}
- However, a colleague changed the name of the variable
user1
and confidently thought there was no problem, so he submitted the test and reported an error directly.
@RestController
@RequestMapping("/config")
@EnableConfigurationProperties(User.class)
public class UserConfigController {
@Autowired
private User user1;
@GetMapping("/username2")
public String username2() {
return user1.getUserName();
}
}
The error is shown in the figure below:
What's going on here, can you report an error even if you modify a variable name?
Cause Analysis
According to the error message, it is not difficult to analyze that the main reason is that there are two objects User
of the class in the container , namely " " and " ".Spring
Bean
bean name
user
my.user-com.alvinlkk.bean.User
Using @Autwired
assembly is actually not just assembling according to the type. If multiple Bean objects of the same type are matched, it will find the same user
name as the variable name " " by default Bean
, so no error will be reported. If you change the variable name to user1
, it will match two Bean objects, and if you bean name=user1
can’t find the right one, you will naturally report an error.
So why are there two beans?
- Because of
@Component
the annotation, a Bean named " " is createduser
.
- Annotations are used
@EnableConfigurationProperties
to createmy.user-com.alvinlkk.bean.User
a Bean named .
Best Practices
When using @ConfigurationProperties
annotated beans, it is recommended to @EnableConfigurationProperties
create beans by using.
Source code analysis
To get to the bottom of it, we continue to understand the root cause of this problem from the Spring source code level. The process of creating a Bean in Spring is actually very simple, roughly divided into two steps:
- 创建Bean的定义信息
BeanDefinition
,包含Bean的类型,名称等信息,注册到Bean定义工厂中。 - 根据Bean定义工厂中的Bean定义信息,创建出Bean实例。
上面的两个过程中在通常在SpringBoot启动的过程中就完成,SpringBoot启动的时候,会调用容器的refresh()
, 其中在invokeBeanFactoryPostProcessors(beanFactory)
方法中创建并注册BeanDefinition
, 在finishBeanFactoryInitialization()
方法中创建Bean实例对象。
创建注册BeanDefinition
@Component
注解
被Compoent
注解的的类会被Spring中的ConfigurationClassPostProcessor
类处理,创建出对应的BeanDefinition
,然后注册到BeanDefinitionRegistry
中,具体流程如下图所示。
被@Component
注解的类User会被扫描到,生成一个名字是user
的BeanDefinition
,然后注册到BeanDefitionRegistry中,如下图所示:
@EnableConfigurationProperties
注解
注解@EnableConfigurationProperties
源码中import
了EnableConfigurationPropertiesRegistrar
类,那么它是在什么阶段创建出BeanDefinition
呢?
最终配置了@EnableConfigurationProperties(User.class)
中被获取,创建出name为my.user-com.alvinlkk.bean.User
的BeanDefinition
,如下图所示。
而且@Component
的顺序是优先于@EnableConfigurationProperties
的。
创建Bean对象
现在BeanDefinition
Bean定义信息已经有了,Spring就可以根据这些信息创建出Bean对象实例了,这一个过程是在finishBeanFactoryInitialization()
方法中进行的,我们这里重点关注下@Autowird
方法是如何进行装配的。
AbstractApplicationContext#refresh()
: 初始化容器AbstractApplicationContext#finishBeanFactoryInitialization()
: 初始化Bean入口DefaultListableBeanFactory#preInstantiateSingletons()
:预先初始化单例BeanDefaultListableBeanFactory#getBean()
: 调用getBean()
创建Bean实例AbstractBeanFactory#doGetBean()
:getBean()
最终调用的方法AbstractAutowireCapableBeanFactory#createBean()
: 创建Bean实例入口DefaultListableBeanFactory#determineAutowireCandidate()
:选择使用哪个候选的Bean
根据类型匹配到Bean有多个的情况,会调用determineAutowireCandidate()
方法进一步去根据name匹配bean。
总结
所以对于配置注解ConfigurationProperties
的类不要使用使用@Component
注解让Spring管理,更推荐的做法是使用@EnableConfigurationProperties
注解进行装载。
欢迎关注个人公众号【JAVA旭阳】交流学习!