在 OSGi 中公開啟動類別路徑

工程 | Costin Leau | 2009 年 1 月 19 日 | ...

我不時收到一個相當常見的問題,是如何在 OSGi 環境中使用 JDK 特定的類別。 在某種程度上,這相當於從 OSGi 存取啟動類別路徑,而不將其捆綁在一起。 為了表達套件相依性,綁定在其 manifest 中使用 OSGi 指令 - 主要是Export-PackageImport-Package用於提供和要求類別套件相依性。 定義綁定佈線是建立模組化應用程式的關鍵步驟; 然而,在某些情況下,如上述問題,所需的套件無法從綁定取得。

NoClassDefFoundError: com.sun...

Notable examples of such packages would be the <tt>sun.*</tt> and <tt>com.sun.*</tt>, present in the JDK jars. Even though these are <a href="http://java.sun.com/products/jdk/faq/faq-sun-packages.html">internal</a> packages and are not guaranteed to be portable, some of them can be found even in non-Sun JDKs, due to their usage. Your application might not use them, but there are various libraries that do (in some cases due to performance, in others because it's the only way to achieve a certain functionality). If the using bundle declares an import on the <tt>com.sun</tt> package, it will fail to resolve since there are no providers for it. If the import is not declared, since the bundle doesn't contain the class definition, the loading process will usually fail. Clearly the packages above are not a corner case; generalizing the example, the packages available in the OSGi framework boot classpath are not visible to the OSGi environment. There are several solutions to this problem but first, let's take a closer look to see why it occurs.

類別空間

在 OSGi 中,每個模組都有自己的類別載入器,用於載入資源和類別。 基於佈線指令,平台會在各種模組之間建立委派網路。 該網路形成一個類別空間,代表(引用 OSGi 規範):「可從給定綁定的類別載入器存取的所有類別」,或者用外行的術語來說,綁定可以看到的東西,綁定的世界視圖。 網路可以交叉,因為同一個套件可以由多個綁定載入; 然而,每個空間都必須保持一致,這是平台在每個綁定的解析階段強制執行的要求。 網路模型的副作用(或目標)之一是類型隔離或類別版本控制:同一個類別的多個版本可以很好地共存在同一個 VM 中,因為每個版本都被載入到其自己的網路、自己的空間中。

然而,有些類別需要以不同的方式載入,例如java. 套件。 這些類別是 Java 執行時期本身的一部分,因此隱含地需要它們。 例如,每個 Java 物件都是 java.lang.Object 的子類別,這實際上意味著每個綁定至少使用一個 Java 套件 (java.lang)。 雖然可以透過綁定 manifest 中的指令表達這種相依性,但由於其強制性,這變得不理想。 這就是為什麼 java. 套件被視為隱含匯入,即使它們未聲明,也可以由每個綁定載入。 事實上,OSGi 規範禁止綁定指定對java.*的匯入,因為類別佈線總是意味著版本控制,這意味著在同一個 VM 中執行多個 Java 版本,這是不可能的(至少目前如此)。

為了載入這些基本類型,OSGi 平台使用父委派而不是網路模型; 也就是說,它使用啟動 OSGi 框架的類別載入器來載入類別,而不是 OSGi 類別空間。 由於這可能看起來比實際情況更複雜,因此我使用 dot 語言建立了一個圖表

network model 網路模型

如上所示,這種載入模型與傳統 Java 慣例有很大的不同,後者依賴父委派來解析所有套件的類別,而不僅僅是java.*。 綁定根據其佈線相互通訊,同時將特殊類型的載入委派給父類別載入器(圖像中的綠色箭頭)

解決方案 A:系統套件

細心的讀者可能已經注意到只有java.*套件已被提及 - JDK 中提供的其他公共套件,例如javax.netjavax.xml不是父委派的,這意味著它們必須在類別空間內解析。 也就是說,綁定需要匯入套件(這意味著需要一個提供者),因為它們不是隱含的。 OSGi 規範允許框架(透過其系統綁定)使用org.osgi.framework.system.packages屬性從其父類別載入器匯出任何相關的套件作為系統套件。 由於重新封裝託管 JDK 作為綁定不是一個可行的選擇,因此可以使用此設定來讓系統綁定(或 ID 為 0 的綁定)匯出這些套件本身。 大多數 OSGi 實作已經使用此屬性來匯出所有公共 JDK 套件(基於偵測到的 JDK 版本)。 以下是 Java 1.6 的 Equinox 組態檔中的程式碼片段

org.osgi.framework.system.packages = \   javax.accessibility,\   javax.activity,\   javax.crypto,\   javax.crypto.interfaces,\   ...   org.xml.sax.helpers

使用此屬性,可以新增額外的套件,這些套件將由框架載入和提供,並且可以佈線到其他綁定。

org.osgi.framework.system.packages = \   javax.accessibility,\   javax.activity,\   ...   org.xml.sax.helpers, \   special.parent.package

透過詢問系統綁定即可看到(以下是 Equinox 中 OSGi 主控台的程式碼片段)

osgi> bundle 0   System Bundle [0]    Id=0, Status=ACTIVE     Registered Services    ...    Exported packages     ...     org.xml.sax.helpers; version="0.0.0"[exported]     special.parent.package; version="0.0.0"[exported]     ...

設定需要在 OSGi 框架啟動之前初始化,因此常見的模式是將其設定為系統屬性。 這種方法將覆寫預設組態,因此即將推出的 OSGi 4.2 定義了另一個屬性,名為org.osgi.framework.system.packages.extra它會將定義的系統套件附加到org.osgi.framework.system.packages組態,使其更容易擴充 OSGi 實作已經定義的組態。 新增套件可以簡單到將引數傳遞給啟動平台的 VM

java -Dorg.osgi.framework.system.packages.extra=special.parent.package;version=1.0 ...

讓我們再次從 OSGi 主控台中檢查套件

osgi> packages special.parent.package   special.parent.package; version="1.0.0" <org.eclipse.osgi_3.5.0.v20081201-1815 [0]>

解決方案 A':擴充綁定

另一種可能的選擇是透過擴充綁定增強系統綁定。 這些充當片段; 它們本身不是綁定,而是附加到主機。 附加後,片段內容(包括任何允許的標頭)將被視為主機的一部分。 擴充綁定是一種特殊的片段,它附加到系統綁定,以便傳遞框架的可選部分(例如啟動層級服務)。 可以使用此機制建立一個空擴充,該擴充僅聲明所需的套件,將載入留給其託管綁定(在本例中為框架)

osgi> ss
 
Framework is launched.
 
id     State      Bundle
0    ACTIVE    org.eclipse.osgi_3.5.0.v20081201-1815
        Fragments=1
1    RESOLVED    a.framework.extension_0.0.0
        Master=0
 
osgi> bundle 1
 
a.framework.extension_0.0.0 [1]
    Id=1, Status=RESOLVED    Data Root=...
    No registered services.
    No services in use.
    Exported packages
       <b>special.parent.package; version="0.0.0"[exported]</b>
    No imported packages
    Host bundles
       <b>org.eclipse.osgi_3.5.0.v20081201-1815 [0]</b>
    No named class spaces
    No required bundles
   
osgi> headers 1
 
Bundle headers:
   Bundle-ManifestVersion = 2
   Bundle-SymbolicName = a.framework.extension
   <b>Export-Package = special.parent.package</b>
   <b>Fragment-Host = system.bundle; extension:=framework</b>
   Manifest-Version = 1.0

請注意Fragment-Host標頭中的特殊主機符號名稱和額外屬性。 這告訴框架,該綁定不僅僅是一個普通的片段,而是一個擴充綁定。 附加後,相關的擴充 manifest 指令會與其主機(系統綁定)的 manifest 合併

 
osgi> packages special.parent.package
 
special.parent.package; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]> 

解決方案 A' 基本上是 A 的變體(因此得名) - 可以使用片段綁定來擴充系統綁定,而不是使用系統屬性,在某些情況下這可能更方便。 值得指出的是,擴充綁定可能會使用 Java 啟動類別路徑執行載入,這是規範定義的可選機制,並非所有符合規範的實作都需要。 然而,目前,我嘗試過的所有 OSGi 框架都沒有實作此功能。

這兩種解決方案的主要優點是套件在 OSGi 內部提供(因此進行版本控制)。 慣例是對系統套件使用預設版本 (0.0.0),但這不是強制性的(如上所示)。 一個強大的副作用是能夠透過不同的綁定提供框架聲明的套件的不同、更新的版本。 我們使用它來解決由 JDK 隨附不完整的javax.transaction套件引起的事務資料存取問題,該套件會由框架自動匯出到 OSGi 環境中

osgi> packages javax.transaction   javax.transaction; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]>

解決方案是安裝一個包含完整javax.transactionAPI 的 綁定,版本更高: osgi> packages javax.transaction   javax.transaction; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]> javax.transaction; version="1.1.0"<com.springsource.javax.transaction_1.1.0 [1]>

以便消耗綁定可以使用它,而不是 JDK 捆綁的那個

osgi> ss   Framework is launched.   id     State      Bundle 0    ACTIVE    org.eclipse.osgi_3.5.0.v20081201-1815 1    ACTIVE    com.springsource.javax.transaction_1.1.0 2    ACTIVE    user.bundle_0.0.0   osgi> headers 2   Bundle headers:    Bundle-ManifestVersion = 2    Bundle-SymbolicName = user.bundle    Import-Package = javax.transaction;version=1.0    Manifest-Version = 1.0   osgi> packages javax.transaction   javax.transaction; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]> javax.transaction; version="1.1.0"<com.springsource.javax.transaction_1.1.0 [1]>    user.bundle_0.0.0 [2] imports

有關更多資訊,請參閱 Spring DM FAQ 部分

解決方案 B:啟動委派

OSGi 支援的另一個選項是啟動委派,您已經看到了java.*套件。 這允許使用者建立「隱含」套件,這些套件總是由框架父類別載入器載入,即使綁定未提供適當的匯入

class loading delegation 類別載入委派

主要建立此選項是為了適應各種特殊情況,尤其是在 JDK 類別中,這些類別期望始終發生父類別載入委派,或者假設系統上的每個類別載入器都具有對整個啟動路徑的完全存取權。 套件sun.com.sun. 是兩個最常見的範例(正如我已經提到的),因此,某些 OSGi 實作(即 Equinox)預設情況下會啟用它們

org.osgi.framework.bootdelegation=sun.*,com.sun.*

附註:Spring DM 預設情況下在其整合測試框架中使用相同的設定 (AbstractConfigurableOsgiTests#getBootDelegationPackages())

哪種解決方案更好?

上述的每個解決方案在大多數情況下都應該有效;然而,我強烈建議使用 A/A' 方法:它們清楚地表達了 Bundle 的接線方式,並允許擴充性。接線易於控制、偵測和診斷。解決方案 B 有點像黑魔法,因為 Bundle 無法控制其載入,也無法選擇特定版本或提供者,因為沒有類別接線。此外,該設定會影響所有 Bundle,這可能不總是您想要的。儘管如此,在某些情況下,啟動委派非常方便;一個很好的例子是儀器化 (instrumentation),例如效能分析 (profiling) 或程式碼涵蓋率 (code coverage)。大多數工具使用位元組碼編織 (byte code weaving) 來新增各種計數器或攔截執行流程。由於新加入的程式碼引用了 Bundle 未知的類別,因此未更新 Manifest 的 'instrumented' Bundle 無法在 OSGi 內部載入。將自訂套件新增到啟動委派清單中,提供了一種非常快速的方法來儀器化 OSGi 應用程式,而無需更改封裝或部署過程。

關於父類別載入器 (parent class loader) 的說明

在這篇文章中,我將父類別載入器稱為載入並啟動 (或引導) OSGi 框架的實體,遵循 OSGi 規格的術語。值得注意的是,某些 OSGi 實作 (特別是 Equinox) 允許將父類別載入器客製化為不同的值 (例如應用程式、啟動或擴充類別載入器)。

連結

有關 OSGi 類別載入的更多資訊,請參閱以下連結
  • OSGi 核心規格,第 3.8、3.14 和 3.15 節
  • ClassLoader API
  • Eclipse Runtime 選項 (特別是osgi.parentClassLoader)

附註:此條目沒有程式碼清單,但程式碼愛好者可以從這裡獲取圖形定義。

取得 Spring 電子報

隨時關注 Spring 電子報

訂閱

取得領先

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部