關於Spring配置文件加載方式變化引發的異常詳解

問題背景

我們項目的配置文件一直是通過Apollo進行管理,但是近期由於某些特殊的部署需求,需要使用K8S的原生對象來獲取配置,如此一來的話,就需要使用環境變量spring.config.location來指定application.properties文件的路徑,以便動態的獲取配置。

說明:項目是一個dubbo項目,配置文件中主要包括一些基礎組件的配置、以及dubbo相關的配置。

這時候問題來瞭,在所有配置及代碼都沒有變化的情況下,如果不指定環境變量使用本地的application.properties,則沒有異常任何,項目可以正常啟動,但是一但通過spring.config.location 來加載配置,則項目會直接啟動失敗,並報如下異常:

NoSuchBeanDefinitionException,這個異常前期誤導我不少時間,它一般是Spring在容器初始化時,進行依賴註入的時候沒有找到對應的bean定義,也就意味著這個bean壓根沒有被註冊到BeanFactory中,這就很奇怪,隻是配置文件的加載方式不同,為何會影響到bean的註冊?

過程

找不到bean,最常見的問題有兩種:要麼是配置問題,比如掃描的包配置錯誤、配置未生效等。要麼就是IoC容器的問題,存在多個容器,導致bean隔離。

定位

在這個問題場景下,兩種原因都有可能,不過問題可以復現,就比較好解決。我們直接驗證一下,最簡單粗暴的法子就是斷點伺候,對比兩種配置加載方式方式的差異。我們知道除瞭延遲加載的bean之外,所有bean都是在org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons初始化的,那麼在這個方法裡以異常信息中的bean名稱,打個條件斷點看看

通過斷點,可以拿到兩個信息

1、通過當前的堆棧,可以看到當前的初始化bean邏輯並不是SpringBoot的IoC容器觸發的,而是SpringCloud

2、beanNames,也就是當前beanFactory中所有已註冊的bean中,沒有加載到任何通過Spring的@Service註解標識的bean,但是卻加載到瞭所有被Dubbo的@Service加載到的bean。

這樣一來我們可以確定的是確實存在多容器隔離,SpringCloud也會通過BootstrapApplicationListener這個監聽器創建一個IoC容器,查看官方說明:

A Spring Cloud application operates by creating a “bootstrap” context, which is a parent context for the main application. It is responsible for loading configuration properties from the external sources and for decrypting properties in the local external configuration files. The two contexts share an Environment, which is the source of external properties for any Spring application. By default, bootstrap properties (not bootstrap.properties but properties that are loaded during the bootstrap phase) are added with high precedence, so they cannot be overridden by local configuration.

SpringCloud創建的容器的加載順序比SpringBoot要早,是它的父容器,並且它們共享同一個Environment。

雖然現在知道瞭異常產生的原因,但是為什麼換瞭配置加載方式就會由父容器加載?根據上面的第二個信息,被Dubbo註解標識的bean都被加載瞭,但是這些bean依賴的SpringBean還沒有加載進來,這意味著由於配置文件加載方式的變化,導致Dubbo標記的bean加載時機發生的改變。

根因

那接下來就是看一下Dubbo的bean加載邏輯,我們的服務比較老瞭,使用的spring-boot-starter-dubbo來整合SpringBoot與Dubbo。一般spring-boot-starter都是通過@EnableXXX或spring.factories來自動裝載相關的bean,而spring-boot-starter-dubbo沒有使用@Enable,那直接找到jar包下的spring.factories文件,找到對應的Initializer:

它實現的是ApplicationContextInitiailizer,這些接口會在準備完Context環境,在prepareContext中調用,那麼很明顯,父容器肯定先會執行,子容器後執行。 看代碼邏輯,它隻有在讀取到spring.dubbo.scan有值時,才會去註冊bean,到這裡原因已經比較明顯瞭,使用application.properties時父容器讀不到配置,而使用spring.config.location加載配置時,父容器可以讀到配置。

配置加載順序

上面提到的SpringCloud文檔中有這麼一句話:

By default, bootstrap properties (not bootstrap.properties but properties that are loaded during the bootstrap phase) are added with high precedence

那麼通過spring.config.location方式加載屬性是不是在bootstrap phase中呢,直接找到SpringCloud的加載類BootstrapApplicationListener,搜索spring.config.location,發現它確實優先加載

而如果是使用application.properties,那麼配置文件則不會被SpringCloud加載到,會由子容器加載。

解決

問題根因找到瞭,想解決就比較簡單瞭,兩種方式:

  • 直接關閉SpringCloud的boostrap listener,通過配置spring.cloud.bootstrap.enabled=false即可
  • 這個問題其實也是dubbo的整合方式不合理導致的,使用Dubbo自帶的註解掃描,不使用配置文件的方式

問題其實比較簡單,但是挺有意思,分享一下過程與思路*~*

到此這篇關於Spring配置文件加載方式變化引發的異常的文章就介紹到這瞭,更多相關Spring配置文件加載方式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: