開發一個真實的 OSGi 應用程序


開發一個真實的 OSGi 應用程序

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

發布和使用服務

由於 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. 修改運行環境,因為我們增加了一個 bundle,所以說也需要在運行配置中加入對新的 bundle 的配置信息,如下圖所示: 

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

  12. 執行,得到下列結果: 

    圖 17. 執行結果
    圖 17. 執行結果 

恭喜您,成功了!

 

使用事件管理服務 EventAdmin

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

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

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

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


圖 18. 事件服務、事件提供者、事件監聽者之間的關系
圖 18. 事件服務、事件提供者、事件監聽者之間的關系 

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

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

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

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

(約定 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 在調用這個服務的時候,將觸發一個事件,由於采用了 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. 定義監聽者,在 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. 注冊監聽器,有了事件處理器,還需要注冊到監聽器中,這里在 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()); 
    }

     

  6. 為了使用框架的事件服務,需要修改運行環境,加入兩個系統 bundle,分別是:
    1. org.eclipse.osgi.services
    2. org.eclipse.equinox.event
  7. 好了一切准備好了,執行: 

    圖 20. 執行
    圖 20. 執行 

    可以看到,post 事件后,不等事件真的被處理完成,就返回了,事件處理在另外的線程執行,最后才打印處理完成的語句。然后 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. 修改運行環境,加入支持 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. 修改運行環境,在 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 環境中,並沒有直接提供分布式部署的支持,我們可以采用 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. 運行結果 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM