上週遇到一個有意思的問題,拿出來分享一下。問題能夠簡單地描述以下:在springboot中以java
@Configurationpublic class DubboRegistryConfig { @Value("${dynamic.dubbo.registries.nacos.address:#{null}}") private String nacosAddress; @Value("${dynamic.dubbo.registries.zk.address:#{null}}") private String zkAddress; @Bean public RegistryConfig nacosRegistry() { RegistryConfig nacosConfig = new RegistryConfig(); nacosConfig.setAddress(nacosAddress); nacosConfig.setProtocol("nacos"); return nacosConfig; } @Bean public RegistryConfig zkRegistry() { RegistryConfig zkConfig = new RegistryConfig(); zkConfig.setAddress(zkAddress); zkConfig.setProtocol("zookeeper"); return zkConfig; } @Bean public ConsumerConfig consumerConfig() { ConsumerConfig consumerConfig = new ConsumerConfig(); List<RegistryConfig> registryConfigs = new ArrayList<>(2); registryConfigs.add(nacosRegistry()); registryConfigs.add(zkRegistry()); consumerConfig.setRegistries(registryConfigs); return consumerConfig; } @Bean public ProviderConfig providerConfig() { ProviderConfig providerConfig = new ProviderConfig(); providerConfig.setRegistry(zkRegistry()); return providerConfig; }}複製代碼
將這段代碼打成jar包,在主工程中引入,啓動類上使用@Import引入spring
@SpringBootApplication@Import(DubboRegistryConfig.class)public class Application { public static void main(String[] args) { SpringApplication application = new SpringApplication(Application.class); application.run(args); }}複製代碼
固然,註冊中心地址寫在application.properties文件中。後端
運行一下,奇怪的事情發生了,方便起見,我把調試截圖貼出來
springboot
當運行到consumerConfig代碼時,兩個註冊中心的地址並無注入進來,這是怎麼回事?下意識地看了下程序運行日誌,發現了一條可疑日誌bash
ConfigurationClassPostProcessor.enhanceConfigurationClasses()|Cannot enhance @Configuration bean definition 'com.lkxiaolou.registry.dynamic.config.DubboRegistryConfig' since its singleton instance has been created too early. The typical cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring such methods as 'static'. 複製代碼
大概意思是,因爲DubboRegistryConfig bean實例化太早,致使沒有被ConfigurationClassPostProcessor加強。好像跟bean的生命週期有關,這個配置類中定義了三個bean,加上配置類自身也是一個bean,想辦法看一下這四個bean的加載順序是否是有助於問題的解決?因而,我在配置代碼中輸出一些調試信息app
@Configurationpublic class DubboRegistryConfig { @Value("${dynamic.dubbo.registries.nacos.address:#{null}}") private String nacosAddress; @Value("${dynamic.dubbo.registries.zk.address:#{null}}") private String zkAddress; @PostConstruct public void init() { System.out.println("=== DubboRegistryConfig inited"); } @Bean @DependsOn("dubboRegistryConfig") public RegistryConfig nacosRegistry() { System.out.println("=== nacosRegistry create"); RegistryConfig nacosConfig = new RegistryConfig(); nacosConfig.setAddress(nacosAddress); nacosConfig.setProtocol("nacos"); return nacosConfig; } @Bean public RegistryConfig zkRegistry() { System.out.println("=== zkRegistry create"); RegistryConfig zkConfig = new RegistryConfig(); zkConfig.setAddress(zkAddress); zkConfig.setProtocol("zookeeper"); return zkConfig; } @Bean public ConsumerConfig consumerConfig() { System.out.println("=== consumerConfig create"); ConsumerConfig consumerConfig = new ConsumerConfig(); List<RegistryConfig> registryConfigs = new ArrayList<>(2); registryConfigs.add(nacosRegistry()); registryConfigs.add(zkRegistry()); consumerConfig.setRegistries(registryConfigs); return consumerConfig; } @Bean public ProviderConfig providerConfig() { System.out.println("=== providerConfig create"); ProviderConfig providerConfig = new ProviderConfig(); providerConfig.setRegistry(zkRegistry()); return providerConfig; }}複製代碼
最後只有三個bean正常輸出了調試信息ide
=== consumerConfig create=== nacosRegistry create=== zkRegistry create複製代碼
發如今nacosRegistry建立的時候,竟然找不到DubboRegistryConfig的bean定義,難道是@Import使用姿式不對?因而把@Import改爲包掃描,再調試發現能夠找DubboRegistryConfigspring-boot
的bean了,可是 @PostConstruct依然不執行,屬性依舊沒有注入。先捋一捋,咱們最初的問題是@Configuration沒法注入@Value,經過@Import導入@Configuration,@Configuration自己不是一個bean,天然@Value就沒法裝配屬性,但當咱們改爲包掃描後,能獲取到@Configuration自己的bean,可是屬性依然沒有注入@PostConstruct依然沒有執行。bean生成了,但@Value與@PostConstruct註解沒有生效,咱們把焦點放到這兩個註解上,這兩個註解是怎麼實現的?查詢了一下資料,@PostConstruct是依靠InitDestroyAnnotationBeanPostProcessor實現,@Value是依靠AutowiredAnnotationBeanPostProcessor實現,從名字上就能看出這兩個類實現了BeanPostProcesser,BeanPostProcesser提供了在bean執行初始化(init-method)先後插入hock的能力。也就是說,DubboRegistryConfig的bean沒有執行BeanPostProcesser,爲了驗證猜測,斷點找出堆棧,順藤摸瓜源碼分析
從堆棧中發現了是dubbo的ReferenceBean觸發了DubboRegistry的bean的初始化post
那麼爲何ReferenceBean初始化DubboRegistryConfig中的bean不會觸發BeanPostProcesser
呢?又是誰初始化 ReferenceBean的? 接着調試,發現Spring容器初始化的時候初始化了那麼問題歸結於,爲何在BeanFactoryPostProcessor中會建立bean呢?接着調試,ServiceAnnotationBeanPostProcessor定義在DubboAutoConfiguration,而且須要注入一個Environment
經過調試發現問題就出在注入Environment這個過程。注入Envioment須要先找到Environment
這個類型的bean,調用 DefaultListableBeanFactory. doGetBeanNamesForType這個方法就會初始化bean了,這是爲何?
原來ReferenceBean實現了FactoryBean接口,FactoryBean能夠理解爲生產bean的bean,能夠根據複雜的狀況生產不一樣的bean,再來看ReferenceBean其實很好理解,由於dubbo的reference是不一樣的調用代理,也就是不一樣的bean,用ReferenceBean來實現很好理解。那爲何判斷類型時FactoryBean產生的bean須要實例化呢?也好理解,若是不實例化是不知道FactoryBean產生的是什麼bean,因此只有這種類型的bean會被提早初始化。差很少到這裏已經水落石出了,咱們簡單總結一下:
調研了一下@Value與@PostConstruct的原理是BeanPostProcesser;
根據堆棧找到dubbo-spring-starter中ServiceAnnotationBeanPostProcessor的實現是BeanFactoryPostProcesser,它的執行要在BeanPostProcesser注入以前;
實例化ServiceAnnotationBeanPostProcessor時注入Environment,須要掃描全部bean的定義;
dubbo的ReferenceBean是基於FactoryBean實現,而4中的掃描對於普通bean不會觸發實例化,但FactoryBean產生的bean會被實例化;
因而ReferenceBean實例化時觸發DubboRegistryConfig中bean的實例化,進而觸發
問題找到了,解決就很簡單,這種狀況下不要使用這兩種註解,能夠實現InitializingBean
的 afterPropertiesSet來代替 @ Postconstruct,實現 EnvironmentAware的 setEnvironment最後,文章中還有幾處沒有詳細寫出來,你們夥能夠試着分析分析,一是爲何@Import導入的jar包中的@Configuration不是一個bean?另外一個是文章中第一次執行程序輸出日誌有什麼意義?
-----
歡迎掃碼關注個人公衆號「捉蟲大師」,一個找bug小能手,專一後端,bug分析,源碼分析。