1 OSGi介绍
OSGI(Open Services Gateway Initiative),或者通俗点说JAVA动态模块系统,定义了一套模块应用开发的框架。OSGI容器实现方案如Knopflerfish, Equinox和Apache Felix允许你把你的应用分成多个功能模块,这样通过依赖管理这些功能会更加方便。和Servlet和EJB规范类似,OSGI规范包含两大块:一个OSGI容器需要实现的服务集合;一种OSGI容器和应用之间通信的机制。开发OSGI平台意味着你需要使用OSGI API编写你的应用,然后将其部署到OSGI容器中。从开发者的视角来看,OSGI提供以下优势:
- 你可以动态地安装、卸载、启动、停止不同的应用模块,而不需要重启容器。
- 你的应用可以在同一时刻跑多个同一个模块的实例。
- OSGI在SOA领域提供成熟的解决方案,包括嵌入式,移动设备和富客户端应用等。
有了Servlet容器来做web 应用,有了EJB容器来做事务处理,OSGI容器被设计专门用来开发可分解为功能模块的复杂的Java应用。OSGi中的Bundle可以理解为在Jar包当中的MANIFEST.MF文件当中加入了新的描述数据的Jar包。
2 模块化
按照以上模块化的定义,可能有的人会问:“在面向对象里面,不是也有对模块化的支持吗?”没错,面向对象的概念可以说也在一定程度上支持模块化编程,那为什么还需要OSGi提供的模块化特性呢?这涉及到“逻辑边界”的不同粒度。
在用Java编写面向对象程序的时候,一个了解面向对象概念的人是不会把所有功能都塞到同一个类里面去的,面向对象让你从问题域中发现多个事物,并且每个事物负责不同的功能,尽量做到高内聚和低耦合。在这里,我们可以说面向对象的模块化粒度是在“类”这个级别上。
而OSGi的模块化,则是通过为JAR包添加metadata来定义哪些类应该暴露哪些类又隐藏在包中,其控制可见性的粒度是在bundle(JAR包)这一层面上的。
所以,它们所带来的能力都是通过控制可见性和可用性来保证高内聚和低耦合的,但是粒度不同,一个是对象层面上的,一个是模块层面上的。 既然负责的是不同的粒度,那么两者并不相互冲突,各有各的作用在里面。
3 Bundle 解析
3.1 Bundle 说明
Bundle 的解析是通过分析定义在 MANIFEST.MF 文件中的元数据 ( 主要定义了包约束条件 ),查找与包约束条件相匹配的 Bundle 并建立关联关系的过程。 MANIFEST.MF 文件中的包约束条件主要是通过 Import-Package、DynamicImport-Package、Export-Package 和 Require-Bundle 这四种表达方式来实现。下面简单介绍一下它们:
- Import-Package:定义需要导入的包。默认是所有需导入的包必须都能够找到相应的导出 Bundle (Exporter),否则解析失败。
- Export-Package:定义导出的包。在一个 Bundle 里面,一个包的导出定义并不意味着相应的包导入定义,而是这些类资源会在 Bundle 自身的类路径里面查找和加载。
- Require-Bundle:定义依赖的 Bundle 。
- DynamicImport-Package:定义需要动态导入的包。这部分定义没有在 Bundle 解析过程中使用,而是在运行时动态解析并加载共享包。
3.2 Bundle之间的交互方式
- 通过Package的Export(对外暴露自己的一个或多个package)和Import(导入别人的一个或多个package)来进行。
- 通过Service的方式进行。一个Bundle作为Service提供方,对外提供Servcie,使用者可以查找到提供的Service。并使用这个ServÎce,而提供/使用Service又存在两种方式:一种是通过BundlcContext ( Bundle 的上下文)来提供和获取,一种是使用Declarative Service来实现。
第一种使用BundleContext方式实现代码如下:
// 注册服务
public class Activator implements BundleActivator {
//在start()中用BundleContext.registerService()注册服务
public void start(BundleContext context) {
context.registerService(Greeting.class.getName(), new GreetingImpl(), null);
}
//无需在stop()中注销服务,因为bundle停止时会自动注销该bundle中已注册的服务
public void stop(BundleContext context) {}
}
第二种使用DS方式实现需要定义一个component.xml,并在Manifest文件中声明
声明:
Service-Component: OSGI-INF/component.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 定义DictQueryService -->
<component name="DictQueryService">
<!-- 定义组件的实现类 -->
<implementation class="com.yang.remotedictquery.impl.RemoteDictQueryServiceImpl"/>
<!-- 定义组件实现的接口-->
<service>
<provide interface="com.yang.osgitest.query.QueryService"/>
</service>
</component>
3.3 Require-Bundle和Import-Package
Require-Bundle和Import-Package功能类似,都是使得其他bundle的导出包对我们的bundle可用。但是Require-Bundle是作用在整个bundle上的;Import-Package是作用在单个包上。
假设bundleB require bundleA,则相当于bundleB中import了bundleA的所有包。但是import了哪些包是由bundleA决定的,bundleB控制不了。从下图中我们可以看出 Require-Bundle 会对整个 Bundle 产生依赖,也就是说 Bundle 所 Export 出的包都会被 A 加入到自己的类空间,而 Import-Package 只会对指定的包产生依赖关系。
引入Require-Bundle主要是因为Eclipse的历史遗留问题。在使用OSGi之前,Eclipse是基于全模块依赖的,如果OSGi仅仅支持Import-Package,要想让成千上万已有的Eclipse插件正常工作,几乎是不可能的。
在大多数情况下,都应该使用 Import-Package 而不是 Require-Bundle 。 Import-Package 比 Require-Bundle 更利于 Bundle 的部署和版本维护,同时在查找类的时候有更高的效率。
4 Bundle 运行
Bundle 的 Class Loader 能加载的所有类的集合构成了 Bundle 的类空间 (Class Space) 。类空间包含的类资源主要来自于以下几个方面:
- 父 Class Loader 可加载的类集合;
- Import-Package 定义的依赖的包;
- Require-Bundle 定义的依赖的 Bundle 的类集合;
- Bundle 自身的类集合,通常在 Bundle-Classpath 中定义;
- 隶属于 Bundle 的 Fragment 类集合。
- 如类资源属于 java.* 包,则将加载请求委托给父加载器;
- 如类资源定义在 OSGi 框架中启动委托列表(org.osgi.framework.bootdelegation)中,则将加载请求委托给父加载器;
- 如类资源属于在 Import-Package 中定义的包,则框架通过 Class Loader 依赖关系图找到导出此包的 Bundle 的 Class Loader,并将加载请求委托给此 Class Loader ;
- 如类资源属于在 Require-Bundle 中定义的 Bundle,则框架通过 Class Loader 依赖关系图找到此 Bundle 的 Class Loader,将加载请求委托给此 Class Loader ;
- Bundle 搜索自己的类资源 ( 包括 Bundle-Classpath 里面定义的类路径和属于 Bundle 的 Fragment 的类资源);
- 若类在 DynamicImport-Package 中定义,则开始尝试在运行环境中寻找符合条件的 Bundle 。
5 合理使用 Manifest 文件
Eclipse-LazyStart
在 OSGi R4 版本中,通过对 Eclipse-LazyStart 属性的设置,可以指定 Bundle 是否支持慢启动功能。当 Eclipse-LazyStart 被设置为 true 的时候,Bundle 不会被默认启动,而是当 Bundle 的类或其它资源被第一次调用时,由框架自动激活 Bundle 。这样就会使得 OSGi 框架在启动的时候,只启动必须的 Bundle 而延迟启动其它的 Bundle,从而确保了启动时间和效率。在默认情况下,Eclipse-LazyStart 为 false 。
Bundle-ManifestVersion
Bundle-ManifestVersion 指定了 Bundle 应该遵循 OSGi 规范的版本号。其默认值是 1,即 R3 版本;值为 2 的时候,表示为 R4 版本。当 Bundle 需要用到 R4 中新功能的时候,如 Bundle 的慢启动,则必须显示设置 Bundle-ManifestVersion 为 2 。
OSGi框架最著名的三个实现包括Apache Felix, Equinox和Knopflerfish,这三个实现也是R4的认证实现。伴随OSGi框架的实现,通常会有相关的扩展,以进一步提供OSGi开发的工具或平台。
6 OSGi的实现
Felix项目包含了一个OSGi R4服务平台(Service Platform)标准的实现,以及大量相关的OSGi功能与技术的实现。Felix下的子项目有二十多个。除了核心框架的实现,也对主要的OSGi服务进行了实现,同时还提供了iPojo这样的OSGi编程模型(后面我们将会详细介绍)。Felix还提供了一个强大的Shell,名叫Gogo, 用以与OSGi的交互。还记得OSGi易于部署的特点吗?基于OSGi提供的管理API,你也可以实现一个于OSGi平台的交互控制台,甚至是图形化或者Web形式的交互方式。Gogo也被接下来要介绍的Virgo所采用。当然,Felix也提供了支持OSGi开发的SDK,同时还提供了一个bundle的中央仓库。Apache还有另外一个项目Aries,这个项目里主要基于Felix,对OSGi企业标准进行了实现。
Equinox:
Equinox是Eclipse 社区开发的 OSGi 框架,也是 Eclipse 强大的插件体系的基础,是Eclipse著名的PDE开发环境的底层。在Equinox的基础上,Eclipse社区还有其它一些针对企业级开发的扩展项目。2008年开始Spring社区开始将Spring的编程模型引入到OSGi中,那时项目叫做Spring-OSGi,后来改名变成Spring DM,之后成为OSGi企业应用的标准,即Blueprint。可见,Gemini Blueprint是从Spring DM发展而来。使用Gemini Blueprint编写的代码更易于测试,同时与OSGi API是松耦合的。Gemini Web是OSGi Web Application Specification的一个参考实现,目的在于在OSGi环境下更好的支持Java EE中的Servlet模型。Virgo 项目EclipseRT项目的一部分,是一个完全模块化的Java运行时。Virgo自身就是设计为在Equinox之上的一个OSGi bundle集合。Virgo可以运行企业级Java应用以及基于Spring的应用。
值得一提的是,Spring社区的OSGi相关项目大多捐献给了Eclipse社区,这些项目也很大程度上影响了OSGi在企业级应用上的发展,从标准和工具支持上,都为OSGi走向企业级应用做出了很大的贡献。Spring Source现在也维护着最大也是最全面的一个bundle仓库,叫做Enterprise Bundle Respository,将绝大多数Java企业级开发的Package转换为了OSGi bundle。当你真正将OSGi应用到实际开发中时,你就能体会到这样一个仓库给我们带来了多大的方便。
Knopflerfish :
Knopflerfish也是一个大名鼎鼎的开源OSGi服务平台实现,由Markwave公司实现,目前最新的版本支持OSGi R4 V4.2。除了提供运行环境外,Knopflerfish也提供了一套Eclipse的SDK,帮助开发者开发OSGi应用。
7 Bundle生命周期的状态转移图
8 为什么会出现OSGi
8.1 Java在模块化方面的局限性
底层代码可见性控制
虽然Java提供了一些控制修饰符,譬如public, protected, private或者包内的默认访问,但是它们都是用来控制这些低层数据对象的封装,而不是解决系统的逻辑单元的划分。Java使用了package关键字来分隔代码,但是只有声明为public的代码才能从另一个包访问(如果是继承关系的话,protected的代码是可以访问的)。所以如果应用的代码分布在不同的包里,而且它们需要相互访问,那就必须把那些方法设置成public的,这样的话,任何人都可以访问了。还有就是,这会暴露接口的实现细节,而导致以后如果想对实现做些修改,要考虑很多事情(很多直接调用实现代码的代码也可能需要修改)。Java中嵌套的包看起来好像有逻辑上的关系,其实不是,它们是两个不同的包。也就是说org.foo和org.foo.hello这两个包是两个包,并没有看起来好像存在的继承关系,org.foo.hello中的类一般也只能访问org.foo中的public的类。这个嵌套的包只是为了避免包名的冲突而已,虽然很多时候它们提供了一些逻辑上代码的分割。所以,在Java里
- 为了防止暴露你不想公开的API,你可以把尽可能多的代码放在同一个包里(削弱应用的逻辑结构)。
- 使用多个包,并且暴露相应的API,来保证应用的逻辑结构。
classpath的局限
我们在classpath中加入jar包的时候,只是简单的给出文件路径,而这个jar包的版本和一致性,它所依赖的jar包是什么,我们都无法在classpath中明确的设置或是从classpath中看出这些属性。
并且classpath中的jar包是按序加载的,例如:
classpath=c:\servlet2.2\servlet.jar;c:\servlet2.3\servlet.jar
那么在实际应用的过程中,Java让你使用的是servlet2.2,而不是servlet2.3。这种情况下我们还能看出来使用的是哪个版本,如果在大型系统中大家分开开发的时候各用各的servlet包,并且版本号不一样,那么在最后将开发结果合并的时候,到时候用的是哪个版本的servlet包就很难搞清楚了,也就说不可控性是比较强的。
即使classpath能注意到版本的问题,也没法精确指出依赖。试着回想你在设置classpath的过程中出现过情况:在你以为classpath已经设置完毕以后,你尝试启动程序,结果虚拟机抛出异常告诉你缺包,然后你再加上你觉得缺少的那些包,然后再启动程序,如此反复直到虚拟机不运行到缺包异常为止。
有限的部署和管理支持
Java中缺乏比较好的部署和管理你的应用的工具。虽然你可以使用类装载器来实现动态插入的功能,但是类装载器本身并不是用来为应用开发人员服务的常用工具。
8.2 OSGi对这些局限性的改善
- 包的可见性:OSGi通过引入包的可见性机制,能够完全控制一个包中的代码对哪些模块可见,而不仅仅局限于无差别的可见性,从而完善了Java的代码访问控制机制。
- 包的版本: OSGi通过为包增加版本信息,可以精确控制代码的依赖,保证代码的版本一致性,弥补了classpath的缺点。
9 创建OSGi程序的通用方法
- 设计:将应用程序分解为一些服务接口、接口实现和使用这些接口的客户端。——基于接口的编程
- 用选定的工具和方法来实现服务提供者、和客户端组件。
- 将服务提供者、和客户端组件打包为独立的JAR,用合适的OSGi元数据扩展每个JAR。
- 启动OSGi框架。
- 安装启动3)中的JAR。
10 OSGi的三个层次
10.1 模块层
模块层关注于打包、共享代码。模块层定义了OSGi模块的概念(bundle,即包含一个元数据MANIFEST.MF的JAR文件)。
- bundle比标准JAR文件更强大,它可以声明哪些包对外可见(Export-Package)(不用暴露细节);所以说它扩展了Java的访问修饰符。
- bundle还可以明确声明依赖哪些外部包(Import-Package),这样就可以自动地管理和验证依赖包的一致性(这个过程称为bundle解析),确保了bundle版本和其他方面约束的一致性。——这样就能清楚地看到类路径上有什么,不用到处搜寻文档了!
// MANIFEST.MF
Bundle-ManifestVersion: 2
Bundle-Name: Greeting API
Bundle-SymbolicName: com.alpha.interface
Bundle-Version: 1.0
// 导入包,
Import-Package: org.log4j;version="2.0",
.....
// 导出包,只有这些包对外可见
Export-Package: com.alpha.interface;version="1.0",
.....
10.2 生命周期层
生命周期层关注于提供执行时模块管理、和对底层OSGi框架的访问。生命周期层定义了在OSGi框架中是如何动态安装和管理的。目的:
在应用程序外部,生命周期层精确地定义了bundle生命周期的操作(安装、更新、启动、停止、卸载),使得你可以用一种明确的方式动态地提供、管理、改进你的应用程序, 意味着可以安全滴在框架中安装和卸载bundle,而不用重启应用程序。
在应用程序内部,生命周期层定义了bundle如何访问它的执行环境。
bundle中可以将一个指定的类声明为激活器Activator,作为该bundle进行自身生命周期管理的钩子。
public class MyActivator implements BundleActivator {
public void start(BundleContext context) {
Greeting.instance = new Greeting();
}
public void stop(BundleContext context) {
Greeting.instance = null;
}
}
然后将Activator通知给OSGi框架:
// MANIFEST.MF
Bundle-Activator: com.alpha.MyActivator
上例中,
在bundle启动时,会构造MyActivator实例,调用其start()方法,初始化Greeting实例;
在bundle停止时,会调用stop()方法,清除该Greeting实例。使用者可以直接使用预先配置好的实例,而不需要自己创建。
10.3 服务层
服务层关注于模块,特别是模块内的组件间的交互和通信。服务层支持和促成了一个灵活的应用编程模型。涉及面向服务的发布、查找和绑定的交互模式:
- 服务提供者将服务发布到服务注册中心,
- 服务客户端搜索服务注册中心,查找可供使用的服务。
其实就是面向接口编程,不同之处在于:
- 如何使服务的实例在程序的其他部分可用。——注册服务
- 如何使程序的其他部分发现这个可用的服务。——检索服务
// 注册服务
public class Activator implements BundleActivator {
//在start()中用BundleContext.registerService()注册服务
public void start(BundleContext context) {
context.registerService(Greeting.class.getName(), new GreetingImpl(), null);
}
//无需在stop()中注销服务,因为bundle停止时会自动注销该bundle中已注册的服务
public void stop(BundleContext context) {}
}
// 检索服务
public class Client implements BundleActivator {
public void start(BundleContext context) {
// 1、从服务注册表中检索间接的“服务引用”
ServiceReference ref = context.getServiceReference(Greeting.class.getName());
// 2、使用“服务引用”去访问服务对象的实例
((Greeting) context.getService(ref)).sayHello();
}
public void stop(BundleContext context) {}
}