前言
之前聊了客戶端的一些功能,例如融入 Spring, @value 註解的自動重新整理實現,長輪詢等,這次從客戶端的整體設計來聊聊。
設計
上圖是 client 專案的包結構。
其中,核心包就是 internals 包,包含了客戶端的主要功能邏輯。主要有以下功能: 0. 獲取 ConfigService 服務的遠端配置。
- 長輪詢/定時輪詢 ConfigService。
- 監聽機制——更新後,立即通知應用程式。
- 相容 Spring 各個版本(這個是在 spring 包中,但我認為也算重要功能
^_^
)。
首先說第一個功能:獲取 ConfigService 服務的遠端配置:
實現此功能的類為:RemoteConfigRepository。該類有以下幾個重要的方法:
- 構造方法:該方法裡包含了很多初始化的過程,雖然我覺得應該放在 init 之類的方法中
- getConfig() 根據 namespace 獲取配置
- onLongPollNotified() 當收到長連線通知時觸發響應
- addChangeListener() 新增監聽器
- removeChangeListener() 刪除監聽器
注意:setUpstreamRepository 是空的。看註釋,是個 fallback 設計。
其中,getConfig 方法是獲取這個 namespace 的配置,返回的是 Properties 物件(就是個 Map)。然後,從這個物件中取出對應的值,就 ok 了。
第二個功能:長輪詢/定時輪詢 ConfigService。
這個功能的主要實現類是:RemoteConfigLongPollService。
該類主要的方法有 2 個,構造方法和 submit 方法。注意,這個類是單例的(由 google 的 inject 實現)。 構造方法中,做了很多的初始化工作。而 submit 方法則是開啟長輪詢,輪詢的方式是:攜帶 AppId 去請求 ConfigServcie,得到所有的 namespace 更新通知,然後通知對應的 RemoteConfigRepository 去請求真正的資料。大概的設計如下圖:
每一個 namespace 在一個應用中,都對應一個 RemoteConfigRepository,所有的 RemoteConfigRepository 都歸屬 RemoteConfigLongPollService 長輪詢服務管理,當長輪詢得到通知,便通知對應的 RemoteConfigRepository 進行服務請求以便執行更新本地快取和通知監聽器操作。
通知,作為 fallback 方案—— 定時輪詢也充當了長輪詢失效的最後屏障。
第三個功能:監聽機制——更新後,立即通知應用程式。
從上圖可以看出,輪詢之後,如果有更新響應,則立即通知 RemoteConfigRepository,然後,RemoteConfigRepository 再次從配置中心拉取配置,從而更新本地 Config 物件的內容。
更新完畢後,則通知 Config 的“配置變化監聽器”。也就是 ConfigChangeListener 的 onChange 方法。這個監聽器是監聽 Config 物件的。
實際上,每個 Config 物件在初始化的時候,都會往 RemoteConfigRepository 物件裡新增一個監聽器,實際上就是新增自己。
當 RemoteConfigRepository 發生變化的時候,觸發 onRepositoryChange 方法,onRepositoryChange 又會觸發 onChange 方法。大概的設計圖就是下面這個樣子:
上圖中,紫色的 DefaultConfig 是核心,他依賴了 RemoteConfigRepository, 而 RemoteConfigRepository 反過來組合了他,同時 DefaultConfig 也聚合了使用者實現的監聽器 ConfigChangeListener 的子類。
那麼,當遠端 Repository 變化的時候,就可以通知 Client 的快取 Config 物件,而 Config 快取物件變化的時候,就可以通知使用者的程式(監聽器)。實現整體的監聽機制。
總的來說,就是通過兩層監聽機制來實現的。其中 DefaultConfig 實現了兩個角色,既是觀察者,也是被觀察者。
第四個功能:相容 Spring 各個版本
首先,如果沒有這個功能,Apollo 也會能夠正常執行的,不過,你只能使用 API 的方式,不能使用註解,標籤等 Spring 應用熟悉的方式。
如果想用 Spring 的方式使用 Apollo ,那麼就得遵守 Spring 的約定,實現 Spring 的介面,將自己融入到 Spring 中。
其中,主要解決的問題就是,如何在 Spring 初始化的時候,Apollo 也初始化?這點我們在之前的文章中說了,也就是 Spring 的 3 個入口。在這些入口裡初始化。
另外,將配置放置到 Spring 的環境中,也是一個工作,因為,如果不放到環境中,Spring 初始時需要的那些引數就無法取到了。
所以,要將 Config 物件包裝成 Spring 熟悉的 ConfigPropertySource 物件,算是一個介面卡模式吧。
在初始化配置的時候,會從遠端配置中心拿到配置,包裝成 ConfigPropertySource 物件,再利用 CompositePropertySource 組合屬性配置(多個 namespace)聚合所有 Config 物件。
CompositePropertySource 最後會新增到 ConfigurableEnvironment 環境物件中,spring 就可以從這個物件 中取出配置進行初始化。
並且,在 SpringBoot 環境下,Apollo 可以優先載入指定的配置,這些配置在 SpringContext 容器初始化的時候就開始被注入到環境中,這樣就可以將一些系統初始化的配置也放到配置中心了,儘量讓本地少一點配置。這個功能的啟用需要引數:apollo.bootstrap.enabled=true
,配置的namespace 則是 apollo.bootstrap.namespaces = XXX
。
並且,該配置的優先順序是最高的,Apollo 將這個配置放在了 Spring 環境物件中的第一個位置,當迴圈獲取配置的時候,優先獲取這個配置。
總結
好了,關於 Apollo 客戶端的設計,大概就是這些,總體來講比較簡單, 4 個功能:
- 獲取遠端配置
- 長輪詢/定時輪詢
- 配置更新監聽機制。
- 相容 Spring。
丟擲一個問題:
Apollo 似乎沒有給使用者留擴充套件介面?如果能像 Spring,Mybatis 一樣,留一個或者多個切面給使用者,讓使用者能夠在載入配置的時候,做一些事情啥的,或許更好。