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

檢視全部