人人都會OSGI--實例講解OSGI開發

/**

*  轉載請註明作者longdick    http://longdick.iteye.com

*

*/

 

 

OSGI(Open Services Gateway Initiative),或者通俗點說JAVA動態模塊系統,定義了一套模塊應用開發的框架。OSGI容器實現方案如Knopflerfish, Equinox, and Apache Felix允許你把你的應用分成多個功能模塊,這樣通過依賴管理這些功能會更加方便。

和Servlet和EJB規範類似,OSGI規範包含兩大塊:一個OSGI容器需要實現的服務集合;一種OSGI容器和應用之間通信的機制。開發OSGI平臺意味着你需要使用OSGI API編寫你的應用,然後將其部署到OSGI容器中。從開發者的視角來看,OSGI提供以下優勢:

 

  1. 你可以動態地安裝、卸載、啓動、停止不同的應用模塊,而不需要重啓容器。
  2. 你的應用可以在同一時刻跑多個同一個模塊的實例。
  3. OSGI在SOA領域提供成熟的解決方案,包括嵌入式,移動設備和富客戶端應用等。

 

OK,你已經有個Servlet容器來做web 應用,有了EJB容器來做事務處理,你可能在想爲什麼你還需要一個新的容器?簡單點說,OSGI容器被設計專門用來開發可分解爲功能模塊的複雜的Java應用。

 

企業應用領域的OSGI

 

OSGI規範最初是由OSGI聯盟在1999年3月發起。它的主要目的是成爲向網絡設備傳輸服務管理的開放規範。核心思想是一旦你向網絡設備中添加了一個OSGI服務平臺,你可以在網絡中的任意位置管理該設備上的服務組件。這些服務組件可以任意安裝,更新或移除而不會對設備產生影響。

多年來,OSGI技術只出現在嵌入式系統和網絡設備市場。現在,Eclipse使OSGI在企業開發領域煥發出新的光彩。

 

OSGI受到越來越廣泛的支持

 

2003年,Eclipse開發團隊開始尋找一種使eclipse成爲一種功能更動態、工具更模塊化的富客戶端平臺。最終,他們的目光鎖定在OSGI框架上。Eclipse3.0,2004年6月發佈,是基於OSGI技術搭建的首個Eclipse版本。

 

幾乎所有企業應用服務提供商支持或計劃支持OSGI。Spring框架同樣支持OSGI,通過Spring DM(Spring Dynamic Modules for OSGI Service Platforms)項目,可以讓我們在Spring上更方便的應用OSGI。

 

 

開源OSGI容器

 

從企業應用開發者的角度看,OSGI容器侵入性非常小,你可以方便地將其嵌入一個企業應用。舉個例子來說,假設你在開發一個複雜的web應用。你希望將這個應用分解成多個功能模塊。一個View層模塊,一個Model層模塊,一個DAO模塊。使用嵌入式OSGI容器來跨依賴地管理這些模塊可以讓你隨時更新你的DAO模塊卻不需要重啓你的服務器。

只要你的應用完全符合OSGI規範,它就可以在所有符合OSGI規範的容器內運行。現在,有三種流行的開源OSGI容器:

 

  1. Equinox是OSGI Service Platform Release 4的一個實現。是Eclipse 模塊化運行時的核心。
  2. Knopflerfish另一個選擇。
  3. Apache Felix是Apache軟件基金會贊助的一個OSGI容器

 

在這篇文章裏我們使用Equinox作爲我們的OSGI容器。

 

 

嘗試開發一個Hello World bundle

 

在OSGI的領域,發佈的軟件是以bundle的形式出現。bundle由java class類和資源文件組成,向設備所有者提供功能,同時可以爲其他的bundles提供服務。Eclipse對開發bundles提供了強大的支持。Eclipse不僅僅提供創建bundles的功能,它還集成了Equinox這個OSGI容器,你可以在其上開發和調試OSGI組件。其實所有的Eclipse插件都是使用Eclipse規範代碼寫的OSGI bundle。接下來,你將可以學到如何使用Eclipse IDE開發一個Hello world osgi bundle。

 

開始開發bundle

 

我們一步步的開始:

 

 

  1. 啓動Eclipse,依次點 File --> New --> Project。
  2. 選擇Plug-in Project,next。
  3. 輸入Project Name項目名稱,比如com.howard.sample.HelloWorld,Target Platform(目標平臺)裏的an OSGI framework,選擇standard。
  4. 剩下的保持默認,next。
  5. 下個對話框也默認,next。
  6. 然後選擇Hello OSGI Bundle作爲模版。Finish。

Eclipse會飛快的爲你創建Hello world bundle的模版代碼。主要包含兩個文件:Activator.java和MANIFEST.MF。

 

Activator.java的代碼如下所示:

 

Java代碼  收藏代碼
  1. import org.osgi.framework.BundleActivator;  
  2. import org.osgi.framework.BundleContext;  
  3. public class Activator implements BundleActivator {  
  4.     public void start(BundleContext context) throws Exception {  
  5.         System.out.println("Hello world");  
  6.     }  
  7.     public void stop(BundleContext context) throws Exception {  
  8.         System.out.println("Goodbye World");  
  9.     }  
  10. }  
 

如果你的bundle在啓動和關閉的時候需要被通知,你可以考慮實現BundleActivator接口。以下是定義Activator的一些注意點:

 

  1. 你的Activator類需要一個公有的無參數構造函數。OSGI框架會通過類反射的方式來實例化一個Activator類。
  2. 容器啓動bundle過程中負責調用你的Activator類的start方法。bundle可以在此初始化資源比如說初始化數據庫連接。start方法需要一個參數,BundleContext對象。這個對象允許bundles以取得OSGI容器相關信息的方式和框架交互。如果某一個bundle有異常拋出,容器將對該bundle標記爲stopped並不將其納入service列表。
  3. 容器關閉的時候會調用你的Activator類方法stop(),你可以利用這個機會做一些清理的操作。

 

MANIFEST.MF

 

這個文件是你的bundle的部署描述文件。格式和Jar裏的MANIFEST.MF是一樣的。包含的不少名值對,就像如下:

 

Xml代碼  收藏代碼
  1. Manifest-Version: 1.0  
  2. Bundle-ManifestVersion: 2  
  3. Bundle-Name: HelloWorld Plug-in  
  4. Bundle-SymbolicName: com.howard.sample.HelloWorld  
  5. Bundle-Version: 1.0.0  
  6. Bundle-Activator: com.howard.sample.helloworld.Activator  
  7. Bundle-Vendor: HOWARD  
  8. Bundle-RequiredExecutionEnvironment: JavaSE-1.6  
  9. Import-Package: org.osgi.framework;version="1.3.0"  
 

分別來看下:

 

Bundle-ManifestVersion

 數值爲2意味着本bundle支持OSGI規範第四版;如果是1那就是支持OSGI規範第三版。

Bundle-Name

 給bundle定義一個短名,方便人員閱讀

Bundle-SymbolicName

 給bundle定義一個唯一的非局部名。方便分辨。

Bundle-Activator

 聲明在start和stop事件發生時會被通知的監聽類的名字。

Import-Package

 定義bundle的導入包。

Hello World bundle完成了,接下來我們運行一下。

 

執行bundle

 

 

  1. 點擊Run --> Run Configuration
  2. 在左邊的OSGI Framework選項裏右鍵 new ,創建一個新的OSGI Run Configuration
  3. 名字隨便取好了,我們取個OSGi hello world。
  4. 你會注意到中間的窗口裏Workspace項目裏有一子項 com.howard.sample.HelloWorld,將其勾選上,其他的不用管。這時的狀態應該如下圖。
  5. 點擊Run按鈕。在控制檯你應該可以看見點東西了。那是叫做OSGI控制檯的東東。與子相伴,還有一個"Hello world"。


OSGI控制檯

 

OSGI控制檯是一個OSGI容器的命令行界面。你可以利用它做些諸如啓動,關閉,安裝bundles,更新和刪除bundles等操作。現在,點擊OSGI控制檯所在的位置,回車,你就會發現可以輸入命令了。這時的OSGI控制檯應該如下圖:


下面列出一些常用的OSGI命令,你可以試着和OSGI容器交互。

ss 顯示已安裝的bundles的狀態信息,信息包括bundle ID,短名,狀態等等。

start 啓動一個bundle

stop  關閉一個bundle

update  載入一個新的JAR文件更新一個bundle

install  安裝一個新的bundle到容器中

uninstall  卸載一個已在容器中的bundle

 

 

依賴管理

 

OSGI規範允許你把你的應用分解成多個模塊然後管理各個模塊間的依賴關係。

這需要通過bundle scope來完成。默認情況下,一個bundle內的class對其他bundle來說是不可見的。那麼,如果要讓一個bundle訪問另一個bundle裏的class要怎麼做?解決的方案就是從源bundle導出包,然後在目標bundle裏導入。

接下來我們對此做一個例子。

 

首先,我們需要先創建一個com.howard.sample.HelloService bundle,我們將通過它導出一個包。

然後,我們在com.howard.sample.HelloWorld 這個bundle裏導入包。

 

 

導出包

 

1、創建名爲com.howard.sample.HelloService的bundle,創建步驟和前面一樣。

2、在這個bundle內,添加一個com.howard.sample.service.HelloService.java 接口,代碼如下:

 

Java代碼  收藏代碼
  1. public interface HelloService {  
  2.     public String sayHello();  
  3. }  
 

3、創建一個com.howard.sample.service.impl.HelloServiceImpl.java類實現剛纔的接口:

 

Java代碼  收藏代碼
  1. public class HelloServiceImpl implements HelloService{  
  2.     public String sayHello() {  
  3.         System.out.println("Inside HelloServiceImple.sayHello()");  
  4.         return "Say Hello";  
  5.     }  
  6. }  
 

4、打開MANIFEST.MF,選擇Runtime標籤項,在Exported Packages選項欄,點擊Add並且選擇com.howard.sample.service這個包。然後MANIFEST.MF的代碼應該如下:

 

Xml代碼  收藏代碼
  1. Manifest-Version: 1.0  
  2. Bundle-ManifestVersion: 2  
  3. Bundle-Name: HelloService Plug-in  
  4. Bundle-SymbolicName: com.howard.sample.HelloService  
  5. Bundle-Version: 1.0.0  
  6. Bundle-Activator: com.howard.sample.helloservice.Activator  
  7. Bundle-Vendor: HOWARD  
  8. Bundle-RequiredExecutionEnvironment: JavaSE-1.6  
  9. Import-Package: org.osgi.framework;version="1.3.0"  
  10. Export-Package: com.howard.sample.service  
 

 

你可以看到,MANIFEST.MF文件和剛纔的HelloWorld的那份很類似。唯一的區別就是這個多了Export-Package這個標記,對應的值就是我們剛纔選擇的com.howard.sample.service。

Export-Package標記告訴OSGI容器在com.howard.sample.service包內的classes可以被外部訪問。

注意,我們僅僅暴露了HelloService接口,而不是直接暴露HelloServiceImpl實現。

 

導入包

 

接下來我們要更新原來的HelloWorld bundle以導入com.howard.sample.service包。步驟如下:

 

1、進入HelloWorld bundle,打開MANIFEST.MF,進入Dependencies標籤頁,在Imported Packages裏添加com.howard.sample.service。MANIFEST.MF文件應該如下所示:

 

Xml代碼  收藏代碼
  1. Manifest-Version: 1.0  
  2. Bundle-ManifestVersion: 2  
  3. Bundle-Name: HelloWorld Plug-in  
  4. Bundle-SymbolicName: com.howard.sample.HelloWorld  
  5. Bundle-Version: 1.0.0  
  6. Bundle-Activator: com.howard.sample.helloworld.Activator  
  7. Bundle-Vendor: HOWARD  
  8. Bundle-RequiredExecutionEnvironment: JavaSE-1.6  
  9. Import-Package: com.howard.sample.service,  
  10.  org.osgi.framework;version="1.3.0"  
 

沒錯,Import-package標記的值也就是導入的包名之間是用逗號隔開的。在這裏導入了兩個包om.howard.sample.service和org.osgi.framework。後者是使用Activator類時必須導入的包。

 

2、接下來,打開HelloWorld項目下的Activator.java文件,這時候你會發現可以使用HelloService這個接口了。但還是不能使用HelloServiceImpl實現類。Eclipse會告訴你:Access restriction(立入禁止)。

 

Class級別可見域

 

爲什麼OSGI容器可以做到讓jar包中的一些classes可見而另一些又不可見呢。

答案其實就是OSGI容器自定義了java class loader來有選擇的加載類。OSGI容器爲每一個bundle都創建了不同的class loader。因此,bundle可以訪問的classes包括

 

  • Boot classpath:所有的java基礎類。
  • Framework classpath:OSGI框架級別的classloader加載的類
  • Bundle classpath:Bundle本身引用的關係緊密的JAR的路徑
  • Imported packages:就是在MANIFEST.MF裏聲明的導入包,一旦聲明,在bundle內就可見了。

 

bundle級別的可見域允許你可以隨時放心的更改HelloServiceImpl實現類而不需要去擔心依賴關係會被破壞。

 

 

OSGI服務

 

OSGI框架是實現SOA的絕佳土壤。通過它可以實現bundles暴露服務接口給其他bundles消費而不需要讓細節暴露。消費bundles甚至可以完全不知道提供服務的bundles。憑着可以良好的隱藏具體實現的能力,OSGI當之無愧是SOA的一種較完美的實現方案。

 

OSGI中,提供服務的bundle在OSGI容器上將一個POJO註冊成一個service。消費者bundle請求OSGI容器中基於某個特殊接口的註冊service。一旦找到,消費者bundle就會綁定它,然後就可以調用service中的方法了。舉個例子會更容易說明。

 

 

導出services

 

1、確保com.howard.sample.HelloService裏的MANIFEST.MF導入org.osgi.framework包

2、創建com.howard.sample.service.impl.HelloServiceActivator.java,代碼如下:

 

Java代碼  收藏代碼
  1. public class HelloServiceActivator implements BundleActivator {  
  2.     ServiceRegistration helloServiceRegistration;  
  3.     @Override  
  4.     public void start(BundleContext context) throws Exception {  
  5.         HelloService helloService = new HelloServiceImpl();  
  6.         helloServiceRegistration = context.registerService(HelloService.class  
  7.                 .getName(), helloService, null);  
  8.     }  
  9.   
  10.     @Override  
  11.     public void stop(BundleContext context) throws Exception {  
  12.         helloServiceRegistration.unregister();  
  13.     }  
  14.   
  15. }  
 

 

OK,我們就是用BundleContext的registerService方法註冊service的。這個方法需要三個參數。

 

  • service的接口名。如果service實現了多個接口,那樣你需要傳入一個包含所有接口名的String數組。在這裏我們傳入的是HelloService這個接口。
  • 真正的service實現。在例子中我們傳了一個HelloServiceImpl實現。
  • service屬性。這個參數可以在有多個service實現同一個接口的情況下,消費者用來區分真正感興趣的service。

3、最後一步就是修改HelloService的MANIFEST.MF文件,將Bundle-Activator改成com.howard.sample.service.impl.HelloServiceActivator

 

現在HelloService bundle已經隨時準備將HelloServiceImpl服務發佈了。OSGI容器啓動HelloServie bundle的時候會讓HelloServiceActivator運作,在那個時候將HelloServiceImpl註冊到容器中,接下來就是創建消費者的問題了。

 

導入service

 

我們的消費者就是HelloWorld bundle,主要修改的就是其中的Activator.java,修改代碼如下:

 

Java代碼  收藏代碼
  1. public class Activator implements BundleActivator {  
  2. ServiceReference helloServiceReference;  
  3.     public void start(BundleContext context) throws Exception {  
  4.         System.out.println("Hello World!!");  
  5.         helloServiceReference=context.getServiceReference(HelloService.class.getName());  
  6.         HelloService helloService=(HelloService)context.getService(helloServiceReference);  
  7.         System.out.println(helloService.sayHello());  
  8.     }  
  9.     public void stop(BundleContext context) throws Exception {  
  10.         System.out.println("Goodbye World!!");  
  11.         context.ungetService(helloServiceReference);  
  12.     }  
  13. }  
 

代碼很簡單,就不多說了。

 

在運行之前我們在Run-->Run Configurations對話框裏,把HelloWorld和HelloService這兩個bundle前面的鉤都打上。然後運行時你會發現HelloServiceImpl.sayHello()方法已經被調用了。

 

在OSGI控制檯輸入ss並回車,所有容器內的bundle狀態一目瞭然。其中id爲0的bundle是OSGI框架基礎bundle,另兩個就是HelloService和HelloWorld了,它倆的id是隨機的,狀態是ACTIVE也就是已啓動狀態。假設HelloService的id爲7,HelloWorld爲8。

 

輸入stop 8就可以暫停bundle的運行,容器內這個bundle還是存在的,只是狀態變成了RESOLVED。再次啓動使用start 8,然後就會看見HelloWorld bundle消費了HelloService的服務。

 

 

創建服務工廠

 

剛纔例子所示,我們會在HelloService bundle啓動時初始化並註冊service。然後不管存不存在消費端,這個service都會存在,而且消費端取得的service 實例其實都是同一個。OK,某些servie是比較耗費資源的主,我們不希望它一直佔用資源,最好是在真正用它的時候創建不用的時候銷燬就最好了。

 

解決如上問題的方案就是使用ServiceFactory接口的實現來代替原先service具體的實現到OSGI容器去註冊。這樣,以後只有當其他bundle請求該服務時,纔會由ServiceFactory實現類來處理請求並返回一個新的service實例。

 

實例步驟如下:

1、在HelloService bundle創建一個實現ServiceFactory接口的類HelloServiceFactory類,代碼如下:

 

Java代碼  收藏代碼
  1. public class HelloServiceFactory implements ServiceFactory {  
  2.     private int usageCounter = 0;  
  3.     @Override  
  4.     public Object getService(Bundle bundle, ServiceRegistration registration) {  
  5.         System.out.println("Create object of HelloService for " + bundle.getSymbolicName());  
  6.         usageCounter++;  
  7.         System.out.println("Number of bundles using service " + usageCounter);  
  8.         HelloService helloService = new HelloServiceImpl();  
  9.         return helloService;  
  10.     }  
  11.     @Override  
  12.     public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) {  
  13.         System.out.println("Release object of HelloService for " + bundle.getSymbolicName());  
  14.         usageCounter--;  
  15.         System.out.println("Number of bundles using service " + usageCounter);  
  16.     }  
  17. }  
 

 

ServiceFactory接口定義了兩個方法:

 

  • getService方法:特定的bundle在第一次調用BundleContext的getService方法時由OSGI框架調用,在實例代碼中,我們用這個方法來返回一個新的HelloService的實現。OSGI框架會緩存這個返回的對象,如果同一個bundle在未來再次調用BundleContext的getService方法的話,會直接返回這個緩存中的對象。
  • ungetService方法:bundle釋放service的時候由OSGI容器調用。

2、修改HelloServiceActivator.java的start方法,將ServiceFactory作爲服務註冊,代碼如下:

 

Java代碼  收藏代碼
  1. public class HelloServiceActivator implements BundleActivator {  
  2.     ServiceRegistration helloServiceRegistration;  
  3.     @Override  
  4.     public void start(BundleContext context) throws Exception {  
  5.         HelloServiceFactory helloServiceFactory = new HelloServiceFactory();  
  6.         helloServiceRegistration = context.registerService(HelloService.class  
  7.                 .getName(), helloServiceFactory, null);  
  8.     }  
  9.   
  10.     @Override  
  11.     public void stop(BundleContext context) throws Exception {  
  12.         helloServiceRegistration.unregister();  
  13.     }  
  14. }  
 

 

現在運行下試試看,你會發現HelloWorld bundle啓動時纔會初始化HelloService,控制檯會打印出"Number of bundles using service 1",當HelloWorld bundle暫停時會打印出"Number of bundles using service 0"。

 

 

services跟蹤

 

某種情形下,我們可能需要在某個特殊的接口有新的服務註冊或取消註冊時通知消費端。這時我們可以使用ServiceTracker類。如下步驟所示:

1、在HelloWorld bundle裏的MANIFEST.MF導入org.util.tracker包。

2、創建HelloServiceTracker類,代碼如下:

 

Java代碼  收藏代碼
  1. public class HelloServiceTracker extends ServiceTracker {  
  2.     public HelloServiceTracker(BundleContext context) {  
  3.         super(context, HelloService.class.getName(),null);  
  4.     }  
  5.     public Object addingService(ServiceReference reference) {  
  6.         System.out.println("Inside HelloServiceTracker.addingService " + reference.getBundle());  
  7.         return super.addingService(reference);  
  8.     }  
  9.     public void removedService(ServiceReference reference, Object service) {  
  10.         System.out.println("Inside HelloServiceTracker.removedService " + reference.getBundle());  
  11.         super.removedService(reference, service);  
  12.     }  
  13. }  
 

我們在HelloServiceTracker的構造函數裏將HelloService接口名傳進去,ServiceTracker會跟蹤實現這個接口的所有的註冊services。ServiceTracker主要有兩個重要方法:

 

  • addingService方法:bundle註冊一個基於給定接口的service時調用。
  • removeService方法:bundle取消註冊一個基於給定接口的service時調用。

 

3、修改Activator類,使用剛剛創建的HelloServiceTracker來獲取service:

 

Java代碼  收藏代碼
  1. public class Activator implements BundleActivator {  
  2.     HelloServiceTracker helloServiceTracker;  
  3.     public void start(BundleContext context) throws Exception {  
  4.         System.out.println("Hello World!!");  
  5.         helloServiceTracker= new HelloServiceTracker(context);  
  6.         helloServiceTracker.open();  
  7.         HelloService helloService=(HelloService)helloServiceTracker.getService();  
  8.         System.out.println(helloService.sayHello());  
  9.     }  
  10.       
  11.     public void stop(BundleContext context) throws Exception {  
  12.         System.out.println("Goodbye World!!");  
  13.         helloServiceTracker.close();  
  14.     }  
  15. }  
 

現在運行一下,可以發現只要HelloService bundle啓動或是暫停都會導致HelloServiceTracker的對addingService或removedService方法的調用。

 

ServiceTracker不僅僅能跟蹤Service的動向,它還能通過getService方法取得Service實例並返回。但是如果同一個接口下有多個service註冊,這時返回哪個service呢?這時候就需要看service的等級哪個高了。這個等級是service註冊時的property屬性裏的一項:SERVICE_RANKING。誰的SERVICE_RANKING高,就返回誰。

如果有兩個一樣高的呢?這時再看這兩個service誰的PID更低了。

 

如果對OSGI的ClassLoader機制有疑問,可以看看這篇解釋ClassLoader機制和自定義ClassLoader相關的文章:

http://longdick.iteye.com/blog/442213

 

OSGI的基本原理和入門開發就到這裏了。希望同學們能對OSGI開發有個簡單而清晰的認識。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章