创建 OSGi 捆绑包

工程 | Costin Leau | 2008 年 2 月 18 日 | ...

在学习 OSGi 时,首先要了解的概念之一是捆绑包的概念。在本篇博文中,我想更仔细地研究一下捆绑包到底是什么,以及如何将一个普通 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 版本 3,而 2 表示 OSGi 版本 4 及更高版本。由于 1 是默认版本,因此强烈建议指定此标题,因为 OSGi 版本 4 捆绑包在 OSGi 版本 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 或 backport-util-concurrent 库用于 JDK 1.4)。

在 OSGi 术语中,根据可用性依赖于包转换为可选包导入。您已经在前面的示例中看到了这样的包

```code Import-Package: [...]edu.emory.mathcs.backport.java.util.concurrent;resolution:=optional ```

由于在 OSGi 中,可以存在同一类的多个版本,因此最佳实践是在导出和导入包时都指定类包的版本。这是通过 version 属性完成的,该属性在每个包声明后添加。OSGi 支持的版本格式为 <major>.<minor>.<micro>.<qualifier>,其中 majorminormicro 是数字,而 qualifier 是字母数字。

版本的含义 完全取决于捆绑包提供商,但是建议使用流行的编号方案,例如 Apache APR 项目 中的编号方案,其中

  • <major> - 表示重大更新,不保证任何兼容性
  • <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 格式

手动

不建议使用这种自己动手的方法,因为拼写错误和多余的空格很容易潜入并使清单变得毫无用处。即使使用智能编辑器,清单格式本身也可能导致一些问题,因为它每行最多只能包含 72 个空格,如果超出限制,可能会导致一些难以理解的问题。手动创建或更新 jar 不是一个好主意,因为 jar 格式要求 META-INF/MANIFEST.MF 条目是存档中的第一个条目 - 如果不是,即使它存在于 jar 中,清单文件也不会被读取。手动方法确实推荐用于没有其他替代方案的情况。

但是,如果确实需要直接使用清单,则应使用可以处理 UNIX/DOS 空格的编辑器以及合适的 jar 创建实用程序(例如 JDK 附带的 jar 工具)来满足所有清单要求。

Bnd

Bnd 代表 BuNDle 工具,是由 Peter Kriens(OSGi 技术负责人)创建的一个不错的实用程序,它“帮助 [...] 创建和诊断 OSGi R4 捆绑包”。Bnd 解析 Java 类以了解可用的和导入的包,以便它可以创建等效的 OSGi 条目。Bnd 提供了一系列指令和选项,可以自定义生成的工件。bnd.jar 本身的好处是它可以从 命令行、通过专用的 任务 使用 Ant 运行,或作为 插件 集成到 Eclipse 中。

Bnd 可以从类路径或 Eclipse 项目中可用的类创建 jar,或者通过添加所需的 OSGi 工件来使现有 jar 成为 osgi 格式。此外,它可以打印和验证给定 jar 的 OSGi 信息,使其成为一个功能强大且易于使用的工具。

首次用户可以使用 Bnd 查看将添加到普通 jar 的 OSGi 清单。让我们选择一个普通 jar,例如 c3p0(这是一个优秀的连接池库)并发出打印命令

```code java -jar bnd.jar print c3p0-0.9.1.2.jar ```

输出相当大,包含几个部分

  1. 通用清单信息
    [MANIFEST c3p0-0.9.1.2.jar]
    Ant-Version Apache Ant 1.7.0
    Created-By 1.5.0_07-87 ("Apple Computer, Inc.")
    Extension-Name com.mchange.v2.c3p0
    Implementation-Vendor Machinery For Change, Inc.
    Implementation-Vendor-Id com.mchange
    Implementation-Version 0.9.1.2
    Manifest-Version 1.0
    Specification-Vendor Machinery For Change, Inc.
    Specification-Version 1.0
    
  2. 包信息
    
    com.mchange.v2.c3p0.management   com.mchange.v1.lang com.mchange.v2.c3p0
                                                                   com.mchange.v2.c3p0.impl com.mchange.v2.debug
                                                                   com.mchange.v2.log com.mchange.v2.management
                                                                   java.sql
                                                                   javax.management
                                                                   javax.sql
    

    指示指定在 jar 中发现的包(左侧)及其导入(右侧)。

  3. 可能的错误 - 通常这些指示类路径中未找到但其他类引用的包。
     One error 1 : Unresolved references to 
    [javax.management, javax.naming, javax.naming.spi, javax.sql, javax.xml.parsers, org.apache.log4j, org.w3c.dom] 
    by class(es) on the Bundle-Classpath[Jar:c3p0-0.9.1.2.jar]: [...] 
    

    。此部分很好地指示了给定 jar 导入的包。

让我们使用以下命令将工件转换为 OSGi 格式

java -jar bnd.jar wrap c3p0-0.9.1.2.jar 

这将创建一个与原始 jar 内容完全相同的新存档,但 MANIFEST.MF 已修改,其中将包含标记为可选的 OSGi 导入。当前的 Bnd 工具使用 .jar$ 扩展名保存存档,而以前的版本则使用 .bar

我们可以选择通过添加版本控制、排除某些导出的包以及将某些导入的包标记为必需(在本例中为 javax.sql)来调整 jar。为此,我们将创建一个 c3p0-0.9.1.2.bnd 文件,如下所示

version=0.9.1.2
Export-Package: com.mchange*;version=${version}
Import-Package: java.sql*,javax.sql*,*;resolution:=optional
Bundle-Version: ${version}
Bundle-Description: c3p0 connection pool
Bundle-Name: c3p0

请注意,对于版本,我们使用了变量替换。要挂接属性文件,请使用以下命令行

```code java -jar bnd.jar wrap -properties c3p0-0.9.1.2.bnd c3p0-0.9.1.2.jar ```

我使用了 .bnd 扩展名,因为默认情况下,Bnd ant 任务将在执行期间查找此文件。

要将 Bnd 工具与 ant 一起使用,只需导入开箱即用的任务并在 jar 创建期间调用它们即可


<taskdef resource="aQute/bnd/ant/taskdef.properties" classpath="${lib.dir}/bnd/bnd.jar"/>
...
<bndwrap definitions="${basedir}/osgi/bnd" output="${dist.dir}">
   <fileset dir="${dist.dir}" includes="*.jar"/>    
</bndwrap>

请注意,通常,后续会使用移动任务将 .jar$ 或 .bar 工件复制到原始 jar 上。

Maven 的 Bundle 插件

对于 Maven,Apache Felix Bundle 插件 提供了 Bnd 和 Maven 2 之间良好的集成。由于 Maven POM 包含有关项目的其他信息,因此 Bnd 插件可以使用项目属性自动填充清单的其他字段,例如 Bundle-License 或 Bundle-Version。

官方的 文档 详细解释了用法,因此我不会在此重复。

为了转换我们的 c3p0 库,我将使用一个简单的 Maven 2 pom,它将下载原始工件,然后将其包装为 bundle


<?xml version="1.0" encoding="UTF-8"?>
<project
        xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>my.company</groupId>
    <artifactId>c3p0.osgi</artifactId>
    <packaging>bundle</packaging>
    <version>0.9.1.2-SNAPSHOT</version>
    <name>c3p0.osgi</name>

    <properties>
        <export.packages>${export.package}*;version=${unpack.version}</export.packages>
        <import.packages>*</import.packages>
        <private.packages>!*</private.packages>
        <symbolic.name>${pom.groupId}.${pom.artifactId}</symbolic.name>
        <embed-dep>*;scope=provided;type=!pom;inline=true</embed-dep>
        <unpack-bundle>false</unpack-bundle>
    </properties>

    <build>
    <plugins>
     <plugin>
        <groupId>org.apache.felix</groupId>
        <artifactId>maven-bundle-plugin</artifactId>
        <version>1.2.0</version>
        <configuration>
            <unpackBundle>${unpack.bundle}</unpackBundle>
            <instructions>
                <Bundle-Name>${artifactId}</Bundle-Name>
                <Bundle-SymbolicName>${symbolic.name}</Bundle-SymbolicName>
                <Bundle-Description>${pom.name}</Bundle-Description>
                <Import-Package>${import.packages}</Import-Package>
                <Private-Package>${private.packages}</Private-Package>
                <Include-Resource>${include.resources}</Include-Resource>
                <Embed-Dependency>${embed-dep}</Embed-Dependency>
                <_exportcontents>${export.packages}</_exportcontents>
            </instructions>
        </configuration>
        <extensions>true</extensions>
     </plugin>
    </plugins>
    </build>

    <dependencies>
      <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
        <scope>provided</scope>
      </dependency>
    </dependencies>
</project>

打包项目将创建一个 OSGi bundle,其内容与原始 bundle 相同,除了 MANIFEST.MF 将包含 OSGi 条目。

请注意使用属性来外部化插件配置。在模块内处理多个项目时,属性允许将通用插件配置放置在顶级 pom 中,并通过指定不同的属性值让每个子模块覆盖它。此类设置的一个实时 示例 是 Spring-DM osgi 存储库。

需要注意的是,Bnd 在创建 bundle 时会考虑类路径上所有可用的类。当从命令行使用 Bnd 时(如前面的示例),类路径仅由 jar 组成,因此除了 c3p0 之外不存在其他额外类。但是,当使用 Maven 或 Ant 等构建工具时,类路径会大得多——在这种情况下,根据您的导出/导入包指令,Bnd 可能会添加或丢弃来自结果 jar 的类。为了防止这种情况,请确保使用仅匹配实际包含的包的模式,即:com.mchange.* 而不是 *。

自定义的内部工具

另一种方法(尽管不太可能遇到)是创建一个自定义工具,通常基于字节码分析。此类实用程序可以针对某些环境进行高度自定义,以提高速度或最大程度地减少内存占用,或支持其他启发式方法或配置文件。Spring Dynamic Modules 为其测试框架包含一个这样的内部 ASM 基于字节码的解析器,以便有效地创建即时 MANIFEST.MF。

但是,对于通用用途,Bnd 工具(无论是原始工具还是通过其 Maven 集成)提供了更多选项并且运行速度很快。事实上,使用越普遍,Bnd 通过其高度可定制性满足需求的可能性就越大。

使用现有的 OSGi 存储库

话虽如此,在将现有库包装为 OSGi bundle 之前,请检查是否有人已经为您完成了此操作。您可以通过检查现有的 OSGi 存储库之一来做到这一点

OSGi Bundle Repository (ORB) - OSGi 联盟 bundle 存储库,提供“bundle 的联合集合”。

Eclipse Orbit - 包含可在 Eclipse 环境中使用的工件。由于 Eclipse 使用 Equinox,因此工件可能包含特定于 Equinox 的清单条目

Apache Felix Commons - 旨在“共享 [...] 捆绑的工件”

Apache OSGified 项目 - 一个简单的页面,指示 Apache Commons 项目已或即将在其官方发行版中包含 OSGi 清单条目。

希望通过社区的帮助,许多现有的流行 Java 库默认情况下将对 OSGi 友好,并且无需使用单独的存储库或包装 jar。在此之前,您可以通过为使用的项目提供补丁或简单地请求此功能来提供帮助。

在结束本文之前,我想邀请所有对 OSGi 和 Spring Dynamic Modules 感兴趣的人参加下周(2 月 27 日星期三)即将举行的 网络研讨会,该研讨会将涵盖核心 OSGi 概念和 Spring DM 基础知识。

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部