開發一個真實的 OSGi 應用程序
我們不能只停留在 hello world 的層面,雖然那曾經對我們很重要 ,但是現實需要我們能夠使用 OSGi 寫出激動人心的應用程序,它能夠被客戶接受,被架構師認可,被程序員肯定。好的,那我們開始吧。下面將會著重介紹一些現實的應用程序可能需要的一些 OSGi 應用場景。
由于 OSGi 框架能夠方便的隱藏實現類,所以對外提供接口是很自然的事情,OSGi 框架提供了服務的注冊和查詢功能。好的,那么我們實際操作一下,就在 Hello world 工程的基礎上進行。
我們需要進行下列的步驟:
- 定義一個服務接口,并且 export 出去供其它 bundle 使用;
- 定義一個缺省的服務實現,并且隱藏它的實現;
- Bundle 啟動后,需要將服務注冊到 Equinox 框架;
- 從框架查詢這個服務,并且測試可用性。
好的,為了達到上述要求,我們實際操作如下:
- 定義一個新的包
osgi.test.helloworld.service
,用來存放接口。單獨一個 package 的好處是,您可以僅僅 export 這個 package 給其它 bundle 而隱藏所有的實現類 - 在上述的包中新建接口
IHello
,提供一個簡單的字符串服務,代碼如下:
清單 2. IHello1 2 package osgi.test.helloworld.service; 3 4 public interface IHello { 5 /** 6 * 得到 hello 信息的接口 . 7 * @return the hello string. 8 */ 9 String getHello(); 10 }
- 再新建一個新的包
osgi.test.helloworld.impl
,用來存放實現類。 - 在上述包中新建
DefaultHelloServiceImpl
類,實現上述接口:
清單 3. IHello 接口實現public class DefaultHelloServiceImpl implements IHello { @Override public String getHello() { return "Hello osgi,service"; } }
- 注冊服務,OSGi 框架提供了兩種注冊方式,都是通過
BundleContext
類實現的:registerService(String,Object,Dictionary)
注冊服務對象object
到接口名String
下,可以攜帶一個屬性字典Dictionary
;registerService(String[],Object,Dictionary)
注冊服務對象object
到接口名數組String[]
下,可以攜帶一個屬性字典Dictionary
,即一個服務對象可以按照多個接口名字注冊,因為類可以實現多個接口;
我們使用第一種注冊方式,修改
Activator
類的start
方法,加入注冊代碼:
清單 4. 加入注冊代碼public void start(BundleContext context) throws Exception { System.out.println("hello world"); context.registerService( IHello.class.getName(), new DefaultHelloServiceImpl(), null); }
- 為了讓我們的服務能夠被其它 bundle 使用,必須在 MANIFEST.MF 中對其進行導出聲明,雙擊 MANIFEST.MF,找到runtime > exported packages > 點擊 add,如圖,選擇 service 包即可:
圖 14. 選擇導出的服務包 - 另外新建一個類似于 hello world 的 bundle 叫:
osgi.test.helloworld2
,用于測試osgi.test.helloworld
bundle 提供的服務的可用性; - 添加 import package:在第二個 bundle 的 MANIFEST.MF 文件中,找到 dependencies > Imported packages > Add …,選擇我們剛才 export 出去的 osgi.test.helloworld.service 包:
圖 15. 選擇剛才 export 出去的 osgi.test.helloworld.service 包 - 查詢服務:同樣,OSGi 框架提供了兩種查詢服務的引用
ServiceReference
的方法:getServiceReference(String)
:根據接口的名字得到服務的引用;getServiceReferences(String,String)
:根據接口名和另外一個過濾器名字對應的過濾器得到服務的引用;
- 這里我們使用第一種查詢的方法,在
osgi.test.helloworld2
bundle 的Activator
的start
方法加入查詢和測試語句:
清單 5. 加入查詢和測試語句public void start(BundleContext context) throws Exception { System.out.println("hello world2"); /** * Test hello service from bundle1. */ IHello hello1 = (IHello) context.getService( context.getServiceReference(IHello.class.getName())); System.out.println(hello1.getHello()); }
- 修改運行環境,因為我們增加了一個 bundle,所以說也需要在運行配置中加入對新的 bundle 的配置信息,如下圖所示:
圖 16. 加入對新的 bundle 的配置信息 - 執行,得到下列結果:
圖 17. 執行結果
恭喜您,成功了!
前面講過,OSGi 規范定義了很多可用的 bundle,您盡管使用它們完成您的工作,而不必另外再發明輪子,OSGi 框架定義的事件管理服務,類似于 JMS,但是使用上比 JMS 簡單。
OSGi 整個框架都離不開這個服務 ,因為框架里面全都依靠事件機制進行通信,例如 bundle 的啟動、停止,框架的啟動、停止,服務的注冊、注銷等等等等都是會發布事件給監聽者,同時也在監聽其它模塊發來的自己關心的事件。 OSGi 框架的事件機制主要核心思想是:
- 用戶(程序員)可以自己按照接口定義自己的事件類型
- 用戶可以監聽自己關心的事件或者所有事件
- 用戶可以將事件同步的或者異步的提交給框架,由框架負責同步的或者異步的分發給監聽者
說明:框架提供的事件服務、事件提供者、事件監聽者之間的關系如下:
事件提供者 Publisher 可以獲取 EventAdmin 服務,通過 sendEvent 同步(postEvent 異步)方式提交事件,EventAdmin 服務負責分發給相關的監聽者 EventHandler,調用它們的 handleEvent
方法。
這里要介紹一個新的概念 Topics,其實在 JMS 里面也有用,也就是說一個事件一般都有一個主題,這樣我們的事件接收者才能按照一定的主題進行過濾處理,例如只處理自己關心的主題的事件,一般情況下主題是用類似于 Java Package 的命名方式命名的。
同步提交(sendEvent)和異步提交(postEvent) 事件的區別是,同步事件提交后,等框架分發事件給所有事件接收者之后才返回給事件提交者,而異步事件則一經提交就返回了,分發在另外的線程進行處理。
下面的程序演示了事件的定義、事件的發布、事件處理,同時還演示了同步和異步處理的效果,以及運行環境的配置。
(約定 osgi.test.helloworld
為 bundle1,osgi.test.helloworld2
為 bundle2)
- 在 bundle1 中的 MANIFEST.MF 的 dependency 頁面中定義引入新的包:
org.osgi.service.event
。 - 在 bundle1 中的
osgi.test.helloworld.event
包中定義新的類MyEvent
,如下(注意其中的 topic 定義的命名方式):
清單 6. 定義新的類 MyEventimport java.util.Dictionary; import org.osgi.service.event.Event; public class MyEvent extends Event { public static final String MY_TOPIC = "osgi/test/helloworld/MyEvent"; public MyEvent(String arg0, Dictionary arg1) { super(MY_TOPIC, arg1); } public MyEvent() { super(MY_TOPIC, null); } public String toString() { return "MyEvent"; } }
- 在 bundle1 的
DefaultHelloServiceHandler
類的getHello
方法中,加入提交事件的部分,這樣 bundle2 在調用這個服務的時候,將觸發一個事件,由于采用了 Post 方式,應該是立刻返回的,所以在postEvent
前后打印了語句進行驗證。
清單 7. getHello 方法import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.osgi.service.event.EventAdmin; @Override public String getHello() { //post a event ServiceReference ref = context.getServiceReference(EventAdmin.class.getName()); if(ref!=null) { eventAdmin = (EventAdmin)context.getService(ref); if(eventAdmin!=null) { System.out.println("post event started"); eventAdmin.postEvent(new MyEvent()); System.out.println("post event returned"); } } return "Hello osgi,service"; }
- 定義監聽者,在 bundle2 中,也引入 osgi 的事件包,然后定義一個新的類:
MyEventHandler
類,用來處理事件,這里故意加入了一個延遲,是為了測試異步事件的調用,實現如下:
清單 8. MyEventHandler 類import org.osgi.service.event.Event; import org.osgi.service.event.EventHandler; public class MyEventHandler implements EventHandler { @Override public void handleEvent(Event event) { System.out.println("handle event started--"+event); try { Thread.currentThread().sleep(5*1000); } catch (InterruptedException e) { } System.out.println("handle event ok--"+event); } }
- 注冊監聽器,有了事件處理器,還需要注冊到監聽器中,這里在 bundle2 的
Activator
類中加入此監聽器,也就是調用context.registerService
方法注冊這個監聽服務,和普通服務的區別是要帶一個監聽事件類型的 topic,這里列出Activator
類的start
方法:
清單 9. start 方法import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.Hashtable; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import osgi.test.helloworld.event.MyEvent; import osgi.test.helloworld.service.IAppService; import osgi.test.helloworld.service.IHello; public void start(BundleContext context) throws Exception { System.out.println("hello world2"); /** * 添加事件處理器 . */ String[] topics = new String[] {MyEvent.MY_TOPIC}; Hashtable<String,String[]> ht = new Hashtable<String,String[]>(); ht.put(EventConstants.EVENT_TOPIC, topics); EventHandler myHandler = new MyEventHandler(); context.registerService( EventHandler.class.getName(), myHandler, ht); System.out.println("event handler registered"); /** * Test hello service from bundle1. */ IHello hello1 = (IHello) context.getService( context.getServiceReference(IHello.class.getName())); System.out.println(hello1.getHello()); }
- 為了使用框架的事件服務,需要修改運行環境,加入兩個系統 bundle,分別是:
org.eclipse.osgi.services
org.eclipse.equinox.event
- 好了一切準備好了,執行:
圖 20. 執行可以看到,
post
事件后,不等事件真的被處理完成,就返回了,事件處理在另外的線程執行,最后才打印處理完成的語句。然后ss
看一下,目前我們已經有五個 bundle 在運行了:
圖 21. ss 查詢 - OK,修改代碼以測試同步調用的情況,我們只需要把提交事件的代碼由
postEvent
修改為sendEvent
即可。其它不變,測試結果如下:
圖 22. 同步調用測試結果
OSGi 的 HTTP 服務為我們提供了展示 OSGi 的另外一個途徑,即我們可以專門提供一個 bundle 用來作為我們應用的 UI,當然這個還比較簡單,只能提供基本的 HTML 服務和基本的 Servlet 服務。如果想提供復雜的 Jsp/Struts/WebWorks 等等,或者想用現有的 Web 中間件服務器例如 Tomcat/Resin/WebSphere Application Server 等,都需要另外的途徑來實現,目前我提供一些基本的使用 HTTP 服務的方式。
要使用 HTTP 服務,必然有三個步驟
- 獲取 HttpService,可以像 上述方式 那樣通過 context 的
getService
方法獲得引用; - 使用 HttpService 的引用注冊資源或者注冊 Servlet:
registerResources
:注冊資源,提供本地路徑、虛擬訪問路徑和相關屬性即可完成注冊,客戶可以通過虛擬訪問路徑 + 資源名稱訪問到資源registerServlet
:注冊 Servlet,提供標準 Servlet 實例、虛擬訪問路徑、相關屬性以及 HttpContext(可以為 null)后即可完成注冊,客戶可以直接通過虛擬訪問路徑獲取該 Servlet 的訪問
- 修改運行環境,加入支持 http 服務的 bundle
那么,接下來我們實際操作一下:
- 首先,在 bundle1 的 src 中建立一個新的 package,名字叫 pages,用來存放一些 HTML 的資源文件,為了提供一個基本的 HTTP 服務,我們需要提供一個 index.html,內容如下:
<html> <h1>hello osgi http service</h1> </html>
- 第二步,注冊資源服務,首先我們要為 bundle1 加入 HTTP 服務的 package 引用,即修改 MANIFEST.MF 文件的 dependencies,加入包:
org.osgi.service.http;version="1.2.0"
,然后在Activator
類的start
方法中加入 HTTP 資源的注冊:
清單 10. 加入 HTTP 資源的注冊代碼httpService = (HttpService)context.getService (context.getServiceReference(HttpService.class.getName())); httpService.registerResources("/", "/pages", null);
- 修改運行環境,在 target platform 的 bundle 列表中加入:org.eclipse.equinox.http 和 javax.servlet 這兩個 bundle 保證了 HttpService 的可用性:
圖 23. 加入 HttpService bundle - 運行,然后打開 IE 訪問本機
http://localhost/index.html
:
圖 24. 運行結果 - 加入 servlet,首先在 bundle1 建立一個包:
osgi.test.hellworld.servlet
,建立一個新的類:MyServlet
,要從HttpServlet
基類繼承,實現其doGet
方法,如下:
清單 11. MyServlet 代碼import java.io.IOException; import java.util.Date; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MyServlet extends HttpServlet { /** * 實現測試 . * @param request the req. * @param response the res. * @throws IOException io exception. */ public void doGet( HttpServletRequest request, HttpServletResponse response ) throws IOException { response.getWriter() .write("hello osgi http servlet.time now is "+new Date()); } }
- 注冊 servlet,在
Activator
類的start
方法中加入注冊 servlet 的代碼,如下:
清單 12. 注冊 servlet 的代碼MyServlet ms = new MyServlet(); httpService.registerServlet("/ms", ms, null, null);
- 運行,打開 IE 訪問
http://localhost/ms
后得到結果:
圖 25. 運行結果
分布式部署的實現方式一般可以通過 Web 服務、RMI 等方式,這里簡單介紹一下基于 RMI 方式的分布式實現。
在 OSGi 環境中,并沒有直接提供分布式部署的支持,我們可以采用 J2SE 提供的 RMI 方式來實現,但是要考慮 OSGi 的因素,即如果您希望您的服務既可以本地使用,也可以被遠程訪問,那么您應該這樣定義接口和類:
說明:
Remote
接口是 J2SE 定義的遠程對象必須實現的接口;IAppService
接口是 OSGi 服務接口,繼承了Remote
接口,即定義方式為:public interface IAppService extends Remote
AppServiceImpl
實現了IAppService
接口,此外注意里面的方法都拋出RemoteException
異常;
實際操作如下:
- 在 bundle1 的
service
包中加入IAppService
接口的定義,繼承自Remote
接口,定義個方法:
清單 13. IAppService 接口定義public interface IAppService extends Remote { /** * 得到一個遠程服務的名稱 . * @return . * @throws RemoteException . */ String getAppName() throws RemoteException; }
- 把這個接口注冊為 OSGi 標準服務以及一個 RMI 服務對象如下:
注冊為標準服務:
清單 14. 注冊為標準服務IAppService appService = new DefaultAppServiceImpl(context); context.registerService( IAppService.class.getName(), appService, null);
注冊為遠程對象:
清單 15. 注冊為遠程對象/** * 啟動 rmi server . * @param service the service. * @throws RemoteException re. */ private void startRmiServer(IAppService service) throws RemoteException { if(registry == null) { registry = LocateRegistry.createRegistry(1099); } // 注冊 appService 遠程服務 . IAppService theService = (IAppService)UnicastRemoteObject.exportObject(service,0); registry.rebind("appService", theService); }
- 在 bundle2 中通過 OSGi 方式使用這個服務:
清單 16. 使用服務IAppService appService = (IAppService)context.getService( context.getServiceReference(IAppService.class.getName())); System.out.println(appService.getAppName());
- 通過 RMI 方式使用這個服務:
清單 17. 通過 RMI 方式使用服務String host = "127.0.0.1"; int port = 1099; try { Registry registry = LocateRegistry.getRegistry(host,port); appServiceStub = (IAppService) registry.lookup("appService"); } catch (Exception e) { e.printStackTrace(); } System.out.println("rmi:"+appServiceStub.getAppName());
- 最終的運行結果如下:
圖 27. 運行結果