《OSGI实战》:第二章 精通模块化( 二)

 由  《OSGI实战》 发布

2.6 完成绘图程序设计

至此,你已经在绘图程序中定义了三个bundle:图形API bundle、图形实现bundle及绘图程序的主bundle。让我们来看一下每个bundle完整的元数据。下面的清单列出了图形API bundle的元 数据:

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

图形实现bundle的元数据如下面的清单所示:

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

绘图程序主bundle的元数据如下面的清单所示:

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

正如你在图2-13中看到的,这三个bundle直接反应了绘图程序的逻辑包结构。

这种方法是合理的,但是能否改善呢?在某种程度上,只有当你了解更多关于绘图程序用途的时候,你才能回答这个问题;但不管怎样,让我们先仔细地看一下。

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

图2-13  绘图程序的bundle结构

2.6.1 提高绘图程序的模块化

在当前的设计中,突出了图形实现bundle。在单个bundle、单个包中实现所有的图形实现是不是存在缺点呢?换个问法也许更好理解。将图形实现分解到多个bundle中有哪些优势呢?试想一下,有些情况下,并非所有的图形都是必要的;例如,小型设备可能没有足够的资源来支持所有的图形实现。如果把图形实现分解到单独的包和bundle中,当需要创建应用程序的不同配置时会有更大的灵活性。

当对应用程序进行模块化时,这是一个很好的参考案例。可选组件或者可能有多个替代实现的组件非常适合分割到单独的bundle中。将你的应用程序分解到多个bundle会带来更大的灵活性,因为基于你定义的bundle的粒度,你只限于部署应用程序的配置。听上去不错吧?你可能想知道为什么不把应用程序分解到尽可能多的bundle中。

为了提供灵活性,需要将应用程序划分到多个bundle中。多个bundle意味着有多个版本独立的构建,创建大量需要管理的依赖关系和配置信息。所以为项目的每个包都建立一个bundle可能不是一个好主意。例如,当决定如何才能最好地划分应用程序时,你需要分析和理解对灵活性的要求。没有适用所有情况的规则。

回到绘图程序中,假设我们的最终目标是,使基于不同的图形集合创建不同的应用程序配置成为可能。为此,你把每个图形的实现放入单独的包中(org.foo.shape.circle、org.foo. shape.square和org.foo.shape.triangle)。现在可以将每个图形实现分别放在不同的bundle中。下面是圆形bundle中的元数据信息:

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

正方形实现bundle和三角形实现bundle的元数据,除了在适当的地方使用了相应的图形名称外,几乎完全相同。图形实现bundle依赖于Swing和公共API,并导出具体实现的图形包。这些变化也需要修改实现bundle的程序元数据;元数据修改如下:

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

绘图程序的实现bundle依赖于Swing、公共API bundle以及三个图形实现bundle。图2-14展现了绘图程序的新结构。

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

图2-14 不同图形在不同模块实现的绘图程序的逻辑结构

现在你有了5个bundle(图形API bundle、圆形实现bundle、正方形实现bundle、三角形实现bundle以及绘图bundle)。好极了。但是你怎样使用这些bundle呢?绘图程序最初的版本是通过在PaintFrame中声明一个静态的main()方法来启动的;你还要用它来启动程序吗?你可以将所有的bundle JAR文件加入到类路径上来使用它,因为所有的示例bundle都可以作为标准的JAR文件使用,但这违背了模块化应用程序的目的。这样也不会执行模块边界检查或一致性检查。要想拥有这些好处,你必须使用OSGi框架启动绘图程序。让我们看一下你需要做什么。

2.6.2 启动新的绘图程序

本章的重点是使用模块层,但是没有生命周期层的帮助是无法启动应用程序的。现在讨论生命周期层不是本末倒置,我们为你创建一个通用的OSGi bundle启动程序来启动绘图程序。这个启动程序很简单:从命令行执行启动程序并指定一个包含bundle的目录路径。它会创建一个OSGi框架并部署指定目录中的所有bundle。最酷的是这个通用的启动程序隐藏了所有的细节和OSGi特有的API。我们将在第13章详细讨论启动程序。

仅仅将绘图程序的bundle部署到OSGi框架还不足以启动绘图程序;仍然需要某种方式来启动它。可以复用绘图程序原始版本中的静态main()方法来启动新的模块化版本。为了能与bundle启动程序一起工作,需要将原始绘图程序中的以下元数据添加到绘图程序bundle的声明清单中:

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

如同在原始的绘图程序中,这是一个标准的JAR文件元数据用于指定包含应用程序的静态main()方法的类。请注意,这不是OSGi规范定义的特性,而是bundle启动程序的特性。构建和启动新模块化的绘图程序,进入配套源代码chapter02/paint-modular/目录下,找到相应代码并输入ant。这样就完成了所有代码的编译和模块的打包。输入 java -jar launcher.jar bundles/ 启动绘图程序。

程序表面上同以往的启动一样,但实际上,是OSGi框架在解析bundle的依赖,验证其一致性并强化逻辑边界。这就是启动程序做的全部工作。现在,你已经使用OSGi的模块层创建了一个很好的模块化应用程序。OSGi基于元数据的方法不需要改动应用代码,但确实将几个类迁移到不同的包中来提高逻辑模块化和物理模块化。

OSGi框架的目标是屏蔽很多复杂性。但有时,了解这些屏蔽的东西是有益处的,比如当程序出错时帮助调试基于OSGi的应用。下一节,我们将看看OSGi框架做了哪些工作,使你更深入的理解每件事情是如何结合在一起的。之后,我们将总结模块化绘图程序的优点,以此结束本章。

2.7 OSGi依赖解析

你已经学会了如何使用Bundle-ClassPath标签描述组成bundle的内部代码,使用Export-Package标签公开内部代码用于共享,以及用Import-Package声明对外部代码的依赖。虽然我们已经隐含地提到了OSGi框架如何使用bundle的导出包,作为另一个bundle的导入包,但没有细讲。bundle元数据中的Export-Package和Import-Package构成了OSGi bundle依赖模型的主干,这是bundle之间包共享的前提。

本节将阐述OSGi如何解析bundle的包依赖,确保bundle间的包一致性。学习完本节,你会清楚地了解OSGi框架是如何使用bundle模块化元数据的。你可能想知道为什么这是必要的,因为bundle解析似乎是OSGi框架的实现细节。诚然,本节涵盖了一些更复杂的OSGi规范的细节。但如果了解了OSGi规范背后的实现细节。对定义bundle元数据是很有帮助的。此外,当你调试基于OSGi的应用程序,这些信息是十分有用的。让我们开始吧。

2.7.1 自动解析依赖

对于开发人员来说,向JAR文件中添加OSGi元数据是额外的工作,那为什么还要这么做呢?最主要的原因是,这样你就可以使用OSGi框架来支持和执行bundle固有的模块化。OSGi框架执行的最重要的任务之一是自动化管理依赖,也就是所谓的bundle依赖解析。

使用bundle之间,它的依赖关系必须由框架来解析,如图2-15所示。OSGi框架的依赖解析算法是复杂的,我们将进行详细阐述,但让我们先从一个简单的定义开始。

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

图2-15 当bundle A依赖bundle B的包,且bundle B依赖bundle C的包时,会导致传递依赖。要使用bundle A,你需要对bundle B和bundle C的依赖都进行解析

解析 将给定bundle的导入包与其他bundle的导出包进行匹配的过程,且以一致的方式匹配,所以任何给定bundle只能访问同一个版本。

如果导出bundle本身没有进行依赖解析,解析bundle可能需要框架去解析其他bundle的传递依赖。bundle解析的结果集是以某种方式从概念上连线(wired)在一起,即一个bundle的任何给定导入包连接到其他bundle匹配的导出包,其中连线意味着导入包与导出包的实际联系。最后结果是所有bundle连接在一起,所有导入包依赖得到满足。如果任何一个依赖没有得到满足,则解析失败,只有等到依赖都得到满足bundle才可以使用。

这样的描述可能会使你想问三个问题。

  • 框架什么时候解析bundle依赖?
  • 框架如何第一时间获取bundle并解析依赖?
  • 导入bundle连接到导出bundle意味着什么?

前两个问题是相关的,因为都涉及生命周期层,关于生命周期层下一章将讨论。对于第一个问题,可以说当一个bundle试图去使用其他budnle时,框架会自动解析这个被使用的bundle。第二个问题,为了进行bundle解析,所有的bundle都必须安装到框架中(第13章将更深入的讨论bundle安装)。由于本节讨论需要,我们会一直谈论到安装bundle。对于第三个问题,因为连接bundle的技术细节并不重要,所以并不会详细回答这个问题。但为了满足好奇心,在更详细地介绍解析过程之前,我们先来简单地解释一下。

在执行时,每个OSGi bundle都有一个与之关联的类加载器,这个类加载器使bundle可以获得其有权访问的所有类(由解析过程确定的类)。当导入bundle连接到导出bundle时,导入bundle的类加载器会得到导出bundle类加载器的引用,因此导入bundle可以委托导出bundle的类加载器去请求导出包中的类。不用管这是如何发生的——放松,让OSGi自己解决。现在,让我们看一下更详细的解析过程。

简单案例 乍一看,依赖解析相当简单;框架只需要将导入导出包进行匹配。下面来思考一下绘图程序中的片段:

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


由此看来,你知道绘图程序对包org.foo.shape有单一依赖。如果只把这个bundle安装到框架中,是不可用的,因为它没有满足依赖关系。要使用绘图程序bundle,你必须安装图形API bundle,其包含以下元数据:

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


当框架试图解析绘图程序bundle时,它知道必须找到一个匹配org.foo.shape的导出。在这种情况下,在图形API bundle中找到候选导出。当框架发现一个匹配的候选bundle时,必须确定候选bundle是否已经解析。如果候选bundle已经解析,那么就可以选择它来满足依赖关系。如果它没有被解析,框架必须在选择它之前先进行解析。这是解析依赖的传递性。如果图形API bundle没有任何依赖,总是可以被成功解析。但是,你知道这个例子确实有一些依赖,即javax.swing:

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


当框架试图解析绘图程序时会怎样呢?默认情况下,在OSGi环境中是不会成功的,这意味着绘图程序无法使用。为什么?因为尽管图形API bundle的包org.foo.shape满足了主程序的导入依赖,却没有bundle能满足图形API bundle的javax.swing导入包的依赖。一般而言,要解析这种情况的bundle,你可以在概念上安装另一个bundle,导出所需的包:

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


现在,当框架试图解析绘图程序时,则会成功。绘图程序主bundle的依赖由图形API bundle满足,图形API bundle的依赖由Swing bundle满足,且Swing bundle没有任何依赖。绘图程序主bundle解析完成后,三个标记为候选的bundle也都完成了解析,而且框架不会再试图解析它们(除非某些情况下需要这么做,下一章将进行介绍)。框架最终将bundle连接在一起,如图2-16所示。 图2-16中的连接线告诉你了什么?它表示当主bundle需要包org.foo.shape中的类时,可从图形API bundle中得到。也表示当图形API bundle需要包javax.swing中的类时,从Swing bundle中得到。尽管这个例子很简单,基本上是框架进行bundle依赖解析时试图做的事情。

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

图2-16 传递bundle解析连线

系统类路径委托
实际上,如果你运行的OSGi框架附带JRE,而JRE包含javax.swing的情况下,在前面的例子中使用javax.swing有点误导。在这种情况下,你可能需要bundle使用JRE中的Swing。框架可以提供使用系统类路径委托的访问。第13章将介绍这方面的使用,但这突显出了重量级JRE方法的缺点。如果能够安装一个bundle来满足对Swing的依赖,为什么还要默认打包在JVM中呢?采用OSGi模式可以极大地避免未来的JVM实现。

你已经学会了在导出和导入包中附加属性。当时,我们就已经足够了解附加到导入包的属性与附加到导出包的属性相匹配。现在你可以更充分地理解其中的意义。下面修改bundle元数据的片段,以更深入地理解这些属性如何影响解析过程。假设像这样修改Swing bundle:

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

在这里,通过将Swing bundle的vendor属性值修改为"Sun",导出包javax.swing。如果其他bundle的元数据不做任何修改,重新执行解析过程,这种变化有什么影响?这种微小的变化没有任何影响。所有的解析都如以往一样,vendor属性没有发挥任何作用。从你的角度看,这似乎看起来有些混乱。正如我们先前描述的属性,导入属性与导出属性相匹配。在这种情况下,导入声明没有涉及vendor属性,因此它被忽略了。让我们还原Swing bundle的变化,像下面这样修改API bundle:

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

现在尝试去解析绘图程序bundle会失败,因为导出javax.swing包的bundle中没有能匹配API bundle vendor属性的。Swing bundle中重新添加vendor属性,使得绘图程序主bundle使用相同的bundle连接再次成功解析,如图2-16所示。导出包的属性只有在导入包指定了它们时才会生效,在这种情况下,属性值必须匹配,否则解析失败。

回想一下,我们也谈到了version属性。除了用于指定范围的更传神的间隔符号,它的工作原理与其他任意属性相同。例如,你可以按如下方式修改图形API bundle:

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

并且可以按如下方式修改绘图程序的bundle:

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

在这个例子中,OSGi框架仍然可以正确处理,这是因为图形API bundle的导出匹配了绘图程序bundle的导入。vendor属性也对应匹配,2.0.0版本属于[2.0.0, 3.0.0)的前闭后开区间。这个特定的例子在import声明中列出了许多匹配的属性,框架自动按照逻辑与进行处理。所以,如果当导入声明中的任何一个属性没有与给定的导出相匹配,那么导出就完全没有匹配上。

总体来说,在解析过程中属性并没有增加其复杂性。这是因为属性对导入导出时的包名匹配添加了额外的限制。接下来,我们将研究一些稍微复杂点的bundle解析场景。

多个匹配包的提供者

在上一节,依赖关系的解析是非常简单的,因为每个依赖关系都只有一个可选项。OSGi框架不限制多个bundle导出相同的包。实际上,OSGi框架的优势之一就是支持并行的多版本,这意味着,可以在正在运行的JVM中使用同一个包的不同版本。在独立开发bundle的高度协作开发环境中,限制使用包的哪些版本是非常困难。同样,在大型系统中,不同的团队可以在不同的子系统中使用库的不同版本。使用不同版本的XML解析器就是最好的例子。

让我们来考虑一下,当解析一个依赖关系时有多个可选项会出现什么情况。当一个Web应用需要导入javax.servlet包,而servlet API bundle和Tomcat bundle同时提供了这个包(见图2-17)。

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

图2-17 多个bundle导出相同的包,框架如何选择

当OSGi架构尝试解析这个Web应用的依赖关系时,发现这个应用请求版本最低为2.4.0的javax.servlet,而Serlvt API bundle和Tomcat bundle同时满足这个要求。因为这个Web应用只能关联到包的一个版本,那OSGi架构要如何在这两个可关联项中选择呢?你可以猜到,框架会选择版本号更高的选项,所以在这个例子中,选择了Tomcat来解析Web应用的依赖关系。这听起来确实很简单。那如果两个bundles同时导出了相同的版本,比如2.4.0呢?

在这种情况下,框架会根据bundle在框架中的安装顺序来选择。先安装的bundle会比后安装的更优先。我们之前已经提过,下一章将会介绍在框架中安装bundle的真正含义。这里,我们假设Servlet API比Tomcat安装得更早,那么servlet API将会被选中来解析Web应用的依赖关系。所以,框架在极端协作的环境下会有更多的考虑,通过区分可选依赖项的优先级来实现。

到目前为止,我们一直假设在明确安装了所需bundle的环境开始解析过程。但是OSGi框架允许在执行过程中动态安装bundle。换句话说,OSGi框架并不总是从一个干净的状态开始。当新的bundle被安装时,框架允许一些bundle已被安装、解析和使用。这就提出了一种新的区分导出信息的方式:已解析导出和未解析导出。已解析的导出有更高的优先级。所以当从两个可选导出中选择时(一个已解析,另一个未解析),OSGi框架会优先选择已解析的。再来一起看一下javax. servlet包有servlet API和Tomcat两个导出版本的例子。假设 servlet API版本为2.4.0,Tomcat版本为2.5.0,如果servlet API已经被解析,那么框架将会选择它来解析Web应用的依赖关系,即使它并不是最高版本,如图2-18所示。这是为什么呢?

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

图2-18 如果一个bundle已解析,由于它已经在其他bundle中使用了,这个bundle优先于只是安装过的bundle

框架必须处理最大可能的工程协作。多个bundle只有在使用同一个共享包的同一个版本时才能进行协作。在解析时,OSGi框架把选择已解析的包作为最小化同一个包的不同版本数量的一种方法。下面再来一起总结一下依赖关系解析时,选择候选bundle的优先级关系。

已解析的候选bundle有最高优先级。当有多个匹配项时,按照先版本号后安装顺序的方式确定优先级。

其次是未解析过的候选bundle。当有多个匹配项时,依旧按照先版本号后安装顺序的方式确定优先级。

看来,我们已经介绍了所有的基础知识,是不是?并不完全是。接下来,我们将一起看看必要的附加限制是如何保证bundle依赖关系解析的一致性的。

2.7.2 使用约束保证一致性

从任意一个bundle的角度来看,都有一个对其可见的包集合,也就是我们所说的类空间。根据当前你的理解,可以将bundle的类空间定义为:导入的包与bundle的类路径中设置的可访问包的并集,如图2-19所示。

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

图2-19 bundle A的类空间被定义为它的类路径和导入包(由bundle B的导出来提供)的并集

一个bundle的类空间必须是一致的,也就是说只有一个给定包的实例对bundle可见。这里,包的实例有相同的名称但来自不同提供者。比如,上一个例子中,servlet API bundle和Tomcat bundle都导出了javax.servlet包。OSGi框架尽力保证所有bundle的类空间一致。上一节中提到的通过区分优先级的方式,为导入包选择导出包的方式就不能满足要求了。为什么呢?一起看一下下面这段代码中简单的 API:

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

这是从第15章的API代码中截取的一段。当前阶段,它的功能细节并不重要。现在,我们只需要知道它的方法签名。假设这个API的实现打包为一个包含包org.osgi.service.http而不是javax.servlet的bundle。这意味着在它的清单文件中包含如下元数据:

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

假设HTTP服务bundle和servlet库bundle都已经安装到了框架中,如图2-20所示。给定了这两个bundle后,OSGi框架会做出唯一的选择,也就是选择Servlet API bundle提供的javax.servlet。

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

图2-20 HTTP服务依赖解析

现在,我们假设在框架中再安装两个bundle:一个是导出2.4.0版本javax.servlet的Tomcat bundle,另一个是导入2.4.0版本javax.servlet字号包含HTTP服务客户端的bundle。当OSGi框架解析了这两个新的bundle之后,会按照图2-21的方式来处理。

HTTP客户端导入org.osgi.service.http和分别由HTTP服务bundle和Tomcat bundle分别提供的2.4.0版本的javax.servlet。看起来一起都很顺利:所有的bundles都正确解析了依赖关系,对吧?并不完全是。在依赖关系解析时出现了一个问题——你能看出来吗?

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

图2-21 后续的HTTP客户端依赖解析

观察HTTPService.register Servlet()方法中的servlet参数,看看使用的是哪个版本的javax.servlet。因为HTTP服务bundle已经连接至servlet API bundle,它的参数是2.3.0版本的javax.servlet.Servlet。当HTTP客户端bundle尝试调用HTTPService.register- Servlet()方法时,哪个版本的javax.servlet.Servlet实例会传递过去呢?因为HTTP客户端已经连接至Tomcat bundle,所以它创建了2.4.0版本的javax.servlet.Servlet实例。这时,HTTP服务bundle和客户端bundle的类空间出现了不一致,两个不同版本的javax.servlet都可以被访问到。在执行时,当HTTP服务bundle和客户端bundle交互时,会导致类抛出异常。到底是哪里出了问题?

框架在进行bundle依赖解析时,每次都做出最优的选择。但是由于解析过程的增量特性,无法做出全局最优的选择。如果同时安装4个bundle,那么框架处理依赖关系时就可以利用现有的规则来满足依赖的一致性。图2-22描述了当4个bundle一起被解析时的依赖解析过程。 http://assets.osgi.com.cn/article/7289381/2-22.jpg

图2-22 HTTP服务bundle和客户端bundle依赖解析的一致性

由于只有javax.servlet包的一个版本正在使用,所以我们知道HTTP服务bundle和客户端bundle的类空间是一致的,它们之间可以正常地交互。但这是否是一个解决类空间一致性问题的一致方法呢?很遗憾,正如第3章将介绍的,并不是。因为OSGi允许在任意时刻动态安装和卸载bundle。而且,类空间的不一致性并不仅仅由依赖的增量解析导致。在静态的bundle集合下解析时,由于约束的不一致性,也可能会出现不一致的情况。比如,假设HTTP服务bundle需要精确地依赖2.3.0版本,而HTTP客户端bundle需要精确地依赖2.4.0版本。很明显这些约束导致了不一致性,但是框架却仍然很自然地按照已经给定的一套依赖解析原则,来解析这个示例的bundle的依赖关系。为什么不能自动检测不一致性呢?

bundle内和bundle间的依赖

这里的难题是,Export-Package和Import-Package只表示了bundle内的依赖关系,但bundle之间的依赖关系可能会导致类空间的一致性冲突。回想一下org.osgi.service. http. HttpService接口,它的register-Servlet()方法定义了一个javax.servlet.Servlet类型的参数,用于标明org.osgi.service.http使用了javax.servlet。图2-23展示了HTTP服务bundle的导入导出包的bundle间的uses关系

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

图2-23 bundle导出uses导入

uses关系是如何出现的呢?这个例子展示了一种典型方式,即导出包中的类方法签名将类公开给其他包的典型方式。这看起来一目了然,因为uses类型是可见的,但其实并不全是这样。还可以提供给用户向前兼容的基类来公共一个类型。uses类型的关系很重要,如何在bundle元数据中进行描述呢?

uses指令 导出包中附带的指令,其值是一组用逗号分割的由相应导出包公开的包。

2.5节中的补充内容“JAR文件清单文件语法”介绍了指令的概念,这是第一个使用指令的例子。指令是附加的元数据,将框架解释元数据的方式改变为指令所申明的内容。用于定位指令的语法与属性类似。例如,HTTP服务的元数据按以下方式修改就展示了如何使用uses指令:

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

指令使用:=赋值语法,指令的顺序和属性并不重要。这个例子表明org.osgi. service. http 使用javax.servlet。OSGi框架到底如何使用这些信息呢?包之间的uses关系就像把包的限制进行了编组。在这个例子中,框架保证org.osgi.service.http的导入程序和HTTP服务实现使用同一个javax.servlet。

这样就得到了之前缺失的bundle间的包依赖关系。在这个特定的例子中,导出包描述了与导入包之间的uses关系,但还可以使用其他的导出包。这些uses关系对框架进行依赖关系解析时的选择进行了约束,这也是它们被归为约束的原因。理论上说,如果包foo使用包bar,那么导入包foo的bundle如果也使用了包bar,它们也会被相同的包bar所限制。图2-24描述了uses关系对原始增量依赖解析的影响。

以增量解析为例,框架可以检测到类空间的不一致性,并且在尝试使用HTTP客户端bundle时解析失败。早期检测总比运行时出错要好得多,因为检测可以警告部署的bundle中存在不一致。下一章将介绍如何让框架重新解析bundle的依赖关系以便修正不一致情况。

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

图2-24 uses关系的约束检测到类空间的不一致,所以框架可以判断不能解析HTTP客户端bundle

我们可以更进一步修改这个例子,以说明uses约束如何帮助找到合适的依赖解析。假设HTTP服务bundle严格地导入2.3.0版本的javax.servlet,但是客户端需要2.3.0或者更高版本。通常,框架会选择最高版本的包来处理依赖关系。但由于uses约束,框架最终会选择一个低版本的包,如图2-25所示。

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

图2-25 uses约束指导依赖解析

如果观察一下HTTP客户端的类空间,可以发现框架是如何用这个方法处理完成的。HTTP客户端的类空间同时包含javax.servlet和org.osgi.service.http,因为它导入了这些包。从HTTP客户端bundle的角度看,可以使用2.4.0或者2.3.0版本的javax.servlet。但是针对org.osgi.service.http,框架只有一个选择。因为HTTP服务bundle中的org.osgi. service.http使用javax.servlet,框架必须为所有客户端选择同一个javax.servlet包。因为HTTP服务bundle只能使用2.3.0版本的javax.servlet,即消除了Tomcat bundle作为客户端包的可能。最终的结果是框架选择了必需包的低版本来满足类空间的一致性,即使它的高版本也同时可用。

使用约束的细枝末节

下面通过介绍uses约束的最后的一些要点来结束这个讨论。首先,uses约束是可传递的,这意味着,如果给定bundle导出包foo使用了导入包bar,被选中导出bar包的bundle使用了baz包,那么导入foo包的bundle的类空间必须与bar包和baz包具有相同的提供者。

此外,即使uses约束是重要的描述指令,你也不想总使用uses约束,因为这样做过分地约束了依赖解析。当解析没有使用uses约束的包依赖时,框架有更多的灵活性,这对支持并行版本是必要的。例如,在大型应用中,独立开发的子系统使用相同XML解析器的不同版本的情况并不少见。uses约束指定得过于宽泛是不可能的。准确的uses约束是很重要的,但幸好已经有工具对导出包生成uses约束。

好极了!你完成了最困难的部分。不要因为不明白每一个细节而担心,因为你有了更多创建和使用bundle的经验时,有些细节会更有意义。让我们把注意力重新集中到绘图程序,回顾一下为什么把程序模块化放在第一位。

2.8 回顾模块化绘图程序的好处

即使创建绘图程序的模块化版本所需的工作并不多,但仍然比放任不管要费些气力。那为什么要创建模块化版本呢? 表2-3列举了模块化的好处。

表2-3 绘图程序模块化的好处
好处描述
逻辑边界增强可以将实现细节私有化,因为你只需要公开那些希望在org.foo.shape的公共API包中公开的部分
重用改进代码更可重用,因为通过Import-Package显示地声明每个bundle依赖的包。这意味着你知道在不同的工程中,需要使用哪些代码
配置校验不需要去猜测是否正确地部署了应用,因为启动应用时,OSGi会验证所有需要的要素是否存在
版本校验与配置校验类似,启动应用时,OSGi会验证应用的所有要素的版本是否正确
配置灵活可以更轻松地通过创建新的配置定制应用的不同方案。想象把绘图程序看做菜单

这些好处更明显一些。有些好处能很容易证明。例如,假设你忘了在启动器里面部署图形API bundle(可以在启动绘图程序前通过删除bundles/shape-2.0.jar的方式来模拟)。如果这样做了,会看到这样的异常:

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

读到第4章时,你将熟悉这个消息的确切语法;但是忽略语法,这个消息告诉你应用缺少org.foo.shape包,这个包由API bundle提供。由于Java按需加载的类加载机制,这样的错误通常只在应用执行时用到缺失类时发生。使用OSGi,可以立即发现缺失bundle或版本不正确等问题。除了检测错误,让我们看一下OSGi模块化如何帮助你创建应用的不同配置。

创建绘图程序的不同配置,如同为启动器调用创建一个新的静态main()方法一样简单。目前,你使用的是PainFrame提供的原始静态方法main()。事实上,使用实现类的静态方法main()不是模块化。最好新建一个独立的类,这样当你要改变应用的配置时,就不需要重新编译实现类了。代码清单2-2是PaintFrame类中已有的静态方法main()。

代码清单2-2 已有的PaintFrame.main()方法实现

http://assets.osgi.com.cn/article/7289381/44.jpg http://assets.osgi.com.cn/article/7289381/45.jpg

已有的静态方法main()很简单。先创建一个PaintFrame实例 ,添加一个监听器 ,当Paint Frame窗口关闭时,使VM退出。然后把图形的各种实现注入到绘图框架 ,最后把应用窗口设置为可见。从模块的角度来看,最重要的部分是 ,因为决定哪些注入的配置决策被硬编码到函数中。如果你希望创建不同的配置,必须重新编译实现bundle。

例如,假设你想在一个小设备上运行只支持绘制单个图形的绘图程序。为此,可以修改Paint Frame.main(),只注入单个形状,但这并不够。还需要修改bundle的元数据,这样它才不会依赖其他形状。当然,在进行这些更改之后,会失去第一个配置。这就是静态方法main()应该放入单独的bundle中的原因。

让我们来解决当前实现中的这种问题。首先,删除PaintFrame.main()方法,并对bundle的元数据作如下修改:

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

主绘图程序主bundle不再依赖各种图形的实现,但需要导出包含绘图框架的包。可以把现有的静态方法main()的方法体放在一个新类org.foo.fullpaint.FullPaint中,并添加以下bundle元数据:

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

要启动这个绘图程序的完整版本,需要使用bundle启动器部署所有相关的bundle,包括FullPaint bundle。同样,你可以创建一个不同的bundle,在代码清单2-3中包含org.foo. smallpaint.SmallPaint类,启动一个仅包含圆形的绘图程序的小型配置。

代码清单2-3 面向绘图程序小型配置的启动器

包含小型绘图程序配置的bundle的元数据如下所示:

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

该小型配置只依赖Swing、公共API、绘图程序实现以及圆形实现。当启动完整配置时,所有图形的实现都是需要的,但小型配置只需要圆形实现。现在你可以基于目标设备部署应用的适当配置,并且由OSGi验证其正确性。非常棒!为了保持完整性,图2-26显示了绘图程序模块化前后的视图。

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

图2-26 绘图程序的模块化和非模块化版本

2.9 小结

本章已经介绍了很多内容,部分重点内容如下。

模块化是分离关注点的一种形式,提供了逻辑上和物理上的类封装。

模块化是可取的,因为它允许你把应用程序从逻辑上分为独立的部分,这些部分可以独立地更改和分析。

bundle是OSGi中模块的名称。是包含代码、资源和模块化元数据的JAR文件。

模块化元数据的详细信息包括可读的信息、bundle标识和代码可见性。

bundle代码可见性包括内部类路径、导出包和导入包可见,这与全局可见的标准JAR文件有很大不同。

OSGi框架使用导入包和导出包的元数据来自动解析bundle依赖,并在bundle可用前确保类型的一致性。

导入包和导出包表达了bundle内的包依赖,而uses约束对表达bundle间的包依赖,确保完整的类型一致是很有必要的。

现在,我们将进入生命周期层——OSGi模块化执行时涉及的方面。本章涵盖的都是OSGi框架如何描述bundle。生命周期层则是关于在执行时如何使用bundle以及OSGi框架提供的便利。


ccfeng 2014-04-28 09:30

很详细,对初学者很有用~

顶(0) 踩(0) 回复

九天 2013-11-02 14:23

读完本章,我对模块化,以及模块化的好处,以及OSGI的优点慢慢的清晰了

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