《<mark>OSGI</mark>实战》:第2章 精通模块化(一)

 由  《<mark>OSGI</mark>实战》 发布

本章内容

  • 模块化及其优势
  • 使用元数据描述OSGi bundle(亦称为模块)
  • 使用bundle元数据管理代码的可见性
  • 使用bundle创建应用程序

前一章走马观花地浏览了OSGi的全貌。我们注意到标准Java在模块化方面有许多不完善的地方,并举例说明了OSGi的用途。还介绍了一些OSGi概念,包括OSGi框架的核心层次:模块层、生命周期层和服务层。 本章将专门介绍模块层,因为大多数Java开发者最初是被模块层的功能吸引才使用OSGi的。模块层是OSGi其他部分的基础。本章将全面讲解OSGi模块是什么,为什么在一般意义上说模块化是重要的,以及模块化在设计、构建和将来维护Java程序时能给予你什么帮助。 本章的目标是使你以模块的方式而不是以JAR文件的方式进行思考。我们将教你有关OSGi模块元数据的知识,你将会掌握如何使用它来描述应用的模块化特征。为了阐明这些概念,我们继续以第1章介绍过的简单绘图程序作为例子进行说明。你将会把它从一个臃肿的程序转变为一个模块化的程序。开始学习模块化吧。

2.1 什么是模块化

模块化涵盖程序设计的很多方面,以至于我们认为这是理所当然的。你所拥有的系统设计经验越多,就越知道好的设计往往是模块化的。但是模块化确切的定义是什么呢?简而言之,就是利用一组逻辑上独立的组件集合设计出完整的系统,这些逻辑上独立的组件可称为模块。你或许有疑问:“那就是模块化吗?”理论上来说,是的。当然这些简单的概念下面蕴涵着许多细节。 模块定义了强制性的逻辑边界:代码要么是模块的一部分(在模块里面),要么不是模块的一部分(在模块外面)。模块内部的(实现)细节只对模块内部的代码可见,而其他代码只能看到模块明确公开的部分(公共API),如图2-1所示。模块的这个特点使得它可以作为设计应用逻辑结构必不可少的组成部分。

http://assets.osgi.com.cn/article/7289381/2-1.jpg

图2-1 模块定义了逻辑边界。模块自身显式地控制哪些类完全封装,哪些类公开给外部使用

模块化与面向对象 你可能会问:“面向对象不是也有这些特性吗?”是的,面向对象也致力于解决这些问题。你会发现模块化与面向对象有很多共通之处。这两种应用程序设计概念相似的一个原因是,它们都是关注点分离(separation of concerns)的一种形式。关注点分离的思想是将一个系统分解为最低限度的重叠功能或关注点,所以你可以独立地推敲、设计、实现和使用每个关注点。模块化是关注点分离最早的一种形式。它在20世纪70年代初就开始流行了,而面向对象则于80年代初开始流行。

如前所述,现在你可能会问:“如果我在Java中已经使用了面相对象,为什么我还同时需要模块化呢?”这是个好问题。对二者的需求缘于粒度(granularity)。

假设你的应用需要某个功能。你坐下来开始写一些Java类去实现这个功能。你通常会用一个类实现所有功能吗?不会。就算功能一点都不复杂,你也会使用一组类来实现。你也有可能会使用你的项目的其他部分或者是JRE中的现有类。完成后,类之间的逻辑关系就建立了。但是如何表示这种关系呢?当然是在代码的底层细节中表现,因为如果所有的类不是在编译时就存在,那么就不会满足编译时的依赖关系。同样地,在运行时,如果在你执行应用程序时,所有的类没有都出现在类路径上,那么这种依赖关系也会被破坏。

很遗憾,类之间的这些关系只能通过细看底层代码或者反复试验的方式才能弄清楚。你可以使用类封装单个逻辑概念的状态和行为。但是创建一个精心设计的应用通常需要众多的类。模块封装了类,使得你可以表达应用中类(或者概念)之间的逻辑关系。图2-2阐明了模块如何封装类以及由此产生的模块间的关系。Java包不也允许表达这种逻辑的代码关系吗?是的。包是Java提供的一种内建的模块化方式,但是包有一些限制,参见1.1.1节。所以要理解模块化如何帮助封装代码,包是一个好的起点,但你需要一种更加深入的机制。最后,面向对象和模块化服务于不同但互补的目标(参见图2-3)。

使用Java进行开发时,可以把面向对象看做是模块的实现方式。像这样开发类时,你是在微观编程,这意味着你并不是考虑应用的整体结构,而是考虑具体的功能。在把相关类逻辑地组织成模块后,开始进行宏观编程,这意味着你关注于更大的系统逻辑组件和这些组件间的关系。

http://assets.osgi.com.cn/article/7289381/2-2.jpg

图2-2 类之间由于代码中包含的引用而有显式的依赖关系。模块由于它们包含的代码而有隐式的依赖关系

http://assets.osgi.com.cn/article/7289381/2-3.jpg

图2-3 尽管面向对象和模块化提供了相同的功能,但是它们从不同粒度层次来实现这些功能

除了通过模块的成员关系表达类之间的关系外,模块还可以显式声明对外部代码的依赖来表达逻辑系统结构。明白这一点后,就可以更加具体地定义本书中模块的含义了。

模块 一个从逻辑上封装实现类的集合,一个基于实现类子集的可选公共API,以及一个对外部代码依赖关系的集合。

尽管这个定义意味着模块包含类,但是从这个观点来说,这种包含纯粹是逻辑意义上的。模块化另一个需要理解的方面是物理模块化,这涉及模块代码的容器。

逻辑模块化与物理模块化
模块在应用程序中定义了逻辑的边界,以一种类似面向对象编程中访问修饰符的方式影响着代码的可见性。逻辑可见性就是指这种形式的代码可见性。物理模块化指的是代码如何封装或部署。

在OSGi中,这两个概念大体上是合二为一的;bundle既是逻辑模块也是物理模块(即JAR文件)。尽管这两个概念在OSGi中差不多是同义词,但是对于一般意义上的模块化来说则不是,因为有可能有逻辑模块化没有物理模块化或者一个物理模块封装着多个逻辑模块。物理模块有时候也称作部署模块或者部署单元。

利用OSGi模块层你可以正确地表达应用的模块化特性,但这并不是没有代价的。让我们更加深入地探究为什么应该使你的应用程序模块化,这样你就可以自己决定是否模块化。

2.2 为什么使用模块化

我们已经介绍过什么是模块化,但是还没有深入地讨论为什么要把应用程序模块化。事实上,你可能会想:“如果模块化已经有大概40年的历史,而且是如此的重要,为什么不是每个人都那么做呢?”这是个好问题,但是很可能没有唯一的答案。计算机产业的发展由创新来驱动,所以当新的事物来临时,我们倾向于抛弃老的事物。公平地说,正如上一节所讨论的,新技术和方法(例如面向对象和面向组件)的确提供了一些模块化能带来的好处。

模块化再次成为关注点的另一个重要原因是Java。从传统意义来说,编程语言属于逻辑模块化机制,而操作系统和部署打包系统则属于物理模块化领域。Java使二者的差别变得模糊,因为Java既是一种编程语言也是一个平台。.NET平台也是如此。鉴于其开发操作系统的历史和饱受DLL折磨的痛楚,微软在开发.NET的早期就认识到二者的联系,这就是.NET有一个称为集合的模块概念的原因。最终,随着应用规模的逐渐增长,模块化成为了控制应用复杂度的重要手段——分而治之!

这个讨论虽然解释了模块化重新流行起来的可能原因,但是并没有回答本节最初的问题:为什么要使你的应用模块化。模块化使你能够推断应用的逻辑结构。两个由模块化引出的重要概念在数十年前就已经出现。

内聚 用于衡量一个模块的类相互关联的程度,以及共同完成模块既定功能的紧密程度。你应该尽量使你的模块高内聚。例如,一个模块不应该处理多个不同的关注点(网络通信、持久化、XML解析,等等),而应该关注于单个关注点。

耦合 与内聚相反,是指不同模块束缚或者依赖于其他模块的程度。你应该尽量降低模块间的耦合度。例如,不能让每个模块都互相依赖。

使用OSGi以模块化方式设计应用程序时,不可避免地会遇到这些问题。以模块化方式构建应用程序有助你以一种全新的方式看待应用程序。

记住这些内聚和耦合的原则,你可以创建可重用性更高的代码,因为可以更加容易地重用一个实现单一功能并且没有过多地依赖其他代码的模块。

更准确地说,使用OSGi创建模块化的应用可以解决1.1.1节讨论过的Java的限制。另外,模块显式地声明了它们对外部代码的依赖,重用可以得到进一步的简化,因为你不再需要四处搜寻文档,或者采取反复试验的方式来弄清楚类路径上有什么。这使得代码更加适合于合作的、独立的开发方式,例如由多个团队在多个地点合作开发的项目,或者大规模开源项目。

现在你知道了模块化是什么以及你需要它的原因,那么让我们开始关注OSGi如何提供模块化功能以及为了在应用程序中使用它,你需要做什么。绘图程序示例将帮助你理解这些概念。

2.3 模块化绘图程序

OSGi模块层提供的功能是成熟的,并且总体看来,似乎是一种不可阻挡的趋势。我们将通过第1章讨论过的简单绘图程序来学习如何使用OSGi的模块层,这个绘图程序不是从头开始创建一个新的绘图程序。已有的实现遵循基于接口的方式,有着具有逻辑关系的包结构,所以是可以模块化的,但是此绘图程序当前被打包成了一个JAR文件。代码清单2-1展示了绘图程序JAR文件的内容。

代码清单2-1 已有绘图程序的JAR文件的内容

http://assets.osgi.com.cn/article/7289381/1.jpg

开头是一个标准的清单文件(manifest file),然后是应用程序的类,接着是各种图形的实现。组成绘图程序的主要的类如表2-1所示。

描述
org.foo.paint.PaintFrame绘图程序的主窗口,包含工具栏和画布。还有一个用于启动程序的静态main()方法
org.foo.paint.SimpleShape表示用于绘图的抽象图形的接口
org.foo.paint.ShapeCompone负责在画布上绘制图形的GUI组件
org.foo.shape.Circle用于绘制圆形的SimpleShape的实现
org.foo.shape.Square用于绘制正方形的SimpleShape的实现
org.foo.shape.Triangle用于绘制三角形的SimpleShape的实现

熟悉Swing的人应该知道,PaintFrame继承并扩展了JFrame并包含一个JToolBar和一个JPanel画布。PaintFrame维护着一组可用的SimpleShape实现,并将它们显示于工具栏中。当用户从工具栏选择一个图形,并在画布中通过单击绘图时,会在画布中用户单击的地方添加一个ShapeComponent(继承并扩展了JComponent)。ShapeComponent通过从其PaintFrame的引用中检索出来的名字与具体的SimpleShape实现关联起来。图2-4展示了绘图程序GUI中一些主要的UI元素。

PaintFrame中的静态方法main()启动绘图程序,为PaintFrame和每个图形实现各创建一个实例,并将每个图形实例添加到创建的PaintFrame实例中。为了进一步解释说明,图2-5展示了绘图程序的类之间的相互关系。

http://assets.osgi.com.cn/article/7289381/2-4.jpg

图2-4 绘图程序是一个简单的Swing应用

http://assets.osgi.com.cn/article/7289381/2-5.jpg

图2-5 绘图程序的类之间的关系

如果想要运行这个非模块化版本的绘图程序,进入配套源码chapter02/paint-nonmodular/目录,输入ant命令构建应用,然后输入java -jar main.jar命令运行该应用。随意点击,看看它是如何工作的;我们不会讨论更多的程序实现细节,因为GUI编程超出了本书的范畴,这里重要的是要理解程序的结构。通过理解结构,你将会把程序分割成一些bundle,从而提高和强化它的模块化。

目前,这个绘图应用程序打包成了单个JAR文件,我们称之为程序的1.0.0版。因为一切都在单个JAR文件中,这意味着程序还没有被模块化。当然单个JAR文件的程序仍然可以使用一种模块化的方法实现——并不能因为一个应用程序由许多JAR文件组成,而认为它是模块化的。绘图程序在逻辑模块化和物理模块化方面都有改进的空间。首先,我们将检查程序的逻辑结构,并通过定义模块来增强这种结构。从哪里入手呢?

你可以找到的最容易实现的目标是公共API。在OSGi中分离出公共API(稍后将会介绍原因),并把它们放进包里,这是一种不错的做法,因为它们可以被方便地共享而无须担心暴露实现细节。绘图程序有一个公共API的好例子:SimpleShape接口。通过这个接口能够很容易地实现应用于程序的中新的、可能是第三方的图形。很遗憾SimpleShape和程序的实现类在相同的包里。为此,需要轻微地调整包的结构,把SimpleShape移到org.foo.shape包,并把所有图形的实现移到名为org.foo.shape.impl的新包中。这些改变使得绘图程序根据包的结构分割成三个逻辑组件。

org.foo.shape——用于创建图形的公共API。
org.foo.shape.impl——各种图形的实现。
org.foo.paint——应用程序实现。

考虑到这种结构(逻辑模块化),你可以将每个包打包成独立的JAR文件(物理模块化)。为了让OSGi检验和强化模块化,只把代码打包成JAR文件是不够的:你必须把它们打包成bundle。为此,你需要理解OSGi的bundle概念,bundle是模块化的逻辑和物理单元。接下来将介绍bundle。

2.4 bundle

如果打算使用OSGi技术,那么你最好熟悉bundle这个术语,因为你将经常听到和说到它。bundle是OSGi对其模块化概念具体实现的定义。模块和bundle这两个术语将会在接下来的章节中交替使用;但是绝大多数情况下,我们特指bundle而不是普通的模块,除非以其他方式注明。我们对如何使用bundle这个术语已经做了足够详细的说明——现在给个定义吧。

bundle 一个模块化的物理单元,以JAR文件形式包含代码、资源和元数据,其中JAR文件的边界也作为执行时逻辑模块化的封装边界。

bundle中内容的图形化表示如图2-6所示。bundle JAR文件与普通JAR文件的主要区别是模块元数据。OSGi框架使用元数据来管理它的模块化特征。所有的JAR文件,甚至包括不是bundle的JAR文件,都在它们的清单文件中保存元数据,更加具体地说,是在JAR文件的META-INF/ MANIFEST.MF条目里保存元数据。OSGi就是把模块元数据保存在这里。无论何时我们提到bundle的清单文件(manifest file),都特指标准JAR 清单文件中与模块相关的元数据。

http://assets.osgi.com.cn/article/7289381/2-6.jpg

图2-6 bundle包含标准JAR文件中所有常用的东西。唯一的主要不同点是清单文件包含描述bundle模块特征的信息

注意,除了将模块化的物理和逻辑方面组合到一个概念中以外,bundle的定义与模块的定义相似。所以在介绍定义bundle元数据的方法之前,需要先我们详细地讨论bundle在物理和逻辑模块化中扮演的角色。

2.4.1 bundle在物理模块化中扮演的角色

bundle在物理模块化中的主要功能是确定模块的成员关系。但是并没有与将一个类变成bundle的成员相关联的元数据。如果一个类包含在bundle的JAR文件中,那么它就是bundle的成员。这样做的好处是,你无须为了使一个类成为bundle的成员,而特意做一些事情:只要把这个类放到bundle的JAR文件中即可。

这种类的物理包含引出了bundle JAR文件另外一个重要的功能——作为部署单元使用。bundle JAR文件是可触知的,当与OSGi一起工作时,可以共享、部署和使用它。bundle JAR文件最后一个重要的角色是作为bundle 元数据的容器,因为正如我们前面提到的,JAR 清单文件用于存储这些元数据。bundle的这些方面如图2-7所示。元数据存储的问题是一个持续争论的部分,我们在补充内容中为感兴趣的读者说明这个问题。

http://assets.osgi.com.cn/article/7289381/2-7.jpg

图2-7 如果将类打包到一个bundle中,那么它就是该bundle的一个成员,bundle里面包含它自己的模块元数据,模块元数据是清单数据的一部分,bundle能够作为一个单元部署到运行时环境中

元数据应该保存到哪里
将模块元数据保存到物理模块而不是类本身是好事吗?对于该问题有两种看法。一种认为将元数据与它所描述的代码放在一起(保存在源代码文件自身中)更好,而不是放在一个单独的文件中,这样更难看出元数据与代码之间的联系。在许多技术中该方法是可行的,例如doclet或者Java 5中引入的注解(annotation)机制。

注解是目前推荐的选择。很遗憾,在1999年OSGi相关工作刚起步时,并没有注解这个选项,因为它们还没出现。除此之外,还有一些很好的理由将元数据保存在一个单独的文件中,这些理由就是第二种观点。

这种观点认为不把元数据保存到源代码中更好,因为这样很难改变元数据。把元数据保存到一个独立的文件中会有很大的灵活性。考虑独立模块元数据的以下好处。

1.不必为修改元数据而重新编译bundle。

2.不必访问源代码就可以添加或者修改元数据,当处理遗留或者是第三3.方库时,有时需要这样做。

4.不必将类加载到JVM中就可以访问相关的元数据。

5.你的代码对OSGi API没有编译时依赖。

6.可以在多个模块中使用相同的代码,这是比较方便的,甚至在封装模块的情况下是必须的。

7.可以在不支持注解的更老的或者更小的JVM中很容易地使用你的代码。

不管你选择的方法是否是注解,都会看到通过在清单文件中维护模块元数据,你会获得很大的灵活性。

2.4.2 bundle在逻辑模块化中扮演的角色

和bundle JAR文件物理地封装成员类的方法相似,bundle在逻辑模块化中扮演的角色是逻辑地封装成员类。准确地讲,这意味着什么?这意味着与代码可见性明确关联。想象一下,在不是项目公共API的util包中有一个工具类。要想在项目的不同包中使用该工具类,它必须是public。遗憾的是,这意味着任何的代码都可以使用这个工具类,尽管它不是你的公共API的一部分。

由bundle创建的逻辑边界改变了这种状况,为bundle内部的类赋予不同于外部代码的可见性规则,如图2-8所示。这意味着在bundleJAR文件内部的public类不必对外部可见。你可能会想:“什么?”这并不是语误:这是bundle与标准JAR文件之间最大的不同。只有通过bundle元数据明确暴露给外部的代码才对外部可见。通过模块私有的可见性(只在模块内部可见),逻辑边界有效地扩展了标准Java访问修饰符(public、 private、 protected和包私有)。如果你熟悉.NET,这类似于internal访问修饰符,将一些东西标记为在一个集合内可见,但是对其他集合则是私有的。

http://assets.osgi.com.cn/article/7289381/2-8.jpg

图2-8 bundle中包含的包(因此也包含包中的类)对于该bundle是私有的,除非显式地暴露,才允许其他bundle共享

正如你看到的,bundle的概念在物理模块化和逻辑模块化中都扮演着重要的角色。现在我们可以开始分析如何使用元数据描述bundle了。

使用元数据定义bundle

本节将详细讨论OSGi bundle元数据。你将使用绘图程序作为用例来理解这个理论。bundle元数据的主要作用是精确地描述bundle中与模块化相关的特性,从而使OSGi框架能够恰当地处理bundle,例如解析依赖关系和强制封装。与模块相关的元数据表示了以下几条有关bundle的信息。

可读信息——纯粹是为了给使用bundle的人提供帮助的可选信息。

bundle识别——识别bundle的必要信息。

代码可见性——定义哪些代码内部可见和哪些代码外部可见的必要信息。

我们将在接下来的小节中详细介绍这几方面。但是因为OSGi依赖于清单文件,我们用补充内容解释那些难以掌握的语法细节和OSGi扩展的清单文件值语法。幸好有用于编辑和产生bundle元数据的工具,所以不必手动创建,但是还是有必要理解语法的细节。

JAR文件清单文件语法
JAR文件清单文件由许多组name-value对(属性)组成。属性声明的一般形式是: name:value name不区分大小写,可以包含字母、数字、下划线和连字符。value可以包含除回车符(carriage return)和换行符(line feed)外的任何字符信息。name和value之间必须用一个冒号和一个空格分隔。一行不超过72个字符。如果必须超过这个长度,必须在下一行继续,下一行由单个空格字符开始,紧跟着value的其余部分。在OSGi中清单行可以非常长,所以知道这点是非常有用的。 把属性声明放到清单文件的连续行(一行紧接着另一行)中定义一个属性组。属性声明之间的空行表示不同的属性组。OSGi使用模块元数据的第一个属性组,称为主属性。在一组中,属性的顺序不重要。可以从清单文件中看到类似下面这样的内容:
在本节接下的部分将说明大部分属性的准确含义。但是现在我们关注的是语法。然而标准Java 清单文件语法是name-value对,OSGi为其特有的属性值定义了一个通用的结构。大部分OSGi清单文件属性值是由逗号分隔的一组子句,例如:
每个子句(clause)进一步分解为一个目标(target)和一组由分号分隔的name-value对参数(parameter):
参数分成两种类型,分别称作属性(attribute)和指令(directive)。指令改变框架处理相关信息的方法,OSGi规范明确地定义了指令。属性是任意的name-value对。你将在后面看到如何使用这些指令和属性。稍微不同的语法用于区分指令(:=)和属性(=),比如:
记住,你可以给每个目标赋予任意数量的指令和属性,这些指令和属性都有不同的值。包含空格或者是分隔符的值应该用引号括起,防止解析错误。有时你有许多有着相同指令和属性集的目标。在这种情况下,OSGi提供了一种避免重复所有重复指令和属性的快捷方式,如下:
相当于使用它们自己的指令和属性单独地列出目标。这是理解OSGi 清单文件属性结构所需的全部内容。并不是所有OSGi 清单文件值都遵循这一通用结构,但大多数是,所以你应该熟悉它。

2.5.1 可读信息

大部分bundle元数据供OSGi框架读取和解释,力求为Java提供一个通用的模块层。但是一些bundle元数据除了帮助人们理解bundle的功能和来源外,并没有其他作用。OSGi规范定义了几条用于这种目的的元数据,但是都不是必须的,对模块化不产生任何影响。OSGi框架完全忽视它们。 下面这个代码片段展示了绘图程序的org.foo.shape bundle中可读的bundle元数据(其他程序的bundle具有相似的描述):

http://assets.osgi.com.cn/article/7289381/7.jpg

Bundle-Name属性的用途是作为bundle的一个缩写名。你可以随意地给你的bundle命名。尽管应该是一个简短的名字,但是并没有强制要求;用不用由你决定。Bundle-Description属性用于更详尽地描述bundle的功能。为了提供更多有关bundle的文档,可使用Bundle-DocURL指定一个引用文档的URL。Bundle-Category定义了一组由逗号分隔的分类名。OSGi并没有定义任何标准的分类名,所以你可以自由地选择你自己的分类名。剩下的Bundle-Vendor、Bundle- ContactAddress和Bundle-Copyright属性提供了有关bundle提供商的信息。 可读的元数据非常简单。OSGi框架忽略元数据意味着你可以使用元数据做很多你想做的事情。但是不要因此放任自流——剩余的元数据要求更加精确。接下来,我们将看看如何使用这些元数据唯一地标识bundle。

bundle标识

上一小节介绍的可读元数据帮助你理解bundle的功能和来源。一些可读的元数据看起来也在标识bundle方面起着作用。例如Bundle-Name似乎可以作为bundle标识的一种形式。实际上不可以。有些是历史原因造成的。OSGi规范的早期版本并没有给出唯一标识一个给定bundle的方法。Bundle-Name纯粹是为了提供信息,因此对它的值并没有施加任何限制。作为OSGi R4规范制订过程的一部分,提出了唯一bundle标示符的概念。由于向后兼容性的原因,不能因为这个目的而征用Bundle-Name,因为在它身上施加新的限制同时维护向后兼容性是不可能的。相反,引入了一个新的清单文件条目:Bundle-SymbolicName。

与只是为了供用户使用的Bundle-Name不同,Bundle-SymbolicName只用于帮助OSGi框架唯一地标识bundle。符号名(symbolic name)的值遵循与Java包命名相似的规则:由一系列点分(dot-separated)字符串组成,推荐使用反向域名命名从而避免名字冲突。尽管框架强制使用点分结构,但是并没有强制使用反向域名。你可以自由地选择一种不同的模式,但是如果你真的这样做,请记住主要的目的是提供唯一标示,所以尽量选择一种不会导致名字冲突的模式。

标识绘图程序(第1部分)

绘图程序分割成基于包的bundle,所以可以使用每个包作为符号名,因为它们已经遵循一种反向域名模式。对于一个公共API bundle,在清单文件中声明符号名,如:

http://assets.osgi.com.cn/article/7289381/8.jpg

尽管只使用Bundle-SymbolicName就可以唯一地标识一个bundle,但是随着时间推移,这会有点棘手。考虑当你发布bundle的第2版时会发生什么:为了使它唯一,你需要改变符号名,例如org.foo.shapeV2。这是可以的,但是显得笨拙;更糟糕的是,版本信息对OSGi框架不是透明的,这意味着模块化层不能利用它。为了弥补这种缺憾,一个bundle不仅由它的Bundle-SymbolicName唯一标识,而且也由它的Bundle-Version标识。Bundle-Version的值遵循OSGi版本号格式(参考补充内容“OSGi版本号格式”)。这个属性对不但形成了一种标示符,同时允许框架表示同一bundle多个版本间的时间顺序关系。

标识绘图程序(第二部分)

例如,下面的元数据唯一地标识绘图程序的公共API bundle:

http://assets.osgi.com.cn/article/7289381/9.jpg

尽管从技术的角度来说只有Bundle-SymbolicName和Bundle-Version与bundle的标识有关,但是Bundle-ManifestVersion属性也起作用。从R4规范开始,给bundle指定Bundle- SymbolicName变成强制性的。这是理念上的一个重大转变。为了维护R4规范以前创建的遗留bundle的向后兼容性,OSGi引入了Bundle-ManifestVersion属性。现在,这个属性唯一有效的值是2,R4规范以及之后创建的bundle都使用这个值。任何没有Bundle-ManifestVersion的bundle都不要求被唯一地标识,但是有它的bundle就必须被唯一地标识。

标识绘图程序(第三部分)

下面的例子展示了标识图形bundle的完整OSGi R4元数据:

http://assets.osgi.com.cn/article/7289381/10.jpg

这是公共API bundle完整的标识元数据。其他绘图程序bundle的标识元数据的定义方式类似。既然bundle标识的问题解决了,我们准备来看看代码的可见性,这可能是元数据中最重要的部分。

OSGi版本号格式
在OSGi中不断地碰到的一个重要概念是版本号,它出现在bundle标识元数据中。OSGi规范定义了一个通用的版本号格式,规范的很多地方都会用到它。因此,值得用几段文字来说明在OSGi世界中版本号究竟是什么。

版本号由三个单独的由点号分隔的数字组成;例如,1.0.0是一个有效的OSGi版本号。第一个值称作主版本号(major number),第二个值称作次版本号(minor number),而第三个称作微版本号(micro number)。这些名字反应了每个数值的相对优先级,而且类似其他版本号模式。也就是版本号根据各个数字部分的比较,按优先级降序排列:换句话说,2.0.0比1.2.0新而1.10.0比1.9.9新。

也可以有第四个版本组成部分,称作限定符(qualifier)。限定符可以包含字母和数字字符;例如,1.0.0.alpha是一个有效的带限定符的OSGi版本号,当比较版本号时,限定符使用字符串比较的方式。如下图所示,用这种方式比较版本号,结果有时并不直观;例如,尽管1.0.0.beta比1.0.0.alpha新,但是1.0.0比两者都老。
OSGi版本化语义有时可能会使结果不直观。

在元数据中预计有一个版本的地方,如果省略了版本,那么它默认为0.0.0。如果省略了版本号的一个数字组成部分,它默认为0,而限定符默认为一个空字符串。例如,1.2等价于1.2.0。棘手的问题是,如果不明确指定版本的所有数字组成部分,就不可能有限定符。所以不能指定1.2.build-59,必须指定1.2.0.build-59。

OSGi使用这种通用的版本号格式给bundle和Java包指定版本。第9章将讨论管理包、bundle和应用程序的版本号的高级方法。

2.5.3 代码可见性

可读和bundle标识元数据非常重要,但是不足以描述bundle的模块化特征。OSGi规范定义了元数据,全面描述哪些代码在bundle内部可见,哪些内部代码对外部可见。用于代码可见性的元数据表达了以下信息。

  • bundle内部类路径——构成bundle的代码。
  • 导出内部代码——显式地公开bundle 类路径的代码,从而可以与其他bundle共享。
  • 导入外部代码——bundle类路径代码依赖的外部代码。

每一项都表达了独立而又相关的信息,这些信息描述了哪些Java类可以在bundle内部访问,哪些通过bundle可以访问。我们将详细阐述每一项,但是在这之前,先退一步来剖析在传统Java编程中,如何使用JAR文件和Java 类路径。这将为比较OSGi代码可见性的方法提供基础。


注意 因为标准JAR文件是假设全局类型可见性的前提下编写的(例如,如果它在类路径上,你就可以使用它),所以它作为bundle通常会失败。如果你打算创建有效的bundle,就必须从这种旧的假设中解放出来,完全理解和接受:bundle的类型可见性纯粹是基于我们在本节描述的原语。为了更清楚地理解这点,我们将深入了解标准JAR文件和bundle JAR文件的类型可见性规则。尽管这可能很难理解,但是了解这些差异是很重要的。


在标准JAR文件中代码的可见性和类路径

通常来说,你将Java源代码文件编译成类,然后使用jar工具从这些产生的类中创建一个JAR文件。如果JAR文件在清单文件中有一个Main-class属性,你可以像这样运行应用程序:

http://assets.osgi.com.cn/article/7289381/12.jpg

否则,你可以把JAR文件添加到类路径,然后像下面这样启动程序:

http://assets.osgi.com.cn/article/7289381/13.jpg

图2-9展示了JVM执行Java程序要经过的步骤。首先搜索Main-class属性中指定的类或者命令行中指定的类。如果找到了该类,则在该类中搜索方法static public void main(String[])。如果找到了该方法,则调用它启动应用程序。当应用程序执行时,它需要的任何其他类都是通过搜索类路径找到。类路径由JAR文件中应用程序的类和标准JRE类(以及添加到类路径的任何东西)组成。类按需加载。

这体现了对Java执行JAR文件中的应用程序的深层理解。但是这个高层次的视角掩盖了标准JAR文件处理过程中的一些隐式决策,例如:

  • 在JAR文件内部的何处寻找请求的类?
  • 哪些内部的类应该对外部公开?

关于第一个决策,JAR文件的隐式策略是寻找相对于JAR文件根的所有目录,就好像它们是所请求的类对应的包名称(例如,类org.foo.Bar位于JAR文件的org/foo/Bar.class目录下)。关于第二个决策,JAR文件的隐式策略是向所有请求者公开相对根的包里面的所有类。这是JAR文件行为的一个具有高度破坏性的视角,但是有助于说明标准JAR文件的隐式模块化决策。当把一个JAR文件放到执行类路径上时,这些隐式的代码可见性决策就会生效。

http://assets.osgi.com.cn/article/7289381/2-9.jpg

图2-9 从类路径中执行一个Java程序时JVM所需步骤的流程图

执行时,JVM通过搜索类路径找出所有需要的类,如图2-10所示。但是对于模块化来说,类路径的准确目的是什么?类路径定义了哪些外部类对JAR文件的内部类可见。在类路径上每一个可访问的类对应用类都是可见的,即使不需要。

http://assets.osgi.com.cn/article/7289381/2-10.jpg

图2-10 从类路径加载类时JVM所需步骤的流程图

了解了标准JAR文件和类路径机制的工作方式后,我们详细看看OSGi如何处理这些相同的代码可见性概念,它的处理方式有很大不同。我们首先介绍OSGi如何在bundle内部搜索代码,然后介绍OSGi如何向外部公开内部的代码,最后介绍如何使外部代码对内部bundle代码可见。让我们开始吧。

bundle内部类路径

标准JAR文件隐式地从JAR文件根目录下的所有目录里搜索内部类,这些目录就像包名称一样,然而OSGi使用一种称为bundle类路径的更加显式的方法。与标准Java类路径概念类似,bundle类路径是搜索类的位置列表。不同的是,bundle类路径是指位于bundle JAR文件内部的位置。

bundle类路径 一个有序的、用逗号分隔的相对bundle JAR文件位置列表,可在这些位置搜索类和资源请求。

当某个bundle类需要位于同一bundle中的另一个类时,为了找出这个类,需要搜索包含该bundle的所有bundle类路径。同一bundle中的类可以访问这些bundle类路径可以到达的所有代码。下面来分析一下声明它的语法。

bundle使用Bundle-ClassPath 清单文件头标识来声明它们的内部类路径。就搜索算法来说,bundle类路径的工作流程与全局类路径一样,所以你可以参考图2-10查看其工作流程;但是在本例中,范围仅限于包含于bundle内部的类。使用Bundle-ClassPath,你可以指定一个bundle内部的路径列表,类加载器从那里查找类或资源。例如:

http://assets.osgi.com.cn/article/7289381/14.jpg

这说明了OSGi框架应该在bundle内部的什么地方搜索类。点号(.)表示bundle JAR文件。就本例而言,bundle首先搜索相对于根的包,然后是称作other-classes的文件夹,最后是bundle的内嵌JAR文件。顺序是重要的,因为OSGi按照声明的顺序搜索bundle 类路径条目。

Bundle-ClassPath在某种意义上说是唯一的,因为OSGi 清单文件头标识通常不会有默认值。如果你不指定一个值,框架提供的默认值是点号(.)。为什么Bundle-ClassPath有一个默认值?答案与如何在标准JAR文件中搜索类有关。bundle类路径的值.对应于标准JAR文件的内部搜索策略。把.放到bundle类路径中,当搜索类时,同样把所有相对根的目录当作包来处理。把.当作默认值,会使标准JAR文件和bundle JAR文件具有相同的默认内部搜索策略。


注意 当且仅当Bundle-ClassPath没有指定值时,它的默认值才是.,这并不是说值.默认就 包含于bundle类路径中,理解这点非常重要。换种说法,如果你给Bundle-ClassPath指定一个值,那么只有当你显式地在逗号分隔的位置列表中指定.时,它才包含于bundle类路径中。如果你指定一个值但是没有包含.,那么当在bundle JAR文件中查找类时,不会搜索相对于根的目录。


正如你所见到的,当定义bundle的内容和内部搜索顺序时,内部bundle类路径概念是强大且灵活的。参考补充内容“bundle类路径的灵活性”,你可以看到一些例子,了解灵活性什么时候是有用的。接下来,你将学习为了与其他bundle共享,如何公开内部代码。

bundle 类路径的灵活性
你可能想知道为什么想将类打包到不同的目录或者将JAR文件嵌入到bundle JAR文件中。首先,bundle类路径机制不但应用于类,而且应用于资源。一种通用的做法是将图片放到一个image/目录,清晰地表明某种内容在JAR文件里面什么地方可以找到。同时,在Web应用中,嵌套的JAR文件放到bundle JAR文件的WEB-INF/lib/目录中,而类可以放到WEB-INF/classes/目录中。

在其他情况下,你可能有一个不能更改的遗留的或私有的JAR文件。通过把JAR文件嵌入到bundle中,并添加bundle元数据,你可以不更改原来的JAR文件就使用它。当你希望你的bundle有某个库的一份私有副本时,内嵌一个JAR文件也非常方便。当你想要避免与其他bundle共享静态库成员时,这特别有用。

内嵌一个JAR文件并不是必须的,因为解压一个标准JAR文件到你的bundle中,也可以达到同样的效果。顺便说一句,不内嵌JAR文件,也可能看到性能上的提升,因为OSGi框架实现必须解压内嵌的JAR文件后才能访问它们。

导出内部代码

Bundle-ClassPath影响bundle中类的可见性,但是如何在bundle间共享类呢?第一步是导出你希望与其他bundle共享的包。

对外部有用的类组成了JAR文件中代码的公共API ,然而没用的类构成实现的细节。标准JAR文件没有提供任何机制,区分对外部有用的类和没用的类,但是OSGi提供了。一个标准JAR文件默认公开相对于根的一切内容,但是OSGi bundle默认是什么都不公开。一个bundle必须使用Export-Package 清单文件头标识来显式地公开希望与其他bundle共享的内部类。

Export-Package 一个为了与其他bundle共享而公开的、由逗号分隔的内部bundle包列表。

与暴露单独的类相反,OSGi在包层次定义bundle间的共享。尽管这会使导出代码的任务简单一些,但是这对于大项目来说可能仍然是一个主要的负担。我们将在附录A中介绍一些工具来简化它。当在Export-Package声明中包含一个包时,该包的所有公共类都向其他bundle公开。绘图程序图形API bundle的一个简单的例子如下所示(图2-11展示了导出模块包的图形化表示):

http://assets.osgi.com.cn/article/7289381/15.jpg

http://assets.osgi.com.cn/article/7289381/2-11.jpg

图2-11  一个导出包的图形化描述

在这里,你导出了org.foo.shape包里面的所有类。你可能想要一次导出bundle中的多个包。可以导出多个包,使用逗号分隔它们:

http://assets.osgi.com.cn/article/7289381/16.jpg

也可以给导出包附加一些属性。因为不同的bundle可能导出相同的包,某个bundle可以使用属性将它的导出包与其他bundle区分开。例如:

http://assets.osgi.com.cn/article/7289381/17.jpg

这把包含值"Manning"的vendor(提供商)属性附加到导出包。在这个特定的例子中,vendor是一个任意的属性,因为对于框架来说它没有特殊的意义——它是我们编造出来的。当讨论导入代码时,你将更好地了解在包共享中可以使用任意的属性来区分不同的导出包。正如我们之前在补充内容“JAR文件清单语法”中提到的,当你想把相同的属性附加到一组目标包上时,OSGi也支持一种缩略的格式,像下面这样:

http://assets.osgi.com.cn/article/7289381/18.jpg

这等价于前面的例子。这种缩写非常方便,但是只有当对所有包来说,所有附加的属性都一样时,才能够使用。使用任意属性使bundle可以区分它的导出包,但是使用属性进行区分更加有意义的原因是版本管理。

代码总是在改进。包包含着随着时间而改变的类。在文档中使用版本号来记录这些变化是很重要的。版本管理不是标准Java开发的一部分,但是它是基于OSGi的Java开发固有的。特别是OSGi不但像前面讨论的那样支持bundle版本化,而且支持包版本化,这意味着每个共享的包都有一个版本号。使用属性把版本号与包关联起来:

http://assets.osgi.com.cn/article/7289381/19.jpg

在这里,使用OSGi通用版本号格式,将值为"2.0.0"的version属性附加到导出包。在本例中,属性不是任意的,因为OSGi规范定义了这个属性名和值的格式。你可能注意到一些以前的Export-Package示例并没有指定version。在那种情况下,version默认是"0.0.0",但是使用这个版本并不是一个好主意。第9章将详细讨论版本化。

有了Bundle-ClassPath和Export-Package,就拥有了一个很好的定义和控制bundle内部类可见性的方法。但是并不是所有你需要的代码都包含于bundle JAR文件中。接下来,你将学习到如何指定bundle对外部代码的依赖。

导入外部代码

Bundle-ClassPath和Export-Package都与内部bundle代码的可见性有关。通常,一个bundle也依赖于外部代码。你需要一些方法来声明bundle需要哪些外部类,这样OSGi框架可以使这些类对它可见。通常,使用标准Java类路径指定哪些外部代码对你的JAR文件中的类可见,但是OSGi没有使用这种机制。OSGi要求所有的bundle包含显式声明依赖外部代码的元数据,这称作导入(importing)。

如果不觉得单调冗长的话,导入外部代码是很简单的。必须声明导入bundle需要的但是不包含在bundle中的所有包。这条规则唯一的例外是java.*包里面的类,OSGi框架自动使这些类对所有bundle都可见。用于导入外部代码的清单文件头标识相应地被命名为Import-Package。

Import-Package 内部bundle代码需要的、来自其他bundle并由逗号分隔的一组包。

Import-Package与import关键字比较
你可能会想你已经在代码中使用import关键字进行导入。从概念上说,使用import关键字和声明OSGi导入是相似的,但是其作用不同。Java中import关键字用于命名空间管理。通过它你可以使用导入类的短名称而不是使用它的完全限定类名称(例如,你可以引用SimpleShape而不是org.foo.shape.SimpleShape)。使用它们的短名称,你可以从任何其他的包导入类,但是它并没有授予任何可见性权限。事实上,根本没有必要使用import,因为可以使用完全限定的类名称。对于OSGi,导入外部代码的元数据是重要的,因为通过它,框架可以知道你的bundle需要什么。

Import-Package头部的值遵循通用的OSGi 清单文件头标识语法。首先,我们从最简单的形式开始。考虑绘图程序的主bundle,它依赖org.foo.shape包。它需要像下面这样声明导入这个包(图2-12展示了导入模块包的图形化表示):

http://assets.osgi.com.cn/article/7289381/20.jpg

http://assets.osgi.com.cn/article/7289381/2-12.jpg

图2-12 导入包的图形化描述

这明确地告诉OSGi框架,bundle除了需要通过bundle类路径可以访问的内部代码外,还需要访问org.foo.shape。注意,导入一个包并没有导入它的子包。记住,嵌套包之间没有关系。如果你的bundle需要访问org.foo.shape和org.foo.shape.other,必须同时导入这两个包,包名之间用逗号分隔,就像下面这样:

http://assets.osgi.com.cn/article/7289381/21.jpg

你的bundle把包列在Import-Package关键字后面并用逗号分隔它们,这样就可以导入任意数量的包。在更大的项目中Import-Package声明部分会列出大量的包,这是常见的(尽管你应该尽量控制包的数量)。

有时,你会想减弱bundle的包依赖。回想一下,为了区分bundle的导出包,Export-Package声明包含哪些属性。导入包时,可以使用这些导出属性作为匹配属性。例如,我们之前讨论了下面导出的和相关的属性:

http://assets.osgi.com.cn/article/7289381/22.jpg

一个包含元数据的bundle导出两个包,这两个包具有相关的vendor属性和值。使用相同的匹配属性可能会减少bundle的导入包数量:

http://assets.osgi.com.cn/article/7289381/23.jpg

包含元数据的bundle声明了对包org.foo.shape的依赖,并且该包有一个vendor属性匹配"Manning"值。附加于Export-Package声明的属性定义了属性的值,然而附加于Import-Package声明的属性定义了将要匹配的值;从本质上说,它们的作用像是一个过滤器。导入和导出如何匹配以及过滤的细节将延后到2.7节再介绍。现在,我们足以理解附加于导入包的属性是与附加于导出包的属性相匹配的。

对于任意属性,OSGi只支持相等匹配。换句话说,它要么匹配指定值要么不匹配。当我们讨论Export-Package和version属性时,你已经学习了一个非任意属性。因为OSGi规范定义了这个属性,所以OSGi支持更加灵活的匹配方法。这是OSGi擅长的一个领域。在简单的例子中,它把一个值当作是一个从指定版本号开始的无穷范围。例如:

http://assets.osgi.com.cn/article/7289381/24.jpg

这个语句声明了导入包org.osgi.framework,并且包的版本范围从1.3.0(包含1.3.0)到无穷。指定一个导入包版本范围的简单形式隐含一个期望:org.osgi.framework的后续版本将总是向后兼容低版本。在一些情况下,例如规范包,希望向后兼容是合理的。在一些你希望限制有关向后兼容假设的情况下,OSGi支持使用区间符号指定一个显式的版本范围,字符[和]表示包含值,而字符(和)表示不包含值。考虑以下例子:

http://assets.osgi.com.cn/article/7289381/25.jpg 这个语句声明了导入包org.osgi.framework,包的版本范围从1.3.0到2.0.0,包含前者但不包含后者。表2-2阐述了版本范围语法各种组合的含义。

表2-2 版本范围语法及其含义

语法含义
"[min,max)"min ≤ x < max
"[min,max]"min ≤ x ≤ max
"(min,max)"min < x < max
"(min,max]"min < x ≤ max
"min"min ≤ x

如果你想指定一个精确的版本范围,必须使用像"[1.0.1,1.0.1]"这样的形式。你可能想知道为什么一个像"1.0.1"这样的单个值是一个无穷的范围而不是一个精确的版本。这部分是由于历史原因造成的。在OSGi规范R4以前,所有的包都假设是规范包,它们保证向后兼容。因为假设了向后兼容,所以只需要指定一个最小的版本。当R4规范增加了对共享实现包的支持后,也有必要增加对任意版本范围的支持。现在重新把单一版本定义为一个精确的版本是有可能的,但是这对于目前的OSGi程序员来说不够直观。同时,规范也需要定义一个表示无穷的语法。最后,OSGi联盟认为定义像这里所示的版本范围是最可行的。

你可能已经注意到一些以前的Import-Package示例并没有指定版本范围。当没有指定版本范围时,默认的值是"0.0.0",从上一个例子你就可以猜到。当然,这里的不同点是值"0.0.0"表示版本范围从0.0.0到无穷。 现在你理解了如何使用Import-Package表示对外部包的依赖以及为了共享而使用Export-Package公开内部包。使用包作为bundle间共享基础的决策,并不是对所有人来说都是一个显而易见的选择,所以我们在补充内容“依赖于包而非bundle”中讨论这种做法的一些争论。

到目前为止,我们已经介绍了OSGi模块层的主要组成部分:Bundle-ClassPath、Export-Package和Import-Package。我们已经在绘图程序的上下文中讨论了这些内容,你将在下一节看到绘图程序的运行,但是我们需要了解的难题的最后一个部分是,应用运行时,这些各种各样的代码可见性机制是如何相互配合的。

依赖于包而非bundle
对于大多数Java程序员来说,导入包是很正常的,因为你导入的是在源文件中使用的类和包。但是源文件中的import语句用于管理命名空间而不是定义依赖关系。OSGi选择使用包级粒度来表达bundle之间的依赖关系,如果没有争议的话,对于Java面向模块的技术来说是新颖的。其他方法通常采用模块级的依赖关系,这意味着依赖关系以一个模块依赖于另一个模块的方式表达。OSGi选择使用包级依赖引发了一些使用哪种方法更好的争论。

针对包级依赖的主要批评是,包级依赖太复杂或者粒度太细。有些人认为,对开发者来说,考虑依赖一个JAR文件比依赖一些单独的包更容易。这个论点是站不住脚的,因为Java开发人员使用任何技术都必须了解它的包命名机制。例如,如果你对相关技术了解得足够多,并想使用Servlet类作为首选,那么你很可能也知道Servlet类是在哪个包里面。

包级依赖是更细粒度的,这导致了需要更多的元数据来描述。例如,如果一个bundle导出了10个包,只需要1个模块级的依赖来表达所有的依赖关系,而如果是包级依赖,则需要10个。但是bundle一般很少依赖于一个bundle的所有导出包,而且这更多的是对工具支持不够的谴责。还记得在IDE开始为你维护导入声明之前,这是多么麻烦的一件事吗?bundle也在开始改变;附录A描述了生成bundle元数据的工具。接下来我们看看包级依赖的一些好处。

模块级依赖和包级依赖的区别是它们依赖谁与依赖什么。模块级依赖是依赖特定模块(依赖谁),而包级依赖是依赖哪些包(依赖什么)。模块级的依赖是脆弱的,因为即使其他bundle提供了相同的包,它们也只能由特定的bundle满足依赖关系。有些人认为这不是问题,或者是因为它们想要依赖已经测试过的bundle,又或者是因为这些包是实现包,不能由其他bundle提供。尽管这些理由是合理的,但它们通常会随着时间的推移被打破。

例如,如果你的bundle随着时间的推移越来越庞大,你可能想通过分离它的不同导出包到多个bundle的方法来重构bundle。如果你使用了模块级依赖,这样的重构会破坏现有的客户端。而当这些客户端来自你不能轻易改变的第三方时,这往往让人十分沮丧。使用包级依赖时,这个问题将不会存在。此外,bundle通常不会依赖另一个bundle的所有导出包,只依赖一个子集。因此,模块级依赖过于笼统,导致传递扇出。最终为了满足所有的依赖关系,需要部署很多不使用的额外bundle。

包级依赖是从高级视角表示代码真正类的依赖关系。我们可以像IDE在源文件中维护导入声明一样,来分析bundle的代码并生成bundle的导入包集合。模块级别的依赖不能通过这种方式发现,因为它们不存在于代码中。包级依赖看起来很棒,是吧?你可能想知道包级依赖是否有什么问题。

主要的问题是OSGi把包看做是一个原子单元。如果没有这个前提,那么OSGi框架就无法将隶属于一个bundle的包替换为另一个属于不同bundle的相同包。这就意味着,一个包不能分割到多个bunlde。一个包只能隶属于一个bundle。如果包在bundle之间被分割,OSGi架构很难知道一个包何时才变得完整。通常来说,这并不是主要限制因素。除此以外,你可以利用包依赖来完成任何使用模块依赖做到的事情。而且,说实话,OSGi规范确实支持模块级依赖和某些形式的包分割,第5章将介绍这些内容。

2.5.4 类搜索顺序

我们已经讨论了代码的可见性,但是最终我们讨论过的所有元数据允许OSGi框架为了bundle,而对它们包含的以及所需要的类进行复杂搜索。实际上,当导入bundle需要一个导出包的类时,会请求该导出该包的bundle。OSGi框架通过类加载器做到这一点,但请求的具体细节并不重要。尽管如此,了解类搜索顺序还是很重要的。

bundle在执行时需要一个类,OSGi框架将按以下顺序搜索该类。

  1. 如果包含该类的包名以java.开头,当前类加载器的父类加载器会去搜索这个类。如果找到这个类,就使用它。如果没有找到,该搜索以异常结束。

  2. 如果这个类是bundle的导入包中的类,OSGi框架从导出bundle中搜索这个类。如果找到了,就使用它。如果没有找到,该搜索以异常结束。

  3. 从bundle的类路径中搜索这个类。如果找到这个类,就使用它。如果没有找到,该搜索以异常结束。

这些步骤是很重要的,因为它们还有助于框架确保一致性。具体来说,步骤1确保所有的bundle使用相同的核心Java类,而步骤2确保导入包不会被分割到导入bundle和导出bundle中。

就是这样!已经介绍完了bundle元数据。我们并没有介绍所有你可能用到的元数据,但是已经讨论了入门新建bundle需要的最重要的bundle元数据。第5章将介绍其他模块化问题。接下来,你要把所有的元数据添加到绘图程序中,然后重新审查当前的设计。再继续下一节之前,如果你想知道是否有可能一个JAR文件同时是bundle和普通JAR文件,请参阅补充内容“bundle 是JAR文件还是JAR文件是bundle”。

bundle是JAR文件还是JAR文件是bundle
也许你对把OSGi元数据添加到现有的JAR文件感兴趣,或者你想从头开始创建bundle,但在非OSGi情况下仍要使用它们。我们之前已经说过,bundle只是一个JAR文件,并且在它的描述文件中附加了模块相关的元数据信息,但是这种说法是否足够精确呢?这是否意味着你可以把OSGi bundle作为一个标准的JAR文件使用呢?标准的JAR文件当做bundle来用怎么样呢?让我们先回答第二个问题,因为它比较容易回答。

标准的JAR文件可以在不做任何改动的情况下安装到OSGi框架中。很遗憾,这样没有任何意义。为什么?主要原因是标准的JAR文件不对外公开任何内容;用OSGi的术语来说,它没有导出任何包。JAR文件的默认Bundle-ClassPath是点号(.),但是Export-Package默认是空。所以即使一个标准的JAR文件是bundle,也是没有意义的bundle。至少,你需要在描述文件中添加Export-Package声明,明确地公开其部分或全部内部内容。

那么bundle JAR文件呢?它们是否可以在OSGi环境以外作为标准的JAR文件使用呢?答案是,视情况而定。创建OSGi环境内外功能相同的bundle是可能的,但是并不是所有的bundle都能作为标准JAR文件使用。这取决于你的bundle使用了OSGi的哪些特性。就目前所学到的元数据特性,只有一个可能会导致问题:Bundle-ClassPath。回想一下,bundle内部类路径是以逗号分割的一组bundle JAR文件中的位置,并且可能包含:

1.代表bundle JAR文件根路径的点号;

2.嵌入的JAR文件的相对路径;

3.嵌入的目录的相对路径。

只有类路径为点号的bundle可以作为标准JAR文件使用。为什么?OSGi概念中bundle类路径中的点号相当于标准JAR文件的类搜索,即从JAR文件的根目录开始,把所有的相对目录作为包名进行搜索。如果bundle指定了嵌入的JAR文件或目录,这是需要特殊处理的,而这种处理只有OSGi环境支持。幸好,避免使用嵌入的JAR文件和目录不是太困难。

试图使bundle JAR文件与标准的JAR文件兼容是个好主意,但最好还是在OSGi环境中使用。不使用OSGi,你将会失去bundle的依赖检查、一致性检查和边界有效性,更不用说在接下来的几章中将要讨论的很棒的生命周期和服务了。

wmz 2015-05-22 09:29

http://osgi.jxtech.net 作为企业级OSGi开发平台,还比较优秀。

顶(3) 踩(0) 回复

ccfeng 2014-04-28 09:29

不错,对初学者很有用~

顶(0) 踩(0) 回复

九天 2013-11-02 14:21

通过绘图程序把模块化进行了详细的解析,很不错

顶(0) 踩(0) 回复
查看评论