创建 OSGi 捆绑包

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

在接触 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>,其中 majorminormicro 是数字,而 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 ```

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

  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. 可能的错误 - 通常这些错误表明 classpath 中未找到但被其他类引用的包
     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 导入了哪些包。

让我们使用以下方法将构件 OSGify 化:

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>

请注意,通常会有一个 move 任务用于将 .jar$ 或 .bar 归档文件复制到原始 jar 上。

Maven 的 Bundle 插件

对于 Maven,Apache Felix Bundle Plug-in 提供了 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 时会考虑 classpath 中的所有可用类非常重要。当从命令行使用 Bnd 时,如上一个示例所示,classpath 只包含一个 jar,因此除了 c3p0 之外没有其他类存在。但是,当使用 Maven 或 Ant 等构建工具时,classpath 会大大增加 - 在这种情况下,基于您的 Export/Import 包指令,Bnd 可能会添加或删除结果 jar 中的类。为防止这种情况,请确保使用仅匹配实际包含的包的模式,例如:com.mchange.* 而不是 *。

自定义、内部工具

另一种方法,虽然不太可能遇到,是创建自定义工具,通常基于字节码分析。这种实用程序可以为特定环境高度定制,以提高速度、最小化内存占用或支持其他启发式方法或配置文件。Spring Dynamic Modules 包含一个基于ASM 的内部字节码解析器,用于其测试框架,以有效地动态创建 MANIFEST.MF。

然而,对于通用用途,Bnd 工具(无论是独立的还是通过其 Maven 集成)提供了更多选项并且速度很快。事实上,用法越通用,Bnd 就越有可能通过其高度的可定制性来满足需求。

使用现有的 OSGi 存储库

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

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

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

Apache Felix Commons - 旨在“共享 [...] bundlized 构件”。

Apache OSGified Projects - 一个简单的页面,显示了 Apache Commons 项目在其官方分发版中已包含或即将包含 OSGi manifest 条目。

希望在社区的帮助下,许多流行的 Java 库将默认支持 OSGi,并且不需要单独的存储库或包装 jar。在此之前,您可以通过为正在使用的项目提供补丁或简单地要求此功能来提供帮助。

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

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

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

查看所有