深入 OSGi:如何构建可扩展动态的 Web 程序
http://www.ibm.com/developerworks/cn/opensource/os-cn-osgi-dyn/
简介: OSGi 为 Java 系统模块化提供支持,本文重点讲解如何基于 OSGi 构建可扩展的应用程序,包含 Spring DM 和 Equinox 扩展点注册表等机制的使用。
在最新发布的的 Java7 中,对程序模块化特性仍然没有涉及,与此同时,OSGi(Open Service Gateway Initial),一种 Java 模块化系统规范,现在已经被广泛的应用于 Java 中间件上,如 IBM WebSphere,Oracle Weblogic,RedHat Jboss,Sun GlassFish 等。
OSGi 致力于解决以下几点:
传统的 Java 应用程序会用到类 (class) 和接口 (interface) 类型,这些类型中又会包含方法 (method),成员变量 (variable)。所有用到的类型通过包 (package) 组装在一块,即包 (package) 定义了全局命名空间,如下图所示,所有的这些类型、包经编译后以 JAR 形式发布 (Java Archives)。在运行时,JVM 会根据字节码中的引用在 Classpath 里面的 JAR 列表中搜索并加载到内存中。
但是缺点在于,
-
- 包作为模块划分的边界,粒度太小,系统中可以有成千上万的包
- JAR 只是一种承载方式,没有任何运行时特性来支持更复杂的场景
Bundle,可以将其理解为自描述的 JAR 文件。在 bundle 的 manifest 文件中,会有对本 bundle 的标识、提供的功能 (Export-package) 及依赖性 (Import-Package/Require-Bundle) 的定义。每个 bundle 在运行时自己的类加载器 (Class Loader),这样可以做到一方面把不同的 bundle 里面的类区别开来,当 bundle 被卸载时,只有这个 bundle 的类加载器中的信息会丢失;另一方面,可以在自己的 bundle 内部充分利用 Java 的成员访问控制机制。
OSGi 框架为 Java 系统提供了一个通用的容器,该系统中的 bundle,无需停止系统,即可实现 bundle 的安装、卸载。
Bundles 之间协作,可以通过直接引用类型实现,但会因直接依赖性导致模块之间的紧耦合。OSGi 提供了服务注册表 (Service Registry),通过服务注册表,bundle 可以使用匿名的服务,即提供服务的 bundle 不知道谁会消费该 Service,而消费服务的 bundle 也不知道谁提供了服务。在基于 OSGi Service 模型编程时,由于该模型的动态性,需要有方法可以跟踪动态的 service,例如,引用的服务随时可能注销。解决这个问题,一种选择是 Listener 模式,该模式最初被用于 Java GUI 编程中的事件模型。下图是 Sun 改进后的事件模型,该模型中的实体包括,
- 事件源 (event source) 该对象产生时间
- 事件对象 (event object) 封装事件信息
- 事件监听者 (event listener),处理事件信息
假设,用户想监听鼠标动作时,需首先实现 MouseMotionListener 接口,并将其注册在时间源上,当有事件发生时,事件源首先将事件相关信息封装成 MouseMotionEvent 对象,然后遍历并向自己维护的监听者列表发送通知。该模型允许同时多个监听者,对 GUI 编程非常方便。但一个潜在的缺点是事件源和事件监听者之间的依赖性。当某一个事件监听者不再监听时,事件源需从自己维护的列表中去掉这个引用,反之亦然,如果使用不当,还会产生内存泄漏。OSGi 被设计用于嵌入式系统领域,受限于系统资源,也不支持在内存中维护大量的引用。OSGi 规范中的白板模式 (Whiteboard pattern) 可以有效的解决这个问题,它充分利用了 OSGi 每个 bundle 不需要自己维护的 registry,所有的事件监听器都在 OSGi registry 注册为 Service, 对于事件源,它所需要做的就是获得 OSGi 框架的 registry,得到监听器,然后调用监听器,其他的事情都交给 OSGi 框架去做。这样使得 bundle 自身的代码更简单和清晰,符合 OSGi 设计的主旨。
OSGi 设计初衷是使系统组件化、模块化,进而实现动态化。对于组件化和动态化,可以定义为,
组件化:每个组件可以独立开发,组织级别的附庸
动态化:动态的改变组件的状态(数据),动态的分配资源,
那么基于 OSGi 的扩展性呢?此处的扩展性,我们定义为可以很方便的将新的功能组件集成到已有框架中。以这样一个应用场景的为例,在某产品 portal 中,客户可以访问到当前系统中不同的功能组件,比如产品目录、相关推荐、热点产品等,系统后端处理 bundle 为 com.company.product.startup。现在需要在不更改已有服务器代码的前提下,添加浏览历史功能,也就说可以以最小的代价扩展系统功能,可以考虑从以下两种方法入手,
Spring DM (Spring Dynamic Module) 使得在 OSGi 的环境中可以运行使用了 Spring 的应用程序。OSGi 4.2 中引入的 Blueprint services 规范也是参考了 Spring DM 的实现机制,并将其作为了参考实现。2009 年时,该项目捐赠给了 Eclipse 基金会,项目代号为 Gemini Blueprint
首先介绍下会用到的部分 Spring DM 中 OSGi 命名空间中的元素
元素名 | 描述 |
---|---|
osgi:service | 将引用的 bean 暴露为会在 OSGi 注册表中的 services,通常以接口的形式注册服务 |
osgi:reference | 以 interface 指定的接口名字向 OSGi 框架中的服务注册表获取已经注册的服务 |
osgi:list/set | 定义了类似 Java 中'list'(或'set')类型的 bean,包含了符合条件的所有服务。在定义这个标签时,通常会指定“cardinality”属性,定义了服务基数,例如定义了 1..1,代表必须存在一个 service 引用,而 0..1 指明该 service 引用是可选的 |
在引入 Spring osgi 元素后,我们可以有两种方式定义 OSGi 的 service,
1. Bundle activator and register
这是最简单同时也是最灵活的方法,需要注意的是,
-
- 每个 bundle 只有一个 Bundle Activator
- Bundle Activator 需实现 org.osgi.framework.BundleActivator,同时有空构造函数
- 在 manifest 中用 Bundle-Activator 指定
- 可以使用 ServiceTracker 来监视其他 service 的状态
以下代码实现了基于 BundleActivator 实现该接口,同时注册为 service,
2. Spring OSGi service 声明
以下代码中将声明的 bean 以接口形式暴露为服务
清单 2. 基于 Spring OSGi 声明 service
基于上面的结论,我们通过以下流程实现扩展性,
-
- 声明通用组件接口,将不同的实现声明为服务
- 利用 Spring osgi:list 集合标签,得到注册的服务列表
- 将此服务列表作为属性去初始化处理请求的服务,该 service 在初始化时,将遍历该列表,在新增功能时,只需将自己的实现声明为通用接口的服务,不需要对已有系统框架进行更改,就可以将新功能无缝嵌入到系统中。
实例
下面实现上面提供的要求,开发环境以 target platform 和 workspace 相结合的方式。首先将 Spring DM 和 Equinox 实现 jar 包放到 target platform 中,在当前的 Workspace 中。
有四个 bundle 来负责相关业务。
在每个 bundle 下有 spring 目录,用于放置 spring 配置信息,com.company.product.startup 负责服务列表的加载,其他 bundle 分别对应功能(产品目录、热点产品、相关推荐)。在 startup bundle 中,我们会定义通用接口 IComponent,定义抽象函数,如 setLabel, setURL 等。如在 product-appcontext.xml 中我们定义。在程序启动时,Spring DM 会负责实例化所有实现了该接口的 service,并存放的该列表中,这个组件列表会用于初始化管理 bean, 在这个 bean 上可以加上 URL map 去处理 http 请求或其他,在这里就不再详述了。
图 4. Product-appcontext.xml 内容
在每个子组件中,实现通用接口,并以接口名字暴露成服务。
如果想添加新的功能模块,只需要仿照上面的功能组件像产品目录,然后在 spring 配置文件中以服务形式暴露出来即可,很方便的实现了扩展性。
从上面的介绍,我们可以看到 OSGi 的 Service 机制可以很好的解决 bundle 直接的协作。其实除了 OSGi 规范中定义的 Service 注册表,Equinox 作为 OSGi 的参考实现,还提供了扩展点注册表,其工作原理如下,
-
-
- 首先, bundles 通过定义扩展点使得自己可以扩展或配置,在扩展点中定义合约
- 其他 bundles 遵守该合约对该扩展点进行扩展,通常会提供要运行的类名或要访问的资源
- 扩展点注册表 (Extension Registry) 将发现扩展点和具体的扩展,并根据 bundle 的生命周期扩展点与扩展联系起来
- 扩展点 (extension point) 的定义者可以自由的使用扩展的动作。
-
扩展点机制的核心就是两个概念,扩展点与扩展。为实现扩展性,可以在负责产品启动 bundle( com.company.product.startup) 定义扩展点,然后各个组件 bundle 去 contribute 这个扩展点,在设置扩展的时会定义显示标签,及实现某接口的类。除此之外,我们还需要实现从扩展点注册表中取得扩展逻辑。
仍以上节的四个 bundle 为例,
- 首先定义扩展点
在启动 bundle(com.company.product.startup) 中,双击 MANIFEST.MF 文件,默认创建的工程下,extension 页面是隐藏的,我们需要在 Overview 页面将其显示出来,然后在 extension point 页面,点击“Add ” 按钮来添加,
同时会自动生成 schema 模板文件,点击 Schema 链接,自定义 Schema 结构,Schema 的结构就是扩展点与扩展之间的合约,双方都须遵守。在这里我们定义了如下的结构,extension 元素下有 id, name 属性,并有一个 sequence 元素引用 new_comp 元素,我们在 new_comp 元素中定义了 url 和 class 属性。属性可以指定类型,比如此处的 class 属性我们指定为 java 类型。
-
定义扩展
在其他产品 bundle 中扩展该 extension point,以 com.company.product.recsys 为例,我们已将相关推荐的逻辑写到该 bundle 的类 RecSysComp 中,该接口实现了通用接口 (IComponent)。
-
组件的发现
所有的扩展点和扩展都有扩展点注册表维护,通过 Extension Registry 提供的接口来实现系统的扩展性,如下面的代码清单,将遍历所有扩展了 productComps 的扩展点并打印属性。
IExtensionRegistry reg = Platform.getExtensionRegistry(); IConfigurationElement[] elements = reg.getConfigurationElementsFor("com.company.product.startup.productComps"); for (int i = 0; i < elements.length; i++) { IConfigurationElement element = elements[i]; System.out.println("Class information is "+element.getAttribute("class")); System.out.println("URL value is "+element.getAttribute("url")); IComponent subComps = null; try{ //Initialized sub component's instance subComps = (IComponent)element.createExecutableExtension("class"); }catch(Exception ex){ ex.printStackTrace(); } } |
通过上面的示例我们看到,扩展点本身将自己作为一个可插拔的框架像所有其他 bundle 开放,所有逻辑都是声明式并且延时加载的。
OSGi 服务和 Equinox 扩展点都实现了 bundle 之间的松耦合,下表是两者几个典型的区别
特点 | 扩展点注册表 (Extension Registry) | OSGi Service Registry |
---|---|---|
生命周期 | 当 bundle 变成 resolved 状态时构造 | 当 bundle 被 activated 时构造 |
使用范围 | 一般来说,扩展点只被它所在的 bundle 使用,同样所有针对该扩展点的扩展也只能被该 bundle 使用。 | 全局的 |
是否单例 | 声明了扩展或扩展点的 bundle 必须为单例 | 没有要求 |
Spring DM 与 Equinox,为开发动态可扩展的应用程序提供了轻量级的高效框架,设计者不需要事先考虑到所有将实现的服务或具体扩展就能实现整个系统的扩展性。但是使用过程中,也需要注意一些细节问题。比如以 Eclipse 作为 IDE,使用 target platform 和 workspace 相结合的开发环境时,当 bundle 存在多个版本,Equinox 和 Spring DM 可能会因为使用不当,加载的不是同一版本,这就需要具体问题具体分析了。