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划分为如下几种类型(实际开发中,并不存在清晰的边界):

*UtilityBundle

这种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的功能接口

组件开发除了应对系统的开发规模之外,其最重要的目标是降低系统耦合。开发人员采用OSGiBundle开发时,应尽量屏蔽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.1OSGi环境的动态特性

OSGi是一个动态的环境,OSGi运行环境内部状态的变化通过事件发布/监听机制进行交互。开发人员在构建Bundle时应充分利用OSGi环境的事件监听机制。如,开发人员可以在自己的Bundle中注册Bundle事件的监听用来处理其他Bundle的安装,启动,停止,卸载等事件。

OSGi运行环境内部的事件主要包括三类:

*框架事件(FrameworkEvent)

STARTED框架已经启动

ERROR某个Bundle启动过程中引发错误

WARNING某一Bundle引发一个警告

INFO某一Bundle引发一个INFO类型的事件

PACKAGES_REFRESHEDPackageAdmin.refreshPackage操作执行完成

STARTLEVEL_CHANGEDStartLevel.setStartLevel操作执行完成

*Bundle事件(BundleEvent)

INSTALLEDBundle被安装到OSGi环境后系统发布该事件

RESOLVEDBundle被成功解析

LAZY_ACTIVATIONBundle将被延迟激活

STARTINGBundle正在被激活

STARTEDBundle被成功激活

STOPPINGBundle被停止

STOPPEDBundle正在被停止

UPDATEDBundle被更新

UNRESOLVEDBundle被UNRESOLVED

UNINSTALLEDBundle被卸载

*服务事件(ServiceEvent)

REGISTERED服务被注册

MODIFIED服务被修改

UNREGISTERING服务正在被注销

关于上述事件的详细信息参考OSGiAPI文档。

下述代码展示了如何在Bundle组件中通过实现FrameworkListener,BundleListener和ServiceListener接口,并使用BundleContext注册监听OSGi环境的各种事件:

packagecom.example;

importorg.osgi.framework.BundleActivator;

importorg.osgi.framework.BundleContext;

importorg.osgi.framework.BundleEvent;

importorg.osgi.framework.BundleListener;

importorg.osgi.framework.FrameworkEvent;

importorg.osgi.framework.FrameworkListener;

importorg.osgi.framework.ServiceEvent;

importorg.osgi.framework.ServiceListener;

publicclassActivatorimplementsBundleActivator,FrameworkListener,

BundleListener,ServiceListener{

publicvoidstart(BundleContextcontext)throwsException{

//注册监听

context.addFrameworkListener(this);

context.addBundleListener(this);

context.addServiceListener(this);

}

publicvoidstop(BundleContextcontext)throwsException{

}

//处理框架事件

publicvoidframeworkEvent(FrameworkEventevent){

if((event.getType()&FrameworkEvent.ERROR)!=0){

System.err.println("FrameworkERROR:"+event.getBundle());

}

}

//处理Bundle事件

publicvoidbundleChanged(BundleEventevent){

if((event.getType()&BundleEvent.STARTED)!=0){

System.err.println("BundleSTARTED:"+event.getBundle());

}elseif((event.getType()&BundleEvent.STOPPED)!=0){

System.err.println("BundleSTOPPED:"+event.getBundle());

}

}

//处理服务事件

publicvoidserviceChanged(ServiceEventevent){

if((event.getType()&ServiceEvent.REGISTERED)!=0){

System.err.println("ServiceREGISTERED:"

+event.getServiceReference());

}

}

}

2.2Bundle的生命周期及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环境进行交互:

publicvoidstart(BundleContextcontext)throwsException{

//获取OSGi环境中的所有安装的bundle

for(Bundlebundle:context.getBundles()){

System.out.println("BundleSymbolicName:"

+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.3Bundle的资源管理特性

在通常的应用系统开发中,开发人员通常要考虑配置文件的存储路径,系统临时文件的存储路径等信息。由于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发布的服务:

publicvoidstart(BundleContextcontext)throwsException{

//获取OSGiLog服务的服务引用

ServiceReferencelogSr=context.getServiceReference(LogService.class.getName());

//根据Log服务引用获取LogService服务实例

log=(LogService)context.getService(logSr);

//创建StringService接口服务实例

StringServicess=newStringServiceImpl(log);

//设定StringService的属性

Hashtable

//使用BundleContext注册发布StringService服务

context.registerService(StringService.class.getName(),ss,properties);

}

3.Bundle的开发实践

实践1、构建Utility类型的Bundle

该示例将第三方log4j工具包(版本为1.2.13)封装为OSGiBundle组件。操作步骤如下:

*步骤一:在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:ApacheLog4j

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的OSGiBundle已经构建完成。开发人员可以在其他Bundle中如同平常的开发模式一样引用Log4j的类包,获取Logger实例记录日志。

实践2、构建Service类型的Bundle

该示例Bundle由StringService接口,StringServiceImpl类和Activator类构成。该Bundle发布StringService接口服务,同时引用OSGi的标准LogService服务。

StringService接口是该示例Bundle发布的String服务实现接口。

packagecom.example;

publicinterfaceStringService{

Stringconcat(Stringstr1,Stringstr2);

}

StringServiceImpl类是StringService接口的实现类,该类使用OSGi的标准LogService服务记录日志。

packagecom.example.internal;

importorg.osgi.service.log.LogService;

importcom.example.StringService;

publicclassStringServiceImplimplementsStringService{

privateLogServicelog;

publicStringServiceImpl(LogServicelog){

this.log=log;

}

publicStringconcat(Stringstr1,Stringstr2){

Stringstr=str1+str2;

if(log!=null)

log.log(LogService.LOG_INFO,"ConcatStringResultis"+str);

returnstr;

}

}

Activator类为Bundle的生命周期入口,该类根据注入的BundleContext上下文获取OSGiLogService服务,然后注册StringService服务。

packagecom.example.internal;

importjava.util.Hashtable;

importorg.osgi.framework.BundleActivator;

importorg.osgi.framework.BundleContext;

importorg.osgi.framework.Constants;

importorg.osgi.framework.ServiceReference;

importorg.osgi.service.log.LogService;

importcom.example.StringService;

publicclassActivatorimplementsBundleActivator{

privateLogServicelog;

publicvoidstart(BundleContextcontext)throwsException{

//获取OSGiLog服务的服务引用

ServiceReferencelogSr=context.getServiceReference(LogService.class.getName());

//根据Log服务引用获取LogService服务实例

log=(LogService)context.getService(logSr);

//创建StringService接口服务实例

StringServicess=newStringServiceImpl(log);

//设定StringService的属性

Hashtable

//使用BundleContext注册发布StringService服务

context.registerService(StringService.class.getName(),ss,properties);

}

publicvoidstop(BundleContextcontext)throwsException{

}

}

Bundle的元数据清单如下:

Manifest-Version:1.0

Bundle-ManifestVersion:2

Bundle-Name:ExamplePlug-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开发时所要面临的一些决策及注意事项。

相关推荐