Java 9 中一個重要的新特性就是模塊化。它的實現機制是什么那?它和已有的模塊框架OSGi有什么差異那?為了回答這些問題,本人在網上找到了一篇比較好的介紹文章,為了加深理解,對文章進行了翻譯。由于原文分為2個部分,所以翻譯對應也分為2篇:
1)《Java 9,OSGi和模塊化的未來(1)》是對《Java 9, OSGi and the Future of Modularity (Part 1)》的翻譯,文章日期為2016年9月22日。介紹的內容包括:背景、高層次比較、復雜性、依賴粒度對比、模塊導出對比、模塊導入對比、反射和服務。
2)《Java 9,OSGi和模塊化的未來(2)》是對《Java 9, OSGi and the Future of Modularity (Part 2)》的翻譯,文章日期為2016年10月4日。介紹的內容包括:動態性、二者協同工作、未來發展、結論。
本文是對原文第二部分的翻譯。
引言
關鍵點
- Java 9 在2017年發布,其中一個重要的特性就是新的模塊化系統,被稱作Java平臺模塊系統(Java Platform Module System,JPMS)。本文將介紹它與Java已有的模塊標準化機制OSGi的關系,以及對OSGi的影響。
- 自1.0版本以來Java平臺已經增長了20倍,整個平臺存在著模塊化的需求。為了解決這個問題,進行了很多不成功的嘗試。與之相對的是,OSGi已經提供了16年的應用模塊化服務。
- OSGi和JPMS在實現細節上差別很大。如果將JPMS作為模塊化的一般解決方案,會存在一些重大的缺陷,并且缺少OSGi的一些特性。
- JMPS的目標是比OSGi更簡單和更容易,但是對一個非模塊化的產品進行模塊化設計本身就是一件很復雜的事情,JMPS看起來好像還沒有實現這個目標。
- JMPS在對Java平臺本身的模塊化方面做得非常好,這意味著我們可以在運行時只加載和任務相關的Java平臺組件。對于應用模塊化,OSGi有很多的優點。我們已經證明這兩者可以很好地結合在一起。
這篇文章是“Java 9, OSGi and the Future of Modularity”文章系列的第二部分。第一部分請查看《Java 9, OSGi and the Future of Modularity (Part 1)》。
本文將繼續深入了解OSGi和JPMS(Java Platform Module System),其中JPMS會作為Java 9的一個組成部分。在上篇文章中,我們在一個較高的層次比較了這兩個模塊化系統,討論了它們是如何解決模塊間的隔離性的。我們研究了依賴關系是如何建立的,并且我們探討了一些關于反射的問題。本文作為第二個部分,將探討版本管理、模塊動態加載以及二者潛在的協同工作可能性。
版本管理
版本管理是所有軟件交付的一個關鍵點。API和實現都會改變,所以無論何時我們依賴它們,我們都隱含地依賴于它們在某個時間點的存在。任何模塊系統必須能夠處理這個現實,通常用顯式的版本來指明依賴關系。
然而并非所有的改變都具有同樣的破壞性。如果我們使用了版本為1.0.0的模塊的來構建和測試我們的軟件,那么當我們部署版本1.0.1或1.0.5時,我們有可能可以繼續工作,但是如果部署的是版本2.0.0或版本5.2.10,那么不能工作的可能性較大。這表明模塊系統需要了解并支持兼容性范圍。
OSGi一直支持這些概念。Bundle在導出包時標注了版本號。在導入包的時候指明了版本范圍,通常使用閉開區間來表示這個范圍,例如“[1.0.0, 2.0.0)” 表示版本介于1.0.0和2.0.0之間,不包括2.0.0。OSGi使用語義版本控制,與流行的語義版本規范完全一致(盡管OSGi早于那些規范)。大致來說,版本號的第一段是主版本號,表示功能和API的重大變化,第二段是次版本號,表示功能的增強,第三段表示增加了一些補丁。
OSGi的開發者不需要推理或顯式地聲明這些版本范圍。和 import 一樣,版本范圍可以在構建時期根據依賴情況自動構建。例如,如果我們僅需要使用一個API包,那么我們可以提供一個比較寬的區間,如“[1.0.0, 2.0.0)”,這個區間包含了最小和最大版本號之間的所有版本。但是如果我們是提供一個服務接口的實現,那么我們需要使用較窄的區間來導入依賴,例如“[1.0.0, 1.1.0)”,意味著包含1.0.0和這個區間內的,但是不包含1.1.0。這里的不同點在于,一個支持1.0.0的服務提供者無法支持1.1.0,因為增加的版本號表明有新的功能被添加了進來,而提供者無法自動提供新的功能。另一方面,一個服務消費者,可以方便地使用1.1.0和1.2.0等版本號,因為它可以忽略新增加的功能。
除了生成導入范圍外,OSGi構建工具(bnd)還可以幫助獲得導出包的正確版本。版本號是包的一個屬性,它可以直接寫在包中(通過在 package-info.java
文件中添加 @Version
注解)。當包中的內容改變的時候,一件很重要的事就是修改這個版本號,例如:當我們在服務接口中增加了一個方法時,我們需要將版本號從1.0.0增加到1.1.0。構建工具檢查版本號是否準確地反映了所做更改的性質。例如,當我們添加了方法而忘記修改版本號的時候,構建將會失敗,或者我們只是將版本號增加為1.0.1的時候也會如此。
最終,OSGi具有這樣的靈活性:可以在單個應用程序中同時部署模塊的多個版本。這種情況會在這樣的場景出現:通過傳遞依賴我們引用了一些通用庫的不同版本,如 slf4j 或 Guava。還有一些限制是:我們不能直接在單個模塊中導入包的多個版本,但是在真正需要的時候,具備這樣的能力還是很有用的。
與之相對的是,JPMS對版本控制基本沒有任何支持。
在 module-info.java 中沒有辦法為一個模塊指明版本。在 module-info.class 文件中存在一個版本屬性,但是它并不是來自于 Java 代碼,目前還不清楚它在實際中該如何使用。依賴聲明同樣沒有版本:JPMS模塊只能通過名字來 require 其它模塊,無法指明版本,當然更無法指明版本的范圍。這些特性需要由外部工具添加,但這種方法是受限的,因為 module-info.java 源文件是不可擴展的,并且在該文件內也無法使用 Java 注解。
JPMS的需求規定:在運行時選擇兼容的版本不在支持范圍內。這意味著其它工具將不得不做這項工作,但沒有合適的元數據它們無法完成這項工作。將版本元數據存儲在與基本模塊元數據相同的描述符中是很自然的,但這是不可能的。
正如我們提到的一樣,JPMS中禁止在運行時同時包含一個模塊的多個版本。此外,它禁止多個模塊導出相同的包,甚至禁止重復的私有包。因此,無論其它工具使用什么方法來構建一個有效的模塊集合,都需要找到一個解決轉遞依賴沖突的方法。在很多案例中,一個簡單的“方法”是:在多個模塊共同存在的場景下,禁止一些模塊的使用。
動態性
OSGi基于類加載的隔離實現方案有一個不錯的用處:可以支持模塊在運行時被動態加載、更新和卸載。這在企業環境中似乎并不重要,事實上大多數OSGi企業在部署中都不會使用動態更新。的確,OSGi沒有要求你一定要使用動態更新!
但是動態更新在其它環境中是非常有用的,比如物聯網。在數千臺甚至數百萬臺設備上,通過緩慢或斷斷續續的網絡更新軟件是一件讓人頭痛的事情。OSGi是少數技術之一,可以在任何平臺上直接支持使用最少數據量進行即時更新:我們只需要發送實際更改的模塊。
最初在2000年,電信運營商在家庭網關和路由器上使用OSGi構建智能家居解決方案的主要原因之一是:能夠在不進行固件更新的情況下管理軟件。固件更新不吸引人的原因有很多:下載固件更新通常需要下載兆字節的軟件到潛在的數百萬設備上;固件是針對設備設計的,因此你可能會有許多不同的更新來創建和管理部署;測試固件更新需要大量的、耗時的、昂貴的壓力測試,每次都要對每個設備執行這個測試。OSGi顯著簡化了這個過程;更新可以應用在模塊中,并快速安裝在運行的網關和路由器上,不需要重啟;同樣的模塊可以在所有設備上使用(通常是底層硬件設備的抽象),重要的是,單元測試可以在更小的軟件集上執行,節省大量的時間、精力和金錢。一個具體的例子是 Qivicon,它是一個由德國電信公司成立的行業聯盟。Qivicon 提供家庭網關,包括:基于OSGi的軟件棧,后端基礎設施、應用程序開發工具以及維護和支持。通過使用OSGi來搭建基礎生態系統的方法,使得Qivicon的合作伙伴能更快地將智能家居產品推向市場。
Qivicon 合作伙伴不斷整合新設備和開發新的創新增值服務。這需要復雜的設備管理和軟件供應能力,以確保針對特定設備平臺的軟件組件的依賴性和兼容性管理。通過利用現有的工業標準(如 TR-069 和 OMA-DM),這些功能已經被寫入OSGi標準規范中了。
此外,動態行為不僅僅體現在軟件更新上。
OSGi服務注冊表本質上是動態的。服務可以注冊和卸載,與之綁定的相關組件也會在收到事件通知。服務允許真實世界不斷變化的狀態被表示和通知。即使在相對穩定的企業應用領域,這也是相關的。例如,OSGi服務可以表示外部數據輸入是否可用,或者是REST服務的負載均衡IP地址,甚至是金融市場的開放時間。每個消費服務的組件可以決定服務不可用時的反應行為:可以繼續,或者注銷自身提供的服務。因此,狀態的變化被可靠地傳播到任何有影響的地方。
協同工作與未來發展
JPMS會在Java 9中發布。目前有非常多的應用程序是用OSGi寫的,同時還有很多代碼正在被書寫。這些代碼是安全的嗎?它們是否需要在JPMS平臺上重新被改寫?
首先需要明確地是:OSGi應用可以不用修改代碼繼續在Java 9上運行,因為它沒有使用不被支持和內部的Java API。對于其他的Java應用代碼也是如此。OSGi只使用了被支持的Java API,并且Oracle承諾Java 9不會使這些應用奔潰。你在使用Java 9時遇到的問題可能是來自一些使用了JDK內部類型的類庫,因為這些類型在Java 9中無法被訪問,除非通過特殊的配置標志進行訪問。OSGi的用戶將能更好地應對這個改變,因為它們的模塊具有顯式的依賴列表。通常,Java應用會在類路徑上放置多個Jar包,與它們相比,基于OSGi的應用對于平臺依賴的范圍會更加清晰。
一個最基本兼容模式是,OSGi框架和bundle會存在于JPMS的“未命名”模塊中。OSGi還會繼續提供所有已經存在的隔離性特性,包括它功能強大的服務注冊和動態加載能力。你對OSGi的投資是安全的,而且OSGi仍然是新項目的一個好選擇。
但我們希望能比這做得更好。當OSGi運行在已經模塊了的Java 9平臺上時,我們應該能夠充分利用平臺中的模塊。例如,可以為一個OSGi bundle聲明它依賴的平臺模塊,這意味著一個OSGi bundle可以直接依賴一個JPMS模塊。OSGi框架應該關注那些運行時的依賴,并且工具應該能夠根據這些依賴準備運行時環境。
此時事情已經看起來很不錯了。在我2015年11月的一篇博客中,我對這個概念進行了驗證性描述,在JPMS上構建并運行了OSGi程序。我詳細介紹了如何讓OSGi bundle在基礎平臺的JPMS模塊中聲明依賴。我展示了OSGi是如何拒絕這樣的一個bundle:它依賴的JPMS模塊不在平臺中。我沒有在運行時提供一個工具原型,但是通過所有描述的部分已經可以構建這樣的一個工具了。
圖3描繪了未來兩個機制可以如何一起工作。我們可以看到:Bundle A 導入了包 javax.activation
,這個包來自JPMS中導出的 javax.activation
模塊。交互層知道平臺中包含了這個模塊,會運行OSGi來處理它。當Bundle A遷移到Java 9上時,并不需要做任何改變。Bundle B使用了 java.net.http
包,這個包來自JPMS的 java.httpclient
模塊,但是它無法在OSGi中Import-Package部分進行聲明,因為它是以 java.
開頭的(需要注意的是,所有的 bundle 和 moudle 都隱含地依賴 java.base
)。
因此,我們提出了一個新的OSGi頭部,稱作“Require-PlatformModule”,它用來表示對JPMS中模塊的依賴。這樣當Bundle B沒有包含 java.httpclient module
模塊的時候,OSGi框架能夠在Bundle B中“快速失敗”。這同時還可以使得工具能夠為應用構建一個完整的運行時環境,這個環境是JPMS中的 modules 和OSGi中的 bundles 的最小集合。
再次聲明,這個工作是一個非官方的概念證明。最終,OSGi如何和JPMS進行交互將由規范來處理。
結論
JPMS,通過Jigsaw原型項目,對Java平臺本身的模塊化工作是非常出色的。通過這個工作,可以構建更小的運行時環境,只需要包括Java平臺中任務依賴的部分。
然而作為應用的模塊化規范,JPMS存在嚴重的缺陷。缺少對版本控制的支持是一個令人震驚的遺漏,而且在沒有外部工具提供額外元數據的情況下,很難在構建應用程序的過程中實現版本管理。整個模塊(whole-module)依賴聲明形式將會導致獲得更多的傳遞依賴項,這將削弱它們遷移到更小平臺的能力。無法通過反射來獲得未導出模塊中的類型,這樣將會無法使用Java生態系統中已有的一些框架。
這些設計對于JDK本身來說可能是合適的:這增加了平臺的健壯性和安全性,同時沒有破壞所有Java應用的向后兼容性。但是它們的做法只是一種折中,對于應用的模塊化來說,這個設計很不友好。
所以OSGi的未來看起來是光明的:通過將OSGi和整理過的模塊化Java平臺相結合,我們可以使這兩個生態都獲得更好的發展。OSGi已經具有了16年的經驗,并且遇到和解決了JPMS還沒有考慮過的問題。OSGi的開發工具和運行時生態是廣泛和深入的。在一個長期有效、獨立的標準機構的支持下,它的未來是得到保障的。你還在等什么那?