Java 9,OSGi以及模塊化的未來
關鍵要點
- Java 9將在2017年發布,一個標志性的特性是新的模塊化系統,命名為Java平臺模塊化系統(JPMS)。本文探討了它與現有的Java標準組件如何關聯起來,以及它對現有的Java標準組件有什么影響。
- 自1.0版本以來,Java已經增長了20倍,模塊化平臺是非常必要的。為了解決這個問題,也曾有過很多失敗的嘗試。而與此同時,OSGi已經提供應用程序模塊化16年。
- OSGi和JPMS在實現細節上有本質上的區別。如果JPMS被當作模塊化的通用解決方案,似乎會有嚴重的缺陷和缺失的功能。
- JPMS的目標是使用起來比OSGi更簡單、更容易。但是,讓現有的非模塊化產品模塊化是非常復雜的,而且JPMS在這個目標上似乎沒有成功。
- JPMS在Java平臺自身模塊化方面做得很好,這意味著我們可以為特定的工作構建一個小的運行時環境,它只包含Java平臺相關的部分。在應用程序模塊化方面OSGi有很多優勢。我們已經證明了兩者可以結合起來,這看起來是一個成功的方式。
Java 9將在明年發布,一個標志性的特性是新的模塊化系統:Java平臺模塊化系統(JPMS)。雖然JPMS的細節還沒有完全確定,我們已經了解了很多有關它方向性的內容。
Java已經有一個預先存在的模塊化系統,自2000年以來一直以各種形式存在。它就是被稱為OSGi的模塊化系統,是一個獨立于供應商的行業標準。它由OSGi聯盟發布,由領先的軟件供應商、電信公司和其他組織(包括Adobe、博世、華為、IBM、Liferay、NTT、Oracle、Paremus 以及Software AG)組成。它推進了幾乎所有的Java EE應用服務器、最流行的IDE、Web應用程序(像eBay、Salesforce.com和Liferay),并用于政府和軍隊,如美國空軍和聯邦航空管理局。
OSGi是為物聯網提供的——OSGi一開始是專為嵌入式設備設計的,那是在很多年前,當時內存和CPU資源明顯受到局限。現在設備有了更多的能力。這提供了構建復雜應用程序和解決方案的機會,并催生了蓬勃發展的生態系統,在這個生態系統中組織和個人貢獻的軟件和硬件元素可以添加到整體解決方案中。這樣的生態系統在市場上很廣泛,包括互聯家庭、車聯網、智能城市和工業4.0(IIoT)。網關通常用于傳感器和設備之間相互連接,并連接到后端系統。應用程序和服務可以在本地網關和/或云上運行。
OSGi還提供多種規范啟用構建開放的物聯網生態系統的基本特性。這些特性包括設備管理、軟件配置以及設備抽象,即從底層通信協議歸納設備。在今天,像AT&T、博世、NTT、德國電信、美國通用電氣、日立、美諾、施耐德電氣等公司都受益于采用OSGi構建物聯網的解決方案,并且做了很多年。目前已經有上百萬的設備連接采用OSGi和物聯網。
當然,OSGi的用戶都很好奇Java 9中新的模塊化系統在短期和長期將會如何影響OSGi。
Java生態系統中很快會出現兩個模塊化系統,這有技術、政治和商業的原因。本文中,我們避開政治原因,從技術的角度對兩者進行比較。我們總結了JPMS和OSGi如何協同工作,思考它們各自的領域是什么以及在嶄新的世界中存在什么樣的機遇。
請注意,本文中,我們使用的信息在2016年8月已公開發布。在該規范確定之前一些細節可能會改變。
背景
自1990年代末誕生以來Java平臺增長顯著。綜觀下載文件的大小,JDK 1.1為10Mb,而Mac OS X下載JDK 8u77卻非常大,有227Mb。安裝占用的空間和內存需求也有了相應的增加。這些增加是因為增加了新的功能,而且大部分功能是受歡迎并且有用的。然而,每一個新的功能都為不需要這個功能的用戶創造了膨脹——沒有人會使用平臺所有的功能。而且即使已經過時,所有現有的功能都會保留,因為Java管理員提供了令人欽佩的奉獻精神——向后兼容性。
多年來,Java體重的增加并不是一個大問題。它是最流行的企業平臺,它的主要競爭對手是微軟的.NET,然而.NET也有著相似的軌跡。在當今世界,Java面臨不同的挑戰。物聯網推動了空間占用新一輪的關注,新的、靈活的平臺和語言(比如Node.js、Go)都是非常有競爭力的對手。
安全也是一個大問題:Java攻擊引起了組織對安全意識的重視,把它從用戶桌面完全移除。如果內部JVM和用戶空間應用程序代碼之間有更好的隔離,這些攻擊是不可能發生的。
很早之前我們就清楚需要為模塊化平臺做一些事情了。在2000年中期有一系列失敗的嘗試,例如,JSR 294和它的“superpackages”,JSR 277的“Java模塊化系統”——最終名為Jigsaw的原型項目出現了。這本來是在2011年Java 7中提交的,但被推遲到Java 8再推遲到Java 9。作為一個原型項目,Jigsaw為JPMS規范提供了參考實現。
而與此同時,OSGi用了16年時間不斷發展和完善。OSGi是應用程序模塊化的標準:由于它不是Java平臺的一部分,它不能影響平臺本身的模塊化。但是,許多應用已經受益于它提供的高于JVM的模塊化模型。
高層比較
JPMS和OSGi之間有很多小的差異,但是有一個很大的不同,就是隔離的實現。
隔離是模塊化系統最基本的特征。每個模塊必須有一些保護措施防止運行在同一應用程序中其他模塊的干擾。隔離是一個連續的而不是二進制的概念:無論OSGi還是JPMS都需要做一些事情來避免那些表現不好的模塊的影響,這些模塊占用了JVM中所有可用的內存,運行了數千個線程或者讓CPU處于繁忙的循環。如果一個模塊可以作為操作系統上獨立的進程運行,是可以提供這類保護的,但即使是這樣,它也是不完美的;有人仍然可以使操作系統崩潰或者擦除磁盤。
OSGi和JPMS都提供了代碼級隔離,這意味著一個模塊不能訪問另一個模塊的內部類型,除非該模塊有明確的許可。
OSGi通過類加載器實現隔離。每個模塊(或者在OSGi術語中稱為“bundle”)有一個類加載器,它知道如何在bundle中加載類型。它也可以將類加載請求委托給它所依賴的其他bundle的加載器。該系統是高度優化的,例如,OSGi不會為一個bundle創建一個類加載器直到最后一刻,而且事實上每個加載器會處理一個更小的類型,這樣每個類型可以加載得更快。
這個系統最大的優勢是,bundle可以包含重疊的包和類型,而且不會相互干擾。實際的結果是,可能某些包和庫有多個版本同時運行在相同的JVM。在處理像Maven這樣的構建工具帶來的復雜的傳遞依賴圖時,這是個福音。在許多企業,Java應用程序幾乎不可能有這樣的一套依賴,該依賴中每個庫只包含一個版本。
例如,我們來看看 JitWatch 庫 1 。JitWatch依賴于 slf4j-api 1.7.7 和 logback-classic 1.1.2,但是logback-classic 1.1.2依賴于slf4j-api 1.7.6,與JitWatch直接的依賴有沖突。JitWatch也傳遞地依賴于 jansi 1.6和1.9版本,如果包含測試范圍的依賴,我們會有另一個slf4j-api的版本1.6。這種混亂是很常見的,傳統的Java中沒有真正的解決方案,只能逐步在依賴樹中添加“excludes”直到奇跡般地得到一套可以運行的依賴庫。不幸的是對于這個問題JPMS也沒有答案,我們很快就會看到。
使用類加載器進行隔離確實有一個缺點:它打破了每一個類型最多可以在一個位置找到的假設。這是模塊化的一個自然結果。如果一個模塊可以不受其他模塊的干擾使用自己的類型,那么不可避免地一個單一類型的名稱可能會在多個模塊中發現。遺憾的是,這造成了一個問題,因為很多保留的Java代碼不是用模塊化的思想編寫的。特別是,調用Class.forName(String)通過名字查詢類型時,在真正模塊化的環境中不是總能得到正確的結果,因為有多個可能的返回類型。
正是由于這個缺點,不能使用OSGi模塊化JDK本身。JDK的許多地方都有一個隱含的假設,任何JDK類型可以從JDK的任何其他部分加載,所以很多事情在OSGi下會被打破,比如模型。為了解決這個問題,也為了減少使用Class.forName代碼的遷移,JPMS選擇在隔離時不使用類加載器。當你在“modulepath”使用一組模塊來啟動應用程序時,所有這些模塊將由相同的類加載器加載。相反,JPMS引入新的訪問規則實現隔離。
OSGi的隔離屏障是 可見的 。在OSGi,我們不能加載一個模塊的內部類,因為它們是不可見的。也就是說,自己模塊的類加載器只能看到自己模塊內部的類型以及從其他模塊明確導入的類型。如果我試圖從其他的模塊中加載一個內部類,我的類加載器是看不到該類型的。就好像是根本不存在的類型。如果試圖繼續加載該類,就會得到NoClassDefFoundError或者ClassNotFoundException的異常。
在JPMS,每一個類型對于任何其他類型都是可見的,因為他們存在于同一個類加載器。但是,JPMS增加了輔助檢查以確定加載類有權訪問它試圖加載的類型。其他模塊的內部類型實際上是private的,即使它們被聲明為public。如果我們試圖繼續加載它,那么我們會得到IllegalAccessError或者IllegalAccessException的異常。如果我們試圖加載private的或者另一個包的默認訪問類型也會得到相同的錯誤,而且在這個類型上調用setAccessible也是無用的。這改變了Java中public修飾符的語義,以前它是普遍可訪問的,現在只可在一個模塊和它的require對象中訪問。
JPMS方法的缺點是,它不可能有重疊內容的模塊。也就是說,如果兩個模塊都包含一個私有(非導出)的包org.example.util,這些模塊不能同時在模塊路徑上被加載——它會導致layerinstantiationexception異常。通過應用程序實例化類加載器可能會解決此限制——但這正是OSGi已經為我們做的!
再次強調,完全是通過設計允許JPMS模塊化JDK的內部。但結果是,你會有不能完全一起工作的模塊,因為它們內部的實現細節有沖突。
復雜性
對于OSGi最常見的抱怨之一是,它給開發人員增加了復雜性。這有一定的道理,但是有這些抱怨的人都搞錯了復雜性的原因。
模塊化并不是一個在應用程序發布前灑在上面的神奇的塵埃。它是在設計和開發各個階段必須遵循的準則。一些開發人員已經意識到了OSGi帶來的巨大收益,他們在早期就開始使用OSGi并且在編寫一行代碼之前會運用模塊化思想,他們發現OSGi實際上是非常簡單的,尤其是在使用現代化OSGi工具鏈時,它自動生成元數據并且在運行前做了大量的一致性檢查捕獲異常。
而另一方面,開發人員試圖把OSGi引入現有的大型代碼庫時遭遇了困難,因為這些代碼很少能夠模塊化以便遷移。沒有執行模塊化的準則,很容易走捷徑,打破封裝性。BEA WebLogic的一個開發人員告訴我,在Oracle收購BEA之前:“我們以為我們是模塊化的,直到我們開始使用OSGi。”
除了非模塊化的應用程序,OSGi的采用也受到非模塊化庫的阻礙。一些流行的Java庫中類加載和全局可見性的假設在模塊化結構中被打破了。OSGi做了大量工作,讓它可以使用這些庫,這是OSGi規范明顯復雜性的來源。我們需要有一定的復雜性來處理混亂的、復雜的現實世界。
我們很快就會看到,JPMS也會有同樣的問題——可能更是如此。如果你的組織曾試圖采用OSGi,卻因為遷移工作量過大而放棄了,那么當你要遷移到JPMS時,至少應該預期會有同樣多的工作量。只需要看看Oracle在模塊化JDK時的經驗:有很多的工作要做,導致Jigsaw從Java 7延遲到Java 8,再到Java 9,甚至Java 9已經延遲了一年(到目前為止)。
Jigsaw項目開始于一個目標就是越來越簡單,但JPMS規范大大增加了復雜性:與類裝載器模塊的相互作用;分層結構和配置;re-exporting要求;弱模塊;靜態要求;qualified導出;dynamic導出;跨層繼承的可讀性;多模塊JAR文件;自動模塊;未命名的模塊等等,已經非常清晰所有的這些功能都會作為需求添加進來。類似的過程也發生在OSGi,只是它有16年領先的優勢。
依賴:包vs全部模塊
隔離只是模塊化的一個難題:模塊仍然需要協同工作和通信。模塊之間建立“墻”后,它們需要以一個可控的方式重新連接。一個模塊化系統必須定義模塊訪問其他模塊功能的方式。可以通過在類型級別上靜態地或者動態地使用對象來實現。
靜態依賴在編譯時就是已知的和可控的。如果一個類型在一個模塊的邊界引用另一個類型,那么模塊系統需要提供一個方法讓該類型可見并且可訪問。有兩種方式:模塊需要有選擇性地暴露一些內部類型,模塊需要指定自己使用了其他模塊的哪些類型。
導出(Exports)
在OSGi和JPMS中,類型暴露在Java包級別就完成了。在OSGi使用Export-Package語句聲明指定名稱的包對其他bundle是可見的。它看起來像這樣:
Export-Package: org.example.foo; version=1.0.1, org.example.bar; version=2.1.0
該聲明在META-INF/ MANIFEST.MF文件中。OSGi初期大多數開發人員會手工指定這樣的聲明;但我們越來越傾向于使用構建工具生成。現在最流行的方式是在Java源代碼中添加注解,Java 5中引入了package-info.java文件允許包級別的注解和文檔,所以OSGi中可以如下編寫:
@org.osgi.annotation.versioning.Version("1.0.1") package org.example.foo;
這是一個有用的模式,因為想要導出一個包時可以直接在該包中表示。版本也可以在這里顯示,包的內容變化時在附近就可以更新 2 。
JPMS中包的導出在module-info.java文件中,如下:
module A { exports org.example.foo; exports org.example.bar; }
請注意,如果缺少version,JPMS中模塊和包都不能被版本化;稍后我們會討論這一點。
Imports/Requires
雖然在導出時OSGi和JPMS是類似的,但是導入或對其他模塊的依賴卻有顯著的差異。
在OSGi,導入包是對導出包的補充。使用Import-Package聲明導入包,例如:
Import-Package: org.example.foo; version='[1,2)', org.example.bar; version='[2.0,2.1)'
OSGi中bundle必須導入它所依賴的所有包,除了java.*開頭的包,如java.util。例如,如果你的bundle中的代碼依賴org.slf4j.Logger(并且你的bundle中實際上并不包含org.slf4j包),那么這個包必須被導入。同樣,如果你依賴于org.w3c.dom.Element,那么你必須導入org.w3c.dom。但是,如果你依賴java.math.BigInteger,你不需要導入java.math,因為Java.*包是由JVM的bootstrap類加載器加載的。
OSGi對于引用所有的bundle還有一個并行機制,稱為Require-Bundle,但在OSGi規范中它已經過時了,現在它的存在只是為了支持很小的邊緣的案例。Import-Package最大的優勢是它允許模塊在不影響下游模塊的前提下被重構或被重命名。如圖1和2所示。
在圖1中,模塊A被重構為兩個新的模塊,A和A',但模塊B不受該操作影響,因為它依賴于提供的軟件包。在圖2中,我們對模塊A執行完全相同的重構,但現在B可能是壞的,因為它引用的包有可能不再存在于模塊A(在這里我們必須說“可能”,因為我們不知道模塊B使用模塊A哪些包——這正是問題所在!)。
圖1:通過 Imported Packages 重構模塊
圖2:通過 Requires 重構模塊
Import-Package語句手動寫是很繁瑣的,所以我們不這樣做。通過OSGi工具檢查依賴生成該語句,并將編譯類型內置到bundle中。這是非常可靠的,比開發人員自己聲明運行時依賴更可靠。當然,開發人員仍然需要管理自己的編譯依賴,按照Maven正常的方式去做(或者你選擇的構建工具)。如果編譯時把太多的依賴放在classpath下并不會有影響:可能發生的最壞情況是編譯失敗,這只會影響源頭的開發人員并且很容易修復。另一方面,太多的運行時依賴會降低模塊的可移植性,因為移植時所有這些依賴關系必須一起移植,而且可能與另一個模塊的依賴發生沖突。
這導致了OSGi和JPMS之間另一個關鍵的理論差異。在OSGi我們始終認為,編譯時依賴和運行時依賴可以并且經常會不同。例如,它的標準做法是,有一套編譯時API和一套運行時API。此外,開發人員通常在我們所能兼容的最老的API版本上編譯,但會選擇可以找到的最新的版本來運行。甚至非OSGi開發人員也很熟悉這種方法:你通常會在準備支持的最低版本的JDK上編譯,卻鼓勵用戶在最高的版本(包含所有的安全補丁和增強性能)上運行。
另一方面JPMS采取了不同的策略。JPMS旨在實現“跨越所有階段的保真度”,這樣“模塊化系統應該…在編譯時、運行時以及在開發或部署的各個階段可以以完全相同的方式工作”。因此,依賴關系是在整個模塊運行時定義的,因為這就是它們在編譯時定義的方式。例如:
module B { require A; }
require語句和OSGi過時的Require-Bundle有相同的效果:模塊B可以訪問所有模塊A的導出包。因此,它也存在Require-Bundle同樣的問題:從模塊的聲明無法確定重構模塊A的內容是否是安全的,所以這樣做一般是不安全的。
我們發現,依賴樹使用requirements而不是imports有更高程度的扇出:每個模塊攜帶比它真正需要的更多的依賴。這些問題是真實和重要的。尤其是Eclipse插件作者深受其害,因為歷史原因Eclipse bundle 傾向于使用requires而不是imports。非常不幸地,JPMS也遵循了這條路線。
有趣的是,雖然編譯/運行時的保真度是JPMS的根本目標,但最近的變化明顯減弱了保真度。目前的早期試用版本允許用static修飾符聲明requirement,這意味著在編譯時依賴是強制性的,但在運行時是可選的。相反,可以用dynamic修飾符聲明導出,這可以使導出包在編譯時無法訪問,但在運行時可以訪問(使用反射)。有了這些新特性可能會創建出成功編譯和鏈接,但在運行時拋出IllegalAccessError/Exception異常的模塊。
反射和服務
Java生態系統是巨大的,包含了用于各種目的的各種各樣的框架:從依賴注入到mocking框架、遠程調用、O/R映射等。從用戶提供的代碼來看,許多框架使用反射來實例化和管理對象。例如,Java持久化架構(JPA),它是Java EE套件規范的一部分:作為對象關系映射,為了將domain類與從數據庫加載的記錄一一映射,必須從用戶代碼加載和實例化domain類。另一個例子是,Spring框架加載和實例化“bean”類實現接口。
這會為包括OSGi和JPMS的模塊化系統帶來問題。理想情況下,domain或bean類應該隱藏在一個模塊內部:如果它被導出,就會成為公共API,這樣會對依賴于它的消費者造成破壞,但是我們希望能夠靈活地隨意改變我們的內部類。另一方面,如所述的,通過反射訪問非導出類型支持框架是非常有效的。
由于OSGi的類加載器是基于設計的,模塊可以獲得其他模塊非導出包和類型的可見性——只要他們知道該類型的全限定名以及知道是哪個模塊發出的請求(請記住幾個模塊可以包含任何給定的類型名稱)。Java長期使用反射的精神有效地減少了隔離,在這里甚至所謂的私有字段都可以通過調用setAccessible方法被公開。
在OSGi中使用此功能是常見的做法,用來提供根本沒有導出模塊的實現!相反,它們可能包含引用內部類型的聲明,這些內部類型可以通過框架加載。例如,使用JPA做持久化的模塊可以引用persistence.xml文件的domain類型,并且在需要時JPA實現模塊將會加載引用類型。
最大的用例是實施服務組件。OSGi規范包含一章節叫聲明式服務(DS),定義了一個模塊如何聲明組件:類的生命周期是由框架管理的。組件可以綁定到OSGi服務注冊表中的服務,并且可以自選地為自己提供服務。例如:
@Component public class CartMgrComponent implements CartManager {@Reference UserAdmin users; @Override public Cart getOrCreateCart(String user) { // ... } }</pre>
在這個例子中,CartMgrComponent是一個提供CartManager服務的組件。它引用了一個服務——UserAdmin,類的生命周期由DS框架管理。當UserAdmin服務可用,CartMgrComponent就會被創建,并且它會發布CartManager服務,該服務同樣可以在其他模塊的其他組件中引用。
這個框架可以工作是因為它加載了CartMgrComponent類,該類已經被@Component注解標記為組件。定義組件和服務是OSGi應用設計和編寫的主要方式。
在JPMS,只有導出包的類型可以被訪問,即使是反射。雖然在非導出包中類型是可見的(你可以調用Class.forName獲取一個類對象),但在模塊外它們是不可訪問的。當一個框架試圖調用newInstance實例化一個對象,會拋出IllegalAccessException異常。這似乎切斷了框架的許多可能性,但是也有一些解決方法。
一種方法是提供個別類型作為服務,可以通過java.util.serviceloader加載。自Java 6開始serviceloader就是標準平臺的一部分,在Java 9它已被更新支持跨模塊工作。serviceloader可以訪問非導出包的類型,只要提供包含provides聲明的模塊。不幸的是,serviceloader是古老的,不能為現代化框架,如DS或spring,提供所需的靈活性。
第二種可能是使用“qualified”導出包。這種導出,只允許指定模塊訪問,而不是所有模塊都通用。例如,你可以導出bean包到Spring Framework模塊。但是這可能無法用于其他方面像JPA,因為JPA是一個規范而不是一個指定的模塊,并且它可以由不同的模塊實現,如Hibernate、EclipseLink等。
第三種可能是“dynamic”導出,這種包任何人都可以訪問,但只能自己使用反射,而且不是在編譯時。這是JPMS一個非常新的特性,在郵件列表上它仍然是有爭議的。它最接近OSGi的permissive方法,但它仍然需要模塊作者為某些包明確添加dynamic導出,這些包中可能包含需要反射地加載的類型。作為一個OSGi用戶感覺它是不必要的復雜性。
參考文獻
- 感謝Alex Blewitt的分析。
- 使用@Version注解意味著導出,因為只有導出包需要版本。在接下來的OSGi發布中一個更明確的@Export注解正在計劃中。
- 就在本篇文章發表之前,本區域在2016年9月12日再次發生了變化。動態導出現在換成“弱模塊”的概念。我們仍在評估這一根本變化的影響,并注意到它在Java 9的發布時間表中已進一步造成4個月的延遲。
來自:http://www.infoq.com/cn/articles/java9-osgi-future-modularity