模塊化是大型Java系統的一個重要特征。在這些項目中構建腳本和項目通常被劃分為 多個模塊,以便改進構建過程,但是在運行時卻很少考慮劃分模塊的問題。
在“模塊化Java”系列文章的第二篇裡,我們將討論靜態模塊化(static modularity )。內容包括如何創建bundle、將其安裝到OSG引擎以及怎樣建立bundle之間的版本依賴 。在下一篇文章中,我們將討論動態模塊化(dynamic modularity)並展示bundle如何對 其他bundle作出響應。
在上篇文章《模塊化Java簡介》中講到,Java在開發時把package作為模塊化單元,部 署時把JAR文件作為模塊化單元。可是盡管像Maven這樣的構建工具能夠在編譯時保證 package和JAR的特定組合,但這些依賴在運行時classpath下仍可能出現不一致的情況。 為了解決這一問題,模塊可以聲明其依賴需求,這樣,在運行時就可以在執行之前進行依 賴檢查。
OSGi是一個Java的運行時動態模塊系統。OSGi規范描述了OSGi運行時的工作行為方式 ;當前版本是OSGi R4.2。
一個OSGi模塊(也稱為bundle)就是一個普通的JAR文件,但在其MANIFEST.MF中帶有 附加信息。一個bundle的manifest必須至少包含如下內容:
Bundle-ManifestVersion:對OSGi R4 bundle來說必須是2(OSGi R3 bundle則默認為 1)
Bundle-SymbolicName:bundle的文本標識符,通常以反向域名的形式出現,如 com.infoq,並且往往對應了包含其中的package
Bundle-Version:major.minor.micro.qualifier形式的版本號,前三個元素是數字( 缺省是0),qualifier則是文本(缺省是空字符串)
創建一個bundle
最簡單的bundle必須在manifest文件中包含如下內容:
Bundle-ManifestVersion: 2
Bundle-SymbolicName: com.infoq.minimal
Bundle-Version: 1.0.0
創建bundle並沒有什麼可稀奇的,那麼讓我們創建一個帶activator的bundle吧。下面 是OSGi特定的代碼片段,在bundle啟動時被調用,有點像是bundle的main方法。
package com.infoq;
import org.osgi.framework.*;
public class ExampleActivator implements BundleActivator {
public void start(BundleContext context) {
System.out.println("Started");
}
public void stop(BundleContext context) {
System.out.println("Stopped");
}
}
為了讓OSGi知道哪個類是activator,我們需要在manifest中加入額外的信息項:
Bundle-Activator: com.infoq.ExampleActivator
Import-Package: org.osgi.framework
Bundle-Activator聲明了在bundle啟動時要實例化並調用其start()方法的類;類似的 ,在bundle停止時將調用該類的stop()方法。
那麼Import-Package又是干什麼的?每個bundle都需要在manifest中定義其依賴,以 便在運行時判斷所有必需代碼是否可用。在本例中,ExampleActivator依賴於 org.osgi.framework包中的BundleContext;如果我們不在manifext中聲明該依賴,在運 行時就會碰到NoClassDefFoundError錯誤。
下載OSGi引擎
要編譯並測試我們的bundle,需要一個OSGi引擎。對OSGi R4.2,下面羅列了幾個可用 的開源引擎。你也可以下載Reference API來編譯(這樣可以確保沒有用到任何平台特定 特性);可是,要運行bundle,還是需要一個OSGi引擎。以下引擎都可供選擇:
Equinox
許可
Eclipse Public License
文獻
http://www.eclipse.org/equinox/
下載
org.eclipse.osgi_3.5.0.v20090520.jar
評注
該org.eclipse.osgi bundle包含了框架、運行時和shell,是全部合一的。它 的文件名也最長,用tab補全(或改名為equinox.jar)可以解決這一問題。
要啟動console,在命令行輸入java -jar org.eclipse.osgi_3.5.0.v20090520.jar -console即可。
框架
org.eclipse.osgi_3.5.0.v20090520.jar
Felix
許可
Apache License
文獻
http://felix.apache.org/
下載
Felix Framework Distribution 2.0.0
評注
這是所見遵守規范最嚴格的OSGi引擎,還被用在GlassFish及許多其他開源產 品中。運行時需要在命令行輸入java -jar bin/felix.jar而不是 java -jar felix.jar ,因為啟動時它要從當前目錄查找bundles 路徑。
框架
bin/felix.jar
Knopflerfish
許可
Knopflerfish License (BSD-esque)
文獻
http://www.knopflerfish.org/
下載
knopflerfish_fullbin_osgi_2.3.3.jar
評注
該JAR是一個自解壓zip文件;剛開始你必須運行java -jar進行解壓。不要下 載“bin_osgi”,它無法啟動。
框架
knopflerfish.org/osgi/framework.jar
另外還有更小的定位於嵌入設備的OSGi R3運行時可用(比如Concierge),但本系列 文章只關注OSGi R4。
編譯並運行bundle
獲得framework.jar之後,把OSGi加入classpath並編譯上面的例子,然後將其打包成 JAR:
javac -cp framework.jar com/infoq/*/*.java
jar cfm example.jar MANIFEST.MF com
每種引擎都有shell,命令也相似(但不相同)。為了便於練習,讓我們看看如何獲得 這些引擎並使之運行、安裝和啟/停bundle。
一旦引擎啟動並運行起來,你就可以安裝bundle(由 file:// URL來定位)了,然後 可以使用安裝bundle所返回的bundle id,啟動或停止該bundle。
Equinox Felix Knopflerfish 啟動應用 java -jar org.eclipse.osgi_*.jar -console java -jar bin/felix.jar java -jar framework.jar -xargs minimal.xargs 幫助 help 列表 ss ps bundles 安裝 install file:///path/to/example.jar 啟動 start id 更新 update id 停止 stop id 卸載 uninstall id 退出 exit shutdown
盡管所有的shell工作起來大都一樣,但命令之間還是有容易混淆的細微差別。有兩個 統一console的項目(Pax Shell,Posh)和運行器(Pax Runner)可以利用;OSGi RFC 132則是一個正在進行中的提案,試圖標准化command shell。另外,Apache Karaf可以運 行在Equinox或Felix之上,提供統一的shell以及其他特性。盡管使用這些項目或工具進 行實際部署是可取的,但本系列文章還是關注於普通的OSGi框架實現。
如果你啟動了OSGi框架,你應該能夠安裝上面所講的com.infoq.minimal-1.0.0.jar( 你還可以用鏈接地址及install命令直接從網站上進行安裝)。每次安裝bundle,引擎都 會打印出該bundle的數字ID。
在安裝好bundle之前不可能知道bundle的ID是多少,這取決於系統中其它bundle的ID 分配情況;但是你可以使用適當的命令羅列出已安裝的bundle將其找出來。
依賴
迄今為止,我們只有一個bundle。模塊化的一個好處是可以把系統分解為多個小模塊 ,在此過程中,減小應用的復雜性。從某種程度上,Java的 package已經做到了這一點: 例如,一個common包有獨立的client和server包,他們都依賴於該common包。但是Jetty 最近的例子(client意外地依賴於server)表明做到這一點並不總是很容易。實際上,有 些由OSGi給項目帶來的好處純粹就是強制模塊間的模塊化約束。
模塊化的另一個好處是把'public'包從非public包中分離出來。Java的編譯時系統允 許隱藏非public類(在特定包中是可見的),但是不支持更大程度的靈活性。然而在OSGi 模塊中,你可以選擇哪些包是exported(輸出)的,這就意味著沒有輸出的包對其他模塊 是不可見的。
讓我們繼續開發一個功能,用來初始化URI Templates(與Restlet中使用的一樣)。 因為該功能可重用,我們想將其放在一個單獨模塊中,讓使用它的客戶端依賴於它。(通 常,bundle不適合這麼細粒度的用法,但是其可用於說明工作原理)。該功能將根據一個 模板(比如http://www.amazon.{tld}/dp/{isbn}/)和一個包含有 tld=com,isbn=1411609255的Map,產生出URL http://www.amazon.com/dp/1411609255/( 這麼做的一個原因是,如果Amazon URL模式發生了變化,我們能夠改變該模板,盡管好的 URI是不會改變的)。
為了提供一個在不同實現之間切換的簡單方法,我們將提供一個接口和一個工廠。這 會讓我們看到在提供功能的同時實現是怎樣對client隱藏的。代碼(對應幾個源文件)如 下:
package com.infoq.templater.api;
import java.util.*;
public interface ITemplater {
public String template(String uri, Map data);
}
// ---
package com.infoq.templater.api;
import com.infoq.templater.internal.*;
public class TemplaterFactory {
public static ITemplater getTemplater() {
return new Templater();
}
}
// ---
package com.infoq.templater.internal;
import com.infoq.templater.api.*;
import java.util.*;
public class Templater implements ITemplater {
public String template(String uri, Map data) {
String[] elements = uri.split("\\{|\\}");
StringBuffer buf = new StringBuffer();
for(int i=0;i
該實現隱藏在com.infoq.templater.internal包中,而public API則位於 com.infoq.templater.api包中。這就給了我們巨大的靈活性,如果需要,以後可以修改 實現以提供更加有效的手段。(internal包名是約定俗成,你可以起其他名字)。
為了讓其他bundle能夠訪問該public API,我們需要將其export(輸出)。我們的 manifest如下:
Bundle-ManifestVersion: 2
Bundle-SymbolicName: com.infoq.templater
Bundle-Version: 1.0.0
Export-Package: com.infoq.templater.api
創建一個client bundle
我們現在可以創建一個使用templater的client。利用上面的例子創建一個activator ,其start()方法如下:
package com.infoq.amazon;
import org.osgi.framework.*;
import com.infoq.templater.api.*;
import java.util.*;
public class Client implements BundleActivator {
public void start(BundleContext context) {
Map data = new HashMap();
data.put("tld", "co.uk"); // or "com" or "de" or ...
data.put("isbn", "1411609255"); // or "1586033115" or ...
System.out.println( "Starting\n" +
TemplaterFactory.getTemplater().template(
"http://www.amazon.{tld}/dp/{isbn}/", data));
}
public void stop(BundleContext context) {
}
}
我們需要在manifest中顯式輸入templater API,否則我們的bundle無法編譯。用 Import-Package或Require-Bundle都可指定依賴。前者可以讓我們單獨輸入包;後者則將 隱式輸入該bundle中所有輸出包。(多個包及bundles可以用逗號分開)。
Bundle-ManifestVersion: 2
Bundle-SymbolicName: com.infoq.amazon
Bundle-Version: 1.0.0
Bundle-Activator: com.infoq.amazon.Client
Import-Package: org.osgi.framework
Require-Bundle: com.infoq.templater
注意在前面的例子中,我們已經使用了Import-Package來輸入org.osgi.framework。 在這個例子中,我們將演示 Require-Bundle的用法,其使用了Bundle-SymbolicName。當 然,用Import-Package: org.osgi.framework, com.infoq.templater.api可以達到相同 的效果。
不管如何聲明依賴於templater的bundle,我們都只能訪問單一的輸出包 com.infoq.templater。盡管client可以通過 TemplaterFactory.getTemplater()來訪問 templater,但是我們不能直接從internal包中訪問該類。這樣我們在未來就可以在不影 響client的情況下改變templater類的實現。
測試該系統
任何OSGi應用實際上都是一組bundle。在本例中,我們需要編譯並把bundle打包為JAR (如前面所述),啟動OSGi引擎,安裝兩個bundle。下面是在Equinox中進行操作的例子 :
java -jar org.eclipse.osgi_* -console
osgi> install file:///tmp/com.infoq.templater-1.0.0.jar
Bundle id is 1
osgi> install file:///tmp/com.infoq.amazon-1.0.0.jar
Bundle id is 2
osgi> start 2
Starting
http://www.amazon.co.uk/dp/1411609255
Amazon client bundle現在已經啟動了;當其啟動時,它先用(硬編碼的)給定值為 我們初始化URI template。然後在該bundle啟動過程中打印出來以確定其已正常工作。當 然,一個真正的系統不會這麼死板;但是Templater服務可以用於任何其他應用(例如, 產生一個基於Web的應用中的鏈接)。將來,我們將能夠在OSGi環境中查看Web應用。
帶版本的依賴
本文最後要指出的是目前我們所談的依賴都沒有版本;更確切的說,我們可以使用任 意版本。兩個bundle整體及各個包都可以有版本,增大minor 號通常 意味著增加了新特 性(但保持向後兼容)。以org.osgi.framework包為例,OSGi R4.1中是1.4.0,OSGi R4.2中是1.5.0。(順便提一句,這是bundle版本和銷售版本保持分離的很好理由,Scala 語言已經這麼做了)。
要聲明依賴處於一個特定版本,我們必須在Import-Package或Require-Bundle中來表 達。比如,我們可以指定 Require- Bundle: com.infoq.templater;bundle- version="1.0.0"以表示工作所需的最低版本為1.0.0。類似的,我們可以用 Import- Package: com.infoq.templater.api;version="1.0.0"做相同的事情 —— 但是要記住 package與bundle版本是完全分開的。如果你不指定版本,缺省為0.0.0,因此,除非指定 了相應的Export-Package: com.infoq.templater.api;version="1.0.0"否則該輸入不會 被解析。
還可以指定一個版本范圍。例如,按慣例OSGi版本的major號增大表示向後兼容發生改 變,因此我們可能想將其限制在1.x的范圍內。我們可以通過 (bundle-) version="[1.0,2.0)"的方式來表達這一依賴約束。該例中,[表示‘包含’,而)表示‘ 不包含’。換句話說,‘從 1.0到2.0但不包括2.0’。實際上,將一個依賴約束表達為‘ 1.0’與“[1.0,∞)”意思是一樣的——換句話說,比1.0版高都可以。
盡管這些內容超出了本文的范圍,但是在一個OSGi系統中,一個bundle同時有兩個版 本也是可能的。如果你有一個老client依賴於1.0版本 API,同時又一個新client依賴於 2.0版本API,這就非常有用了。只要每個bundle的依賴是一致的(換句話說,一個bundle 不能直接或間接同時輸入1.0和2.0)那麼應用程序將工作良好。作為讀者練習,你可以創 建一個使用泛型的2.0版的Templater API,然後讓一個client依賴於1.x,而另一個依賴 於2.x。
總結
在本文中,我們探討了開源OSGi引擎Equinox、Felix和Knopflerfish,並且創建了兩 個有依賴關系的bundle。我們還談到了帶版本的依賴。截止目前,模塊化還是靜態的;我 們還沒有涉及到OSGi的動態本質。在下一篇文章中我們將涉及這一內容。