在接触 OSGi 时,首先要掌握的概念之一就是“捆绑包”(bundle)。在这篇文章中,我想更详细地探讨一下捆绑包究竟是什么,以及一个普通的 jar 文件如何能被转换成一个 OSGi 捆绑包。那么,废话不多说,
什么是捆绑包?
OSGi 规范将捆绑包描述为“模块化的单元”,它“由 Java 类和其他资源组成,这些资源可以一起为最终用户提供功能”。到目前为止都还好,但捆绑包究竟*是什么*?再次引用规范:
捆绑包是一个 JAR 文件,它
- 包含 [...] 资源
- 包含一个清单文件,描述 JAR 文件的内容并提供有关捆绑包的信息
- 可以在 JAR 文件内的 OSGI-OPT 目录或其子目录中包含可选文档
总之,捆绑包 = jar + OSGI 信息(在 JAR 清单文件 - META-INF/MANIFEST.MF 中指定),不需要额外的文件或预定义的文件夹布局。这意味着要从一个 jar 创建一个捆绑包,只需在 JAR 清单中添加一些条目即可。
OSGi 元数据
OSGi 元数据由清单条目表示,这些条目告诉 OSGi 框架捆绑包提供或/和需要什么。规范中列出了大约 20 个清单头,但我们将只关注你最有可能使用的那些。
Export-Package
顾名思义,此头指定了捆绑包中要导出的包,以便其他捆绑包可以导入。*只有*此头指定的包会被导出,其余的将是私有的,不会在包含捆绑包外部可见。
Import-Package
与 Export-Package 类似,此头指定了由捆绑包导入的包。同样,*只有*此头指定的包才会被导入。默认情况下,导入的包是强制性的——如果导入的包不可用,导入捆绑包将无法启动。
Bundle-SymbolicName
这是唯一必需的头,此条目指定了一个捆绑包的唯一标识符,基于反向域名约定(也用于 Java 包)。
Bundle-Name
为此捆绑包定义一个不带空格的可读名称。建议设置此头,因为它比
Bundle-SymbolicName 能提供更短、更有意义的捆绑包内容信息。
Bundle-Activator
BundleActivator 是一个 OSGi 特定的接口,允许 Java 代码在捆绑包被 OSGi 框架启动或停止时收到通知。此头的值应包含激活器类的完全限定名,该类应为公共的并包含一个没有参数的公共构造函数。
Bundle-Classpath
当 jar 包含嵌入式库或不同文件夹下的类包时,此头非常有用,它可以扩展默认的捆绑包类路径(默认情况下,类期望直接在 jar 根目录下可用)。
Bundle-ManifestVersion
这个鲜为人知的头指定了用于读取此捆绑包的 OSGi 规范版本。
1 表示 OSGi release 3,而
2 表示 OSGi release 4 及更高版本。由于
1 是默认版本,强烈建议指定此头,因为 OSGi release 4 捆绑包在 OSGi release 3 下可能无法按预期工作。
下面是一个示例,取自 Spring 2.5.x 核心捆绑包清单,使用了上面提到的一些头。
Bundle-Name: spring-core
Bundle-SymbolicName: org.springframework.bundle.spring.core
Bundle-ManifestVersion: 2
Export-Package:org.springframework.core.task;uses:="org.springframework.core,org.springframework.util";version=2.5.1 org.springframework.core.type;uses:=org.springframework.core.annotation;version=2.5.1[...]
Import-Package:org.apache.commons.logging,edu.emory.mathcs.backport.java.util.concurrent;resolution:=optional[...]
在 OSGi 元数据上花费的大部分时间可能都在 Export/Import 包条目上,因为它们描述了捆绑包(即你的模块)之间的关系。对于包来说,没有任何东西是隐式的——只有提到的包才会被导入/导出,其他的则不会。这同样适用于子包:导出 org.mypackage 只会导出*此*包,而不会导出其他任何东西(如 org.mypackage.util)。导入也是如此——即使一个包在 OSGi 空间中可用,除非被某个捆绑包明确导入,否则该捆绑包也看不到它。
总而言之,如果捆绑包 A 导出了 org.mypackage 包,而捆绑包 B 想使用它,那么捆绑包 A 的 META-INF/MANIFEST.MF 应该在其 Export-Package 头中指定该包,而捆绑包 B 应该在其 Import-Package 条目中包含它。
包的考虑
虽然导出相当直接,但导入稍微复杂一些。应用程序通常会通过搜索环境中可用的库来优雅地降级,并仅使用可用的库,或者库会包含用户未使用的代码。例如,日志(使用 JDK 1.4 或 Log4j)、正则表达式(Jakarta ORO 或 JDK 1.4+)或并发实用程序(JDK 5 中的 java.util 或 JDK 1.4 的 backport-util-concurrent 库)。
在 OSGi 术语中,根据包的可用性来依赖包相当于一个*可选*的 Package-Import。你在前面的示例中已经见过这样的包
```code Import-Package: [...]edu.emory.mathcs.backport.java.util.concurrent;resolution:=optional ```
由于在 OSGi 中,同一类的多个版本可以存在,因此最好在导出和导入包时都指定类包的版本。这是通过添加到每个包声明后的 version 属性来完成的。OSGi 支持的版本格式为 <major>.<minor>.<micro>.<qualifier>,其中 major、minor 和 micro 是数字,而 qualifier 是字母数字。
版本*的含义*完全由捆绑包提供者决定,但是,建议使用流行的编号方案,例如 Apache APR 项目的方案,其中
- <major> - 表示不保证兼容性的重大更新
- <minor> - 表示保留与旧 minor 版本兼容的更新
- <micro> - 从用户角度来看,表示微不足道的更新,与旧版本完全兼容
- <qualifier> - 是用户定义的字符串 - 它不常用,可以为版本号提供一个额外的标签,例如构建号或目标平台,但没有标准化含义。
默认版本(如果属性缺失)是 "0.0.0"。
虽然导出的包必须指定一个特定版本,但导入者可以使用数学区间表示法来指定一个范围——例如
[1.0.4, 2.0) 将匹配版本 1.0.42 及以上到 2.0(不包括)。请注意,仅指定版本而不是区间将匹配所有大于或等于指定版本的软件包,即
Import-Package: com.mypackage;version="1.2.3"
等同于
Import-Package: com.mypackage;version="[1.2.3, ∞)"
最后一个提示是,无论版本是范围还是单个版本,请*务必*在指定版本时使用引号。
使用 OSGi 元数据
现在我们已经了解了捆绑包是什么,让我们看看我们可以使用哪些工具来将现有的 jar 文件 osgi 化。
手动
这种 DIY 方法并不推荐,因为很容易出现拼写错误和多余的空格,这可能会导致清单无效。即使使用智能编辑器,清单格式本身也可能导致一些问题,因为它每行限制为 72 个空格,如果超过此限制,可能会导致难以理解的问题。手动创建或更新 jar 不是一个好主意,因为 jar 格式要求 META-INF/MANIFEST.MF 条目是归档文件中的第一个条目——如果不是,即使它存在于 jar 中,清单文件也不会被读取。手动方法确实推荐用于没有其他选择的情况。
但是,如果有人确实想/需要直接处理清单,那么应该使用一个可以处理 UNIX/DOS 空格的编辑器,并配合一个合适的 jar 创建实用程序(例如 JDK 自带的 jar 工具)来处理所有 MANIFEST 要求。
Bnd
Bnd 代表 BuNDle tool,是 Peter Kriens(OSGi 技术官)创建的一个好用的工具,它“帮助 [...] 创建和诊断 OSGi R4 捆绑包”。Bnd 解析 Java 类以了解可用的和导入的包,从而可以创建相应的 OSGi 条目。Bnd 提供一系列指令和选项,可以自定义生成的工件。bnd.jar 本身的好处在于它可以从 命令行运行,通过 Ant 的专用 任务运行,或者作为 插件集成到 Eclipse 中。
Bnd 可以从类路径或 Eclipse 项目中的类创建 jar,或者通过添加所需的 OSGi 工件来 osgi 化现有的 jar。此外,它可以打印和验证给定 jar 的 OSGi 信息,使其成为一个强大而易于使用的工具。
首次使用的用户可以使用 Bnd 来查看将添加到普通 jar 中的 OSGi 清单。让我们选择一个普通 jar,例如 c3p0(一个优秀的连接池库),然后发出一个打印命令。
```code java -jar bnd.jar print c3p0-0.9.1.2.jar ```
输出相当大,包含几个部分。
- 通用清单信息
[MANIFEST c3p0-0.9.1.2.jar]
Ant…