创建 OSGi Bundle

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

在接触 OSGi 时,首先需要学习的概念之一就是 bundle 的概念。在这篇文章中,我想仔细探讨一下 bundle 到底是什么,以及一个普通的 jar 文件如何转换成 OSGi bundle。现在,言归正传, 

什么是 bundle?

OSGi 规范将 bundle 描述为“模块化单元”,它“由 Java 类和其他资源组成,这些类和资源共同为终端用户提供功能”。到目前为止都很好理解,但 bundle 到底是什么?再次引用规范中的话:

bundle 是一个 JAR 文件,它:

  • 包含 [...] 资源
  • 包含一个清单文件,描述 JAR 文件的内容并提供有关 bundle 的信息
  • 可以在 JAR 文件的 OSGI-OPT 目录或其子目录中包含可选文档

简而言之,bundle = jar + OSGi 信息(在 JAR 清单文件 META-INF/MANIFEST.MF 中指定),不需要额外的文件或预定义的文件夹布局。这意味着从 jar 创建 bundle 所需要做的,就是向 JAR 清单添加一些条目。

OSGi 元数据

OSGi 元数据由清单条目表示,这些条目指示 OSGi 框架 bundle 提供或/和需要什么。规范指出了大约 20 个清单头,但我们只会看你最有可能使用的那些。

Export-Package

顾名思义,此头指示(bundle 中可用的)哪些包被导出,以便其他 bundle 可以导入它们。只有由该头指定的包才会被导出,其余包将是私有的,且在包含 bundle 外部不可见。

Import-Package

Export-Package 类似,此头指示 bundle 导入的包。同样,只有由该头指定的包才会被导入。默认情况下,导入的包是强制性的——如果导入的包不可用,导入的 bundle 将无法启动。

Bundle-SymbolicName
这是唯一必需的头,此条目指定 bundle 的唯一标识符,基于反向域名约定(Java 包也使用此约定)。
Bundle-Name
定义 bundle 的人类可读名称,不含空格。建议设置此头,因为它比 Bundle-SymbolicName 能提供关于 bundle 内容更简短、更有意义的信息。
Bundle-Activator
BundleActivator 是一个 OSGi 特有的接口,允许 Java 代码在 bundle 由 OSGi 框架启动或停止时收到通知。此头的值应包含激活器类的完全限定名,该类必须是 public 的,并且包含一个无参数的 public 构造函数。
Bundle-Classpath
当 jar 包含嵌入式库或在各种文件夹下的类包时,此头非常有用,它扩展了默认的 bundle 类路径(默认类路径期望类直接在 jar 根目录下可用)。
Bundle-ManifestVersion
这个不太为人所知的头指示用于读取此 bundle 的 OSGi 规范。1 表示 OSGi release 3,而 2 表示 OSGi release 4 及更高版本。由于 1 是默认版本,强烈建议指定此头,因为 OSGi release 4 的 bundle 在 OSGi release 3 下可能无法按预期工作。

下面是一个示例,取自 Spring 2.5.x 核心 bundle 的清单,其中使用了上面提到的一些头

 
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 package 条目上,因为它们描述了 bundle(即你的模块)之间的关系。关于包,没有任何隐式规定——只有被提及的包才会被导入/导出,其余的则不会。这也适用于子包:导出 org.mypackage 只会导出此包,而不会导出其他任何东西(例如 org.mypackage.util)。导入也是如此——即使一个包在 OSGi 空间中可用,除非某个 bundle 显式导入它,否则该 bundle 将无法看到它。

总而言之,如果 bundle A 导出包 org.mypackage,并且 bundle B 想要使用它,那么 bundle A 的 META-INF/MANIFEST.MF 应该在其 Export-Package 头中指定该包,而 bundle 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 是字母数字。

版本的含义完全取决于 bundle 提供者,但是建议使用流行的编号方案,例如 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 元数据

现在我们已经了解了 bundle 的一些信息,接下来看看可以使用哪些工具来将现有 jar 文件 OSGi 化。

手动方式

不建议采用这种手动方式,因为很容易出现拼写错误和多余空格,导致清单文件无效。即使使用智能编辑器,清单文件格式本身也可能引起一些问题,因为它每行有 72 个字符的限制,如果违反此限制,可能会导致一些难以理解的问题。手动创建或更新 jar 文件不是一个好主意,因为 jar 格式要求 META-INF/MANIFEST.MF 条目是归档文件中的第一个条目——如果不是,即使它存在于 jar 中,清单文件也不会被读取。手动方式只在没有其他替代方案的情况下才真正推荐。

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

Bnd

Bnd 代表 BuNDle 工具,是由 Peter Kriens(OSGi 技术官员)创建的一个很棒的工具,它“帮助 [...] 创建和诊断 OSGi R4 bundle”。Bnd 解析 Java 类以了解可用和导入的包,从而创建相应的 OSGi 条目。Bnd 提供了一系列指令和选项,可以自定义生成的文件。bnd.jar 本身的一个优点是它可以从命令行运行,也可以通过专用的 Ant 任务运行,或者作为 Eclipse 插件集成使用。

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

初次使用的用户可以使用 Bnd 查看哪些 OSGi 清单信息会被添加到普通的 jar 文件中。我们选择一个普通的 jar 文件,例如 c3p0(这是一个优秀的连接池库),并执行 print 命令:

```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 任务在执行时会查找此文件。

要在 Ant 中使用 Bnd 工具,只需导入其提供的开箱即用的任务并在创建 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 插件在 Bnd 和 Maven 2 之间提供了很好的集成。由于 Maven POM 包含有关项目的附加信息,Bnd 插件可以利用项目属性自动填充清单文件的其他字段,例如 Bundle-LicenseBundle-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,除了 MANIFEST.MF 文件包含 OSGi 条目外,其内容与原始文件相同。

注意使用属性来外部化插件配置。在模块内部处理多个项目时,属性允许将通用插件配置放在顶级 pom 文件中,并且每个子模块可以通过指定不同的属性值来覆盖它。Spring-DM osgi 仓库就是一个这样的配置的真实例子

重要的是要知道,Bnd 在创建 bundle 时会考虑类路径上所有可用的类。当从命令行使用 Bnd 时,如前面的示例所示,类路径仅由一个 jar 文件组成,因此除了 c3p0 外没有额外的类。然而,当使用 Maven 或 Ant 等构建工具时,类路径要大得多——在这种情况下,根据你的 Export/Import package 指令,Bnd 可能会从生成的 jar 中添加或丢弃类。为防止这种情况,请确保使用仅匹配实际包含的包的模式,例如:使用 com.mchange.* 而不是 *

定制化的内部工具

另一种方法(尽管不太可能遇到)是创建一个定制化的工具,通常基于字节码分析。这种工具可以针对特定环境进行高度定制,以提高速度或最小化内存占用,或者支持额外的启发式算法或配置文件。Spring Dynamic Modules 为其测试框架包含了一个内部的、基于 ASM 的字节码解析器,用于高效地即时创建 MANIFEST.MF 文件。

然而,对于通用用途,Bnd 工具(无论是原生使用还是通过其 Maven 集成)提供了更多选项并且运行速度相当快。事实上,使用场景越通用,Bnd 凭借其高度可定制性就越有可能胜任。

使用现有的 OSGi 仓库

话虽如此,在将现有库包装成 OSGi bundle 之前,请检查是否有人已经帮你完成了这项工作。你可以通过查看以下现有 OSGi 仓库来做到这一点:

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

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

Apache Felix Commons - 旨在“共享 [...] 打包好的文件”。

Apache OSGi 化项目 - 一个简单的页面,指示哪些 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 社区所有即将举行的活动。

查看全部