OSGi Bundle的构建策略及实践
从另一个角度,历史是今天存在的基础,没有历史就没有今天。同样的道理,应用系统采用面向组件的开发实现,组件仍需要对象来构造,而对象在一定程度上是封 装的功能过程,三者相辅相成。不管是从上而下还是从下而上,应用系统需求模型在其实现过程中,系统设计者应该充分关注组件、对象和功能过程三个层面。
OSGi可以看作是面向组件开发的一种思路和基础环境。在OSGi环境中实现应用系统的需求模型要求开发人员对组件、对象开发具有充分的理解。衡量一个应 用系统实现好坏的标准之一是该系统是否是松耦合高内聚。面向对象的开发实现此目标的关键是接口/抽象的应用,OSGi面向组件的开发在此基础之上充分利用 了对象类包封装的机制。
1. Bundle的构建策略
通常,当我们构建应用系统时,我们需要充分重视所构建应用的业务需求模型即领域模型,同样的,在用软件代码实现业务需求模型时,我们也应当对软件系统的架 构模型给予充分的重视。采用OSGi技术实现应用系统时,展现在我们面前的将是一个个的Bundle组件,此时,我们必须首先弄清楚我们要构建什么样的 Bundle。
1.1 明确Bundle的构建需求及实现粒度
从需求模型的角度看,Bundle可以看作是需求模型中的一个功能模块实现,因此,在开发Bundle之前系统开发人员必须明确Bundle的功能需求。 需求模型中的功能模块的边界可大可小,同样,Bundle的实现粒度也是可大可小,极端情况下,小的Bundle可以仅实现一个微小的功能,大的 Bundle可以实现整个业务系统。
以记录日志为例,如果记录日志仅输出到控制台,则一个类就可以实现整个的功能,如果记录日志输出到数据库系统,则开发人员需要在整个功能用一个 Bundle实现,还是将日志记录功能和记录信息的数据库存储分为两个不同的Bundle实现,两个选择甚至更多的选择中做出决策。通常这个问题在现有的 软件开发方式中不需要过多的重视,但在OSGi开发中,开发人员必须根据需求确定Bundle的实现粒度。
1.2 确定Bundle的类型
我们在面向对象的系统开发过程中,经常会将一些通用的功能设计成为工具类,多个工具类封装为一个工具类包。由于OSGi开发建立在面向对象的开发之上, Bundle的开发也存在这种特点。OSGi开发的另一个特点是服务机制的引入,一个Bundle可以将其提供的功能发布成为一个或多个服务,供其他 Bundle查找使用。此外,建立在OSGi环境之上的组件开发也区别于通常的组件开发方式,因为我们可以利用OSGi环境提供的某些特性(参见下一节 充分利用OSGi环境的特性)。综上所述,我们可以将Bundle划分为如下几种类型(实际开发中,并不存在清晰的边界):
- Utility Bundle
这种Bundle与通常开发方式中的工具类或工具类包没有本质的区别,仅仅是将这些工具类发布到OSGi环境中,这些工具类也不依赖任何的OSGi环境或 特性。通常,这种Bundle特别适合目前存在的众多的第三方组件引入到OSGi环境中直接供OSGi开发人员使用。例如,我们可以将Apache开源项 目Log4j发布的工具包通过修改其META-INF目录下的MANIFEST.MF文件,添加Bundle的元数据信息就可以直接将其封装为可供 OSGi环境中其他Bundle使用的工具类Bundle。
- 依赖OSGi特性的Bundle
这类Bundle可以被其他Bundle引用,为其提供功能,但是这类Bundle不能离开OSGi运行环境,否则不能使用。举例来说,一个提供数据缓存 功能的Bundle可能使用Bundle的数据存储区来缓存数据。Bundle的数据存储区的位置对Bundle开发者来说是透明的(参见下一节 充分利用OSGi环境的特性)。如果将该Bundle的数据缓存功能迁移到OSGi运行环境之外,则必须修改实现添加对缓存区位置的处理功能。
- 引用和(或)发布OSGi服务的Bundle
这类Bundle也可以认为是依赖OSGi特性的Bundle,唯一的区别就是,该类Bundle充分利用的OSGi中服务的特性,引用其他Bundle 发布的服务和(或)向OSGi环境中发布自己的服务。OSGi环境提供的服务机制可以使得系统的实现遵循最大程度的松耦合。
1.3 隐藏Bundle的功能接口
组件开发除了应对系统的开发规模之外,其最重要的目标是降低系统耦合。开发人员采用OSGi Bundle开发时,应尽量屏蔽Bundle所提供功能的内部实现机制,仅将为其他Bundle提供的交互接口暴露出来。在OSGi中,这可以通过两种方 式,一种是通过Export供其他Bundle引用的类包;一种是向OSGi环境发布服务。
在Equinox中,Bundle内部实现的类包通常以"internal"标注,如org.eclipse.equinox.internal.cm.reliablefile。标注为"internal"的类包通常不包含在Export列表中,或者通过"x-internal"属性标记(该属性仅在Eclipse开发环境中提供)。
隐藏Bundle功能接口的一种良好机制是通过OSGi服务。开发人员仅将自己开发的Bundle的接口类包发布给其他Bundle可见,同时,将功能接 口的实现注册为OSGi中的服务,其他Bundle通过查找OSGi服务注册表获取该服务,通过服务的接口调用服务的功能。
1.4 尽可能应用OSGi提供的标准服务
OSGi联盟为一些经常用到的功能定义了标准服务,如应用程序管理服务,日志服务,事件服务,配置管理服务,用户管理服务,HTTP服务等等。如果开发人 员采用的OSGi环境提供了上述服务的实现,且这些服务实现满足系统的需求,则开发人员应尽可能使用这些服务,而不是为这些类似的功能定义自己的实现。
2.充分利用OSGi环境的特性
2.1 OSGi环境的动态特性
OSGi是一个动态的环境,OSGi运行环境内部状态的变化通过事件发布/监听机制进行交互。开发人员在构建Bundle时应充分利用OSGi环境的事件 监听机制。如,开发人员可以在自己的Bundle中注册Bundle事件的监听用来处理其他Bundle的安装,启动,停止,卸载等事件。
OSGi运行环境内部的事件主要包括三类:
- 框架事件(FrameworkEvent)
STARTED 框架已经启动
ERROR 某个Bundle启动过程中引发错误
WARNING 某一Bundle引发一个警告
INFO 某一Bundle引发一个INFO类型的事件
PACKAGES_REFRESHED PackageAdmin.refreshPackage操作执行完成
STARTLEVEL_CHANGED StartLevel.setStartLevel操作执行完成
- Bundle事件(BundleEvent)
INSTALLED Bundle被安装到OSGi环境后系统发布该事件
RESOLVED Bundle被成功解析
LAZY_ACTIVATION Bundle将被延迟激活
STARTING Bundle正在被激活
STARTED Bundle被成功激活
STOPPING Bundle被停止
STOPPED Bundle正在被停止
UPDATED Bundle被更新
UNRESOLVED Bundle被UNRESOLVED
UNINSTALLED Bundle被卸载
- 服务事件(ServiceEvent)
REGISTERED 服务被注册
MODIFIED 服务被修改
UNREGISTERING 服务正在被注销
关于上述事件的详细信息参考OSGi API文档。
下述代码展示了如何在Bundle组件中通过实现FrameworkListener,BundleListener和ServiceListener接口,并使用BundleContext注册监听OSGi环境的各种事件:
package com.example;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;public class Activator implements BundleActivator, FrameworkListener,
BundleListener, ServiceListener {public void start(BundleContext context) throws Exception {
//注册监听
context.addFrameworkListener(this);
context.addBundleListener(this);
context.addServiceListener(this);
}public void stop(BundleContext context) throws Exception {
}//处理框架事件
public void frameworkEvent(FrameworkEvent event) {
if ((event.getType() & FrameworkEvent.ERROR) != 0) {
System.err.println("Framework ERROR: " + event.getBundle());
}
}//处理Bundle事件
public void bundleChanged(BundleEvent event) {
if ((event.getType() & BundleEvent.STARTED) != 0) {
System.err.println("Bundle STARTED: " + event.getBundle());
} else if ((event.getType() & BundleEvent.STOPPED) != 0) {
System.err.println("Bundle STOPPED: " + event.getBundle());
}
}//处理服务事件
public void serviceChanged(ServiceEvent event) {
if ((event.getType() & ServiceEvent.REGISTERED) != 0) {
System.err.println("Service REGISTERED: "
+ event.getServiceReference());
}
}}
2.2 Bundle的生命周期及OSGi上下文特性
在Java虚拟机运行环境中,类的生命周期开始于虚拟机的加载,终止于被提前卸载或虚拟机停止,通常不需要开发人员干预。在OSGi环境中,开发人员可以参与Bundle的生命周期过程并能够获取Bundle在OSGi环境中运行的上下文。
通过实现org.osgi.framework.BundleActivator接口,并在Bundle的元数据头属性"Bundle- Activator"中指明该接口的实现类,如"Bundle-Activator: com.example.Activator"。当Bundle被启动或停止时,OSGi框架会将该Bundle的运行上下文BundleContext 注入到该实现类实例中,开发人员可以在该Bundle的运行过程中使用该上下文访问OSGi环境信息,访问OSGi环境中的其他Bundle,注册事件监 听,发布或引用服务等。
下面的代码片段展示了如何利用Bundle运行上下文与OSGi环境进行交互:
public void start(BundleContext context) throws Exception {
// 获取OSGi环境中的所有安装的bundle
for (Bundle bundle : context.getBundles()) {
System.out.println("Bundle Symbolic Name: "
+ bundle.getSymbolicName());
}
// 获取OSGi运行环境中的属性(查找范围包括系统属性)
System.out.println("osgi.framework = "
+ context.getProperty("osgi.framework"));
//注册其他Bundle
context.installBundle("file:C://test_bundle_1.0.0.jar");
//在该Bundle的数据存储区中构建datacache.file文件
context.getDataFile("datacache.file");
//注册监听
context.addFrameworkListener(this);
context.addBundleListener(this);
context.addServiceListener(this);
}
2.3 Bundle的资源管理特性
在通常的应用系统开发中,开发人员通常要考虑配置文件的存储路径,系统临时文件的存储路径等信息。由于OSGi环境中Bundle实际上是由一组文件资源 构成,开发人员在开发Bundle时可以充分利用这种特点,使用Bundle存储与Bundle相关的配置信息。OSGi环境在运行时为每一个 Bundle构建一个序列化数据存储区,开发人员可以使用该存储区存储Bundle在运行时生成的临时信息。
下述代码片段展示了如何通过Bundle提供的功能接口获取Bundle内部的资源:
- context.getBundle.getEntry("") 只查询Bundle内部资源,返回Bundle的URL,该URL与实现相关,如:bundleentry://256/
- context.getBundle.getEntry("/") 只查询Bundle内部资源,返回Bundle的URL,该URL与实现相关
- context.getBundle.getEntry("/build.properties") 只查询Bundle内部资源,返回Bundle内部build.properties文件的URL,该URL与实现相关,如:bundleentry://256/build.properties
- context.getBundle().getResource("build.properties") 使用Bundle的ClassLoader查询Bundle类域内的资源,如果Bundle不能被解析(RESOLVED),则只查询Bundle内部资源,返回Bundle内部build.properties文件的URL,该URL与实现相关。
开发人员在获取上述资源的URL后,可以转换成与Bundle运行系统相关的文件URL,如:file:C:/test_bundle_1.0.0。
下述代码片段展示了通过BundleContext接口获取Bundle运行时数据存储区资源:
- context.getDataFile("") 返回该Bundle的运行时数据存储区位置,如:E:/worksapce/.metadata/.plugins/org.eclipse.pde.core/CobWeb/org.eclipse.osgi/bundles/256/data
- context.getDataFile("config.xml") 返回该Bundle的运行时数据存储区内的config.xml文件,如果该文件不存在,则创建该文件。
2.4 面向服务的组件特性
OSGi的服务层提供了一个动态服务发布,绑定和查找模型,所谓的服务即指实现了某个(些)接口的Java对象。在面向对象的系统开发中,接口在分离系统的耦合方面扮演至关重要的角色,OSGi环境充分利用了这一点。
OSGi运行框架维护着一个服务注册表,Bundle可以通过Bundle上下文向该服务注册表中发布服务(只需指明服务实现的接口,服务的实例和服务的 属性),也可以使用Bundle上下文从服务注册表中根据接口名称及服务的属性查找其他Bundle注册的服务。在此模式下,Bundle之间的耦合关系 只存在接口层面,而与接口的实现无关。
下述代码片段展示了如何向OSGi服务注册表发布服务以及如何查找其他Bundle发布的服务:
public void start(BundleContext context) throws Exception {
//获取OSGi Log服务的服务引用
ServiceReference logSr = context.getServiceReference(LogService.class.getName());
//根据Log服务引用获取LogService服务实例
log = (LogService) context.getService(logSr);//创建StringService接口服务实例
StringService ss = new StringServiceImpl(log);
//设定StringService的属性
Hashtable//使用BundleContext注册发布StringService服务
context.registerService(StringService.class.getName(), ss, properties);
}
3.Bundle的开发实践
实践1、构建Utility类型的Bundle
该示例将第三方log4j工具包(版本为1.2.13)封装为OSGi Bundle组件。操作步骤如下:
- 步骤一:在C盘根目录下创建名称为"org.apache.log4j"目录,将log4j.1.2.13.jar文件拷贝到该目录中;
- 步骤二:在"org.apache.log4j"目录下构建META-INF目录,并在此目录下创建名称为"MANIFEST.MF"文件;
- 步骤三:编辑"MANIFEST.MF"文件,在此文件中添加以下内容:
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Apache Log4j
Bundle-SymbolicName: org.apache.log4j
Bundle-Version: 1.2.13
Bundle-ClassPath: .;log4j.1.2.13.jar
Export-Package: org.apache.log4j;version="1.2.13",
org.apache.log4j.config;version="1.2.13",
org.apache.log4j.helpers;version="1.2.13",
org.apache.log4j.jdbc;version="1.2.13",
org.apache.log4j.jmx;version="1.2.13",
org.apache.log4j.net;version="1.2.13",
org.apache.log4j.or;version="1.2.13",
org.apache.log4j.or.jms;version="1.2.13",
org.apache.log4j.or.sax;version="1.2.13",
org.apache.log4j.spi;version="1.2.13",
org.apache.log4j.varia;version="1.2.13",
org.apache.log4j.xml;version="1.2.13"
经过上述步骤后,名称为org.apache.log4j的OSGi Bundle已经构建完成。开发人员可以在其他Bundle中如同平常的开发模式一样引用Log4j的类包,获取Logger实例记录日志。
实践2、构建Service类型的Bundle
该示例Bundle由StringService接口,StringServiceImpl类和Activator类构成。该Bundle发布StringService接口服务,同时引用OSGi的标准LogService服务。
StringService接口是该示例Bundle发布的String服务实现接口。
package com.example;
public interface StringService {
String concat(String str1, String str2);
}
StringServiceImpl类是StringService接口的实现类,该类使用OSGi的标准LogService服务记录日志。
package com.example.internal;
import org.osgi.service.log.LogService;
import com.example.StringService;
public class StringServiceImpl implements StringService {
private LogService log;public StringServiceImpl(LogService log) {
this.log = log;
}public String concat(String str1, String str2) {
String str = str1 + str2;
if (log != null)
log.log(LogService.LOG_INFO, "Concat String Result is " + str);
return str;
}}
Activator类为Bundle的生命周期入口,该类根据注入的BundleContext上下文获取OSGi LogService服务,然后注册StringService服务。
package com.example.internal;
import java.util.Hashtable;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;import com.example.StringService;
public class Activator implements BundleActivator {
private LogService log;
public void start(BundleContext context) throws Exception {
//获取OSGi Log服务的服务引用
ServiceReference logSr = context.getServiceReference(LogService.class.getName());
//根据Log服务引用获取LogService服务实例
log = (LogService) context.getService(logSr);//创建StringService接口服务实例
StringService ss = new StringServiceImpl(log);
//设定StringService的属性
Hashtable//使用BundleContext注册发布StringService服务
context.registerService(StringService.class.getName(), ss, properties);
}public void stop(BundleContext context) throws Exception {
}}
Bundle的元数据清单如下:
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Example Plug-in
Bundle-SymbolicName: com.example
Bundle-Version: 1.0.0
Bundle-Activator: com.example.internal.Activator
Bundle-Vendor: EGRID
Eclipse-LazyStart: true
Import-Package: org.osgi.framework;version="1.3.0",
org.osgi.service.log;version="1.3.0"
Export-Package: com.example
4. 结论
通过本文的探讨,开发人员可以明确在OSGi环境下进行Bundle开发时所要面临的一些决策及注意事项。下一篇文章我们将要详细探讨OSGi环境下Bundle编程的细节。本文中的点击此处下载。