建立 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 社群所有即將舉行的活動。

檢視所有