情境導入
你是否遇到過包衝突問題?又是如何解決的?
有些項目都是多年的歷史「遺留財產」,老馬甚至還遇到過一個應用中有 3 個不同版本的 spring,只能說能跑起來就是奇蹟。
不過有時候會進行各種版本升級,然後會發現各種版本衝突,浪費時間在排除各種版本衝突的問題上。
那有沒有一種方法,可以幫助我們更好的解決包衝突呢?
類衝突
今天就讓我們一起學習下螞蟻金服開源的利器——SOFAArk。
SOFAArk
SOFAArk 是一款基於 Java 實現的輕量級類隔離容器,主要提供類隔離和應用(模塊)合併部署能力,由螞蟻金服公司開源貢獻;
在大型軟體開發過程中,通常會推薦底層功能插件化,業務功能模塊化的開發模式,以期達到低耦合、高內聚、功能復用的優點。
特性
基於此,SOFAArk 提供了一套較為規範化的插件化、模塊化的開發方案,產品能力主要包括:
定義類加載模型,運行時底層插件、業務應用(模塊)之間均相互隔離,單一插件和應用(模塊)由不同的 ClassLoader 加載,可以有效避免相互之間的包衝突,提升插件和模塊功能復用能力;
定義插件開發規範,提供 maven 打包工具,簡單快速將多個二方包打包成插件(Ark Plugin,以下簡稱 Plugin)
定義模塊開發規範,提供 maven 打包工具,簡單快速將應用打包成模塊 (Ark Biz,以下簡稱 Biz)
針對 Plugin、Biz 提供標準的編程界面,包括服務、事件、擴展點等機制
支持多 Biz 的合併部署,開發階段將多個 Biz 打包成可執行 Fat Jar,或者運行時使用 API 或配置中心(Zookeeper)動態地安裝卸載 Biz
基於以上能力,SOFAArk 可以幫助解決依賴包衝突、多應用(模塊)合併部署等場景問題。
classloader 加載
jvm認為不同classloader加載的類即使包名類名相同,也認為他們是不同的。
sofa-ark將需要隔離的jar包打成plugin,對每個plugin都用獨立的classloader去加載。
螞蟻金服快速入門定義 2 個不同版本的 jar
為了模擬不同版本之間的衝突,你可以自己定義 2 個不同版本的 jar 安裝到本地,也可以直接使用常用的一些工具包進行模擬。
我這裡直接使用了自己的一個工具包:
項目結構
一共下面 3 個模塊:
我們讓 serviceone 和 servicetwo 依賴不同的 heaven 版本,然後在 run 模塊中同時依賴二者,模擬 jar 版本衝突。
serviceone
pom.xml
指定依賴了 0.0.1 版本的 heaven。
是為了將當前模塊打包成為 ark-plugin。
ServiceOne.java
服務定義比較簡單,輸出一下當前類的 classloader。
servicetwo
這個和 serviceone 基本一樣,只是依賴的 heaven 版本不同。
pom.xml
ServiceTwo.java
run
這個模塊會依賴二者的實現。
pom.xml
注意這裡的 ,實際上是引入了上面編譯後的 ark-plugin,為了讓 idea 識別。
plugins 中的 為了將當前的模塊打包成為一個可以執行的 ark 包。
Main.java
運行的方法如下:
我們需要指定 ,讓 ark 啟動生效。
這樣整個入門流程就完成了,對應的日誌如下:
可以發現,所有的 classloader 都變成了 ark 對應的容器 BizClassLoader。
接下來,我們可以繼續學習一下,這背後的原理。
實現原理sofa-ark-plugin-maven-plugin 插件原理
這 3 個模塊中,都反覆出現一個核心插件:sofa-ark-plugin-maven-plugin。
這個插件做了什麼?
最好的答案就在源碼之中,我們可以到 sofa-ark-plugin 查看對應的源碼。
ArkPluginMojo
ark-plugin 核心實現類 ArkPluginMojo 定義如下:
這裡通過註解 定義了 ark-plugin,並將其生效的階段綁定為 package 打包階段。
execute 方法
也就是每次執行 mvn package 時,會執行其對應的 execute 方法進行處理。
核心實現精簡如下:
這個方法主要做了下面幾步:
建立一個zip格式的歸檔,用來保存引入的jar包和其他文件,建立輸出路徑。
獲取引入的所有依賴(Artifacts),並且將需要exclude的包排除出去。
將所有依賴寫入zip歸檔中的lib目錄下
將配置信息寫入zip歸檔中,包括 export.index,MANIFEST.MF,mark 等
SofaArkBootstrap ark 引導類
容器加載機制初始化 Ark Container
我們使用的方式,和普通的 main() 方法相比,就是多了一句
對應的源碼如下:
核心目的:
(1)將 ark container 啟動起來
(2)讓 ark container 加載 ark-plugin 和 ark-biz
isSofaArkStarted ark 是否已經啟動
實現如下:
remain()
實現如下:
作用:
獲取classpath下的所有jar包,包括jdk自己的jar包和maven引入的jar包。
將所有依賴jar包和自己寫的啟動類及其main函數以url的形式傳入ClasspathLauncher,ClasspathLauncher反射調用ArkContainer的main方法,並且使用ContainerClassLoader加載ArkContainer。
至此,就開始啟動ArkContainer了。
啟動 ArkContainer
接著就運行到了ArkContainer中的main方法,傳入的參數args即之前 ClasspathLauncher 傳入的url。
ClasspathLauncher 繼承自 ArkLauncher,實現如下:
所以後續反射調用 main 實際上會調用到 方法。
完整實現如下:
最後都會調用 start 方法進行 ArkContainer 容器啟動:
初始化 ArkService
實現如下:
這裡是選擇的 google Guice 作為注入實現。
pipeline 流水線
arkServiceContainer中包含了一些Container啟動前需要運行的Service,這些Service被封裝到一個個的PipelineStage中,這些PipelineStage又被封裝成List到一個pipeline中。
主要包含這麼幾個PipelineStage,依次執行:
(1)HandleArchiveStage
篩選所有第三方jar包中含有mark標記的plugin jar,說明這些jar是sofa ark maven插件打包成的需要隔離的jar。
從jar中的export.index中提取需要隔離的類,把他們加入一個PluginList中,並給每個plugin,分配一個獨立的PluginClassLoader。同時以同樣的操作給Biz也分配一個BizClassLoader
(2)DeployPluginStage
創建一個map,key是需要隔離的類,value是這個加載這個類使用的PluginClassLoader實例。
(3)DeployBizStage
使用BizClassLoader反射調用Biz的main方法。
至此,Container就啟動完了。
後面再調用需要隔離的類時,由於啟動Biz的線程已經被換成了BizClassLoader,在loadClass時BizClassLoader會首先看看在DeployPluginStage創建的Map中是否有PluginClassLoader能加載這個類,如果能就委託PluginClassLoader加載。
就實現了不同類使用不同的類加載器加載。
小結
對於類衝突,ark 確實是一種非常優雅輕量的解決方案。
背後核心原理就是對於 jvm classloader 和 maven plugin 的理解和應用。
學習好原理,並且和具體的應用場景結合起來,就產生了新的技術工具。
希望本文對你有所幫助,如果喜歡,歡迎點讚收藏轉發一波。
我是老馬,期待與你的下次相遇。