本教程着重介绍 OSGi 框架知识、环境搭建、服务使用、应用设计、部署。
开始之前
OSGi 是目前动态模块系统的事实上的工业标准,虽然一开始只是作为嵌入式设备和家庭网关的框架来使用,但是实际上它适用于任何需要模块化、面向服务、面向组件的应用程序。而 Equinox 则是的 Eclipse 所使用的 OSGi 框架,是 Eclipse 强大的插件体系的基础,Eclipse 的稳定可靠性也为该框架带来了声誉。
本教程就将演示如何在 Eclipse 环境下利用 Equinox 框架进行 OSGi 应用开发。首先解释了实现上述应用程序所必需了解的基本概念和基础知识,并结合示例代码演示 OSGi 开发的一些重要技术,最后探讨了基于 OSGi 应用程序一般所采用的架构,以及如何将 Equinox OSGi 应用程序脱离 Eclipse 而部署为一个标准的 Java 应用程序。
在本教程中,您将学习:
本教程假设读者熟悉基本 Java 语言以及 Eclipse 开发环境的使用。
本教程假设您有一个可以工作的 Eclipse 3.x 环境。如果还没有,请在 Eclipse 网站 上找到相关下载的链接,以帮助您在自己的系统上操作示例步骤以及运行示例代码。
回页首
OSGi 是目前动态模块系统的事实上的工业标准,虽然一开始只是作为嵌入式设备和家庭网关的框架来使用,但是实际上它适用于任何需要模块化、面向服务、面向组件的应用程序。
目前 OSGi 规范已经发展到第四版(R4), 由 OSGi 联合组织(OSGi Alliance)负责进行维护管理,相关的规范资料也可以从该网站获得。(参考资料)
开发基于 OSGi 的应用程序离不开实现了 OSGi 标准的框架,就好比是基于 J2EE 的开发离不开应用服务器一样。目前比较流行的基于 OSGi R4 标准实现的 OSGi 框架有三个:
Equinox:这是大名鼎鼎的 Eclipse 所使用的 OSGi 框架,Eclipse 强大的插件体系就是构建在 OSGi bundles 的基础之上,Eclipse 的稳定可靠性为该框架带来了声誉,而且由于有 IBM 公司的强力支持,其后续的开发和文档资料也有了一定的保障。一般情况下,我们推荐您使用该框架进行 OSGi 开发。本教程的后续部分也将演示如何使用 Equinox 框架来进行 OSGi 应用程序的开发。
Makewave Knopflerfish:这是另外一个比较知名的 OSGi 框架,目前的版本已经支持 R4 规范,其特点在于为应用程序的开发提供了大量的 bundle 。
Apache Flex:由 Apache 基金组织开发的面向社区的 OSGi 框架实现,提供了标准的服务和一些有趣的和 OSGi 相关的服务实现。
回页首
一般情况下,学习一门新的技术,程序员都习惯于首先开发一个 hello world 应用程序,这似乎也是一种“工业标准”。好的,让我们开始吧,开发一个简单的 OSGi 应用程序并不难,步骤如下:
Activator:这是 bundle 启动时首先调用的程序入口,相当于 Java 模块中的 main 函数。不同的是,main 需要通过命令行调用,而 OSGi 的 Activator 是被动的接受 OSGi 框架的调用,收到消息后才开始启动。
最佳实践:不要在 Activator 中写太多的启动代码,否则会影响 bundle 启动速度,相关的服务启动可以放到服务的监听器中。
package osgi.test.helloworld;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class Activator implements BundleActivator {
/*
* (non-Javadoc)
* @see org.osgi.framework.BundleActivator
* #start(org.osgi.framework.BundleContext)
*/
public void start(BundleContext context) throws Exception { System.out.println("hello world"); }
/*
* (non-Javadoc)
* @see org.osgi.framework.BundleActivator
* #stop(org.osgi.framework.BundleContext)
*/
public void stop(BundleContext context) throws Exception {
}
}
我们可以看到每个 Activator 实际都是实现了BundleActivator接口,此接口使 Activator 能够接受框架的调用。在框架启动后,启动每个 bundle 的时候都会调用每个 bundle 的 Activator 。
注意:bundle 的 Activator 必须含有无参数构造函数,这样框架才能使用Class.newInstance()方式反射构造 bundle 的 Activator 实例。
这里我们在start方法中填入了我们希望输出的 hello world 字符串。那么,怎么才能启动这个 bundle 呢?
在右边的运行环境对话框中,输入运行环境的名字、start level 和依赖的插件,由于我们目前不需要其它的第三方插件,因此只需要勾上系统的 org.eclipse.osgi 插件,如果不选择此插件,hello world 将无法运行。如图 7,只有当您点击了 validate bundles 按钮 ,并且提示无问题之后,才表明您的运行环境基本 OK 了。
依赖插件的选择:
好的,如果您的运行环境已经 OK,那么就点击 Run 吧。
恭喜您,成功了!
OSGi 控制台对于习惯开发普通 Java 应用程序的开发人员来说,还是比较新鲜的。一般来说,通过 OSGi 控制台,您可以对系统中所有的 bundle 进行生命周期的管理,另外也可以查看系统环境,启动、停止整个框架,设置启动级别等等操作。如图 10,键入SS就可以查看所有 bundle 的状态:
下面列出了主要的控制台命令:
类别 | 命令 | 含义 |
控制框架 | launch | 启动框架 |
shutdown | 停止框架 | |
close | 关闭、退出框架 | |
exit | 立即退出,相当于 System.exit | |
init | 卸载所有 bundle(前提是已经 shutdown) | |
setprop | 设置属性,在运行时进行 | |
控制 bundle | Install | 安装 |
uninstall | 卸载 | |
Start | 启动 | |
Stop | 停止 | |
Refresh | 刷新 | |
Update | 更新 | |
展示状态 | Status | 展示安装的 bundle 和注册的服务 |
Ss | 展示所有 bundle 的简单状态 | |
Services | 展示注册服务的详细信息 | |
Packages | 展示导入、导出包的状态 | |
Bundles | 展示所有已经安装的 bundles 的状态 | |
Headers | 展示 bundles 的头信息,即 MANIFEST.MF 中的内容 | |
Log | 展示 LOG 入口信息 | |
其它 | Exec | 在另外一个进程中执行一个命令(阻塞状态) |
Fork | 和 EXEC 不同的是不会引起阻塞 | |
Gc | 促使垃圾回收 | |
Getprop | 得到属性,或者某个属性 | |
控制启动级别 | Sl | 得到某个 bundle 或者整个框架的 start level 信息 |
Setfwsl | 设置框架的 start level | |
Setbsl | 设置 bundle 的 start level | |
setibsl | 设置初始化 bundle 的 start level |
MANIFEST.MF 可能出现在任何包括主类信息的 Jar 包中,一般位于 META-INF 目录中,所以此文件并不是一个 OSGi 特有的东西,而仅仅是增加了一些属性,这样也正好保持了 OSGi 环境和普通 Java 环境的一致性,便于在老的系统中部署。表 2 列出此文件中的重要属性及其含义:
属性名字 | 含义 |
---|---|
Bundle-Activator | Bundle 的启动器 |
Bundle-SymbolicName | 名称,一般使用类似于 JAVA 包路径的名字命名 |
Bundle-Version | 版本,注意不同版本的同名 bundle 可以同时上线部署 |
Export-Package | 导出的 package 声明,其它的 bundle 可以直接引用 |
Import-Package | 导入的 package |
Eclipse-LazyStart | 是否只有当被引用了才启动 |
Require-Bundle | 全依赖的 bundle,不推荐 |
Bundle-ClassPath | 本 bundle 的 class path,可以包含其它一些资源路径 |
Bundle-RequiredExecutionEnvironment | 本 bundle 必须的执行环境,例如 jdk 版本声明 |
回页首
好的,刚才我们已经从头到尾开发了一个基于 Equinox 框架的 Hello world 应用程序。我们发现似乎并不是很困难,很多工作 Eclipse 已经帮我们做好了,例如 Activator 代码框架和 MANIFEST.MF 文件,我们也学会了如何控制 OSGi 的控制台和编写 MANIFEST.MF 文件,但是,您真的明白它们是如何运行的么?下面我们将重点介绍一些 OSGi 运行必备的基础知识。
我们已经看到,编写一个很普通的 Hello world 应用,必须首先创建一个 plug-in 工程,然后编辑其 Activator 类的start方法,实际我们这样做的本质是为 OSGi 运行环境添加了一个 bundle,那么一个 bundle 必须的构成元素是哪些呢?
好了,我们已经明白 bundle 是什么了,也知道如何开发一个基本的 bundle 了,那么我们还必须要明白,我的 bundle 放在 Equinox 框架中,它对我们的 bundle 做了些什么?
实际上,目标平台已经为我们准备了 N 个 bundle,它们提供各种各样的服务,OSGi 中,这些 bundle 的名字叫 system bundle,就好比精装修的房子,您只需要拎包入住,不再需要自己铺地板,装吊顶了。
我们的 bundle 进入 Equinox 环境后,OSGi 框架对其做的事情如下:
OK, 现在我们大概明白了一个 bundle 的定义和其在 OSGi 框架中的生命周期,前面我们看到控制台可以通过ss命令查看所有装载的 bundle 的状态,那么 bundle 到底具有哪些状态,这些状态之间是如何变换呢?我们知道了这些状态信息,对我们有何益处?
首先,了解一下一个 bundle 到底有哪些状态:
状态名字 | 含义 |
---|---|
INSTALLED | 就是字面意思,表示这个 bundle 已经被成功的安装了 |
RESOLVED | 很常见的一个状态,表示这个 bundle 已经成功的被解析(即所有依赖的类、资源都找到了),通常出现在启动前或者停止后 |
STARTING | 字面意思,正在启动,但是还没有返回,所以您的 Activator 不要搞的太复杂 |
ACTIVE | 活动的,这是我们最希望看到的状态,通常表示这个 bundle 已经启动成功,但是不意味着您的 bundle 提供的服务也是 OK 的 |
STOPPING | 字面意思,正在停止,还没有返回 |
UNINSTALLED | 卸载了,状态不能再发生变更了 |
下面请看一张经典的 OSGi bundle 变更状态的图:
OK,到现在为止,似乎一切都是新鲜的,但是您似乎在考虑,OSGi 到底有什么优势,下面介绍一下其中的一个特点,几乎所有的面向组件的框架都需要这一点来实现其目的:面向服务、封装实现。这一点在普通的 Java 应用是很难做到的,所有的类都暴露在 classpath 中,人们可以随意的查看您的实现,甚至变更您的实现。这一点,对于希望发布组件的公司来说是致命的。
OSGi 很好的解决了这个问题,就像上面的图显示的,每个 bundle 都可以有自己公共的部分和隐藏的部分,每个 bundle 也只能看见自己的公共部分、隐藏部分和其它 bundle 的公共部分。
bundle 的 MANIFEST.MF 文件提供了 EXPORT/IMPORT package 的关键字,这样您可以仅仅 export 出您希望别人看到的包,而隐藏实现的包。并且您可以为它们编上版本号,这样可以同时发布不同版本的包。
这一点比较难理解,一般情况下您不需要关心这个事情,除非事情出现了问题,您发现明明这个类就在这里,怎么就是报告 ClassNotFoundException/NoClassDefExcpetion 呢?在您垂头丧气、准备砸掉电脑显示器之前,请看一下 bundle 中的类是如何查找的:
在 Equinox 环境中,我们在配置 hello world 应用的时候,看到我们将 framework start level 保持为 4,将 Hello world bundle 的 start level 设置为 5 。 start level 越大,表示启动的顺序越靠后。在实际的应用环境中,我们的 bundle 互相有一定的依赖关系,所以在启动的顺序上要有所区别,好比盖楼,要从打地基开始。
实际上,OSGi 框架最初的 start level 是 0,启动顺序如下:
停止顺序,也是首先将系统的 start level 设置为 0:
回页首
我们不能只停留在 hello world 的层面,虽然那曾经对我们很重要 ,但是现实需要我们能够使用 OSGi 写出激动人心的应用程序,它能够被客户接受,被架构师认可,被程序员肯定。好的,那我们开始吧。下面将会着重介绍一些现实的应用程序可能需要的一些 OSGi 应用场景。
由于 OSGi 框架能够方便的隐藏实现类,所以对外提供接口是很自然的事情,OSGi 框架提供了服务的注册和查询功能。好的,那么我们实际操作一下,就在 Hello world 工程的基础上进行。
我们需要进行下列的步骤:
好的,为了达到上述要求,我们实际操作如下:
package osgi.test.helloworld.service; public interface IHello { /** * 得到 hello 信息的接口 . * @return the hello string. */ String getHello(); }
public class DefaultHelloServiceImpl implements IHello { @Override public String getHello() { return "Hello osgi,service"; } }
我们使用第一种注册方式,修改Activator类的start方法,加入注册代码:
public void start(BundleContext context) throws Exception { System.out.println("hello world"); context.registerService( IHello.class.getName(), new DefaultHelloServiceImpl(), null); }
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()); }
恭喜您,成功了!
前面讲过,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)
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"; } }
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"; }
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); } }
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()); }
可以看到,post事件后,不等事件真的被处理完成,就返回了,事件处理在另外的线程执行,最后才打印处理完成的语句。然后ss看一下,目前我们已经有五个 bundle 在运行了:
OSGi 的 HTTP 服务为我们提供了展示 OSGi 的另外一个途径,即我们可以专门提供一个 bundle 用来作为我们应用的 UI,当然这个还比较简单,只能提供基本的 HTML 服务和基本的 Servlet 服务。如果想提供复杂的 Jsp/Struts/WebWorks 等等,或者想用现有的 Web 中间件服务器例如 Tomcat/Resin/WebSphere Application Server 等,都需要另外的途径来实现,目前我提供一些基本的使用 HTTP 服务的方式。
要使用 HTTP 服务,必然有三个步骤
那么,接下来我们实际操作一下:
<html> <h1>hello osgi http service</h1> </html>
httpService = (HttpService)context.getService (context.getServiceReference(HttpService.class.getName())); httpService.registerResources("/", "/pages", null);
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()); } }
MyServlet ms = new MyServlet(); httpService.registerServlet("/ms", ms, null, null);
分布式部署的实现方式一般可以通过 Web 服务、RMI 等方式,这里简单介绍一下基于 RMI 方式的分布式实现。
在 OSGi 环境中,并没有直接提供分布式部署的支持,我们可以采用 J2SE 提供的 RMI 方式来实现,但是要考虑 OSGi 的因素,即如果您希望您的服务既可以本地使用,也可以被远程访问,那么您应该这样定义接口和类:
说明:
public interface IAppService extends Remote
实际操作如下:
public interface IAppService extends Remote { /** * 得到一个远程服务的名称 . * @return . * @throws RemoteException . */ String getAppName() throws RemoteException; }
注册为标准服务:
IAppService appService = new DefaultAppServiceImpl(context); context.registerService( IAppService.class.getName(), appService, null);
注册为远程对象:
/** * 启动 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); }
IAppService appService = (IAppService)context.getService( context.getServiceReference(IAppService.class.getName())); System.out.println(appService.getAppName());
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());
回页首
到目前为止,我们已经涉及到了 OSGi 的诸多方面,那么在实际进行应用程序的架构设计的时候我们要考虑哪些因素呢,这一节我们详细讨论一下这个问题。
应用架构的设计应该充分考虑到可靠性、可扩展性、可维护性等因素,使用了 OSGi 框架后,我们可以更加容易的实现系统分层,组件化的设计方式。通过使用 HTTP 服务我们可以设计出一个基于 HTTP 服务的程序维护平台。架构如下:
说明:
这种架构的优势在于:
一般的应用架构可能都比较多的考虑可靠性、灵活性、可扩展性等,对可维护性却没有提供太多的关注,使用 OSGi 后,将对可维护性提供类似于 JMX 的支持,当然这不需要您实现 MBEAN,就像上述介绍的架构设计,我们在最上层可以设计一个基于 HTTP 的维护层,这样,提供了一个小的 Web 控制台,供管理员进行维护。
维护的方面包括:
回页首
我们的 bundle 不会只能在 Eclipse 环境运行,我们需要能够将 bundle 部署到实际的操作系统中,可能是 Windows/Linux/Unix 等环境,这要求我们按照下列步骤进行:
发布 bundle 的工作其实很简单,通过 eclipse 平台即可完成:
为了让我们的 Jar 文件跑起来,需要 OSGi 的运行环境支持,所以我们需要拷贝一些 system bundle 到 plugins 目录中,包括:
然后,把 eclipse 目录的 org.eclipse.osgi_3.3.2.R33x_v20080105 文件拷贝到 osgi.test.deploy 根目录,重命名为 equinox.jar 文件。
在 osgi.test.deploy 目录新建子目录 configuration,新建一个文本文件 config.ini,用来配置 bundle 的启动环境,配置如下:
注意最后两个 bundle 的启动顺序配置格式为:bundle@start_leve:start。
好了,config.ini 也已经准备好了。
下面进行启动脚本编写,这个和普通的 Java 程序没有什么大的区别,都是调用 Java 程序执行一个 jar 文件,关键是其中的一些参数定义:
注意1/2/3/117/118参数都是 OSGi 环境特有的。
双击 run.bat,可以看到如下结果:
回页首
通过阅读本文您应该已经掌握了使用 Equinox 开发基于 OSGi 的应用程序的方法,了解了其关键的理论知识,还学习了如何开发分层的, 模块化的、分布式的应用程序,掌握了在 Windows 平台部署基于 Equinox 平台的 OSGi 应用程序的方法。总体上看,OSGi 能够有效的降低模块 之间的耦合程度,将软件设计的开闭原则(Open-Close Principle)提高到一个新的水平,另外 OSGi 也为系统架构设计提供了更大的灵活性,使得我们开发出像 Eclipse 那样插件化的平台系统不再遥不可及。