軟體編程發展到今天可以看作是一個量變引發質變的過程。最初,程式開發面向過程,開發人員需要編寫大量的過程代碼,隨著過程代碼的不斷積累(量變產 生),從代碼維護和重用的角度,過程開發變得越來越不適應,質變產生,物件導向的開發逐漸被採用。由於物件導向的開發很好的封裝了過程,而且從物件導向的 角度可以很好的描述實際應用中的需求模型,因此物件導向的開發逐漸成為主流。同樣,隨著物件導向開發的不斷應用(量變產生),出現了大量的可複用的類及 包,維護這些類/包變得越來越困難,而且,儘管物件導向的編程機制可以很好的適應小規模應用的開發,但隨著應用系統的規模越來越大,如同用細小的沙粒構建 堤壩,物件導向的機制難於適應,質變產生,面向組件的開發被引入開發過程。面向組件的開發目前仍可以認為是處於一種探索狀態,目前還不存在一種統一的標準 可以遵循。
從另一個角度,曆史是今天存在的基礎,沒有曆史就沒有今天。同樣的道理,應用系統採用面向組件的開發實現,組件仍需要對象來構造,而對象在一定程度上是封 裝的功能過程,三者相輔相成。不管是從上而下還是從下而上,應用系統需求模型在其實現過程中,系統設計者應該充分關注組件、對象和功能過程三個層面。
OSGi可以看作是面向組件開發的一種思路和基礎環境。在OSGi環境中實現應用系統的需求模型要求開發人員對組件、對象開發具有充分的理解。衡量一個應 用系統實現好壞的標準之一是該系統是否是松耦合高內聚。物件導向的開發實現此目標的關鍵是介面/抽象的應用,OSGi面向組件的開發在此基礎之上充分利用 了對象類包封裝的機制。
1. Bundle的構建策略
通常,當我們構建應用系統時,我們需要充分重視所構建應用的業務需求模型即領域模型,同樣的,在用軟體代碼實現業務需求模型時,我們也應當對軟體系統的架 構模型給予充分的重視。採用OSGi技術實現應用系統時,展現在我們面前的將是一個個的Bundle組件,此時,我們必須首先弄清楚我們要構建什麼樣的 Bundle。
1.1 明確Bundle的構建需求及實現粒度
從需求模型的角度看,Bundle可以看作是需求模型中的一個功能模組實現,因此,在開發Bundle之前系統開發人員必須明確Bundle的功能需求。 需求模型中的功能模組的邊界可大可小,同樣,Bundle的實現粒度也是可大可小,極端情況下,小的Bundle可以僅實現一個微小的功能,大的 Bundle可以實現整個業務系統。
以記錄日誌為例,如果記錄日誌僅輸出到控制台,則一個類就可以實現整個的功能,如果記錄日誌輸出到資料庫系統,則開發人員需要在整個功能用一個 Bundle實現,還是將日誌記錄功能和記錄資訊的資料庫儲存分為兩個不同的Bundle實現,兩個選擇甚至更多的選擇中做出決策。通常這個問題在現有的 軟體開發方式中不需要過多的重視,但在OSGi開發中,開發人員必鬚根據需求確定Bundle的實現粒度。
1.2 確定Bundle的類型
我們在物件導向的系統開發過程中,經常會將一些通用的功能設計成為工具類,多個工具類封裝為一個工具類包。由於OSGi開發建立在物件導向的開發之上, Bundle的開發也存在這種特點。OSGi開發的另一個特點是服務機制的引入,一個Bundle可以將其提供的功能發布成為一個或多個服務,供其他 Bundle尋找使用。此外,建立在OSGi環境之上的組件開發也區別於通常的組件開發方式,因為我們可以利用OSGi環境提供的某些特性(參見下一節 充分利用OSGi環境的特性)。綜上所述,我們可以將Bundle劃分為如下幾種類型(實際開發中,並不存在清晰的邊界):
這種Bundle與通常開發方式中的工具類或工具類包沒有本質的區別,僅僅是將這些工具類發布到OSGi環境中,這些工具類也不依賴任何的OSGi環境或 特性。通常,這種Bundle特別適合目前存在的眾多的第三方組件引入到OSGi環境中直接供OSGi開發人員使用。例如,我們可以將Apache開源項 目Log4j發布的工具包通過修改其META-INF目錄下的MANIFEST.MF檔案,添加Bundle的中繼資料資訊就可以直接將其封裝為可供 OSGi環境中其他Bundle使用的工具類Bundle。
這類Bundle可以被其他Bundle引用,為其提供功能,但是這類Bundle不能離開OSGi運行環境,否則不能使用。舉例來說,一個提供資料緩衝 功能的Bundle可能使用Bundle的資料存放區區來快取資料。Bundle的資料存放區區的位置對Bundle開發人員來說是透明的(參見下一節 充分利用OSGi環境的特性)。如果將該Bundle的資料緩衝功能遷移到OSGi運行環境之外,則必須修改實現添加對緩衝區位置的處理功能。
這類Bundle也可以認為是依賴OSGi特性的Bundle,唯一的區別就是,該類Bundle充分利用的OSGi中服務的特性,引用其他Bundle 發布的服務和(或)向OSGi環境中發布自己的服務。OSGi環境提供的服務機制可以使得系統的實現遵循最大程度的松耦合。
1.3 隱藏Bundle的功能介面
組件開發除了應對系統的開發規模之外,其最重要的目標是降低系統耦合。開發人員採用OSGi Bundle開發時,應盡量屏蔽Bundle所提供功能的內部實現機制,僅將為其他Bundle提供的互動介面暴露出來。在OSGi中,這可以通過兩種方 式,一種是通過Export供其他Bundle引用的類包;一種是向OSGi環境發布服務。
在Equinox中,Bundle內部實現的類包通常以"internal"標註,如org.eclipse.equinox.internal.cm.reliablefile。標註為"internal"的類包通常不包含在Export列表中,或者通過"x-internal"屬性標記(該屬性僅在Eclipse開發環境中提供)。
隱藏Bundle功能介面的一種良好機制是通過OSGi服務。開發人員僅將自己開發的Bundle的介面類包發布給其他Bundle可見,同時,將功能接 口的實現註冊為OSGi中的服務,其他Bundle通過尋找OSGi服務註冊表擷取該服務,通過服務的介面調用服務的功能。
1.4 儘可能應用OSGi提供的標準服務
OSGi聯盟為一些經常用到的功能定義了標準服務,如應用程式管理服務,Log Service,事件服務,組態管理服務,使用者管理服務,HTTP服務等等。如果開發人 員採用的OSGi環境提供了上述服務的實現,且這些服務實現滿足系統的需求,則開發人員應儘可能使用這些服務,而不是為這些類似的功能定義自己的實現。
2.充分利用OSGi環境的特性
2.1 OSGi環境的動態特性
OSGi是一個動態環境,OSGi運行環境內部狀態的變化通過事件發布/監聽機制進行互動。開發人員在構建Bundle時應充分利用OSGi環境的事件 監聽機制。如,開發人員可以在自己的Bundle中註冊Bundle事件的監聽用來處理其他Bundle的安裝,啟動,停止,卸載等事件。
OSGi運行環境內部的事件主要包括三類:
STARTED 架構已經啟動
ERROR 某個Bundle啟動過程中引發錯誤
WARNING 某一Bundle引發一個警告
INFO 某一Bundle引發一個INFO類型的事件
PACKAGES_REFRESHED PackageAdmin.refreshPackage操作執行完成
STARTLEVEL_CHANGED StartLevel.setStartLevel操作執行完成
INSTALLED Bundle被安裝到OSGi環境後系統發布該事件
RESOLVED Bundle被成功解析
LAZY_ACTIVATION Bundle將被延遲啟用
STARTING Bundle正在被啟用
STARTED Bundle被成功啟用
STOPPING Bundle被停止
STOPPED Bundle正在被停止
UPDATED Bundle被更新
UNRESOLVED Bundle被UNRESOLVED
UNINSTALLED Bundle被卸載
REGISTERED 服務被註冊
MODIFIED 服務被修改
UNREGISTERING 服務正在被登出
關於上述事件的詳細資料參考OSGi API文檔。
下述代碼展示了如何在Bundle組件中通過實現FrameworkListener,BundleListener和ServiceListener介面,並使用BundleContext註冊監聽OSGi環境的各種事件:
package com.example;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
public class Activator implements BundleActivator, FrameworkListener,
BundleListener, ServiceListener {
public void start(BundleContext context) throws Exception {
//註冊監聽
context.addFrameworkListener(this);
context.addBundleListener(this);
context.addServiceListener(this);
}
public void stop(BundleContext context) throws Exception {
}
//處理架構事件
public void frameworkEvent(FrameworkEvent event) {
if ((event.getType() & FrameworkEvent.ERROR) != 0) {
System.err.println("Framework ERROR: " + event.getBundle());
}
}
//處理Bundle事件
public void bundleChanged(BundleEvent event) {
if ((event.getType() & BundleEvent.STARTED) != 0) {
System.err.println("Bundle STARTED: " + event.getBundle());
} else if ((event.getType() & BundleEvent.STOPPED) != 0) {
System.err.println("Bundle STOPPED: " + event.getBundle());
}
}
//處理服務事件
public void serviceChanged(ServiceEvent event) {
if ((event.getType() & ServiceEvent.REGISTERED) != 0) {
System.err.println("Service REGISTERED: "
+ event.getServiceReference());
}
}
}
2.2 Bundle的生命週期及OSGi上下文特性
在Java虛擬機器運行環境中,類的生命週期開始於虛擬機器的載入,終止於被提前卸載或虛擬機器停止,通常不需要開發人員幹預。在OSGi環境中,開發人員可以參與Bundle的生命週期過程並能夠擷取Bundle在OSGi環境中啟動並執行上下文。
通過實現org.osgi.framework.BundleActivator介面,並在Bundle的中繼資料頭屬性"Bundle- Activator"中指明該介面的實作類別,如"Bundle-Activator: com.example.Activator"。當Bundle被啟動或停止時,OSGi架構會將該Bundle的運行上下文BundleContext 注入到該實作類別執行個體中,開發人員可以在該Bundle的運行過程中使用該上下文訪問OSGi環境資訊,訪問OSGi環境中的其他Bundle,註冊事件監 聽,發布或引用服務等。
下面的程式碼片段展示了如何利用Bundle運行上下文與OSGi環境進行互動:
public void start(BundleContext context) throws Exception {
// 擷取OSGi環境中的所有安裝的bundle
for (Bundle bundle : context.getBundles()) {
System.out.println("Bundle Symbolic Name: "
+ bundle.getSymbolicName());
}
// 擷取OSGi運行環境中的屬性(尋找範圍包括系統屬性)
System.out.println("osgi.framework = "
+ context.getProperty("osgi.framework"));
//註冊其他Bundle
context.installBundle("file:C://test_bundle_1.0.0.jar");
//在該Bundle的資料存放區區中構建datacache.file檔案
context.getDataFile("datacache.file");
//註冊監聽
context.addFrameworkListener(this);
context.addBundleListener(this);
context.addServiceListener(this);
}
2.3 Bundle的資源管理特性
在通常的應用系統開發中,開發人員通常要考慮設定檔的儲存路徑,系統臨時檔案的儲存路徑等資訊。由於OSGi環境中Bundle實際上是由一組檔案資源 構成,開發人員在開發Bundle時可以充分利用這種特點,使用Bundle儲存與Bundle相關的配置資訊。OSGi環境在運行時為每一個 Bundle構建一個序列化資料存放區區,開發人員可以使用該儲存區儲存Bundle在運行時產生的臨時資訊。
下述程式碼片段展示了如何通過Bundle提供的功能介面擷取Bundle內部的資源:
- context.getBundle.getEntry("") 只查詢Bundle內部資源,返回Bundle的URL,該URL與實現相關,如:bundleentry://256/
- context.getBundle.getEntry("/") 只查詢Bundle內部資源,返回Bundle的URL,該URL與實現相關
- context.getBundle.getEntry("/build.properties") 只查詢Bundle內部資源,返回Bundle內部build.properties檔案的URL,該URL與實現相關,如:bundleentry://256/build.properties
- context.getBundle().getResource("build.properties") 使用Bundle的ClassLoader查詢Bundle類域內的資源,如果Bundle不能被解析(RESOLVED),則只查詢Bundle內部資源,返回Bundle內部build.properties檔案的URL,該URL與實現相關。
開發人員在擷取上述資源的URL後,可以轉換成與Bundle運行系統相關的檔案URL,如:file:C:/test_bundle_1.0.0。
下述程式碼片段展示了通過BundleContext介面擷取Bundle運行時資料存放區區資源:
- context.getDataFile("") 返回該Bundle的運行時資料存放區區位置,如:E:/worksapce/.metadata/.plugins/org.eclipse.pde.core/CobWeb/org.eclipse.osgi/bundles/256/data
- context.getDataFile("config.xml") 返回該Bundle的運行時資料存放區區內的config.xml檔案,如果該檔案不存在,則建立該檔案。
2.4 面向服務的組件特性
OSGi的服務層提供了一個動態服務發布,綁定和尋找模型,所謂的服務即指實現了某個(些)介面的Java對象。在物件導向的系統開發中,介面在分離系統的耦合方面扮演至關重要的角色,OSGi環境充分利用了這一點。
OSGi運行架構維護著一個服務註冊表,Bundle可以通過Bundle上下文向該服務註冊表中發布服務(只需指明服務實現的介面,服務的執行個體和服務的 屬性),也可以使用Bundle上下文從服務註冊表中根據介面名稱及服務的屬性尋找其他Bundle註冊的服務。在此模式下,Bundle之間的耦合關係 只存在介面層面,而與介面的實現無關。
下述程式碼片段展示了如何向OSGi服務註冊表發布服務以及如何尋找其他Bundle發布的服務:
public void start(BundleContext context) throws Exception {
//擷取OSGi Log服務的服務引用
ServiceReference logSr = context.getServiceReference(LogService.class.getName());
//根據Log服務引用擷取LogService服務執行個體
log = (LogService) context.getService(logSr);
//建立StringService介面服務執行個體
StringService ss = new StringServiceImpl(log);
//設定StringService的屬性
Hashtable
//使用BundleContext註冊發布StringService服務
context.registerService(StringService.class.getName(), ss, properties);
}
3.Bundle的開發實踐
實踐1、構建Utility類型的Bundle
該樣本將第三方log4j工具包(版本為1.2.13)封裝為OSGi Bundle組件。操作步驟如下:
- 步驟一:在C盤根目錄下建立名稱為"org.apache.log4j"目錄,將log4j.1.2.13.jar檔案拷貝到該目錄中;
- 步驟二:在"org.apache.log4j"目錄下構建META-INF目錄,並在此目錄下建立名稱為"MANIFEST.MF"檔案;
- 步驟三:編輯"MANIFEST.MF"檔案,在此檔案中添加以下內容:
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Apache Log4j
Bundle-SymbolicName: org.apache.log4j
Bundle-Version: 1.2.13
Bundle-ClassPath: .;log4j.1.2.13.jar
Export-Package: org.apache.log4j;version="1.2.13",
org.apache.log4j.config;version="1.2.13",
org.apache.log4j.helpers;version="1.2.13",
org.apache.log4j.jdbc;version="1.2.13",
org.apache.log4j.jmx;version="1.2.13",
org.apache.log4j.net;version="1.2.13",
org.apache.log4j.or;version="1.2.13",
org.apache.log4j.or.jms;version="1.2.13",
org.apache.log4j.or.sax;version="1.2.13",
org.apache.log4j.spi;version="1.2.13",
org.apache.log4j.varia;version="1.2.13",
org.apache.log4j.xml;version="1.2.13"
經過上述步驟後,名稱為org.apache.log4j的OSGi Bundle已經構建完成。開發人員可以在其他Bundle中如同平常的開發模式一樣引用Log4j的類包,擷取Logger執行個體記錄日誌。
實踐2、構建Service類型的Bundle
該樣本Bundle由StringService介面,StringServiceImpl類和Activator類構成。該Bundle發布StringService介面服務,同時引用OSGi的標準LogService服務。
StringService介面是該樣本Bundle發布的String服務實現介面。
package com.example;
public interface StringService {
String concat(String str1, String str2);
}
StringServiceImpl類是StringService介面的實作類別,該類使用OSGi的標準LogService服務記錄日誌。
package com.example.internal;
import org.osgi.service.log.LogService;
import com.example.StringService;
public class StringServiceImpl implements StringService {
private LogService log;
public StringServiceImpl(LogService log) {
this.log = log;
}
public String concat(String str1, String str2) {
String str = str1 + str2;
if (log != null)
log.log(LogService.LOG_INFO, "Concat String Result is " + str);
return str;
}
}
Activator類為Bundle的生命週期入口,該類根據注入的BundleContext上下文擷取OSGi LogService服務,然後註冊StringService服務。
package com.example.internal;
import java.util.Hashtable;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;
import com.example.StringService;
public class Activator implements BundleActivator {
private LogService log;
public void start(BundleContext context) throws Exception {
//擷取OSGi Log服務的服務引用
ServiceReference logSr = context.getServiceReference(LogService.class.getName());
//根據Log服務引用擷取LogService服務執行個體
log = (LogService) context.getService(logSr);
//建立StringService介面服務執行個體
StringService ss = new StringServiceImpl(log);
//設定StringService的屬性
Hashtable
//使用BundleContext註冊發布StringService服務
context.registerService(StringService.class.getName(), ss, properties);
}
public void stop(BundleContext context) throws Exception {
}
}
Bundle的中繼資料清單如下:
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Example Plug-in
Bundle-SymbolicName: com.example
Bundle-Version: 1.0.0
Bundle-Activator: com.example.internal.Activator
Bundle-Vendor: EGRID
Eclipse-LazyStart: true
Import-Package: org.osgi.framework;version="1.3.0",
org.osgi.service.log;version="1.3.0"
Export-Package: com.example
4. 結論
通過本文的探討,開發人員可以明確在OSGi環境下進行Bundle開發時所要面臨的一些決策及注意事項。下一篇文章我們將要詳細探討OSGi環境下Bundle編程的細節。本文中的點擊此處下載。