記一次spring註解@Value不生效的深度排查

​上週遇到一個有意思的問題,拿出來分享一下。問題能夠簡單地描述以下:在springboot中以java

starter 的方式引入 dubbo ,但註冊中心的配置使用 javaConfig 的方式,先貼出配置代碼

@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複製代碼

providerConfig 沒有輸出能夠理解,由於程序掛掉了,可是爲何 DubboRegistryConfig 沒有執行 @PostConstruct ?它的bean是存在的嗎?仍是沒來得及初始化?試着給它注入個 ApplicationContext ,調試一把

發如今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實現,從名字上就能看出這兩個類實現了BeanPostProcesserBeanPostProcesser提供了在bean執行初始化(init-method)先後插入hock的能力。也就是說,DubboRegistryConfig的bean沒有執行BeanPostProcesser,爲了驗證猜測,斷點找出堆棧,順藤摸瓜源碼分析

從堆棧中發現了是dubbo的ReferenceBean觸發了DubboRegistry的bean的初始化post

那麼爲何ReferenceBean初始化DubboRegistryConfig中的bean不會觸發BeanPostProcesser

呢?又是誰初始化 ReferenceBean的? 接着調試,發現Spring容器初始化的時候初始化了
ServiceAnnotationBeanPostProcessor ,這是 dubbo-spring-boot-starter 提供的一個類,它實現了 BeanFactoryPostProcessor 接口的方法postProcessBeanDefinitionRegistry,能夠在bean定義加載完成後,bean還沒有初始化時對bean的定義進行修改。它和 BeanPostProcesser
的執行順序是先調用 BeanFactoryPostProcessor,再註冊 BeanPostProcesser,若是BeanFactoryPostProcessor中建立了bean,那麼它就不會執行 BeanPostProcesser,也就是 @Value@PostConstruct沒法生效。

那麼問題歸結於,爲何在BeanFactoryPostProcessor中會建立bean呢?接着調試,ServiceAnnotationBeanPostProcessor定義在DubboAutoConfiguration,而且須要注入一個Environment

經過調試發現問題就出在注入Environment這個過程。注入Envioment須要先找到Environment

這個類型的bean,調用 DefaultListableBeanFactory. doGetBeanNamesForType

doGetBeanNamesForType 須要遍歷全部bean定義,判斷出須要的bean(並不須要初始化bean),可是在循環到dubbo的 ReferenceBean 時,發現它的 isFactoryBean 爲true,此時會調用 AbstractBeanFactory . getTypeForFactoryBean 方法

這個方法就會初始化bean了,這是爲何?

原來ReferenceBean實現了FactoryBean接口,FactoryBean能夠理解爲生產bean的bean,能夠根據複雜的狀況生產不一樣的bean,再來看ReferenceBean其實很好理解,由於dubbo的reference是不一樣的調用代理,也就是不一樣的bean,用ReferenceBean來實現很好理解。那爲何判斷類型時FactoryBean產生的bean須要實例化呢?也好理解,若是不實例化是不知道FactoryBean產生的是什麼bean,因此只有這種類型的bean會被提早初始化。差很少到這裏已經水落石出了,咱們簡單總結一下:

  1. @ Configuration 中沒法注入@ Value 與@ PostConstruct 沒法執行,咱們查到是 Configuration 的bean已經初始化,可是註解失效;

  2. 調研了一下@Value與@PostConstruct的原理是BeanPostProcesser

  3. 根據堆棧找到dubbo-spring-starterServiceAnnotationBeanPostProcessor的實現是BeanFactoryPostProcesser,它的執行要在BeanPostProcesser注入以前;

  4. 實例化ServiceAnnotationBeanPostProcessor時注入Environment,須要掃描全部bean的定義;

  5. dubbo的ReferenceBean是基於FactoryBean實現,而4中的掃描對於普通bean不會觸發實例化,但FactoryBean產生的bean會被實例化;

  6. 因而ReferenceBean實例化時觸發DubboRegistryConfig中bean的實例化,進而觸發

    DubboRegistryConfig 實例化,而這一切發生在 BeanPostProcesser 生效以前。

問題找到了,解決就很簡單,這種狀況下不要使用這兩種註解,能夠實現InitializingBean

afterPropertiesSet來代替 @ Postconstruct,實現 EnvironmentAwaresetEnvironment
接口來代替 @ Value獲取配置。

最後,文章中還有幾處沒有詳細寫出來,你們夥能夠試着分析分析,一是爲何@Import導入的jar包中的@Configuration不是一個bean?另外一個是文章中第一次執行程序輸出日誌有什麼意義?

-----

歡迎掃碼關注個人公衆號「捉蟲大師」,一個找bug小能手,專一後端,bug分析,源碼分析。

相關文章
相關標籤/搜索