@ConfigurationProperties該如何裝載到Spring容器中呢?
問題描述
最近項目中遇到了一個Spring中@ConfigurationProperties注解的問題,如下:
- 定義了一個注解了@ConfigurationProperties的User Bean。
@ConfigurationProperties(prefix = "my.user")
@Component
@Data
public class User {
private String userName;
}
- 通過@Autowired使用UserBean,沒有問題。
@RestController
@RequestMapping("/config")
@EnableConfigurationProperties(User.class)
public class UserConfigController {
@Autowired
private User user;
@GetMapping("/username1")
public String username1() {
return user.getUserName();
}
}
圖片
- 但是,有個同事修改了下變量名為user1,自信的以為沒有問題,就提交測試了,然后直接報錯了。
@RestController
@RequestMapping("/config")
@EnableConfigurationProperties(User.class)
public class UserConfigController {
@Autowired
private User user1;
@GetMapping("/username2")
public String username2() {
return user1.getUserName();
}
}
報錯如下圖所示:
圖片
這是怎么一回事呢,修改個變量名都能報錯?
原因分析
根據報錯信息不難分析出來主要原因在于User類在Spring容器中兩個Bean對象,bean name分別是“user”和“my.user-com.alvinlkk.bean.User”。
使用@Autwired裝配,實際上不只是根據類型裝配,如果匹配到同類型有多個Bean對象,默認會去找和變量名“user”同名的Bean,所以不會報錯。如果修改變量名改成user1, 它就匹配到兩個Bean對象,然后用bean name=user1無法找到合適的,自然就報錯了。
那么為什么會出現兩個Bean呢?
- 因為使用@Component注解,創建了一個名稱為“user”的Bean。
圖片
- 使用了@EnableConfigurationProperties注解創建了名稱為my.user-com.alvinlkk.bean.User的Bean。
最佳實踐
使用@ConfigurationProperties注解的Bean的時候,建議通過使用@EnableConfigurationProperties創建Bean。
源碼解析
刨根問底,我們繼續從Spring源碼層面深入了解下這個問題的產生的根源。Spring創建Bean的過程其實很簡單,大致分兩個步驟:
- 創建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對象
現在BeanDefinitionBean定義信息已經有了,Spring就可以根據這些信息創建出Bean對象實例了,這一個過程是在finishBeanFactoryInitialization()方法中進行的,我們這里重點關注下@Autowird方法是如何進行裝配的。
- AbstractApplicationContext#refresh(): 初始化容器
- AbstractApplicationContext#finishBeanFactoryInitialization(): 初始化Bean入口
- DefaultListableBeanFactory#preInstantiateSingletons():預先初始化單例Bean
- DefaultListableBeanFactory#getBean(): 調用getBean()創建Bean實例
- AbstractBeanFactory#doGetBean():getBean()最終調用的方法
- AbstractAutowireCapableBeanFactory#createBean(): 創建Bean實例入口
- DefaultListableBeanFactory#determineAutowireCandidate():選擇使用哪個候選的Bean
圖片
根據類型匹配到Bean有多個的情況,會調用determineAutowireCandidate()方法進一步去根據name匹配bean。
總結
所以對于配置注解ConfigurationProperties的類不要使用使用@Component注解讓Spring管理,更推薦的做法是使用@EnableConfigurationProperties注解進行裝載。