4. 依賴性管理 OSGi允許您把您的應用程序分成多個模塊,並能管理這些模塊之間的依賴性。爲了達到這個目的,它引入了Bundle訪問域的概念。Bundle中類的缺省訪問範圍只對本Bundle內部可見,但對其它任何Bundle都是不可見的;在Bundle內部,類的可訪問性遵循Java語言的一般規範。那麼,您如果想要從一個Bundle中訪問另一個Bundle中的類,您應該怎麼辦呢?解決方法是將源Bundle中的包導出來,然後把它們導入到目標Bundle中。在本小結中,我們將通過一個示例程序說明這個概念。 首先,我們新建一個名com.javaworld.sample.HelloService的Bundle,並從其中導出一個包,然後將該包導入到我們的com.javaworld.sample.HelloWorld Bundle中。 4.1. 導出Java包 我們開始新建一個com.javaworld.sample.HelloServiceBundle,並從其中導出一個Java包,具體步驟如下: 1) 新建com.javaworld.sample.HelloService Bundle,具體步驟請參見上小節中新建com.javaworld.sample.HelloWorldBundle的步驟; 2) 在HelloService Bundle中,新建一個com.javaworld.sample.service.HelloService.java接口,其源代碼如清單3所示。
源代碼清單3. HelloService.java package com.javaworld.sample.service; public interface HelloService { public String sayHello(); }
3) 新建類com.javaworld.sample.service.impl.HelloServiceImpl.java,該類實現HelloService接口,其源代碼如清單4所示。
源代碼清單4. HelloServiceImpl.java package com.javaworld.sample.service.impl; import com.javaworld.sample.service.HelloService; public class HelloServiceImpl implements HelloService { public StringsayHello() { System.out.println("InsideHelloServiceImple.sayHello()"); return"Say Hello"; } } 4) 請在您的Eclipse Manifest編輯器中打開HelloService包中的MANIFEST.MF文件,點擊“Runtime(運行時)” 標籤,在“導出包”小節,單擊“Add(添加)”按鈕,並選擇com.javaworld.sample.service包。這時,HelloServiceBundle中的MANIFEST.MF文件代碼應如源代碼清單5所示。
源代碼清單5. HelloService Bundle中的Manifest文件 Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: HelloService Plug-in Bundle-SymbolicName:com.javaworld.sample.HelloService Bundle-Version: 1.0.0 Bundle-Vendor: JAVAWORLD Bundle-Localization: plugin Export-Package: com.javaworld.sample.service Import-Package:org.osgi.framework;version="1.3.0"
您可以看到,HelloService Bundle中的MANIFEST.MF文件和HelloWorldBundle非常相似,唯一的區別就是多了一個Export-Package屬性頭,該屬性頭的值爲com.javaworld.sample.service;Export-Package屬性頭通知OSGi容器,其它Bundle可以從HelloService Bundle外面訪問com.javaworld.sample.service包中的類。請注意,在示例代碼中,我們只暴露了接口類HelloService,而沒有暴露其實現類的HelloServiceImpl。
4.2. 導入Java包 下面,我們將從HelloServiceBundle中導出的com.javaworld.sample.service包並將其導入到HelloWorldBundle中,具體步驟如下: 1). 請在com.javaworld.sample.HelloWorld Bundle中找到MANIFEST.MF文件,並在Manifest編輯器中打開,點擊“Dependencies(依賴性)”標籤,然後點擊“ImportPackage(導入包)”按鈕,將com.javaworld.sample.service添加爲導入包,這時,您的HelloWorldBundle中的MANIFEST.MF文件內容應如源代碼清單6所示: 源代碼清單6. HelloWorld Bundle中的MANIFEST.MF文件 Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: HelloWorld Plug-in Bundle-SymbolicName: com.javaworld.sample.HelloWorld Bundle-Version: 1.0.0 Bundle-Activator: com.javaworld.sample.helloworld.Activator Bundle-Vendor: JAVAWORLD Bundle-Localization: plugin Import-Package: com.javaworld.sample.service, org.osgi.framework;version="1.3.0"
從上面的代碼可以看出,Import-Package屬性頭的值是一個由逗號分隔的字符串,這是您想導入包的列表。在HelloWorldBundle示例代碼中,我們引入了兩個包,即com.javaworld.sample.service和org.osgi.framework。 org.osgi.framework包中包含有OSGi框架類,比如,在HelloWorldBundle中的Activator.java中用到的BundleContext和BundleActivator類都屬於這個包。 2) 下面,請在Eclipse Java編輯器中打開com.javaworld.sample.helloworld.Activator.java,您會注意到,您現在可以訪問HelloService接口,但不能訪問HelloServiceImpl實現類,這是因爲HelloServiceBunlde只導出了com.javaworld.sampel.service包,同時HelloWorldBundle也導入了這個包。HelloServiceImpl是HelloServiceBundle的一個內部類,任何其它的Bundle都不能訪問它。 4.3. 類級別上的訪問域 如果您運行示例的HelloService服務包,它會在Eclipse控制檯上打印出”HelloWorld”。但是,如果您想在HelloWorld Bundle的Activator中訪問HelloServiceImpl類,這時,編譯沒有問題,但在OSGi容器中運行這個Bundle時會拋出異常。 OSGi容器是如何能將jar文件中的一些類隱藏掉,而讓另外一些類可見呢?這是因爲OSGi容器使用Java類加載器來管理類的可見性,OSGi容器爲每個Bundle創建不同的類加載器,因此每個Bundle能訪問位於下列位置中的類: a) 位於Java啓動類路徑下的、所有以Java.*開頭的包中的類; b) 位於OSGi框架類路徑下的類,通常有一個獨立的類加載器負責加載框架的實現類及關鍵的接口類; c) 位於Bundle空間中的類,這些類通常包含在與Bundle相關的jar文件中,以及加到這個Bundle中的其它jar包中的類。 d) 導入包中的類,例如,HelloWorld Bundle導入了com.javaworld.sample.service包,因此它能訪問該包中的類。Bundle級別的訪問域是OSGi一個非常強大的功能,例如,它可以讓您安全地更新HelloServiceImpl.java類,而不必擔心依賴於這個類的代碼受到破壞。 5. OSGi服務 前面我們提到,OSGi架構非常適合我們實現面向服務的應用(SOA)。它可以讓Bundles導出服務,而其它的Bundles可以在不必瞭解源Bundles任何信息的情況下消費這些導出的服務。由於OSGi具有隱藏真實的服務實現類的能力,所有它爲面向服務的應用提供了良好的類與接口的組合。 在OSGi框架中,源Bundle在OSGi容器中註冊POJO對象,該對象不必實現任何接口,也不用繼承任何超類,但它可以註冊在一個或多個接口下,並對外提供服務。目標Bundle可以向OSGi容器請求註冊在某一接口下的服務,一旦它發現該服務,目標Bundle就會將該服務綁定到這個接口,並能調用該接口中的方法。下面我們舉個例子,以便我們能更好理解與OSGi相關的這些概念。 5.1. 導出服務 在本小節中,我們將更新HelloService Bundle,以便它能把HelloServiceImpl類的對象導出爲服務,具體步驟如下: 1) 修改com.javaworld.sample.HelloService Bundle中的MANIFEST.MF文件,讓它導入org.osgi.framework包(譯者注,這一步我們已經完成); 2) 新建Java類com.javaworld.sample.impl.HelloServiceActivator.java,其源代碼如清單7所示; 源代碼清單7. HelloServiceActivator.java publicclass HelloServiceActivator implements BundleActivator { ServiceRegistrationhelloServiceRegistration; public void start(BundleContext context)throws Exception { HelloService helloService = newHelloServiceImpl(); helloServiceRegistration=context.registerService(HelloService.class.getName(), helloService, null); } public void stop(BundleContext context)throws Exception { helloServiceRegistration.unregister(); } } 請注意,在源Bundle中,我們應使用BundleContext.registerService()方法導出服務,這個方法帶三個參數: a) 該方法第一個參數爲您要註冊的服務的接口名稱。如果您想把您的服務註冊到多個接口下,您需要新建一個String數組存放這些接口名,然後把這個數組作爲第一個參數傳給registerService()方法。在示例代碼中,我們想把我們的服務導出到HelloServer接口名下; b) 第二個參數是您要註冊的服務的實際Java對象。在示例代碼中,我們導出HelloServiceImpl類的對象,並將其作爲服務; c) 第三個參數爲服務的屬性,它是一個Dictionary對象。如果多個Bundle導出服務的接口名相同,目標Bundle就可以使用這些屬性對源Bundle進行過濾,找到它感興趣的服務。 3) 最後,請修改HelloServiceBundle中的MANIFEST.MF文件,將Bundle-Activator屬性頭的值改爲com.javaworld.sample.service.impl.HelloServiceActivator。 現在HelloService Bundle就可以導出HelloServiceImpl對象了。當OSGi容器啓動HelloServiceBundle時,它會將控制權交給HelloServiceActivator.java類,HelloServiceActivator將HelloServiceImpl對象註冊爲服務。下面,我們開始創建該服務的消費者。 5.2. 導入服務 在本小節中,我們將修改上面開發的HelloWorld Bundle,以便讓它成爲HelloService服務的消費者。您主要需要修改HelloWorldBundle中的Activator.java代碼,修改後的代碼如源代碼清單8所示: 源代碼清單8. HelloWorld Bundle中的Activator.java packagecom.javaworld.sample.helloworld;
importorg.osgi.framework.BundleActivator; importorg.osgi.framework.BundleContext; importorg.osgi.framework.ServiceReference; importcom.javaworld.sample.service.HelloService;
publicclass Activator implements BundleActivator { ServiceReference helloServiceReference; public void start(BundleContext context)throws Exception { System.out.println("HelloWorld!!"); helloServiceReference=context.getServiceReference(HelloService.class.getName()); HelloService helloService=(HelloService)context.getService(helloServiceReference); System.out.println(helloService.sayHello());
} public void stop(BundleContext context)throws Exception { System.out.println("Goodbye World!!"); context.ungetService(helloServiceReference); } } 在上面的代碼中,BundleContext.getServiceReference()方法將爲註冊在HelloService接口下的服務返回一個ServiceReference對象。如果存在多個HelloService服務,該方法會返回排行最高的服務(服務的排行是通過Constants.SERVICE_RANKING屬性指定的)。您一旦獲得ServiceReference對象,您就可以調用其BundleContext.getService()方法獲取真實的服務對象。 您可以參照運行Bundle的方法運行上面的示例應用,請點擊“RunàRun…”菜單,並確保HelloWorld和HelloService這兩個Bundle被選中。當您啓動HelloServiceBundle時,您會在控制檯上看到“InsideHelloServiceImple.sayHello()”,這個消息是由HelloServiceImpl.sayHello()方法打印出來的。 5.3. 創建服務工廠 在上節中,我們學會了如何使用OSGi框架新建一個Java對象,並把它註冊爲一個服務,然後讓其它的Bundle去消費這個服務。如果您看一下HelloServiceActivator.start()方法,您會注意到我們在start()方法中新建了HelloServiceImpl類對象,然後將它註冊到HelloService接口名下。這樣註冊後,任何其它的Bundle在請求HelloService服務時,OSGi容器將返回同一對象。 在大多數情況下,這樣的實現方法沒有問題。但是,比如說我們要爲每一個Bundle消費者返回不同的HelloServiceImpl對象,再比如說,您的服務對象要提供的服務爲打開一個數據庫連接,但並不是馬上就打開它,而是在真正需要的時候纔打開這個數據庫連接。 對這兩種情況,我們的解決方法是,新建一個類實現ServiceFactory接口,並把該類的對象註冊爲服務,但並不是註冊實際的服務對象。一旦您完成這一步,其它Bundle在請求該服務時,您的ServiceFactory實現類將接管該請求,ServiceFactory會爲每個Bundle新建一個服務對象,並將真實服務的創建時間延遲到有人真正需要該服務的時候。 下面我們將使用ServiceFactory更新我們上面開發的com.javaworld.sample.HelloServiceBundle,具體步驟如下: 1) 新建工廠 類HelloServiceFactory.java,源代碼如清單9所示。 源代碼清單9 . HelloServiceFactory.java publicclass HelloServiceFactory implements ServiceFactory{ private int usageCounter = 0; public Object getService(Bundle bundle,ServiceRegistration registration) { System.out.println("Create objectof HelloService for " + bundle.getSymbolicName()); usageCounter++; System.out.println("Number ofbundles using service " + usageCounter); HelloService helloService = newHelloServiceImpl(); return helloService; } public void ungetService(Bundle bundle,ServiceRegistration registration, Object service) { System.out.println("Release objectof HelloService for " + bundle.getSymbolicName()); usageCounter--; System.out.println("Number ofbundles using service " + usageCounter); } } 從上面的代碼中,我們可以看到,ServiceFactory接口定義了兩個方法: a) getService()方法:當某個Bundle第一次使用BundleContext.getService(ServiceReference)方法請求一個服務對象時,OSGi框架會調用該方法。在源代碼清單9中,我們用這個方法爲每個Bundle新建並返回不同的HelloServiceImpl對象,如果這個對象不是null,OSGi框架會緩存這個對象。如果同一個Bundle再次調用BundleContext.getService(ServiceReference)方法,OSGi將返回同一個服務對象。 b) ungetService()方法:當Bundle釋放服務時,OSGi容器可以調用該方法銷燬服務對象。在源代碼清單9中,我們使用usageCounter變量來跟蹤服務的使用數目,並打印出該服務的客戶端數量。 2) 修改HelloService Bundle中的HelloServiceActivator.java的start()方法,讓它註冊到ServiceFactory接口名下,而不是註冊到HelloService接口。詳細代碼如清單10所示: 源代碼清單10. 修改後的HelloServiceBundle中的HelloServiceActivator.java packagecom.javaworld.sample.service.impl; importorg.osgi.framework.BundleActivator; importorg.osgi.framework.BundleContext; importorg.osgi.framework.ServiceRegistration;
importcom.javaworld.sample.helloservice.HelloServiceFactory; importcom.javaworld.sample.service.HelloService;
publicclass HelloServiceActivator implements BundleActivator { ServiceRegistrationhelloServiceRegistration; public void start(BundleContext context)throws Exception { HelloServiceFactory helloServiceFactory= new HelloServiceFactory(); helloServiceRegistration=context.registerService(HelloService.class.getName(), helloServiceFactory,null); } public void stop(BundleContext context)throws Exception { helloServiceRegistration.unregister(); } }
現在,您可以試運行示例代碼。您會注意到,當HelloWorld Bundle啓動時,服務計數器變爲1;當HelloWorldBundle停止時,服務計數器的數目將變爲0。 5.4. 跟蹤服務 在“OSGi服務”小節,您學會了如何使用服務的接口名搜索服務。但如果有多個Bundle使用同一接口名註冊服務,那會發生什麼呢?這時,OSGi容器將返回排行最高的服務,即,返回註冊時那個SERVICE_RANKING屬性值最大的服務。如果有多個服務的排行值相等,那麼OSGi容器將返回PID值最小的那個服務。 但是,如果您的服務消費者需要了解某一接口下的服務對象何時註冊、何時取消註冊,這時,您應使用ServiceTracker類。下面,我們看看如何使用服務跟蹤器來修改我們的示例代碼,具體步驟如下。 1) 修改HelloWorldBundle的MANIFEST.MF文件,讓它導入org.osgi.util.tracker包; 2) 新建類HelloServiceTracker.java,其源代碼參見清單11。 源代碼清單11.HelloServiceTracker.java
public class HelloServiceTracker extends ServiceTracker {
public HelloServiceTracker(BundleContext context) {
super(context, HelloService.class.getName(),null);
}
public Object addingService(ServiceReference reference) {
System.out.println("Inside HelloServiceTracker.addingService " + reference.getBundle());
return super.addingService(reference);
}
public void removedService(ServiceReference reference, Object service) {
System.out.println("Inside HelloServiceTracker.removedService " + reference.getBundle());
super.removedService(reference, service);
}
}
在上面的HelloSerivceTracker類的構造函數中,您可以看到,我們把HelloService接口名傳入其父類中,這相當於說,HelloServiceTracker應跟蹤註冊到HelloService接口名下的所有服務,HelloServiceTracker繼承自ServiceTracker類,實現了下面兩個方法: a) addingService()方法:當Bundle使用接口名註冊服務時,該方法將會被調用; b)removedService()方法:當Bundle取消註冊某個接口名下的服務時,該方法將會被調用。 3) 用HelloServiceTracker類更新我們的Activator.java類,以便讓它來管理服務,而不是直接去查找它們,源代碼請參見清單12。 源代碼清單12. 使用了HelloServiceTracker的Activator.java
public class Activator implements BundleActivator {
HelloServiceTracker helloServiceTracker;
public void start(BundleContext context) throws Exception {
System.out.println("Hello World!!");
helloServiceTracker= new HelloServiceTracker(context);
helloServiceTracker.open();
HelloService helloService = (HelloService)helloServiceTracker.getService();
System.out.println(helloService.sayHello());
}
public void stop(BundleContext context) throws Exception {
System.out.println("Goodbye World!!");
helloServiceTracker.close();
}
}
我們看到,在初始的start()方法中,我們首先新建一個HelloServiceTracker對象,然後要求這個對象跟蹤HelloService接口下的服務。這時,我們可以調用getService()方法獲得HelloService對象。 如果您試運行上面的示例代碼,您會注意到,在啓動或停止HelloSerivceBundle時,OSGi容器都會調用HelloServiceTracker對象的addingService()方法或removedService()方法。
6. 結論
這是“你好,OSGi”系列三篇文章的第一篇,我向您介紹了使用OSGi進行模塊化應用開發的一些基本概念。您現在已瞭解到,當前有三種開源的OSGi容器,而且您還練習瞭如何使用Eclipse自帶的OSGi容器Equinox開發一個簡單的Bundle;同時,您也學會了Bundle之間是怎樣通過導出導入彼此的包和服務,從而達到彼此交互的目的。 在本文中,您也許注意到,開發OSGi Bundle的一個挑戰是,您的每個Bundle都需要了解OSGi API,在某些開發場景中,這可能意味着我們要許多基礎代碼。在本系列的下一篇文章中,我將向您介紹“SpringDynamic Modules for OSGi Service Platforms(OSG服務平臺的Spring動態模塊,亦稱爲SpringOSGi)”項目,該項目將會簡化OSGi Bundle 的開發。另外,您也可以參考本文的資源部分,學習更多關於OSGi的知識。
7. 作者介紹
Sunil Patil是一位Java企業/門戶開發者,現就職於加州三藩市的AscendentTechnology公司。他是Java Portlets 101一書的作者(該書由SourceBeat公司2007年4月出版),而且,他還寫了許多文章,通過O’ReillyMedia發表。Sunil曾經在IBM的WebspherePortal Server開發團隊工作過3年。現在,他積極參與Pluto社區。另外,他擁有IBM WebspherePortal Server 5.0和5.1版本的開發者證書,Sun 的SCJP證書和Web組件開發者證書,同時,他還是商務組件的開發者。您可以訪問Sunil的博客:http://jroller.com/page/SunilPatil。
8. 資源
非官方的OSGi規範開始於1999年,最近,JSR對Java模塊化支持分爲兩個規範,即,JSR 277(Java模塊系統,http://jcp.org/en/jsr/detail?id=277 )和JSR 291(Java動態組件支持,http://jcp.org/en/jsr/detail?id=291 )。在這兩個JSR規範中,人們對哪個規範應該包含在Java EE6中,有過很多爭議,請參考http://www.infoq.com/news/2007/08/osgi-jsr277-debate 。 和許多Java開發者一樣,SebastienArbogast最近意識到OSGi的重要性(請參見http://osgi.dzone.com/news/why-osgi-zone ),他在Javalobby上開闢了OSGi專區(請參見http://osgi.dzone.com/ ),作爲OSGi相關信息和討論的平臺。 如果您想了解OSGi服務規範第4版的信息,請參考http://www2.osgi.org/Release4/HomePage;關於OSGi服務規範第4版的Java API,請參考http://www2.osgi.org/javadoc/r4/;關於OSG服務平臺的Spring動態模塊項目,有時也稱作Spring OSGi項目,請參考http://www.springframework.org/osgi。
|