《OSGI实战》:第三章 生命周期(三)

 由  《OSGI实战》 发布

3.5 生命周期与模块化

在OSGi中,生命周期层与模块层之间存在一种双向关系。生命周期层会对安装到框架之中的bundle进行管理,显然这会影响到模块层对bundle依赖的解析。模块层使用bundle元数据,确保bundle在被使用前它的所有所依赖都已满足。使用bundle时,模块层与生命周期层之间的这种共生关系,会产生一种类似先有鸡还是先有蛋的情景。使用bundle前你需要先安装,若要安装bundle,你又必须拥有一个bundle上下文环境,该环境仅供bundle使用。框架如何解析bundle依赖,特别是当bundle动态地安装或移除时,这种密切关系同样体现得非常明显。

3.5.1 解析bundle

在bundle加载类之前,框架会完成对bundle的解析。通常情况下,当解析给定的bundle时,框架会解析另一个bundle,以此实现对给定bundle的依赖解析。由此会导致级联依赖解析,因为当一个bundle使用另一个bundle时,被引用的bundle同样需要由框架解析,以此类推。由于框架仅在需要时才进行依赖解析,所以对于向RESOLVED状态过渡的bundle,可以基本忽略对它的解析。你可以启动一个bundle,此时你应该清楚,如果可能的话,框架会在启动bundle之前对其进行解析。这种方式比标准的Java方式要好,在标准Java中,你可能会在应用生命周期中的任何时刻遇到缺失依赖的情况。

但如何才能判定一个给定的bundle被正确解析了?举例来说,也许你想预先知道一个已安装bundle能否启动。这时有一种方法可以通知框架直接对bundle进行解析,但这与大多数生命周期操作不太一样,它不是bundle的方法。但你可以使用包管理服务(Package Admin Service)。包管理服务的表现形式是接口。如下所示:

http://assets.osgi.com.cn/article/7289386/29.jpg
通过使用resolveBundles()方法,你可以直接对某个bundle进行解析,该方法的参数是一个存放bundle的数组,方法返回一个布尔标志,并以此来表示数组中的bundle能否被解析。包管理服务不光用来解析bundle,它还是框架中相当重要的一部分。除了其他功能外,它还支持以下一些操作。

判断哪个bundle拥有指定的类——在某些特定情况下,你可能需要知道哪个bundle拥有指定的类。这时使用getBundle()方法可以满足上述要求,将一个Class传入该方法,方法执行后返回该类所属的Bundle。

分析框架如何解析bundle依赖——你可以使用getExportedPackage()系列方法查看指定的包被哪些bundle导入,其他方法如getRequiredBundles()和getFragments()用来分析另外一些类型的依赖,这些内容将放在第5章讨论。

对bundle的依赖解析进行刷新——由于已经安装的bundle集合会不断变化,有些情况下你需要让框架重新计算bundle依赖,这时可以使用refreshBundles()方法。

解析bundle以及处理依赖并非是包管理服务最重要的功能;其最重要的功能是刷新bundle依赖,这是另一个管理bundle所必须的工具。但在开始讨论刷新bundle的细节之前,先结束关于显示地对bundle进行解析的讨论。

为了演示如何使用包管理服务显示地对一个bundle进行解析,需要为shell创建一个新的bundle resolve命令来解析bundle,如代码清单3-19所示。

代码清单3-19 bundle resolve命令

http://assets.osgi.com.cn/article/7289386/30.jpg

我们先不讲如何获得包管理服务的细节,这些内容放在下一章。现在,直接使用getPackage AdminService()方法。如果解析命令在执行时不带参数,你将调用参数为null的resolve Bundles()方法,这时框架将尝试对所有未解析的bundle进行解析。你也可以将一组由空格分离的bundle标识符作为参数传递给该方法。对于每一个bundle标识符,你可以获得该bundle依赖的bundle对象,并将其加入到一个列表中。当重新获得完整的bundle列表之后,将它们以数组的方式传递给resolveBundles()方法。框架将尝试解析列表中未解析的bundle。

框架除了对那些指定的bundle进行解析外,还会解析其他bundle,理解这一点非常重要。那些被指定的bundle是框架解析过程的基础。框架会解析与指定的bundle相关的任何其他未解析的bundle。 对单个bundle进行解析是件很容易的事,因为框架替你完成了那些复杂的工作。你也许认为这是理所当然的。只要bundle的依赖都已被解析了,也就没有其他顾虑了,对吗?bundle生命周期的动态特性使这成为一个无效的假设。某些情况下你需要让框架重新计算一个bundle的依赖,也许你会问为什么。下一节将会给出答案。

3.5.2 刷新bundle

生命周期层允许你部署并管理自己应用的bundle。到目前为止,我们重点了解了安装、解析、以及启动bundle,但是还有一些其他有趣的bundle生命周期操作。如何更新或卸载一个bundle?就操作本身来说,它们的概念很简单,与其他生命周期操作并无两样。我们肯定清楚对bundle来说卸载或者更新意味着什么。但是具体细节就有些复杂了。当对bundle执行更新或者卸载操作时,你正处在这样一个时刻,即中断你的系统。透过这里你开始发现框架的动态生命周期管理所发挥的作用。

一种简单的情形是更新或者卸载一个自包含的bundle。 这时,中断被限定在某个特定的bundle内。即便这个bundle从其他bundle导入了包,中断也仅局限在被更新或者卸载的bundle内。无论哪种情况,如果bundle处于激活状态,框架会将其停止。至于更新操作,框架会更新bundle的内容,若该bundle之前处于激活状态,则框架在更新bundle后会将其重启。如果更新或者卸载的bundle被其他bundle所依赖,处理起来会复杂一些。如果依赖其他bundle的bundle本身又被其他的bundle依赖,这将导致应用产生级联中断。

为何依赖使情况变得复杂?考虑为指定的bundle执行更新操作。其他依赖该bundle的bundle也许正在加载被更新bundle旧版本的类。此时它不能开始加载新版本bundle的类,否则将看到,已经加载的旧版本的类与执行更新操作后加载的新版本的类混合在一起。由此会引发不一致。对于未安装的bundle,情况会更加复杂,因为你无法对一个未安装的bundle执行任何操作。

bundle更新或者卸载引发了中断,如果能够约束这种中断那将是非常有意义的。通过对更新以及卸载的bundle执行两步操作,框架提供了一种约束中断的控制机制。从概念上理解,第一步是准备操作;第二步是刷新,即真正执行操作。刷新操作会重新计算受影响的bundle的依赖。这是如何实现的呢?当触发更新或者卸载操作时,允许你分别对新bundle版本或删除的bundle进行控制,如图3-19所示。

http://assets.osgi.com.cn/article/7289386/3-19.jpg

图3-19 对bundle执行更新以及刷新操作是一个两步过程,在框架执行刷新操作时, 大部分工作一般发生在第二步

我们提到这是一个两步的过程,但是在第一步中发生了什么?对于更新操作,新版本的bundle被放到正确的位置,但旧版本的bundle仍然存在,这样依赖它的bundle可以继续从中加载类。你也许会问,“这是否意味着在同一时刻安装了两个版本的bundle?”可以肯定地回答,的确如此。每当你在执行更新但尚未执行刷新操作时,你都将引入另一个版本。对于卸载操作,将会从已安装bundle列表中移除相应的bundle,但并没有将其从内存中移除。同样地,框架会将其保留,这样依赖它的bundle可以继续从中加载类。

举个例子,假设你打算对一组bundle执行更新操作。在每个bundle独立更新后,框架对有依赖关系的bundle进行统一刷新,这种处理方式显然不太方便。在两步法中,你可以对该组bundle进行更新,并在最后由框架统一进行一次刷新操作。如果你安装一个bundle,并且该bundle提供了较新版本的包,此时你会遇到类似的情况。已经存在的被解析的bundle,在导入了旧版本的包后并不能自动更新到新版bundle,除非它们被刷新。同样,若能在变化发生的第一时间进行控制将是一个很好的处理方式。更新应用时,你的一些bundle已经更新,有些已经卸载,有些已经安装。这是一种非常普遍的场景。所以如果能够在这些变化发生时进行有效的控制,那将会非常有用。

再一次借助包管理服务触发一个刷新操作。为了说明如何使用,我们在shell中添加一个refresh命令,如代码清单3-20所示。

代码清单3-20 bundle refresh命令

http://assets.osgi.com.cn/article/7289386/31.jpg
> 与解析命令一样,你依赖那个神奇的方法获得包管理服务。使用PackageAdmin.Refresh Packages()方法刷新bundle。在执行命令时如果不指定参数,则会将null传递给包管理服务。这时框架将刷新自上次刷新以来,被更新以及被卸载的bundle。这适用于之前提到的更新和卸载的情况,但对于重新连线的情况是毫无帮助的。你也可以通过传递你想刷新的指定bundle来到达目的。在这种情况下,refresh命令接收以空格分隔的bundle标识符作为参数。对提供的参数进行解析从而得到它们的标识符,获取它们相关联的Bundle对象,并将其添加到将要刷新的列表当中1。然后你传递将要刷新的bundle数组到包管理服务2。

PackageAdmin.refreshPackages()方法会对被刷新的bundle导出的包执行更新或者删除操作。该方法会立即返回至其调用者,并在一个独立的线程上执行如下步骤。

  1. 从某一指定的bundle开始(如果参数为null则从所有被更新或卸载的bundle开始),计算受影响并存在依赖关系的bundle的结构图。任何引用了导出包的bundle都会被添加至结构图中,而那些提供导出包的bundle已经存在于当前的结构图中。当结构图外部不存在引用了结构图内部bundle的bundle时,该结构图被认定为完全绘制完成。
  2. 结构图中每一个处于ACTIVE状态的bundle被停止并被切换至RESOLVED状态。
  3. 结构图中每一个处于RESOLVED状态的bundle,包括那些已经停止的bundle会变成未解析的,并且状态切换至INSTALLED。这意味着这些bundle的依赖关系不再被解析。
  4. 结构图中每一个处于UNINSTALLED状态的bundle会被从结构图中移除,同时也会被彻底地从框架中移除(届时由垃圾回收器回收)。受到影响的bundle均会回退到最原始的状态。
  5. 对于结构图中余下的bundle,框架会重启之前处于ACTIVE状态的bundle,重启前框架会对这些bundle以及其所依赖的bundle进行解析。
  6. 当所有工作都完成后,框架会触发一个FrameworkEvent.PACKAGES_REFRESHED类型的事件。

上述步骤执行完后,一些之前处于ACTIVE状态的bundle可能无法解析。也许提供所需包的bundle未安装。在这种情况下,或者针对其他一些错误,框架会触发一个FrameworkEvent. ERROR类型的事件。

接下来的shell会话展示了通过结合使用resolve和refresh命令,来管理一个系统。

http://assets.osgi.com.cn/article/7289386/32.jpg

安装一个bundle并使用resolve命令对其解析1,bundle状态转换为RESOLVED。使用refresh命令2,将bundle状态回退为INSTALLED。

到这里,你已经学习了很多知识来帮助理解生命周期层。但在结束学习之前,需要介绍bundle更新的一些细节。现在开始吧。

3.5.3 更新操作没有完成更新

很多人在更行bundle时经常遇到的一个陷阱是,在执行完更新操作后bundle可能并未使用它的新类。我们之前了解过更新bundle是一个两步的过程,第一步为执行更新操作做准备;第二步真正执行更新操作,但是当你更新bundle时,并非完全如此。按照规范中的描述,框架会立即执行更新操作,所以执行完更新的bundle理论上使用的应该都是新类。但没必要立即开始使用这些新类。有些情况下,一个bundle更新之后开始使用新类;在另外一些情况下,使用的仍是旧类。这听起来有些让人迷惑,是不是?的确如此。为何不等到执行刷新操作后统一使用新类呢?

答案你有可能已经猜到了,是历史原因。最初的R1规范中定义的更新操作是指对一个bundle进行更新。仅此而已。不存在包管理服务。在实际情况下,不难发现规范中对更新的定义是不充分的。太多的细节留给了框架实现去作决定,比如何时处理旧类启用新类。由此产生了不一致,这使得跨不同的框架实现来管理bundle的生命周期变得很困难。上述情况导致在R2规范中引入了包管理服务,解决在进行一劳永逸的一次彻底更新时产生不一致的问题。遗憾的是,考虑到向后兼容问题,更新操作的最初行为没有发生改变。这些因素导致了今天这种不那么简洁的处理bundle更新的方法,但至少保证了在不同的框架实现间bundle更新的概念是一致的。

现在回到之前讨论过的问题:已经更新的bundle有时使用旧类,有时使用新类。这看起来可能很神奇,有一种方式可以解释这种现象的原因。在对bundle进行更新后,它所使用的类新旧与否取决于如下两个因素。

这些类源自私有包还是导出包。

如果源自导出包,那么它们是否还被其他bundle使用。 考虑第一个因素。

如果这些类源自bundle中的一个私有包(该包未被导出),无论如何新类都会立即可用。

如果它们源自一个导出包,那么其可见性取决于它们是否被其他bundle使用。

如果没有其他bundle使用该导出包,新类当即生效。旧版本的类将不再需要。

如果这些导出包被其他bundle使用,新类不会立即可用,因为旧版本的类仍在使用中。这种情况下,除非调用PackageAdmin.refreshPackages()方法,否则新类不可用。

这其中还有另外一个细节。第5章将介绍bundle可以导入它自己导出的包。如果一个bundle导入了一个它自己导出的包,并且更新后的bundle的导入包与其旧版本导出包版本相匹配,则更新后的bundle的导入包将指向之前旧版本的导出包。这种处理方式在有些情况下是很好的,比如你正在修正私有包中的bug。但是这同样会导致奇怪的现象产生,因为更新后的bundle在使用新版本私有类的同时,还在使用旧版本的导出类。bundle导入其自身的包时,可以指定版本范围,避免上述情况的发生。

另外一种情形是,如果更新后的bundle导入其自身的包,但是导入包的版本与导出包的旧版本不匹配。这种情况与bundle仅仅将该包导出是类似的。在这种情况下,更新后的提供导出包的bundle以及未来其他bundle的解析,都可以立即使用导出包的新类,但对于现有导入包的bundle来讲不会立即可用,它看到的还是旧版本。这种情况下一般需要使用PackageAdmin. Refresh Packages()方法使该bundle回退到一种有用状态。

采用基于接口的编程以及bundle划分,你可以避免一些上述问题。举例来说,如果你可以将共享API(bundle之间交互的接口)分离成接口,并把这些接口放置到独立bundle中相互分离的包中,这种处理方式在有些情况下可以帮你简化上述问题。在这样的设计中,客户端bundle以及接口实现bundle共同依赖于共享API bundle,而非相互依赖。换句话说,你限定了功能提供者与功能使用者之间的耦合度。

3.6 小结

通过本章的学习你已经了解:无论是打算部署bundle,以此来执行自己的应用,还是创建一个复杂的自适应系统,生命周期层均为你提供了所需的一切。现在让我们回顾一下所学内容。

仅当将bundle安装至OSGi框架的一个运行实例中,该bundle才可用。

生命周期层API由三个主要接口构成:BundleActivator、BundleContext和 Bundle。

BundleActivator实现了bundle与生命周期层之间的挂接,从而使 bundle具备了生命周期的特性,这使bundle有权使用框架提供的监视以及修改框架运行时状态的功能。

框架将一个生命周期状态与每一个已经安装的bundle关联起来,使用生命周期接口BundleContext和Bundle可以完成bundle状态的运行时转换。

OSGi框架提供了动态扩展的特性,该特性基于动态修改已安装的bundle集合。监控bundle的生命周期事件是该特性的一种表现形式(也被称之为扩展者模式)。

生命周期层与模块层之间的关系很密切,当更新或者安装bundle时,这种密切关系便可体现。使用包管理服务可以管理这种交互。

接下来我们将进入OSGi框架的下一层:服务层。服务不仅促成了bundle间的基于接口的编程,同时还提供了另外一种形式的动态扩展性。


ll_123 2013-11-28 16:57

感觉还是有点抽象。

顶(0) 踩(0) 回复

九天 2013-11-22 11:14

不错

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