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

 由  《OSGI实战》 发布

3.3 在bundle中使用生命周期API

到目前为止,我们并没有为shell实现太多的功能——只是创建了激活器并执行了关闭和启动操作。在本节中,我们将向读者展示如何实现更多的功能。通过一个简单的命令模式提供可执行的操作,你可以交互式地安装、启动、停止、更新以及卸载bundle。我们甚至还将为shell增加永久的历史记录,记录过去执行的命令。

在开始之前,站在较高的层次理解这种方法,是非常有帮助的。最主要的部分是telnet绑定,它会监听发送到配置好的端口的连接请求,并为每个连接的客户端生成一个新的线程。客户端发送命令行到本地线程,命令行包括命令的名称和参数。线程解析命令行后,选择适当的命令,并使用指定的参数进行调用,如图3-7所示。

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

图3-7 TelnetBinding概览

命令会处理传给它的参数。我们并不讨论telnet绑定的实现和连接线程等,不过所有的源代码都可以在配套代码中找到。我们将剖析命令的实现,从而分析如何使用Bundle和BundleContext。沿着这个思路,接下来首先向你展示配置bundle的方法。

3.3.1 配置bundle

Shell有两个配置属性:一个用于配置端口,另一个用于配置最大并发连接数。在普通的Java编程中,可以通过System.getProperty()获得上述属性。创建bundle时,可以使用Bundle- Context对象获得配置属性。这种方法的最大好处是,可以避免System.getProperty()这类全局的特性,并允许为每个框架实例设置不同的属性。

OSGi规范并没有指定一种面向用户的方法来设置bundle的配置属性方法,所以不同的框架具有不同的处理方式。通常的做法是提供一个设置属性的配置文件。但是OSGi规范确实需要bundle配置属性作为系统属性的补充,所以你仍然可以在必要时使用系统属性。通过BundleContext .getProperty()方法获得bundle配置属性值是标准化的做法,如代码清单3-4所示。

代码清单3-4 bundle配置示例

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

该代码清单继续了清单3-1中激活器的实现。在该激活器中,你可以使用这两个方法获取配置属性。这里的方法使用了BundleContext.getProperty()方法获取属性1。该方法访问框架属性,查找指定属性的值。如果找不到该属性,它会查找系统属性,如果最终都没有找到,则返回null。对于shell,如果没有找到配置值时,将返回默认值。OSGi规范同样也定义了一些标准的框架属性,如表3-1所示。如果你需要使用这些标准的属性,可以使用org.osgi. framework. Constants类中已经为它们定义的常量。

表3-1 标准OSGi框架属性

属性名称描  述
org.osgi.framework.versionOSGi 框架版本
org.osgi.framework.vendor框架实现的提供商
org.osgi.framework.language使用的语言,可能的值参考ISO 639
org.osgi.framework.os.name主机操作系统
org.osgi.framework.os.version主机操作系统版本号
org.osgi.framework.processor主机处理器名称

就是这样:我们开始了与OSGi框架的第一次交互。这只是一小部分可以在bundle中使用的API,下一节将涉及更多内容。你可能会想:“嘿,这种配置机制看起来似乎过于简单了!”的确如此。还有一些其他更复杂的配置bundle的方式,但在第9章之前,不会涉及这些内容。bundle属性是最简单的机制,只能被用于不经常变化的属性。就这一点而言,这对shell来说可能不是最好的选择,不过事实上还是取决于你希望达到什么样的目标。例如,它使动态地修改shell的端口变得比较困难。从现在开始,我们尽量简单些,这就足够了。

3.3.2 部署bundle

每一个安装到框架中的bundle都以Bundle对象的形式存在,并且可以通过bundle标识符、路径或者符号名来识别。对于下面将要实现的大多数shell命令,我们会使用bundle标识符获得一个Bundle对象,因为这些标识符非常友好且简明。大多数命令都接收bundle标识符作为参数,所以接下来我们将学习使用bundle标识符和bundle上下文来访问与其他bundle关联的Bundle对象。作为设计的一部分,你可以构建一个抽象的BasicCommand类,去定义一个共享方法getBundle(),从而通过标识符获取bundle,如下所示:

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

你需要做的只是对上下文对象调用BundleContext.getBundle()方法,该对象带有解析后的bundle标识符,它们是以String的形式传进来的。唯一特别需要注意的是给定标识符对应的bundle不存在。在这种情况下,你可以抛出一个异常。

安装命令

具备这些基本功能后,我们可以启动第一个命令了。代码清单3-5展示了一个install命令的实现,图3-8提示我们该过程涉及了bundle生命周期的哪部分。

代码清单3-5 bundle install命令

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

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

图3-8 bundle生命周期状态图中与安装相关的部分

可以使用BundleContext.installBundle()方法安装bundle。在大部分框架实现中,installBundle()方法的参数可以被方便地解释为String类型的URL,通过该URL可以找到JAR文件。因为用户输入的是String类型的URL,所以你可以直接使用它安装bundle。如果安装成功,接下来将会返回一个新的Bundle对象,该对象与新安装的bundle对应。该bundle通过这个URL唯一标识,并作为它的路径。以后该路径值也可以用于判断bundle是否已安装。如果已有bundle与该路径值相关联,那么与之前已安装的bundle相关联的Bundle对象将会被返回,而不是再次安装。如果安装成功,该命令将输出已安装的bundle的标识符。

bundle上下文还提供了一个重载的installBundle()方法,可用于通过一个输入流安装bundle。这里我们并不展示这种方法,但其他形式的installBundle()接收一个路径和一个打开的输入流。此时,路径仅仅用于标识,bundle的JAR文件会从传入的输入流中读取。框架会负责关闭输入流。

启动命令

现在已经有一个安装bundle的命令了,所以接下来你希望执行的操作是启动bundle。代码清单3-6展示了用start命令启动bundle(参见图3-9)。

同样,上面的实现非常简单。首先使用基础命令类的方法,获取与用户指定的标识符关联的Bundle对象,然后调用Bundle.start()方法启动与该标识符关联的bundle。

代码清单3-6 bundle start命令

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

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

图3-9 bundle生命周期状态图中与启动相关的部分

Bundle.start()方法的结果依赖于相关bundle的当前状态。如果bundle的当前状态是INSTALLED,那么其在经过RESOLVED和STARTING状态后,最终转变为ACTIVE状态。如果bundle的状态是UNINSTALLED,该方法会抛出一个IllegalStateException异常。如果bundle的状态是STARTING或STOPPING,则start()方法将被阻塞,直到bundle进入ACTIVE或者RESOLVED状态。如果bundle的状态已经是ACTIVE,那么调用start()方法将无任何作用。bundle在启动前必须是已经过解析的。不需要显式地解析bundle,因为按照规范要求,如果bundle未被解析,框架必须对其进行隐式的解析。如果bundle的依赖不能被满足,start()方法将抛出BundleException异常,并且在所有依赖关系都被满足之前,该bundle都不可用。如遇到该情况,只需安装额外的bundle,用于满足缺失的依赖,并尝试重新启动bundle。

如果bundle包含一个激活器,当启动bundle时,框架会调用BundleActivator.start()方法。从激活器中抛出的任何异常都会导致启动bundle失败,同时也会导致Bundle.start()方法抛出异常。最后一个可能会导致异常的情形是,bundle尝试启动自己。规范中说明,如果出现该情形,应抛出一个IllegalStateException异常。

停止命令

上面的内容介绍了启动bundle,接下来我们研究如何停止bundle,与启动bundle类似,参见代码清单3-7和图3-10。

与启动bundle类似,停止bundle只需简单地调用Bundle对象的Bundle.stop()方法,该Bundle对象是通过给定的标识符获取的。如前所述,需要注意bundle的状态。如果是UNINSTALLED状态,则会抛出IllegalStateException异常。在没有到达ACTIVE或RESOLVED状态时,无论是STARTING还是STOPPING状态都会被阻塞。对于ACTIVE状态,bundle将经过STOPPING状态后过渡到RESOLVED状态。如果bundle包含一个激活器,则激活器的stop()方法会抛出一个BundleException异常。总的来说,bundle都不可以改变自身的状态。任何这种尝试都会导致一个IllegalStateException异常。

代码清单3-7 bundle stop命令

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

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


图3-10 生命周期状态图中与停止相关的部分

更新命令

我们接下来讨论代码清单3-8中列出的更新命令(参见图3-11)。

代码清单3-8 bundle update命令

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

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

图3-11 bundle生命周期状态图中与更新相关的部分

到目前为止,你应该已经注意到我们在最开始提到的模式。大多数生命周期操作都是Bundle和BundleContext对象的方法。正如你看到的,Bundle.update()方法不会抛出异常。该方法存在两种形式:一种没有参数(如上所示),另一种使用输入流作为参数。这里采用了没有参数的形式,即使用原始的URL形式的路径值,并从该路径中读取需要更新的bundle的JAR文件。如果bundle在ACTIVE状态时被更新,根据bundle生命周期的要求,应首先停止bundle。这并不需要你去做,因为框架会自动处理,但是理解其中发生了什么还是有好处的,因为这会影响应用的行为。更新操作将在RESOLVED和UNINSTALLED状态下发生,最终会产生该bundle的新的修订版本并处于INSTALLED状态。如果bundle处于UNINSTALLED状态,则会抛出IllegalStateException异常。就像停止命令一样,bundle不应该尝试更新自己。

Bundle-UpdateLocation反模式
在更新bundle时存在一个反模式。OSGi规范提供了基于bundle元数据的额外选项。bundle可以在其bundle 清单文件中声明一系列元数据,称为Bundle-UpdateLocation。如果这些元数据存在,那么没有参数的Bundle.update()方法将使用元数据中提供的URL形式的更新路径,以获取更新后的bundle JAR文件。不建议使用这种方法,因为一旦忘记设置会带来很多困惑,同时将这类信息放在bundle中也是无意义的。

卸载命令

实现uninstall命令后,生命周期操作就算是完成了,如图3-12所示。

为了卸载bundle,需要用户提供bundle标识符,在获得与之关联的Bundle对象后,就可以调用Bundle.uninstall()方法了,如代码清单3-9所示。如果需要,框架将停止bundle。如果bundle已处于UNINSTALLED状态则会抛出IllegalStateException异常。如其他生命周期操作一样,bundle也不能尝试卸载自己。

代码清单3-9 bundle uninstall 命令

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

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

图3-12 生命周期状态图中与卸载相关的部分

就是以上这些了。现在已经创建了一个基于telnet的shell bundle,你可以在任意OSGi框架中使用它。但美中不足的是,大多数shell命令在执行操作时都需要bundle标识符,而shell的使用者如何知道使用哪个标识符呢?我们还需要通过某种方式,检查已安装到框架中的bundle的状态。接下来我们将构建这样一个命令。

3.3.3 检查框架状态

你需要另外一个命令,用于显示当前已安装到框架中的bundle的信息。代码清单3-10展示了一个bundles命令的简单实现。

代码清单3-10 bundle信息示例

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


这个命令的实现也非常简单,因为你只需要使用BundleContext.getBundles(),获取一个包含所有已安装到框架中的bundle的数组。剩下的实现只是循环遍历数组,并输出每个Bundle对象的信息。在这里,你可以打印每个bundle的标识符、生命周期状态、名称、路径、以及符号名等。

有了这个命令,这个简单shell需要的所有功能都具备了。你可以安装、启动、停止、更新和卸载bundle,同时也可以列出当前已安装的bundle。非常简单,不是吗?对比思考一下获得的灵活性与为了实现这个shell所做的努力。现在你可以创建应用,该应用是一个非常容易部署配置的bundle,这样你就可以在必要的时候很好地管理和改进该应用了。

使用这种方式,可以实现绘图程序的动态扩展。但是在转向绘图程序之前,为了能够充分理解这种方式,还有最后两个生命周期的概念值得研究一下:持久性和事件。我们将结合shell实例来描述它们。但是你将在后面几页的绘图示例中看到,当构建OSGi应用时,要记住,它们通常都是非常有用的工具。

3.3.4 持久化bundle状态

正如我们在讨论bundle激活器时提到的,框架创建bundle激活器类的一个实例,并使用相同的实例来启动bundle,随后还用于停止bundle。一个激活器实例只在框架启动和停止bundle的时候使用一次,随后就会被丢弃。如果bundle随后被重启,就会创建一个新的激活器实例。在这种情况下,bundle如何在停止和重启过程中保存状态呢?前面提到过框架如何将已安装的bundle保存到缓存中,下次框架重启后可以重新加载它们。在不同的框架会话之间,bundle如何保存状态呢?有如下几种可能。

一种可能是将信息保存在框架之外,例如数据库或文件,如图3-13所示。这种方法的缺点是状态不被框架管理,而且在bundle卸载后,也不能被清空。

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

图3-13 在外部存储状态

另一种可能是bundle向其他不会被停止的bundle提供自己的状态;然后在它重启后再取回它的状态,如图3-14所示。这是一种可行的方式,并在某些场景下非常有用。

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

图3-14 通过其他bundle保存状态

为了简单起见,最好能够使用文件,并由框架负责管理。这种可能是存在的。框架为每个已安装的bundle在文件系统中维护一块私有的数据区域。

BundleContext.getDataFile()方法提供了访问bundle私有数据区域的方式。当使用私有数据区域时,你并不必知道它存放在文件系统的什么位置,因为框架会负责这些,还要在bundle卸载时清空这些数据(如图3-15所示)。看起来可能比较奇怪的是,没有直接使用文件存储数据;但如果这么做,bundle在卸载时难以清空这些数据。这是因为在被卸载时,bundle并不会收到通知。进一步地,该方法简化了安全模式下的运行,因为bundle可以通过框架获得访问私有数据区域的权限。

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

图3-15 在内部保存状态

在shell的例子中,你希望使用私有区域持久保存命令的历史记录。下面是history命令的工作原理。它会反向输出通过shell运行的命令:

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

代码清单3-11展示了如何使用bundle的私有数据区域保存命令的历史记录。为了调用这些方法,bundle激活器的start()和stop()方法同样需要修改,但是这些改变未在下面展示出来,所以请参考配套代码,了解完整的实现细节。

代码清单3-11 bundle永久存储示例

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

你可以使用BundleContext.getDataFile()方法在bundle的私有存储区域中得到一个File对象。该方法具有一个String类型的表示相对路径的参数,并返回存储区域中一个有效的File对象。得到File对象后,你可以像平常一样用它来创建文件、子目录,或者做任何想做的事情。当bundle请求文件时,框架也可能返回null,如2所示,你需要处理这种可能性。这是因为OSGi框架被设计用于在多种设备上运行,其中一些可能不支持文件系统。在shell的例子中对不支持文件系统的情况进行了忽略,这是因为history命令并不是非常重要的功能。

如果你想获取位于bundle存储区域根目录中的File对象,可以调用不带任何参数的getData File()方法。你的bundle负责管理数据区域的内容,但你不必在卸载时清空存储空间,因为框架会处理此事。

未雨绸缪
请记住,bundle可能会被更新。由于这种可能性,你需要设计bundle,以便它们能够恰当地处理过去保存的状态,因为它们可能从旧版本bundle的私有存储区域中启动。如果可行的话,最佳实践是bundle从旧状态格式无缝地迁移到新状态格式。不过,比较棘手的是,升级生命周期的操作也可能用于降级一个bundle。在这种情况下,bundle可能难以处理最新的状态格式,所以可能最好的方式是你实现你的bundle,当它们不能理解新的状态格式时,就删除任何存在的状态。否则,你总是需要先卸载最新的bundle,然后再安装旧版本,而不是降级。

你可以完成history命令了,但通过实现追踪框架中发生的事情,尝试使它更加有趣一些。你记录的不仅仅是执行的命令,也需记录它们对框架的影响。下一节展示通过框架的事件通知机制来实现这功能。

3.3.5 事件监听

OSGi框架是一个动态的执行环境。在创建bundle,以及最终的应用时,为了实现足够的灵活性,即不仅要处理动态性,同时也要发挥这种动态性的优势,我们需要注意OSGi框架在执行时发生的变化。生命周期层API提供了对很多信息的访问方式,但轮询这些变化并不容易。如果在发生变化时,我们能收到相应的通知,那就方便多了。为此,OSGi框架支持两种类型的事件:BundleEvents和FrameworkEvents。前者报告bundle生命周期的变化,后者则报告与框架相关的事件。

你可以在bundle中使用普通的Java监听模式接收这些事件。BundleContext对象里包含注册BundleListener和FrameworkListener对象的方法,分别用于接收BundleEvent和FrameworkEvent通知。代码清单3-12展示了如何实现history命令,它会记录所有已执行的命令以及在执行时引发的事件。

代码清单3-12 bundle和框架事件监听器示例

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


我们使用拦截器模式来封装命令,从而可以记录已执行的命令。包装器也可以通过实现Bundle Listener和FrameworkListener接口在历史记录中记录所有的事件。我们维护了一个所有已执行命令列表,并在1中定义的m_history成员中接收事件。历史包装器命令转发命令执行到命令2,并将其存储到历史列表中。

包装器实现了单个FrameworkListener.frameworkEvent(),在历史列表中记录事件信息。事件中最重要的部分是其类型。框架的事件应是如下类型之一。

  • FrameworkEvent.STARTED——表示框架已经执行完所有的初始化,并已完成启动。
  • FrameworkEvent.INFO——表示不同场合下共同关注的一些信息。
  • FramewrokEvent.WARNING——表示一个警告,虽然并不重要,但是可能预示了潜在的错误。
  • FrameworkEvent.ERROR——表示一个错误,需要立即处理。
  • FramewrokEvent.PACKAGES_REFRESHED——表示框架已经刷新了一些共享包。3.5节将讨论其含义。
  • FrameworkEvent.STARTLEVEL_CHANGED——表示框架已经改变了其启动等级。第10章将讨论其含义。包装器还实现了一个BundleListener.bundleChanged()方法,也需要在历史记录表中记录事件信息。bundle事件为下列类型之一:
  • BundleEvent.INSTALLED——表示bundle已安装;
  • BundleEvent.RESOLVED——表示bundle已解析;
  • BundleEvent.STARTED——表示bundle已启动;
  • BundleEvent.STOPPED——表示bundle已停止;
  • BundleEvent.UPDATED——表示bundle已更新;
  • BundleEvent.UNINSTALLED——表示bundle已卸载;
  • BundleEvent.UNRESOLVED——表示bundle未解析。

使用bundle上下文注册监听器如下所示:

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

上述示例没有展示如何删除监听器,它需要在bundle上下文中调用removeBundleListener()和removeFrameworkListener()方法。删除监听器是没有必要的,这事因为当bundle停止时,框架会自动删除它。很显然,因为当bundle停止后,bundle上下文不再有效。只有想在bundle激活时停止监听事件,才需要显式地删除监听器。

在很大程度上,框架采用异步的方式传递事件。实现同步传递也是可以的,但是一般不这么做,因为这会使并发处理更加复杂。有时的确需要同步传递,比如我们需要在事件发生时执行一个操作。注册一个实现了SynchronousBundleListener接口而不是BundleListener的监听器,Bundle Events可以实现这个功能。这两个接口看上去相同,但框架同步地向Synchronous Bundle- Listeners传递事件,这就意味着在处理事件时,监听器就会收到通知。同步的bundle监听器在普通bundle监听器之前就已经得到处理了。这样你就可以在触发某个操作时执行某些操作。例如,可以在bundle安装时给它权限。如下事件类型只能发送给SynchronousBundleListeners:

  • BundleEvent.STARTING——表示bundle将要启动;
  • BundleEvent.STOPPING——表示bundle将要停止。

同步bundle监听器在某些时候是必须的(你将在下节的绘图示例中看到),但必须小心使用。如果在回调中尝试做许多事情,它们可能导致并发问题。一如既往,尽量使回调精简,同时在持有锁时,不要调用外部代码。在其他情况下,调用监听器回调方法的线程是不确定的。如果开始编写充分利用bundle生命周期特性的更加复杂的bundle时,那么事件将变得更加重要。

3.3.6 bundle自我销毁

我们已经多次提到:bundle不应改变自身的状态。但是如果bundle希望改变自己的状态呢?好问题。这是生命周期层中更加复杂的方面之一,可能涉及一些负面问题。

核心问题是bundle自行停止时,它发现自己处于一个不应该处于的状态。其Bundle Activator.stop()方法已被调用,意味着bundle上下文不再有效。另外,框架已经清空bundle的记录,并释放所有它使用的框架设施,例如注销所有事件监听器。如果bundle试图自行卸载,则情况会更加糟糕,因为框架可能会释放它的类加载器。简而言之,bundle处于恶劣的环境中,不能正确地工作。

因为其bundle上下文不再有效,一个停止的bundle不能再使用框架提供的功能。无效的bundle上下文中的大部分方法调用都会抛出IllegalStateExceptions异常。尽管bundle的类加载器被释放了,如果bundle不需要任何新类,那么也不一定造成严重的问题,因为类加载器不会执行垃圾收集,直到bundle停止使用它。但是如果卸载了bundle,并不能保证可以加载新的类。在这种情况下,框架可能已经关闭了与bundle关联的JAR文件。已经加载的类继续被加载,但是当尝试加载新类时,后果将会难以预料。

根据bundle的情况,你可能也会遇到其他问题。如果你的bundle创建并使用线程,一般来说,当调用BundleActivator.stop()方法时,等待所有线程完成是明智的。如果 bundle尝试在自己的线程中停止自身,该线程最后可能会循环地等待其他兄弟线程完成。最终,该线程将一直等待下去。例如,简单的shell使用一个线程监听telnet连接,并使用辅助线程执行那些连接发出的命令。如果其中一个辅助线程尝试停止shell bundle自身,最后会停留在shell bundle的BundleActivator.stop()方法中,等待连接线程停止所有辅助线程。因为调用线程是辅助线程之一,它最终会一直等待连接线程完成。你需要关注这些类型的状况,它们并不总是显而易见的。

通常情况下,你不应尝试停止、卸载,或者更新自己的bundle。可以了——已经足够多免责声明了。我们研究一种不得不这么做的情况。我们将使用shell作为示例,因为它提供了更新bundle的方法,同时可能需要升级自身。为了允许使用者通过shell命令行升级shell bundle本身,应该如何去做呢?为了安全起见,需要做如下两件事情。

  1. 当停止、更新或者卸载bundle时使用一个新的线程。
  2. 在新线程中调用停止、更新或卸载后不做任何事情。

这么做是为了防止当新线程停止后,你会一直等待shell线程返回,同时避免线程陷入潜在的危险境地。代码清单3-13展示了为了适应这种情形,stop命令的实现做出的改变。

代码清单3-13 bundle如何停止自身

http://assets.osgi.com.cn/article/7289386/21.jpg
使用BundleContext.getBundle()方法获得bundle的一个引用,并与目标bundle进行比 较1。当目标是shell bundle时,你需要使用不同的线程停止它。因此,创建并启动一个SelfStop Thread类型的新线程,它执行Bundle.stop()方法2。该示例中最后一点要注意的是:你改变了停止bundle的行为,在本例中是从同步变成了异步。最后,这并不会有太大影响,因为bundle无论如何都会停止。

你也需要按照同样的方式修改update命令和uninstall命令的实现。使用shell停止框架(系统bundle)也需要特殊考虑。为什么?因为停止系统bundle将导致框架停止,也会每隔一个停止一个bundle。这意味着将间接地停止你的bundle,所以需要确保你正在使用一个新的线程。

我们希望你现在对OSGi的生命周期层可能发生的事情有一个很好的理解。接下来,将这些知识应用于绘图程序。

3.4 动态扩展绘图程序

了解一下如何使用生命周期层的各部分动态扩展画图程序。如上章所述,通过在架构上采用基于接口的编程方法,我们首先将一个非模块化版本的绘图程序转化为一个模块化程序。这非常了不起,因为你只需最少的额外工作就可以重用了已有的bundle。包含图形实现的那些bundle,除了在清单文件中添加一些额外的元数据,其他无需改动。你要做的仅是修改绘图程序,以实现在执行时添加和删除图形。

你将采用的方法是一个在OSGi世界中很有名的模式,扩展者模式(extender pattern)。扩展者模式背后的主要思想是,在其他bundle的生命周期事件(安装、解析、启动、停止等)上构建动态扩展性。通常,应用中的某个bundle扮演扩展者的角色:它负责监听bundle的启动、停止。当一个bundle启动时,扩展者会对其进行检测并判断它是否是一个扩展bundle。扩展者检查bundle的清单文件(使用 Bundle.getHeaders())或者bundle的内容(使用Bundle.getEntry()),以此来寻找它能够识别的特定元数据。如果这个bundle确实包含一个扩展,那么该扩展会通过元数据来描述。扩展者读取元数据并执行必要的任务,这有助于将扩展bundle集成到应用中。如果扩展bundle被停止,扩展者同样会监听到,此时扩展者会将相关的扩展从应用中移除。

图3-16是对扩展者模式的一般描述。接下来我们将学习如何在绘图程序中应用该模式。

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


图3-16 扩展者模式概述

将图形接口的实现视为扩展。与扩展相关的元数据将被包含在bundle的清单文件中,该元数据将描述在图形bundle中哪些类实现了图形接口。当一个扩展bundle 被激活时,扩展者将利用上述信息从bundle中加载图形类,完成实例化,并将其注入到应用中。如果一个图形bundle被停止了,扩展者会将它从应用中移除。图3-17是该使用场景的一个说明。

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

图3-17 扩展者模式的实现示例——绘图程序

让我们进一步深入分析并开始改进这个应用程序。你要做的第一件事是为图形bundle定义扩展元数据,以此来描述它们对图形接口的实现。在接下来的代码片段中,为SimpleShape接口增加一些常量,以此来表示扩展元数据的属性名称。尽管这不是必须要增加的内容,但在编程时使用常量是一种很好的做法。

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

图形的名称、图形图标用到的bundle资源文件以及用来表示图形类的bundle类名均由常量来表示。draw()方法负责将图形绘制到画布上。

通过这些常量可以很容易理解如何描述一个特定的图形接口的实现。你只需要知道扩展名称、图标以及实现图形接口的类。举个例子,若把圆作为图形接口的一个实现,需要在其bundle的清单文件中添加如下条目:

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

名称是一个String类型的字符串,图标和类分别指向bundle JAR文件中的资源文件和某个具体的类。对于所有图形接口的实现bundle,在其清单文件中增加类似的元数据,将把这些bundle转换为扩展。接下来,你要做的是调整绘图程序架构使其可以动态添加和删除图形。图3-18描述了修改后的设计。

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

图3-18 动态绘图程序相关类及关联关系

将新设计和原始设计进行比较,你会发现增加了两个新类:ShapeTracker和DefaultShape。它们可以帮助你动态调整绘图框架,从而完成实现SimpleShape接口的图形的动态显示和消失。在shell中,ShapeTracker用来追踪扩展bundle的启动和停止,同时相应地将默认图形移除或移入PaintFrame。

ShapeTracker类的具体实现是另一个类BundleTracker的子类。BundleTracker是一个抽象类,用来追踪bundle的启动和停止。由于该类比较大,我们将其分解为几个部分,在多个代码清单中介绍,代码清单3-14显示的是该类的第一部分。

代码清单3-14 bundleTracker 类的声明和构造函数

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

构造bundle 追踪器类时需要BundleContext对象并以此来监听bundle的生命周期事件。当bundle进入STOPPED状态时,一般的BundleListener都能监听到,但当bundle进入STOPPING状态时则无法监听到。追踪器使用SynchronousBundleListener来监听事件,从而实现了对上述两种情况的监听。你需要应对的是STOPPING事件而非STOPPED事件,因为处于STOPPING状态的bundle还没有停止,可以继续使用。如果某个子类需要访问处于STOPPING状态的bundle的Bundle Context对象,那它可能会进行类似的操作。bundle监听器的唯一方法1确保追踪器对bundle的追踪2。因此,当监听到启动事件时,追踪器会将相关bundle加入到bundle列表中3,同时调用抽象方法addedBundle()。同样,当监听到STOPPING事件时,会将bundle从bundle列表中移除并调用抽象方法removedBundle()。

代码清单3-15显示的是BundleTracker的下一部分。

代码清单3-15 打开并使用BundleTracker

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

若要启动一个BundleTracker实例追踪bundle,必须调用其open()方法。该方法注册了一个bundle事件监听器,该监听器处理已经存在的并处于激活状态的bundle,处理过程是将这些bundle加入它的bundle列表并调用addedBundle()抽象方法。通过getBundles()方法,可以访问当前列表中被追踪的处于激活状态的bundle。由于BundleTracker是抽象类,所以其子类可以分别实现addedBundle() 和 removedBundle()方法,以此来完成添加、删除bundle的自定义处理。

BundleTracker最后一部分代码如代码清单3-16所示。

代码清单3-16 BundleTracker的清除处理

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

调用BundleTracker.close()方法后,BundleTracker会停止追踪bundle。BundleTracker将它的bundle监听器移除,同时将它当前监听的每一个bundle从bundle列表中移除,并调用removed Bundle()抽象方法。

bundle追踪器的标准化
追踪bundle是一个有用的构建块。它非常有用,因此OSGi联盟决定在OSGi规范R4.2中定义一个标准的bundle追踪器。R4.2中的BundleTracker比起刚才的要复杂得多,但是它遵循同样的原则。我们将在第15章进一步讨论bundle追踪器。

现在你已经清楚了BundleTracker的工作原理,下面回过头来介绍BundleTracker的子类ShapeTracker,该子类的核心是processBundle()方法(如代码清单3-17所示),它负责处理添加和移除bundle,接下来的章节将进行讨论。

代码清单3-17 在ShapeTracker中处理图形

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


ShapeTracker复写了BundleTracker中的addedBundle()和removedBundle ()抽象方法,用以在不同情况下调用processBundle()方法。通过查看bundle 清单文件中的Extension-Name 属性可以判断该bundle是否为扩展bundle 1。在清单文件中无该属性的bundle将被忽略。如果被添加的bundle包含一个图形,代码将从bundle的清单文件头中获得元数据并将图形添加至绘图框架,该框架已被包装成DefaultShape 2。至于图标元数据,你可以通过Bundle.getResource()来加载。如果被移除的bundle包含一个图形,你可以将该图形从绘图框架中移除3。

DefaultShape有两种作用,如代码清单3-18所示。它实现了SimpleShape接口,并使用Extension-Class元数据创建图形接口的实现。DefaultShape的另一种作用是当把图形从应用中移除时充当占位符。在原始的绘图程序中你无需处理该情况,但在下列情况下需要进行处理:当bundle被安装、启动、停止及卸载时,如何控制图形实现的随时出现与消失。在这种情况下,对于任何已经过期的图形实现来说,DefaultShape都将会在绘图画布上绘制一个占位图标。

代码清单3-18 DefaultShape示例

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


总之,当绘图应用启动后它的激活器会创建并打开一个ShapeTracker。ShapeTracker可以追踪STARTED 和 STOPPED状态的bundle事件,并从相关联的bundle中寻找扩展元数据。对于每一个启动的扩展bundle,它将向绘图框架中增加一个新的DefaultShape,以此来创建图形的实现,根据具体情况可能会用到扩展元数据。当bundle停止时,ShapeTracker会将图形从绘图框架中移除。当一个已经绘制的图形不再可用时,DefaultShape会在画布上面绘制一个图形占位符代替它。当过期的图形重新显示时,占位符被移除,真正的图形再一次被绘制在画布上。

如3.2.1节所示,你现在已经拥有了一个动态可扩展的绘图程序。尽管我们并没有展示绘图程序的激活器,但是它非常简单,仅仅是在启动时创建框架和图形追踪器,并在停止时进行相关回收处理工作。总的来说,这是一个很好的例子,它阐释了一个模块化的应用利用生命周期层的特性,可以很容易地实现自身的动态扩展性。作为奖励,你不再需要导入图形实现的相关包。至于生命周期层与模块层之间是如何交互的,我们并没有讨论,后续章节将介绍。

查看评论