在 OSGi 中暴露引導類路徑

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

我不時遇到的一個相當常見的問題是如何在 OSGi 環境中使用 JDK 特定的類。在某種程度上,這相當於無需將其打包即可從 OSGi 訪問引導類路徑。為了表達包依賴關係,捆綁包(bundle)在其 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':擴充套件捆綁包

另一個可能的選項是透過擴充套件捆綁包來增強系統捆綁包。這些擴充套件捆綁包充當片段;它們本身不是捆綁包,而是附加到宿主上。一旦附加,片段內容(包括任何允許的頭資訊)將被視為宿主的一部分。擴充套件捆綁包是一種特殊型別的片段,它們附加到系統捆綁包上,以便提供框架的可選部分(例如 Start Level 服務)。可以使用這種機制建立一個空的擴充套件,只宣告所需的包,將載入留給其宿主捆綁包(在本例中為框架)。

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 指令會與系統捆綁包(其宿主)的指令合併。

 
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' 方法:它們清楚地表達了捆綁包的連線並允許擴充套件。連線易於控制、檢測和診斷。解決方案 B 有點像黑魔法,因為捆綁包無法控制其載入並選擇特定版本或提供者,因為沒有類連線。此外,該設定會影響所有捆綁包,這可能並非總是您想要的。儘管如此,在某些情況下,引導委託非常方便;一個很好的例子是 instrumentation,例如效能分析或程式碼覆蓋。大多數工具使用位元組碼織入來新增各種計數器或攔截執行流程。由於新新增的程式碼引用了捆綁包未知的類,因此“被 instrumentation”的捆綁包無法在 OSGi 內部載入,除非更新其 manifest。將自定義包新增到引導委託列表提供了一種非常快速的方法來 instrumentation OSGi 應用程式,而無需更改打包或部署過程。

關於父類載入器的說明

在本文中,我按照 OSGi 規範的術語,將父類載入器稱為載入和啟動(或引導)OSGi 框架的實體。值得注意的是,某些 OSGi 實現(特別是 Equinox)允許將父類載入器自定義為不同的值(例如應用程式、引導或擴充套件類載入器)。

連結

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

附註:本文沒有程式碼列表,但程式碼愛好者可以此處獲取圖表定義。

訂閱 Spring 新聞通訊

訂閱 Spring 新聞通訊,保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您快速提升。

瞭解更多

獲取支援

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

瞭解更多

近期活動

檢視 Spring 社群所有即將舉行的活動。

檢視全部