本文是IBMDW的王志強撰寫的文章,原文名稱為《實現 WebSphere Application Server 上應用程序對 OSGi 的支持》。
51CTO編輯推薦:OSGi入門與實踐全攻略
為了解決現實工程中遇到的版本沖突問題,我研究了 OSGi 技術。為此,我在網上搜索了很多的有關於 OSGi 的文章,但是最後發現很少有人能清楚的闡述 OSGi 的由來和 OSGi 最本質的特性,直到我發現 Neil Bartlett 的 OSGi In Practice。它讓理解了 OSGi 的本質,從而清楚的明白:解決 Java 工程中的版本沖突問題,OSGi 是最好的選擇。這是由 OSGi 的本質決定的,這也是為什麼大的 Java 軟件產品紛紛開始采用 OSGi 框架的原因。Java 有天生的缺陷,而 OSGi 彌補了它。然後我又花了大量的時間來研究 WebSphere Application Server 是如何支持 OSGi 的,最後才能成功的使我的應用支持 OSGi,從而解決了版本沖突問題。但這一整個過程耗費了我大量的時間和精力,為了能讓遇到同樣問題的人少走彎路,我寫了這篇文章,希望能夠有所幫助。
OSGi 的由來
隨著科技和需求的發展和變化,現在的軟件變得越來越龐大。這樣,隨之而來的最大挑戰就是軟件在設計上的越來越復雜和維護上的越來越困難。為了解決這個問題,軟件架構師將軟件切分成比較小的並且易於理解的多個模塊。那麼軟件模塊化會給我們帶來什麼樣的好處呢?
基於上述的 4 個優點,在當前的軟件設計中,軟件模塊化是軟件架構師的主流思想。為了實現軟件模塊化,應運而生的就是面向對象的高級編程語言,Java 是其中的典型代表。Java 用其獨有的 Jar 格式文件去包裝 Java 類和其他的資源文件,從而可以將軟件組件封裝成獨立的 Jar 文件。這些 Jar 文件可以相互依賴並共同完成同一個工作,從而實現了軟件的模塊化。但是 Java 卻不能真正的帶給我們軟件模塊化的那 4 個優點,為什麼呢?為了解釋這個問題,我們需要知道模塊的定義。
什麼是一個模塊?
一個模塊應該有以下 3 個特性:
而 Java 語言的 Jar 文件並不能完美的實現一個模塊這 3 個特性,它主要會遇到以下的 3 個問題:
這 3 個問題,使 Jar 文件在模塊的“自包含”和“低耦合”這兩個特性上做的不好,從而使 Java 在模塊的“拆分人力”和“易於維護”這兩個優點上沒有好的表現,而更嚴重的是第 2 個問題,這使 Java 應用軟件存在難以處理的版本沖突問題。
Java 語言為了安全的考慮,它的類加載器(注意不是類加載)是多層立體的並且對其類加載采用了父委托機制。並且,當同一個類加載器加載類文件時,JVM 會加載最先發現類。所以當同一個類的新舊兩個版本被分別封裝在兩個不同的 jar 文件中(比如:ClassVersion2.jar 和 ClassVersion1.jar),而這兩個 jar 文件又都在 JVM 的類加載路徑裡的話,最先被加載的 Jar 文件中的類才會被使用到。而現在的 Java 應用軟件因為長時間的更新維護,同一個模塊的多個版本共存的情況比比皆是,這就會產生版本沖突問題。比如說,在 Java 應用軟件中存在一個模塊 ThirdComponent,這個模塊依賴 ClassVersion 模塊而且必須同老版本的 ClassVersion 模塊(ClassVersion1.jar)一起工作,當新 ClassVersion 模塊(ClassVersion2.jar)被升級到軟件中後,ThirdComponent 模塊很有可能就不會工作,因為類加載器很有可能會先加載的是 ClassVersion2.jar,而不是 ClassVersion1.jar。其實產生這個問題的根本原因是,Java 的 Jar 文件在自包含上做的不好。Jar 文件只是在軟件的封裝上起到了作用,而在 JVM 運行時中,Java 只關心類而忽略了 Jar,從而使 Java 的類加載變成了平面的線性的而非立體的網狀的。
為了解決 Java 在模塊化中存在的問題,OSGi 模塊系統出現了。OSGi 是基於 Java 之上開發的,它提供了一種建立模塊化的 Java 應用程序的方法並定義了這些模塊在運行時中如何相互交互。
OSGi 最本質的特性
OSGi 是一個由大概 40 個公司組成的聯盟來共同定義的一個標准。依照這個標准,目前,有 4 種獨立實現了的 OSGi 框架,他們分別是:
關於 OSGi 這個名字,2 個最常被問到的問題是 OSGi 代表什麼?為什麼 i 是小寫的?
關於這兩個問題,權威性的回答是:官方上 OSGi 不代表任何東西,然而,通常上說 OSGi 代表“Open Service Gateway initiative. 而小寫字母“i”來自單詞“initiative”。
至於 OSGi 的中心思想,它非常地簡單。傳統 Java 軟件問題的根源就是全局的扁平的類加載路徑(Classpath),所以 OSGi 采用了一種完全不同的類加載機制,那就是每個模塊都有其獨立的類加載路徑。這幾乎解決了傳統 Java 在模塊化中遇到的所有問題,然而一個新的問題又產生了,軟件中的模塊是要在一起工作的,這就意味著不同的模塊之間存在類共享(不然的話,一個模塊如何能夠調用到另外一個模塊呢),如果每個模塊有一個類加載路徑,模塊間的類共享如何解決?為了解決這個問題,OSGi 定義了一個特殊的並完善的類共享機制。OSGi 將會采用顯示的導入和導出機制來控制模塊間的類共享。
在 OSGi 中,模塊被起了另外一個名字,叫做 bundle。實際上 OSGi 的 bundle 就是一個 Java 的 Jar 文件。OSGi 並沒有定義一個新的標准去封裝 Java 類和其他的資源文件,標准 Jar 文件可以很好地工作在 Java 應用軟件中。在 OSGi 體系裡,只是一些新的元數據信息被加入到 Jar 文件中,這些元數據信息使的 Jar 文件變成一個 OSGi 體系中的 bundle。那麼什麼樣的元數據信息被加入進來了呢?
這些元數據信息被放到 Jar 文件的 MANIFEST.MF 文件中,而這個文件是每個標准 Jar 文件的一部分。用一個標准的 Jar 文件作為 OSGi bundle 的一個好處是 bundle 可以被用在 Jar 文件可以出現的任何一個地方,因為 bundle 就是一個純粹的 Jar 文件。當一個 bundle 用在 OSGi 的運行時之外的時候,這些額外多出來的元數據信息會被 Java 運行時簡單地忽略掉,所以說,bundle 是向前兼容的。那麼除了這個,OSGi 的 bundle 還給我們帶來了什麼樣的好處呢?
為每一個 bundle 提供一個類加載路徑意味著什麼呢?簡單地說我們為每一個 bundle 提供了一個類加載器,這個類加載器能夠看到這個 bundle 文件裡的類和其他資源文件。但是為了達到多個 bundle 共同工作的目的,在 OSGi 的類加載器之間,類加載請求可以從一個 bundle 的類加載器被委托到另外一個 bundle 的類加載器。回想一下在標准 Java 和 J2EE 中,類加載器是一個樹形結構的,類加載請求總是被向上委托給每一個類加載器的父親。這中類加載機制不允許在水平的樹節點之間進行類加載委托。為了讓一個類庫可以被類加載器樹的多個樹枝共同所見,我們就需要將這個類庫推到這些樹枝共同的祖先節點。這樣這個版本的類庫就會被這些樹枝上的所有節點所見,而不管是否這些節點都想看到這個版本的類庫。圖 1 是一個典型的 J2EE 類加載器層次結構,它展示了為什麼類庫會不斷推高的原因。
圖 1. 典型的 J2EE 類加載器層次結構
而樹型結構並不是我們需要的,我們真正需要的是網狀結構。兩個組件之間的依賴關系不是簡單的上下級的關系,而應該是一種提供者和使用者的網絡關系。類的加載請求被從一個 bundle 的類加載器委托到另外一個 bundle 的類加載器,而這種委托是基於 bundle 之間的這種網狀的依賴關系。圖 2 給了我們一個 OSGi 中 bundle 之間的網狀的依賴關系的例子。
圖 2. OSGi 中 bundle 之間的網狀的依賴關系
在 OSGi 中,bundle 之間的依賴關系是通過顯示的 import 和 export 類包列表來決定的。比如說,在圖 2 的 bundle B 中包含一個類包,名字為 com.ibm.bundle.b.somePackage.。Bundle b 就可以選擇在它的 MANIFEST.MF 文件去 export 這個類包。而 bundle A 也可以選擇去在它的 MANIFEST.MF 文件中 import 類包 com.ibm.bundle.b.somePackage。然後在 OSGi 運行時中,OSGi 框架將負責匹配不同 bundle 的 import 和 export 列表。而這個匹配過程被稱為 OSGi 的解決過程(resolution process)。OSGi 的解決過程是相當復雜的,但是這個過程是被 OSGi 框架實現了的,不需要每個 bundle 自己來關心它。每個 bundle 只需要寫一些很簡單的 import 和 export 聲明語句在各自的 MANIFEST.MF 文件裡。一旦 OSGi 框架匹配了 bundle A 的 import 列表裡的類包 com.ibm.bundle.b.somePackage 與 bundle B 的 export 列表裡的類包 com.ibm.bundle.b.somePackage,那麼這兩個 bundle 就會被連接在一起,而這就意味著當 bundle A 需要載入任何類包 com.ibm.bundle.b.somePackage 裡的類時,這個類載入請求就會被委托給 bundle B 的類載入器,而 bundle B 的類載入器將會載入這個類,並將類實例傳給 bundle A。而因為 bundle A 依賴 bundle B 和 bundle C, 如果 bundle A import 列表裡的所有類包都能在 bundle B 和 bundle C 的 export 列表裡發現,那麼 bundle A 將會被稱為解決成功,它會進入解決了狀態(resolved)。如果 bundle A 的 import 列表裡需要的某些類包沒有在 bundle B 和 bundle C 的 export 列表裡發現,那麼 bundle A 的解決就沒有成功,bundle A 將不能被使用和啟動。由此可見,OSGi 中,bundle 之間的依賴關系是通過顯示的 import 和 export 列表來決定的。
因為在 OSGi 中,bundle 之間的依賴關系是通過顯示的 import 和 export 列表來決定的,所以我們沒有必要 export 一個 bundle 中的所有的類包,進而能起到隱藏 bundle 中信息的作用。在 OSGi 中,只有那些被顯示的 export 出來的類包才能被其他的 bundle import。
OSGi 不僅僅使 bundle 之間通過類包名相互依賴,它還可以為類包加入版本信息。這是我們能夠應付 bundle 的版本變化問題。Export 的類包也可以攜帶一個版本信息,而 import 卻可以引用一個版本范圍內的所有類包,這讓我們的 bundle 可以依賴一個版本范圍內的所有類包。於是說,在 OSGi 中,同一 bundle 的多個版本就可以同時存在。在本文的第三章,我將詳細介紹,OSGi 是如何允許同一個 bundle 的多個版本並行存在,進而客服了 Java 應用軟件中組件的版本沖突問題
解釋 OSGi 是如果克服版本沖突問題的
OSGi 之所以能夠解決 Java 應用軟件中組件的版本沖突問題,原因就是 OSGi 的網狀類加載器和 OSGi bundle 的版本信息控制。為什麼這麼說呢?
那麼 OSGi 是如何進行版本控制的呢?
OSGi 通過在 MANIFEST.MF 文件中添加 Bundle-Version 屬性來個為每一個 bundle 添加一個版本信息,而且這個版本信息必須嚴格遵循:3 數字段 +1 字符段的格式,6.2.0.beta_3 是一個典型有效的 bundle 版本信息。這前面的 3 個數字段就是大家都知道的主版本,小版本和微版本,而那個最後的字母段則是校正段。當前面的 3 個任意一個數字段沒有值時,OSGi 將會隱式地將 0 付給這個字段,所以版本 1 是和 1.0、1.0.0 相同的。而如果沒給 bundle 指定任意一個版本,那麼 0.0.0 將被認為是這個 bundle 的版本信息。
另外 OSGi 中,版本的比較是采用從前到後的比較方式。如果在版本比較時,第一個數字段就不同,那麼後面的 3 個字段就不用比較了,因為 OSGi 的前一個版本段是後面所有字段值的總和,所以大版本就不相同的時候,後面的小版本就不需要比較了,比如說:2.0.0 是大於 1.999.999。而如果兩個 bundle 的版本信息,在前面的 3 個數字段都相同的時候,OSGi 就會對最後的字母段進行比較。而最後的字母段可以包含大寫或小寫的字母 A 到 Z、數字、連接線和下劃線,所以它的比較比較復雜。OSGi 采用了標准 Java String 類的 compareTo() 方法的算法來進行比較,而標准 Java 的 String 類的 compareTo() 方法會對校正段的每一個字母按順序進行比較,直到出現差異。另外如果字母相同,那麼短的那個校正段的值將被認為小於長的校正段,beta_01 將會比 beta_010 小。
最後需要提的是 OSGi 不但可以為 bundle 指定一個版本信息,還可以為每一個類包指定一個版本信息,即 bundle 的版本控制是可以做到類包級別的(而且這是推薦的 OSGi 版本控制方式)。當 bundle 在 export 類包時,用戶可以為每個類包指定一個版本信息。而當 bundle 需要 import 某特定版本的類包時,用戶除了可以指定一個特定的版本信息外,還可以指定一個版本信息范圍。而這個范圍可以用方括號“【”和圓括號“(”來作為邊界,方括號“【”表示邊界值也在范圍之內,而圓括號“(”則相反 . 比如說【 1.0.0,2.0.0)表示從版本 1.0.0 開始到 2.0.0 之間的所有的小版本,2.0.0 不在這個范圍只內。下面是一些進一步的范圍列表的例子,在下面的表 1 中 x 代表有效的范圍列表:
表 1. 版本范圍舉例
解釋 WAS 在引入 OSGi 之後的類載入機制
OSGi 是如此優秀的一個框架,因此很多 Java 應用軟件開始采用它,WebSphere Application Server(WAS)作為 IBM 最中要的 J2EE 服務器從版本 6.1 開始采用 OSGi 框架,因此當你的應用是基於 WAS 的並遇到了版本沖突問題,你就可以將你的應用轉換成 OSGi 的 bundle,從而解決版本沖突問題。在將你的 WAS 的應用轉變成 OSGi 的 bundle 之前,我們需要了解 WAS 是如何支持 OSGi 的,進而采取相應的行動。
圖 3. WAS 從 6.1 以後的類加載器層次結構
圖 3 是 WAS 從版本 6.1 以後的類加載器層次結構,從此圖,我們可以知道 WAS 並不是完全采用 OSGi 框架(可能是出於向前兼容等因素考慮),IBM 只是將 WAS 的一部分變成了 OSGi 框架的類加載模式,而其他的部分繼續延續了以前版本的類加載器層 次結構。而 WAS 中 OSGi 的那部分類加載器,以網關的形式與 WAS 的擴展類加載器連接在一起。
接下來,大家可能奇怪,WAS 原有的類加載器是如何同 WAS 中的 OSGi 部分的類加載器一起工作的呢? WAS 將如何進行類加載呢?
為了解釋這個問題,我們首先要清楚,WAS 中的那一部分采用了 OSGi 的框架? WAS 將其常用的重要的插件部分做成了 OSGi,而所有的這些插件被放置在 WAS 的 plugins 目錄中,因此 plugins 目錄下的所有的插件是以 OSGi bundle 的形式被載入到 WAS 中來的。接下來,我將解釋從版本 6.1 以後 WAS 將如何進行類加載。從圖 3,我們可以得知,WAS 的 OSGi 框架是與 Ext 類加載器連接在一起的,這就是說 WAS 的 OSGi 框架在 WAS 整體的類加載器中處於一個很高的層次。而 Java 的類加載是采用父委托機制的,這就使普通的 WAS 上的應用程序會一層層的向上請求加載類,這樣當某個特定的類在 WAS 的 OSGi 部分中被發現,那麼 WAS 的類加載委托就會進入到 WAS 的 OSGi 運行時中。這樣當這個類加載以後,而它又需要加載其他類時,這之後的類加載委托請求就會在 WAS 的 OSGi 運行時中相互傳遞。
由此可見,如果我們需要將自己的 WAS 上的應用程序轉變成支持 OSGi,我們則需要將這個應用程序的模塊轉變成 OSGi bundle,然後將其放置在 WAS 的 plugins 目錄下。
介紹一個具有版本沖突問題的 WAS 上的樣例程序
在本章,我會模擬一個 WAS 上版本沖突的問題,讓大家能清楚地看到這種版本沖突問題是如何出現的。然後在第六章,我會告訴大家如何將這個樣例程序的某些組件轉變成 bundle,從而解決了版本沖突問題。
為了說明問題,我構建如下一個郵件系統,這個系統可以閱讀各種各樣類型的郵件。因為郵件的類型可以存在很多種,而且每一種的郵件可以使用獨特的浏覽方式,為了設計和維護的簡單,我們將它拆分成多個模塊。我們將各種郵件類型共用的接口和基礎類拆分成一個模塊,而將每種不同的郵件類型拆分成各自獨立的模塊。而為了便於描述將來會出現的版本沖突問題,我們只是用兩種簡單的硬編碼的郵件類型(FixedMailbox 和 XMLMailbox)來說明這個系統的整體結構,請看圖 4。
圖 4. 郵件系統結構圖
這個結構圖能夠清晰得告訴我們,整個系統的通用接口和基礎類被封裝在 MailboxAPI 模塊中,而每個具體的郵件類型將分別是一個模塊(FixedMailbox 模塊和 XMLMailbox 模塊),這些模塊將實現這些通用接口和每種郵件類型各自的業務邏輯,然後將郵件展示給用戶浏覽。所以在這個圖中將最少有 3 個模塊,他們會由不同的團隊來分別負責。當整個系統開發完畢之後,各個團隊需要將各自的組件發布到運行環境當中。因為 MailboxAPI 模塊(MailboxAPI.jar)是被所有的其他組件所依賴的,為了將來維護和發布的方便,一般 MailboxAPI.jar 會被放置到 WAS 的 lib 目錄下,這樣,其他的每一個組件都能輕松的找到它,而 MailboxAPI 模塊團隊也可以輕松地發布和維護 MailboxAPI,因為發布位置的唯一,MailboxAPI 模塊的每一次發布和維護將不需要通知到所有其他的團隊。而其他的模塊,因為包含業務邏輯和展示層,他們一般都是一個 ear 文件,所以他們的發布位置明顯是與 MailboxAPI.jar 不同的。圖 5 和圖 6 展示給我們 FixedMailbox 和 XMLMailbox 正常運行後的樣子。
圖 5. FixedMailbox 的運行情況
圖 6. XMLMailbox 的運行情況
注意:附件裡的每一個文件裡,都含有源碼,以方便感興趣的讀者深入研究。
在整個郵件系統運行一段時間後,某些用戶較多的個別郵件模塊就可能面臨著升級。因為客戶多,需求就多,而用戶的需求又是千奇百怪的,必然會出現某個特定的需求波及到了基礎模塊 MailboxAPI。這個時候,就可能會出現如下的情況:模塊 MailboxAPI 和 XMLMailbox 被要求升級,而模塊 FixedMailbox 為了穩定,要求不產生任何變化。然而當 XMLMailbox 的需求很大時,可能使 MailboxAPI 也產生了很大的變化,從而影響了 FixedMailbox,使之不能正常工作,於是版本沖突問題產生了。
為了演示這個問題,我改變了模塊 MailboxAPI 和 XMLMailbox 的代碼,產生了版本 2.0 的 MailboxAPI 和 XMLMailbox 模塊,他們被放置在附件 2(Version2.zip)中。為了看到真實的版本沖突問題,請解壓縮 Version2.zip,然後停止 WAS 並刪除 WAS 的 lib 目錄下的 MailboxAPI.jar,接著將 MailboxAPI2.jar 放置在 WAS 的 lib 目錄下,然後重啟你的 WAS 系統,接著你需要用 XMLMailboxReaderEAR2.ear 去升級已經存在的 XMLMailboxReader 應用,然後分別訪問兩個模塊的 Servlet,你就會看到 FixedMailbox 模塊將不能正常工作,而 XMLMailbox 則一切正常,如圖 7 或圖 8 所示。
圖 7. FixedMailbox 不能正常運行
圖 8. XMLMailbox 可以運行
注意:如果你不刪除 MailboxAPI.jar,而是讓 MailboxAPI.jar 和 MailboxAPI2.jar 並存在 WAS 的 lib 目錄下,那麼就有可能是 FixedMailbox 能正常工作,而 XMLMailbox 不能正常工作,如圖 9 和圖 10 所示,原因是 MailboxAPI.jar 可能在 MailboxAPI2.jar 之前被載入到類加載器中了。總之,FixedMailbox 和 XMLMailbox 將不能同時工作,這就是版本沖突問題。
圖 9. 當 MailboxAPI.jar 和 MailboxAPI2.jar 並存時,FixedMailbox 可以正常運行
圖 10. 當 MailboxAPI.jar 和 MailboxAPI2.jar 並存時,XMLMailbox 不能運行
講述如何將這個樣例程序中的某些組件轉變成 WAS 上的 OSGi bundle,從而解決了版本沖突問題
為了解決在第五章中的版本沖突問題,我們只需要將模塊 FixedMailbox 和 XMLMailbox 的業務邏輯部分(FixedMailbox.jar 和 XMLMailbox.jar)轉變成 OSGi 的 bundle,然後將 MailboxAPI 的版本 1 和版本 2(MailboxAPI.jar 和 MailboxAPI2.jar)也都轉變成 bundle,並使 FixedMailbox.jar 依賴於 MailboxAPI.jar,使 XMLMailbox.jar 依賴於 MailboxAPI2.jar。接下來,將 WAS 的 lib 目錄下的 MailboxAPI 模塊移除掉,然後將 FixedMailbox.jar、XMLMailbox.jar、MailboxAPI.jar 和 MailboxAPI2.jar 同時放入 WAS 的 plugins 目錄下,最後重新啟動 WAS,你會發現模塊 FixedMailbox 和 XMLMailbox 能同時工作,如圖 11 和圖 12 所示。
圖 11. 在將 FixedMailbox.jar 轉變為 bundle 後,FixedMailbox 可以正常運行
圖 12. 在將 XMLMailbox.jar 轉變為 bundle 後,XMLMailbox 可以正常運行
在將整個郵件系統轉變成支持 OSGi bundle 後,會使郵件系統的各個模塊之間的依賴關系從圖 4 編程圖 13 的情況,因為在 OSGi 中,同一模塊的多個版本可以並存,並且模塊之間的依賴關系可以通過版本進行限制,從而是 Java 應用軟件中的版本沖突問題得到了完美的解決。
圖 13. 最後的郵件系統的模塊之間的依賴關系
將已有的 Jar 文件轉換成 bundle 非常簡單,我們不需要改變任何 Java 代碼,只需要將必要的 OSGi 的 bundle 信息加入 Jar 文件的 MANIFEST.MF。為了方便大家的查看,我將已經轉好的 bundle 文件 FixedMailbox.jar、XMLMailbox.jar、MailboxAPI.jar 和 MailboxAPI2.jar 做成了附件 3。
總結
OSGi 框架是實現 Java 應用軟件模塊化的重要手段。當前,OSGi 已經變的非常流行,很多的著名的 Java 應用在底層已經開始采用 OSGi 框架,比如說 Spring,IBM 的 Eclipse 和 WebSphere Application Server。所以將你自己的 Java 應用轉變成支持 OSGi 是必要的趨勢,而本文將給你一個范例,告訴你如何在 WebSphere Application Server 中將你的應用轉變成支持 OSGi,從而就可以避免版本沖突問題的發生。