osgi框架基礎原理與例項一
OSGI(Open Services Gateway Initiative),或者通俗點說JAVA動態模組系統,定義了一套模組應用開發的框架。OSGI容器實現方案如Knopflerfish, Equinox, and Apache Felix允許你把你的應用分成多個功能模組,這樣通過依賴管理這些功能會更加方便。
和Servlet和EJB規範類似,OSGI規範包含兩大塊:一個OSGI容器需要實現的服務集合;一種OSGI容器和應用之間通訊的機制。開發OSGI平臺意味著你需要使用OSGI API編寫你的應用,然後將其部署到OSGI容器中。從開發者的視角來看,OSGI提供以下優勢:
- 你可以動態地安裝、解除安裝、啟動、停止不同的應用模組,而不需要重啟容器。
- 你的應用可以在同一時刻跑多個同一個模組的例項。
- 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容器:
- Equinox是OSGI Service Platform Release 4的一個實現。是Eclipse 模組化執行時的核心。
- Knopflerfish另一個選擇。
- 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
我們一步步的開始:
- 啟動Eclipse,依次點 File --> New --> Project。
- 選擇Plug-in Project,next。
- 輸入Project Name專案名稱,比如com.howard.sample.HelloWorld,Target Platform(目標平臺)裡的an OSGI framework,選擇standard。
- 剩下的保持預設,next。
- 下個對話方塊也預設,next。
- 然後選擇Hello OSGI Bundle作為模版。Finish。
Eclipse會飛快的為你建立Hello world bundle的模版程式碼。主要包含兩個檔案:Activator.java和MANIFEST.MF。
Activator.java的程式碼如下所示:
Java程式碼- import org.osgi.framework.BundleActivator;
- import org.osgi.framework.BundleContext;
- public class Activator implements BundleActivator {
- public void start(BundleContext context) throws Exception {
- System.out.println("Hello world");
- }
- public void stop(BundleContext context) throws Exception {
- System.out.println("Goodbye World");
- }
- }
如果你的bundle在啟動和關閉的時候需要被通知,你可以考慮實現BundleActivator介面。以下是定義Activator的一些注意點:
- 你的Activator類需要一個公有的無引數建構函式。OSGI框架會通過類反射的方式來例項化一個Activator類。
- 容器啟動bundle過程中負責呼叫你的Activator類的start方法。bundle可以在此初始化資源比如說初始化資料庫連線。start方法需要一個引數,BundleContext物件。這個物件允許bundles以取得OSGI容器相關資訊的方式和框架互動。如果某一個bundle有異常丟擲,容器將對該bundle標記為stopped並不將其納入service列表。
- 容器關閉的時候會呼叫你的Activator類方法stop(),你可以利用這個機會做一些清理的操作。
MANIFEST.MF
這個檔案是你的bundle的部署描述檔案。格式和Jar裡的MANIFEST.MF是一樣的。包含的不少名值對,就像如下:
Xml程式碼- Manifest-Version: 1.0
- Bundle-ManifestVersion: 2
- Bundle-Name: HelloWorld Plug-in
- Bundle-SymbolicName: com.howard.sample.HelloWorld
- Bundle-Version: 1.0.0
- Bundle-Activator: com.howard.sample.helloworld.Activator
- Bundle-Vendor: HOWARD
- Bundle-RequiredExecutionEnvironment: JavaSE-1.6
- 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
- 點選Run --> Run Configuration
- 在左邊的OSGI Framework選項裡右鍵 new ,建立一個新的OSGI Run Configuration
- 名字隨便取好了,我們取個OSGi hello world。
- 你會注意到中間的窗口裡Workspace專案裡有一子項 com.howard.sample.HelloWorld,將其勾選上,其他的不用管。這時的狀態應該如下圖。
- 點選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程式碼- public interface HelloService {
- public String sayHello();
- }
3、建立一個com.howard.sample.service.impl.HelloServiceImpl.java類實現剛才的介面:
Java程式碼- public class HelloServiceImpl implements HelloService{
- public String sayHello() {
- System.out.println("Inside HelloServiceImple.sayHello()");
- return "Say Hello";
- }
- }
4、開啟MANIFEST.MF,選擇Runtime標籤項,在Exported Packages選項欄,點選Add並且選擇com.howard.sample.service這個包。然後MANIFEST.MF的程式碼應該如下:
Xml程式碼- Manifest-Version: 1.0
- Bundle-ManifestVersion: 2
- Bundle-Name: HelloService Plug-in
- Bundle-SymbolicName: com.howard.sample.HelloService
- Bundle-Version: 1.0.0
- Bundle-Activator: com.howard.sample.helloservice.Activator
- Bundle-Vendor: HOWARD
- Bundle-RequiredExecutionEnvironment: JavaSE-1.6
- Import-Package: org.osgi.framework;version="1.3.0"
- 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程式碼- Manifest-Version: 1.0
- Bundle-ManifestVersion: 2
- Bundle-Name: HelloWorld Plug-in
- Bundle-SymbolicName: com.howard.sample.HelloWorld
- Bundle-Version: 1.0.0
- Bundle-Activator: com.howard.sample.helloworld.Activator
- Bundle-Vendor: HOWARD
- Bundle-RequiredExecutionEnvironment: JavaSE-1.6
- Import-Package: com.howard.sample.service,
- 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程式碼- public class HelloServiceActivator implements BundleActivator {
- ServiceRegistration helloServiceRegistration;
- @Override
- public void start(BundleContext context) throws Exception {
- HelloService helloService = new HelloServiceImpl();
- helloServiceRegistration = context.registerService(HelloService.class
- .getName(), helloService, null);
- }
- @Override
- public void stop(BundleContext context) throws Exception {
- helloServiceRegistration.unregister();
- }
- }
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程式碼- public class Activator implements BundleActivator {
- ServiceReference helloServiceReference;
- public void start(BundleContext context) throws Exception {
- System.out.println("Hello World!!");
- 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);
- }
- }
程式碼很簡單,就不多說了。
在執行之前我們在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程式碼- public class HelloServiceFactory implements ServiceFactory {
- private int usageCounter = 0;
- @Override
- public Object getService(Bundle bundle, ServiceRegistration registration) {
- System.out.println("Create object of HelloService for " + bundle.getSymbolicName());
- usageCounter++;
- System.out.println("Number of bundles using service " + usageCounter);
- HelloService helloService = new HelloServiceImpl();
- return helloService;
- }
- @Override
- public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) {
- System.out.println("Release object of HelloService for " + bundle.getSymbolicName());
- usageCounter--;
- System.out.println("Number of bundles using service " + usageCounter);
- }
- }
ServiceFactory介面定義了兩個方法:
- getService方法:特定的bundle在第一次呼叫BundleContext的getService方法時由OSGI框架呼叫,在例項程式碼中,我們用這個方法來返回一個新的HelloService的實現。OSGI框架會快取這個返回的物件,如果同一個bundle在未來再次呼叫BundleContext的getService方法的話,會直接返回這個快取中的物件。
- ungetService方法:bundle釋放service的時候由OSGI容器呼叫。
2、修改HelloServiceActivator.java的start方法,將ServiceFactory作為服務註冊,程式碼如下:
Java程式碼- public class HelloServiceActivator implements BundleActivator {
- ServiceRegistration helloServiceRegistration;
- @Override
- public void start(BundleContext context) throws Exception {
- HelloServiceFactory helloServiceFactory = new HelloServiceFactory();
- helloServiceRegistration = context.registerService(HelloService.class
- .getName(), helloServiceFactory, null);
- }
- @Override
- public void stop(BundleContext context) throws Exception {
- helloServiceRegistration.unregister();
- }
- }
現在執行下試試看,你會發現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程式碼- 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);
- }
- }
我們在HelloServiceTracker的建構函式裡將HelloService介面名傳進去,ServiceTracker會跟蹤實現這個介面的所有的註冊services。ServiceTracker主要有兩個重要方法:
- addingService方法:bundle註冊一個基於給定介面的service時呼叫。
- removeService方法:bundle取消註冊一個基於給定介面的service時呼叫。
3、修改Activator類,使用剛剛建立的HelloServiceTracker來獲取service:
Java程式碼- public class Activator implements BundleActivator {
- HelloServiceTracker helloServiceTracker;
- public void start(BundleContext context) throws