開發(fā)一個真實的 OSGi 應用程序

我們不能只停留在 hello world 的層面,雖然那曾經對我們很重要 ,但是現實需要我們能夠使用 OSGi 寫出激動人心的應用程序,它能夠被客戶接受,被架構師認可,被程序員肯定。好的,那我們開始吧。下面將會著重介紹一些現實的應用程序可能需要的一些 OSGi 應用場景。

發(fā)布和使用服務

由于 OSGi 框架能夠方便的隱藏實現類,所以對外提供接口是很自然的事情,OSGi 框架提供了服務的注冊和查詢功能。好的,那么我們實際操作一下,就在 Hello world 工程的基礎上進行。

我們需要進行下列的步驟:

  1. 定義一個服務接口,并且 export 出去供其它 bundle 使用;
  2. 定義一個缺省的服務實現,并且隱藏它的實現;
  3. Bundle 啟動后,需要將服務注冊到 Equinox 框架;
  4. 從框架查詢這個服務,并且測試可用性。

好的,為了達到上述要求,我們實際操作如下:

  1. 定義一個新的包 osgi.test.helloworld.service ,用來存放接口。單獨一個 package 的好處是,您可以僅僅 export 這個 package 給其它 bundle 而隱藏所有的實現類
  2. 在上述的包中新建接口 IHello,提供一個簡單的字符串服務,代碼如下: 

    清單 2. IHello
    復制代碼
     1                               2 package osgi.test.helloworld.service;   3   4 public interface IHello {   5     /**   6      * 得到 hello 信息的接口 .   7      * @return the hello string.   8      */   9     String getHello();  10 }
    復制代碼

     

  3. 再新建一個新的包 osgi.test.helloworld.impl,用來存放實現類。
  4. 在上述包中新建 DefaultHelloServiceImpl 類,實現上述接口: 

    清單 3. IHello 接口實現
    復制代碼
                                 public class DefaultHelloServiceImpl implements IHello {       @Override      public String getHello() {          return "Hello osgi,service";      }    }
    復制代碼

     

  5. 注冊服務,OSGi 框架提供了兩種注冊方式,都是通過 BundleContext 類實現的:
    1. registerService(String,Object,Dictionary) 注冊服務對象 object 到接口名 String 下,可以攜帶一個屬性字典Dictionary
    2. 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);           }
    復制代碼

     

  6. 為了讓我們的服務能夠被其它 bundle 使用,必須在 MANIFEST.MF 中對其進行導出聲明,雙擊 MANIFEST.MF,找到runtime > exported packages > 點擊 add,如圖,選擇 service 包即可: 

    圖 14. 選擇導出的服務包
    圖 14. 選擇導出的服務包 

  7. 另外新建一個類似于 hello world 的 bundle 叫:osgi.test.helloworld2,用于測試 osgi.test.helloworld bundle 提供的服務的可用性;
  8. 添加 import package:在第二個 bundle 的 MANIFEST.MF 文件中,找到 dependencies > Imported packages > Add …,選擇我們剛才 export 出去的 osgi.test.helloworld.service 包: 

    圖 15. 選擇剛才 export 出去的 osgi.test.helloworld.service 包
    圖 15. 選擇剛才 export 出去的 osgi.test.helloworld.service 包 

  9. 查詢服務:同樣,OSGi 框架提供了兩種查詢服務的引用 ServiceReference 的方法:
    1. getServiceReference(String):根據接口的名字得到服務的引用;
    2. getServiceReferences(String,String):根據接口名和另外一個過濾器名字對應的過濾器得到服務的引用;
  10. 這里我們使用第一種查詢的方法,在 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());  }
    復制代碼

     

  11. 修改運行環(huán)境,因為我們增加了一個 bundle,所以說也需要在運行配置中加入對新的 bundle 的配置信息,如下圖所示: 

    圖 16. 加入對新的 bundle 的配置信息
    圖 16. 加入對新的 bundle 的配置信息 

  12. 執(zhí)行,得到下列結果: 

    圖 17. 執(zhí)行結果
    圖 17. 執(zhí)行結果 

恭喜您,成功了!

 

回頁首

使用事件管理服務 EventAdmin

前面講過,OSGi 規(guī)范定義了很多可用的 bundle,您盡管使用它們完成您的工作,而不必另外再發(fā)明輪子,OSGi 框架定義的事件管理服務,類似于 JMS,但是使用上比 JMS 簡單。

OSGi 整個框架都離不開這個服務 ,因為框架里面全都依靠事件機制進行通信,例如 bundle 的啟動、停止,框架的啟動、停止,服務的注冊、注銷等等等等都是會發(fā)布事件給監(jiān)聽者,同時也在監(jiān)聽其它模塊發(fā)來的自己關心的事件。 OSGi 框架的事件機制主要核心思想是:

  1. 用戶(程序員)可以自己按照接口定義自己的事件類型
  2. 用戶可以監(jiān)聽自己關心的事件或者所有事件
  3. 用戶可以將事件同步的或者異步的提交給框架,由框架負責同步的或者異步的分發(fā)給監(jiān)聽者

說明:框架提供的事件服務、事件提供者、事件監(jiān)聽者之間的關系如下:


圖 18. 事件服務、事件提供者、事件監(jiān)聽者之間的關系
圖 18. 事件服務、事件提供者、事件監(jiān)聽者之間的關系 

事件提供者 Publisher 可以獲取 EventAdmin 服務,通過 sendEvent 同步(postEvent 異步)方式提交事件,EventAdmin 服務負責分發(fā)給相關的監(jiān)聽者 EventHandler,調用它們的 handleEvent 方法。

這里要介紹一個新的概念 Topics,其實在 JMS 里面也有用,也就是說一個事件一般都有一個主題,這樣我們的事件接收者才能按照一定的主題進行過濾處理,例如只處理自己關心的主題的事件,一般情況下主題是用類似于 Java Package 的命名方式命名的。

同步提交(sendEvent)和異步提交(postEvent) 事件的區(qū)別是,同步事件提交后,等框架分發(fā)事件給所有事件接收者之后才返回給事件提交者,而異步事件則一經提交就返回了,分發(fā)在另外的線程進行處理。

下面的程序演示了事件的定義、事件的發(fā)布、事件處理,同時還演示了同步和異步處理的效果,以及運行環(huán)境的配置。

(約定 osgi.test.helloworld 為 bundle1,osgi.test.helloworld2 為 bundle2)


圖 19. 同步和異步處理演示
圖 19. 同步和異步處理演示 

  1. 在 bundle1 中的 MANIFEST.MF 的 dependency 頁面中定義引入新的包:org.osgi.service.event
  2. 在 bundle1 中的 osgi.test.helloworld.event 包中定義新的類 MyEvent,如下(注意其中的 topic 定義的命名方式): 

    清單 6. 定義新的類 MyEvent
    復制代碼
    import 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";      }   }
    復制代碼

     

  3. 在 bundle1 的 DefaultHelloServiceHandler 類的 getHello 方法中,加入提交事件的部分,這樣 bundle2 在調用這個服務的時候,將觸發(fā)一個事件,由于采用了 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";  }
    復制代碼

     

  4. 定義監(jiān)聽者,在 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);       }   }
    復制代碼

     

  5. 注冊監(jiān)聽器,有了事件處理器,還需要注冊到監(jiān)聽器中,這里在 bundle2 的 Activator 類中加入此監(jiān)聽器,也就是調用context.registerService 方法注冊這個監(jiān)聽服務,和普通服務的區(qū)別是要帶一個監(jiān)聽事件類型的 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());  }
    復制代碼

     

  6. 為了使用框架的事件服務,需要修改運行環(huán)境,加入兩個系統(tǒng) bundle,分別是:
    1. org.eclipse.osgi.services
    2. org.eclipse.equinox.event
  7. 好了一切準備好了,執(zhí)行: 

    圖 20. 執(zhí)行
    圖 20. 執(zhí)行 

    可以看到,post 事件后,不等事件真的被處理完成,就返回了,事件處理在另外的線程執(zhí)行,最后才打印處理完成的語句。然后 ss 看一下,目前我們已經有五個 bundle 在運行了:



    圖 21. ss 查詢
    圖 21. ss 查詢 

  8. OK,修改代碼以測試同步調用的情況,我們只需要把提交事件的代碼由 postEvent 修改為 sendEvent 即可。其它不變,測試結果如下: 

    圖 22. 同步調用測試結果
    圖 22. 同步調用測試結果 

 

回頁首

使用 Http 服務 HttpService

OSGi 的 HTTP 服務為我們提供了展示 OSGi 的另外一個途徑,即我們可以專門提供一個 bundle 用來作為我們應用的 UI,當然這個還比較簡單,只能提供基本的 HTML 服務和基本的 Servlet 服務。如果想提供復雜的 Jsp/Struts/WebWorks 等等,或者想用現有的 Web 中間件服務器例如 Tomcat/Resin/WebSphere Application Server 等,都需要另外的途徑來實現,目前我提供一些基本的使用 HTTP 服務的方式。

要使用 HTTP 服務,必然有三個步驟

  1. 獲取 HttpService,可以像 上述方式 那樣通過 context 的 getService 方法獲得引用;
  2. 使用 HttpService 的引用注冊資源或者注冊 Servlet:
    1. registerResources:注冊資源,提供本地路徑、虛擬訪問路徑和相關屬性即可完成注冊,客戶可以通過虛擬訪問路徑 + 資源名稱訪問到資源
    2. registerServlet:注冊 Servlet,提供標準 Servlet 實例、虛擬訪問路徑、相關屬性以及 HttpContext(可以為 null)后即可完成注冊,客戶可以直接通過虛擬訪問路徑獲取該 Servlet 的訪問
  3. 修改運行環(huán)境,加入支持 http 服務的 bundle

那么,接下來我們實際操作一下:

  1. 首先,在 bundle1 的 src 中建立一個新的 package,名字叫 pages,用來存放一些 HTML 的資源文件,為了提供一個基本的 HTTP 服務,我們需要提供一個 index.html,內容如下:
    <html>     <h1>hello osgi http service</h1> </html>

     

  2. 第二步,注冊資源服務,首先我們要為 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);

     

  3. 修改運行環(huán)境,在 target platform 的 bundle 列表中加入:org.eclipse.equinox.http 和 javax.servlet 這兩個 bundle 保證了 HttpService 的可用性: 

    圖 23. 加入 HttpService bundle
    圖 23. 加入 HttpService bundle 

  4. 運行,然后打開 IE 訪問本機 http://localhost/index.html: 

    圖 24. 運行結果
    圖 25. 運行結果 

  5. 加入 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());      }   }
    復制代碼

     

  6. 注冊 servlet,在 Activator 類的 start 方法中加入注冊 servlet 的代碼,如下: 

    清單 12. 注冊 servlet 的代碼
    MyServlet ms = new MyServlet();  httpService.registerServlet("/ms", ms, null, null);

     

  7. 運行,打開 IE 訪問 http://localhost/ms 后得到結果: 

    圖 25. 運行結果
    圖 26. 運行結果 

 

分布式部署的實現

分布式部署的實現方式一般可以通過 Web 服務、RMI 等方式,這里簡單介紹一下基于 RMI 方式的分布式實現。

在 OSGi 環(huán)境中,并沒有直接提供分布式部署的支持,我們可以采用 J2SE 提供的 RMI 方式來實現,但是要考慮 OSGi 的因素,即如果您希望您的服務既可以本地使用,也可以被遠程訪問,那么您應該這樣定義接口和類:


圖 26. 以被遠程訪問需要定義的接口和類
圖 26. 以被遠程訪問需要定義的接口和類 

說明:

  1. Remote 接口是 J2SE 定義的遠程對象必須實現的接口;
  2. IAppService 接口是 OSGi 服務接口,繼承了 Remote 接口,即定義方式為:
    public interface IAppService extends Remote

     

  3. AppServiceImpl 實現了 IAppService 接口,此外注意里面的方法都拋出 RemoteException 異常;

實際操作如下:

  1. 在 bundle1 的 service 包中加入 IAppService 接口的定義,繼承自 Remote 接口,定義個方法: 

    清單 13. IAppService 接口定義
    復制代碼
                                 public interface IAppService extends Remote {      /**       * 得到一個遠程服務的名稱 .       * @return .       * @throws RemoteException .       */      String getAppName() throws RemoteException;   }
    復制代碼

     

  2. 把這個接口注冊為 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);  }
    復制代碼

     

  3. 在 bundle2 中通過 OSGi 方式使用這個服務: 

    清單 16. 使用服務
    IAppService appService =      (IAppService)context.getService(          context.getServiceReference(IAppService.class.getName()));  System.out.println(appService.getAppName());

     

  4. 通過 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());
    復制代碼

     

  5. 最終的運行結果如下: 

    圖 27. 運行結果
    圖 27. 運行結果