利用 R-OSGi 实现分布式 OSGi 应用

OSGi(Open Service Gateway Initiative)是业界中最知名的 Java 模块化技术规范,其核心设计思想是面向服务的组件模型(Service-Oriented Component Model)。OSGi 发展至今已经得到了众多企业,厂商,开源组织的支持,尤其当主流的 Java 应用服务器都采用 OSGi 时,OSGi 俨然已经成为 Java 模块化的标准。

OSGi v4.2 规范中定义了远程调用的统一标准,使模块化思想进一步得到深化。R-OSGi 作为其中的一种实现,提供了面向 Service 的远程调用组件,让我们能非常简单地实现处于不同地域的两个 OSGi service 的互相调用。 本文通过介绍传统 OSGi 应用程序及 R-OSGi 的实现方式入手,阐述了 R-OSGi 对于 OSGi 规范的实现方式。然后通过一个简单的功能实现由浅入深地讲述传统 OSGi 和 R-OSGi 上的两种不同实现,让您对实际操作加深印象。最后,探讨了 R-OSGi 的目前使用情况以及整个分布式 OSGi 应用的发展前景。望能与广大读者一起做进一步的讨论和研究。

OSGi 应用程序架构

传统基于 OSGi 的应用程序架构如下图所示:


图 1. 基于 OSGi 的应用程序架构
利用 R-OSGi 实现分布式 OSGi 应用

这套架构体系在许多企业的大型项目中广泛使用着的同时,当 OSGi 的 Bundle(OSGi 中的模块,具有独立运行处理业务逻辑的单元组件)随着开发增多时,渐渐发现了以下问题:

  • 程序整体运行缓慢
  • 运行时不稳定
  • 较难定位运行时错误
  • 庞大程序主体发布困难。

我们在思考着有没有一种框架能够将一个独立运行的 OSGi 应用程序分割成若干个,每个分布式应用提供自己独立领域的 Service,如果能找到适合的分割方法,理论上可以解决上面所述的问题。

而 R-OSGi 就是已经实现这样功能的一个框架,接下来让我们来了解下 R-OSGi。

使用 R-OSGi 实现分布式 OSGi 应用程序

R-OSGi 是一套适用于任意满足 OSGi 架构的分布式通讯组件。它以 jar 的形式发布,部署容易,使用也较为便捷。

概括下用户只需要完成如下几步。

在 Server 端:

  • OSGi 容器内启动 R-OSGi 的 Bundle
  • Service 的 Bundle 里 MENIFEST 文件中 import 对 R-OSGi 的引用
  • 将需要被 Client 调用的接口暴露给 R-OSGi 模块即可。

在 Client 端:

  • OSGi 容器内启动 R-OSGi 的 Bundle
  • Client 的 Bundle 里 MENIFEST 文件中 import 对 R-OSGi 的引用
  • 取得 R-OSGi 暴露的 Service 调用接口即可

R-OSGi 运行流程

下图简要说明了 R-OSGi 实现的流程及原理:


图 2. R-OSGi 实现流程
利用 R-OSGi 实现分布式 OSGi 应用
  1. 远程 Service 通过 R-OSGi 框架注册到 OGSi 容器。
  2. R-OSGi 在 OS 上打开一个端口(默认 9278)来创建 Socket 监听器。
  3. Client 端 Bundle 启动时调用指定 Server 的默认端口,请求 Socket 通信。
  4. 在本 OSGi 容器内生成一个代理 Bundle,用于本地 Client 调用。
  5. 在 Client 端注册签名一样的 Service。
  6. 客户端调用 Service,实际上调用的是通过代理 Bundle 调用远程的 Service,等待通讯返回。

使用 R-OSGi 来创建第一个分布式 OSGi 应用

接下来我们来创建一个简单的 Client-Service 调用实例 OSGi 应用程序,并且在稍后把 Service 移到别处,通过 R-OSGi 进行远程调用,而 Client 浑然不知调用 Service 的地方,却能正常完成整个 Service 调用过程。

如下图实例:


图 3. 将一个简单 OSGi 应用改为基于 R-OSGi 的分布式应用
利用 R-OSGi 实现分布式 OSGi 应用

传统 OSGi 模式实例

在本示例中一共包含两个 Bundle。Customer Bundle 和 Service Bundle,Customer Bundle 启动后用户可以在 HTTP 容器内访问一张只带有求和功能的页面。而 Service Bundle 则提供当用户点击求和按钮后的具体逻辑服务。

在 Eclipse 中建立一下两个 Bundle:


图 4. Eclipse 中建立 2 个 Bundle
利用 R-OSGi 实现分布式 OSGi 应用

Customer Bundle

Customer Bundle 提供一张带有求和功能的页面。并接受参数,调用 Service 接口。以下示例了关键代码。


清单 1. operation.html
				
 
 <html> 
 <head> 
 <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> 
 <title>Demo</title> 
 </head> 
 <body> 
 <h3>Click to consume the Service</h3> 
 <form action="/ServiceServlet" method="get"> 
  <p><input type="text" name="number1" /></p> + 
  <p><input type="text" name="number2" /></p> 
  <input type="submit" value="equals"/> 
 </form> 
 </body> 
 </html> 

说明:operation.html 提供了带有求和功能的 html 代码


清单 2. ServiceServlet.java
				
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
 throws ServletException, IOException { 
 String n1 = req.getParameter("number1"); 
 String n2 = req.getParameter("number2"); 

 int result = new MathImpl().sum(Integer.parseInt(n1), Integer.parseInt(n2)); 

 resp.setContentType("text/html"); 
 resp.getWriter().println("<h3>Result:</h3>" + result); 
 } 

说明:ServiceServlet.java 是 html 中 Form 的请求响应的 Servlet,用来调用 Service 接口


清单 3. plugin.xml
				
 <plugin> 
 <extension 
 id="helloServlet" 
 point="org.eclipse.equinox.http.registry.servlets"> 
 <servlet alias="/ServiceServlet" 
 class="com.ibm.sample.http.ServiceServlet"> 
 </servlet> 
 </extension> 
 <extension id="helloResource" 
 point="org.eclipse.equinox.http.registry.resources"> 
 <resource alias="/sample.html" 
 base-name="/web/operation.html" /> 
 </extension> 
 </plugin> 

说明:配置 plugin.xml 将资源注册到 OSGi 自带的 web application 容器里。


清单 4. MANIFEST.MF
				
 Manifest-Version: 1.0 
 Bundle-ManifestVersion: 2 
 Bundle-Name: Customer 
 Bundle-SymbolicName: Customer;singleton:=true 
 Bundle-Version: 1.0.0.qualifier 
 Bundle-Activator: com.ibm.sample.customer.Activator 
 Bundle-RequiredExecutionEnvironment: JavaSE-1.6 
 Import-Package: com.ibm.sample.service.calculate, 
 javax.servlet;version="2.5.0", 
 javax.servlet.http;version="2.5.0", 
 org.osgi.framework;version="1.3.0", 
 org.osgi.service.http;version="1.2.1"
 Bundle-ActivationPolicy: lazy 
 Require-Bundle: org.eclipse.equinox.http.registry;bundle-version="1.1.0", 
 org.mortbay.jetty.server;bundle-version="6.1.23", 
 org.eclipse.equinox.http.jetty;bundle-version="2.0.0", 
 org.eclipse.equinox.http.servlet;bundle-version="1.1.0"

说明: Customer Bundle 的 MANIFEST.MF 引用了 Service 的 Package 和 HTTP 用到的 Bundle

Service Bundle

Service Bundle 提供了当用户点击求和按钮后的具体逻辑服务。本示例里就是简单的加法求和。


清单 5. MathImpl.java
				
 package com.ibm.sample.service.calculate; 
 public class MathImpl { 
 public int sum(int a, int b) { 
 return a + b; 
 } 

 public int quadrature(int a, int b) { 
 return a*b; 
 } 
 } 


清单 6. MANIFEST.MF
				
 Manifest-Version: 1.0 
 Bundle-ManifestVersion: 2 
 Bundle-Name: Service 
 Bundle-SymbolicName: Service 
 Bundle-Version: 1.0.0.qualifier 
 Bundle-Activator: com.ibm.sample.service.Activator 
 Bundle-RequiredExecutionEnvironment: JavaSE-1.6 
 Import-Package: org.osgi.framework;version="1.3.0"
 Bundle-ActivationPolicy: lazy 
 Export-Package: com.ibm.sample.service.calculate 

说明:Service Bundle export 了 com.ibm.sample.service.calculate 接口,供 Customer Bundle 使用。

启动实例

Eclipse 集成了基于 Equinox 的 OSGi 框架,使用如下方式可以在 Eclipse 中启动 OSGi 容器。


图 5. 在 Eclipse 中启动 OSGi 实例
利用 R-OSGi 实现分布式 OSGi 应用

在 OSGi Framework 中新建一个名为 Local 的 run configuration,包含 Customer 和 Service 两个 Bundle,点击 Run 按钮。

看到成功启动信息后,在浏览器中打开:http://localhost/sample.html


图 6. 用户界面
利用 R-OSGi 实现分布式 OSGi 应用

图 7. 成功调用 Service
利用 R-OSGi 实现分布式 OSGi 应用

当出现如上正确求和结果后,说明成功的调用了本地的 Service。

接下来我们把这个模式改成利用 R-OSGi 的远程调用模式。

应用 R-OSGi 后的分布式应用实例

为了转变为分布式的 OSGi 应用,我们不需要修改原有的 Customer 和 Service Bundle,只要在其基础上另添加两个 Bundle,LocalService Bundle 和 Remote Service Bundle。

  • LocalService Bundle 包含了 R-OSGi 模块,负责接收本地 Customer 的请求并调用远程模块。
  • RemoteService Bundle 也包含了 R-OSGi 模块,负责接受远程 Customer 发来的请求并调用本地 Service 模块,并将其结果返回给 Customer 模块。

简图如下所示:


图 8. 转换示例
利用 R-OSGi 实现分布式 OSGi 应用

实现远程调用的步骤大致如下:

  1. 将 Service Bundle 放到远程 OSGi 运行环境中,导入 R-OSGi 的运行 Bundle。
  2. 在注册远程 Service 的时候,加入以下声明:

    properties.put(RemoteOSGiService.R_OSGi_REGISTRATION, Boolean.TRUE);

  3. 将 Customer Bundle 放到本地 OSGi 运行环境中,导入 R-OSGi 的运行 Bundle。
  4. 输入 Service 端的 URI,通过以下接口取得 Service,调用远程方法:

    remoteSvc.getRemoteServiceReferences(new URI("r-osgi://localhost:9278"), "com.ibm.sample.service.calculate.interfaces.IMathImpl", null);

接下来就具体说明这些步骤的实现方法。

远程 OSGi

在本小节,将会创建一个名为 RemoteService 的 Bundle,连同之前创建的 Service Bundle 一起组成远程 OSGi 用于提供 Service,远程 OSGi 将会启动如下两个 Bundle。


图 9. 远程 OSGi 启动 Bundle
利用 R-OSGi 实现分布式 OSGi 应用

Service Bundle 已经在前面说明过,以下清单为 RemoteBundle 中的文件。


清单 7. Activator.java
				
 public class Activator implements BundleActivator { 
 public void start(BundleContext context) throws Exception { 
 System.out.println("Start to register Remote Service!!"); 

 Dictionary properties = new Hashtable(); 
 properties.put(RemoteOSGiService.R_OSGi_REGISTRATION, Boolean.TRUE);  context.registerService(IMathImpl.class.getName(), new MathImpl(), properties); 
 System.out.println("Register Service successful!!"); 
 } 

 public void stop(BundleContext context) throws Exception { 
 System.out.println("Stop Server Successful!!"); 
 } 
 } 

说明:在 Bundle 启动时,将代理 Service 注册给 R-OSGi,注意调用接口时,需填入特别的参数用来辨识该 Service 专供远程 R-OSGi 调用。


清单 8. IMathImpl.java
				
 public interface IMathImpl { 
 public int sum(int a, int b); 
 } 

说明:R-OSGi 需要接口来提供给远程 Customer,所以这里定义一个 sum 方法的接口。


清单 9. MathImpl.java
				
 package com.ibm.sample.service.calculate.proxy; 

 import com.ibm.sample.service.calculate.interfaces.IMathImpl; 
 public class MathImpl implements IMathImpl{ 

 public int sum(int a, int b) { 
 System.out.println("Run by remote service"); 
 return new com.ibm.sample.service.calculate.MathImpl().sum(a, b) + 1; 
 } 
 } 

说明:在代理 Bundle 中调用真正 Service 提供的 sum 方法,为了避免调用混淆,这里填入全部的包名。此外在求和时特地讲结果 +1,用以在 UI 上分辨这是远程调用的而非本地。


清单 10. MANIFEST.MF
				
 Manifest-Version: 1.0 
 Bundle-ManifestVersion: 2 
 Bundle-Name: RemoteService 
 Bundle-SymbolicName: RemoteService 
 Bundle-Version: 1.0.0.qualifier 
 Bundle-Activator: com.ibm.sample.service.Activator 
 Bundle-RequiredExecutionEnvironment: JavaSE-1.6 
 Import-Package: ch.ethz.iks.r_osgi, 
 com.ibm.sample.service.calculate, 
 org.osgi.framework;version="1.3.0"
 Bundle-ActivationPolicy: lazy 

说明:Import 真正 Service 所提供的方法。


图 10. 启动远程 OSGi 应用
利用 R-OSGi 实现分布式 OSGi 应用

启动后,会在本地打开默认的 9278 端口监听远程请求。


图 11. 默认的 9278 端口已经打开
利用 R-OSGi 实现分布式 OSGi 应用

本地 OSGi

在本小节,将会创建一个名为 LocalService 的 Bundle,连同之前创建的 Customer Bundle 一起,本地 OSGi 将会启动如下两个 Bundle。


图 12. 本地 OSGi 启动 Bundle
利用 R-OSGi 实现分布式 OSGi 应用

Customer Bundle 已经在前面说明过,以下清单为 LocalBundle 中的文件。


清单 11. MathImpl.java
				
 public class MathImpl { 
 static BundleContext context = Activator.context; 

 Object obj; 

 public MathImpl() { 
 final ServiceReference ref = context 
 .getServiceReference(RemoteOSGiService.class.getName()); 
 RemoteOSGiService remoteSvc = (RemoteOSGiService) context 
 .getService(ref); 
 RemoteServiceReference[] rrefs = null; 
 try { 
 remoteSvc.connect(new URI("r-osgi://localhost:9278")); 
 } catch (RemoteOSGiException e) { 
 e.printStackTrace(); 
 } catch (IOException e) { 
 e.printStackTrace(); 
 } 

 rrefs = remoteSvc.getRemoteServiceReferences(new URI( 
"r-osgi://localhost:9278"), 
"com.ibm.sample.service.calculate.interfaces.IMathImpl", null); 

 if (rrefs != null && rrefs.length > 0) { 
 obj = remoteSvc.getRemoteService(rrefs[0]); 
 } 
 } 

 public int sum(int a, int b) { 
 try { 
 Method m = obj.getClass().getMethod("sum", int.class, int.class); 
 return Integer.parseInt("" + m.invoke(obj, a, b)); 
 } catch (Exception ex) { 
 System.out.println(ex); 
 } 

 return 0; 
 } 
 } 

说明:

  1. LocalService 中需将 Class 名与调用方法名与原 Service 提供的相一致,这样可以保证不用修改 Customer 中的代码。
  2. MathImpl 构造方法里从 BundleContext 里取得 R-OSGi 的 Service,并通过其调用远程的 Service,在这里远程Service 的 IP 是 localhost,Port 是 9278。
  3. sum 方法中通过反射调用从 RemoteService 取得的 Object(即 Service)中的 sum 方法,来完成远程调用过程。

清单 12. MANIFEST.MF
				
 Manifest-Version: 1.0 
 Bundle-ManifestVersion: 2 
 Bundle-Name: LocalService 
 Bundle-SymbolicName: LocalService 
 Bundle-Version: 1.0.0.qualifier 
 Bundle-Activator: com.ibm.sample.service.Activator 
 Bundle-RequiredExecutionEnvironment: JavaSE-1.6 
 Import-Package: ch.ethz.iks.r_osgi, 
 org.osgi.framework;version="1.3.0"
 Bundle-ActivationPolicy: lazy 
 Export-Package: com.ibm.sample.service.calculate 

说明:Export 出供 Customer 调用的 Service。


图 13. 启动本地 OSGi 应用
利用 R-OSGi 实现分布式 OSGi 应用

在 UI 上再次使用求和功能


图 14. 用户界面没有变化
利用 R-OSGi 实现分布式 OSGi 应用

图 15. 结果为实际结果 +1,确实为调用远程 Service
利用 R-OSGi 实现分布式 OSGi 应用

至此,整个利用 R-OSGi 实现的分布式 OSGi 应用程序就完成了。在这个例子中并没有改变原来两个 Customer 和 Service Bundle 的代码而是直接将 R-OSGi 模块加入进去,对于 Customer 来说他并不知道调用的是本地 OSGi 还是远程的模块,它只管向它所依赖的 Bundle 发出请求。而对于 Service 来说也不在乎到底谁来消费他,而只管向外提供接口返回结果。在没有代码 修改的情况下将两个相互依赖的 Bundle 解开耦合关系并分离到不同的 Server 上,这种模式在实际中也有一定的借鉴意义。

结束语

本文先是介绍了 OSGi 的基本概念,OSGi 在许多企业的大型项目中广泛使用着的同时也暴露了一些系统过于庞大带来的问题。接下来介绍 R-OSGi 这个工具对于分布式 OSGi 应用程序的支持。最后通过一个实例讲述了如何利用 R-OSGi 在没有原先代码修改的情况下将两个相互依赖的 Bundle 解开耦合关系并分离到不同的 Server 上,手把手地教读者如何运用 R-OSGi 来搭建分布式 OSGi 的运行环境及实例。

为了实现 OSGi 分布式应用,目前有两大阵营:Eclipse ECF Vs Apache CXF,ECF 的代表为 R-OSGi,而 D-OSGi 则是 CXF 的代表。R-OSGi 相对来说历史更久,而 D-OSGi 作为将来的 R 4.2 的 RFC119 的实现,也值得期待。其核心原理均为利用一个透明的远程 RPC,利用代理、反射、网络通信以及服务发现加以实现。本文不涉及比较两大阵营的优缺点,他们为我们实现分布式 OSGi 提供了可能,也成为了未来分布式 OSGi 的发展方向奠定了基础。

原文出处: IBMdeveloperWorks