剖析 | 詳談 SOFABoot 模組化原理

螞蟻金服分散式架構發表於2019-02-27

SOFA(Scalable Open Financial Architecture)

是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘鍊出來的最佳實踐。

SOFABoot 是螞蟻金服中介軟體團隊開源的基於 Spring Boot 的一個開發框架,SOFABoot 從 2.4.0 版本開始支援基於 Spring 上下文隔離的模組化開發能力,SOFABoot 模組除了包括 Java 程式碼外,還會包含 Spring 配置檔案,每個 SOFABoot 模組都是獨立的 Spring 上下文。

SOFABoot 的 Github 的地址是:

https://github.com/alipay/sofa-boot

傳統模組化的陷阱

在介紹 SOFABoot 模組化之前,先讓我們再回顧一遍傳統模組化的弊端,這部分內容參考自魯直(SOFA 開源負責人)發表的 螞蟻金服的業務系統模組化 —- 模組化隔離方案

在一個簡單的 Spring/SpringBoot 的系統中,我們常常見到一個系統中的模組會按照如下的方式進行分層,如下圖中的左邊部分所示,一個系統就簡單地分為 Web 層、Service 層、DAL 層。

剖析 | 詳談 SOFABoot 模組化原理

當這個系統承載的業務變多了之後,系統可能演化成上圖中右邊的這種方式。在上圖的右邊的部分中,一個系統承載了兩個業務,一個是 Cashier(收銀臺),另一個是 Pay(支付),這兩個業務可能會有一些依賴的關係,Cashier 需要呼叫 Pay 提供的能力去做支付。

但是在這種模組化的方案裡面,Spring 的上下文依然是同一個,類也沒有任何做隔離,這就意味著,Pay Service 這個模組裡面的任何的一個 Bean,都可以被 Cashier Service 這個模組所依賴。極端的情況下,可能會出現下面這種情況:

剖析 | 詳談 SOFABoot 模組化原理

Cashier Service 錯誤地呼叫了 Pay Service 中的一個內部的 Bean,造成了兩個模組之間的緊耦合。

這種傳統的模組化的問題在於模組化地不徹底。雖然在研發的時候,通過劃分模組,把特定職責的類放到特定的模組裡面去,達到了類的「物理位置」的內聚。但是在執行時,由於沒有做任何隔離的手段,作為一個模組的開發者,並沒有辦法清楚地知道對方模組提供的對外的介面到底是什麼,哪些 Bean 我是可以直接注入來用的,哪些 Bean 是你的內部的 Bean,我是不能用的。長此以往,模組和模組之間的耦合就會越來越嚴重,原來的模組的劃分形同虛設。當系統越來越大,最後需要做服務化拆分的時候,就需要花費非常大的精力去梳理模組和模組之間的關係。

SOFABoot 模組化簡介

為了解決傳統的模組化方案模組化不徹底的問題,SOFABoot 從 2.4.0 版本開始支援基於 Spring 上下文隔離的模組化能力,每個 SOFABoot 模組使用獨立的 Spring 上下文,每個模組自包含,模組與模組之間通過 JVM Service 進行通訊,避免模組間的緊耦合:

剖析 | 詳談 SOFABoot 模組化原理

通過上面的系統架構圖可以看到,SOFABoot 模組化一共包含三個基本概念,分別是 SOFABoot 模組、JVM Service 以及 Root Application Context:

  • SOFABoot 模組: SOFABoot 模組是一個包括 Java 程式碼、Spring 配置檔案、SOFABoot 模組標識等資訊的普通 Jar 包,每個 SOFABoot 模組都是一個獨立的 Spring 上下文。

  • JVM Service: 上下文隔離後,模組與模組間的 Bean 無法直接注入,JVM Service 用於實現模組間通訊,用於釋出及引用模組服務。

  • Root Application Context: SOFABoot 應用呼叫 SpringApplication.run(args) 方法後產生的 Spring 上下文,是所有 SOFABoot 模組的 Parent。

本文接下來部分將分別介紹 SOFABoot 模組的查詢與重新整理、JVM Service 與元件管理以及 Root Application Context 的基本概念,之後會對 SOFABoot 模組的 Require-Module、Spring-Parent 以及並行啟動進行簡單介紹,並在最後給出進行模組化開發的實踐建議。

SOFABoot 模組的查詢與重新整理

SOFABoot 模組查詢與重新整理的時序圖如下:

剖析 | 詳談 SOFABoot 模組化原理

在 SOFABoot 中,我們定義了 SofaModuleContextRefreshedListener,該類會監聽 Root Application Context 傳送的 ContextRefreshedEvent 事件,選擇響應該事件是出於以下兩方面考慮:

  1. ContextRefreshedEvent 是標準的 Spring 事件,比較通用。

  2. SOFABoot 模組重新整理需要等待 Root Application Context 重新整理完畢後進行,因為 SOFABoot 模組可能依賴 Root Application Context 中的 bean 定義。

SofaModuleContextRefreshedListener 監聽到 ContextRefreshedEvent 事件後會建立 PipelineContext 物件,在 PipelineContext 中會新增 ModelCreatingStage、SpringContextInstallStage、ModuleLogOutputStage 三個 Stage,三個 Stage 的作用分別如下:

  • ModelCreatingStage: 在當前 ClassPath 查詢所有合法的 SOFABoot 模組;

  • SpringContextInstallStage: 為每個查詢到的 SOFABoot 模組新建一個 Spring Context,載入 SOFABoot 模組中的 Spring 配置檔案;

  • ModuleLogOutputStage: 輸出所有重新整理成功和重新整理失敗的 SOFABoot 模組,方便使用者快速定位問題。

呼叫 PipelineContext 的 process 方法,會觸發這三個 Stage 依次執行,對當前 ClassPath 包含的所有 SOFABoot 模組都進行重新整理。

JVM Service 與元件管理

上下文隔離後,模組與模組間的 Bean 無法直接注入,JVM Service 用於實現模組間通訊,用於釋出及引用模組服務。

為了實現跨模組的服務發現,我們在 SOFABoot 內部定義了一個元件管理介面 ComponentManager:

public interface ComponentManager {
    /**
     * register component in this manager
     *
     * @param componentInfo component that should be registered
     */
    void register(ComponentInfo componentInfo);

    /**
     * get concrete component by component name
     *
     * @param name component name
     * @return concrete component
     */
    ComponentInfo getComponentInfo(ComponentName name);
}複製程式碼

ComponentInfo 是一個介面,用於表示元件資訊,目前包含兩個具體實現,分別是 ServiceComponent 和 ReferenceComponent,分別表示服務與引用。ComponentName 是 ComponentInfo 的唯一標識,用於區分不同的 ComponentInfo 實現。

在 ComponentManager 的實現類中包含一個以 ComponentName 為 key,ComponentInfo 為 value 的 Map,用於儲存所有註冊過的 ComponentInfo:

public class ComponentManagerImpl implements ComponentManager {
    /** container for all components */
    protected ConcurrentMap<ComponentName, ComponentInfo> registry;
    
    // other definition

}複製程式碼

在釋出 JVM 服務時,我們會呼叫 register 方法,將 JVM 服務註冊到 ComponentManager 中,在發生服務呼叫時,我們會呼叫 getComponentInfo 方法,在 ComponentManager 中查詢其他模組釋出的服務並進行呼叫。

Root Application Context

SOFABoot 擴充套件自 Spring Boot,SOFABoot 應用是通過 SpringApplication.run(args) 方法啟動的,在呼叫此方法後應用將產生一個 Spring 上下文,我們把它叫做 Root Application Context。Root Application Context 在 SOFABoot 模組化中是一個很特別的存在,它是每個 SOFABoot 模組上下文的 parent,這樣設計的目的是為了保證開箱即用:SOFABoot 應用在每增加一個 Starter 定義時,Starter 中可能定義了一些 Bean,這些 Bean 預設只會在 Root Application Context 中生效,將Root Application Context 定義為每個 SOFABoot 模組上下文的 Parent 後,每個 SOFABoot 模組就能發現這些 Starter 新增的 Bean 定義,保證開箱即用。

除了會定義一些 Bean,Starter 還可能定義一些 BeanPostProcessor 和 BeanFactoryPostProcessor,對於這兩類特殊的 Bean 定義,子上下文光能夠發現 Bean 定義還不夠,必須將這兩類 Bean 的定義複製到當前上下文才能生效,所以我們在重新整理 SOFABoot 模組對應的上下文時,會將 Root Application Context 中定義的所有 BeanPostProcessor 和 BeanFactoryPostProcessor 複製到 SOFABoot 模組的上下文中,這樣一些 Starter 定義的 Processor 就可以直接在 SOFABoot 模組上下文中使用了,例如 runtime-sofa-boot-starter 中會定義 ServiceAnnotationBeanPostProcessor,該類主要用於實現註解釋出服務,自動拷貝後,只要增加了 runtime-sofa-boot-starter 依賴,就支援在 SOFABoot 模組中基於註解釋出服務。

Require-Module、Spring-Parent 以及並行啟動模組

在重新整理 SOFABoot 模組時,可能出現 A 模組中釋出了一個 JVM Service,在 B 模組的某一個 Bean 的 init 方法裡面需要呼叫這個 JVM Service,假設 B 模組在 A 模組之前啟動了,那麼 B 模組的 Bean 就會因為 A 模組的 JVM Service 沒有釋出而 init 失敗,導致 Spring 上下文啟動失敗。此時,我們可以在 sofa-module.properties 中指定 Require-Module 來強制 A 模組在 B 模組之前啟動。

在 SOFABoot 應用中,每一個 SOFABoot 模組都是一個獨立的 Spring 上下文,並且這些 Spring 上下文之間是相互隔離的。雖然這樣的模組化方式可以帶來諸多好處,但是,在某些場景下還是會有一些不便,這個時候,你可以通過 Spring-Parent 來打通兩個 SOFABoot 模組的 Spring 上下文。例如,可以將 DAL 模組作為 Service 模組的 Parent,這樣 Service 模組就可以直接使用 DAL 模組定義的 DataSource 定義,無需將一個 DataSource 釋出成一個 JVM Service。

SOFABoot 會根據 Require-Module 和 Spring-Parent 計算模組依賴樹,例如以下依賴樹表示模組B 和模組C 依賴模組A,模組E 依賴模組D,模組F 依賴模組E:

剖析 | 詳談 SOFABoot 模組化原理

該依賴樹會保證模組A 必定在模組B 和模組C 之前啟動,模組D 在模組E 之前啟動,模組E 在模組F 之前啟動,但是依賴樹沒有定義模組B 與模組C,模組B、C與模組D、E、F之間的啟動順序,這幾個模組之間可以序列啟動,也可以並行啟動。SOFABoot 預設會對模組進行並行啟動,這樣可以大大加快應用的啟動速度。

實踐建議

每個 SOFABoot 模組使用獨立的 Spring 上下文,模組與模組之間通過 JVM Service 進行通訊,在釋出服務時,我們建議以服務維度進行服務釋出,類似 RPC 的使用方法,提供一個 Facade 包,包含介面定義,然後由模組實現介面,併發布服務。

有些實現是不適合放在 SOFABoot 模組中定義的,例如 Controller 定義,Controller 定義是展示層的實現,而 SOFABoot 模組屬於業務層,我們不建議也不支援在 SOFABoot 模組中定義 Controller 元件。Controller 元件的定義建議放在 Root Application Context 中,如果 Controller 元件需要呼叫 SOFABoot 模組釋出的服務,可以直接使用註解的方式引用服務,具體的例子可以看 實操 | 基於 SOFABoot 進行模組化開發 中的例子。

有一些模組是不適合定義為 SOFABoot 模組的,例如 Util 模組或者 Facade 模組,前者主要定義一些工具類,後者主要定義一些服務介面,都不涉及服務的釋出與引用,建議不要將這些模組定義為 SOFABoot 模組。

總結

本文主要介紹了 SOFABoot 模組化的實現原理,著重介紹了 SOFABoot 模組化中的三個重要概念:SOFABoot 模組的查詢與重新整理、JVM Service 與元件管理以及 Root Application Context 的基本概念,通過這三個重要概念的介紹能幫助使用者快速理解 SOFABoot 模組化的實現原理。在原理解析之後,在文章最後我們還給出了實踐建議,幫助使用者快速上手 SOFABoot 模組化。

文章提到的:

剖析 | 詳談 SOFABoot 模組化原理

長按關注,獲取分散式架構乾貨

歡迎大家共同打造 SOFAStack https://github.com/alipay


相關文章