在 OSGi 中暴露引導類路徑

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

我時常會收到一個相當普遍的問題,那就是如何在 OSGi 環境中使用 JDK 特定的類。在某種程度上,這相當於在不捆綁引導類路徑的情況下,從 OSGi 中訪問它。為了表達包依賴關係,bundle 在其 manifest 中使用 OSGi 指令 - 主要Export-Package還是Import-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規範):“一個給定捆綁包的類載入器可達到的所有類”,或者通俗地說,捆綁包可以看到的東西,捆綁包的世界檢視。網路可以交叉,因為同一個包可以被多個捆綁包載入;然而,每個空間必須是一致的,這是平臺在每個捆綁包的解析階段強制執行的要求。網路模型的一個副作用(或目標)是型別隔離或類版本控制:同一類的多個版本可以在同一個虛擬機器中很好地共存,因為每個版本都載入到其自己的網路、其自己的空間中。

然而,有些類需要以不同的方式載入,例如java.* 包。這些類是Java執行時本身的一部分,因此隱式地被其需要。例如,每個Java物件都是java.lang.Object的子類,這意味著每個捆綁包至少使用一個Java包(java.lang)。雖然這種依賴可以透過捆綁包清單中的指令來表達,但由於其強制性,它變得不受歡迎。這就是為什麼java. 包被認為是隱含匯入的,即使它們沒有宣告,也可以由每個捆綁包載入。事實上,OSGi規範禁止捆綁包指定對java.*的匯入,因為類連線始終意味著版本控制,這意味著在同一個VM中執行多個Java版本,這(至少目前)是不可能的。

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

網路模型 網路模型

如上所示,這種載入模型與傳統的Java約定有很大的不同,傳統的Java約定依賴於父委託來解析所有包(而不僅僅是java.*)。捆綁包根據它們的連線進行通訊,同時將特殊型別的載入委託給父類載入器(圖中綠色的箭頭)。

解決方案 A:系統包

細心的讀者可能會注意到,提到了java.*包——JDK中沒有提到其他公共包,例如javax.netjavax.xml不是透過父委託載入的,這意味著它們必須在類空間內解析。也就是說,捆綁包需要匯入這些包(這意味著需要有一個提供者),因為它們不是隱含的。OSGi規範允許框架(透過其系統捆綁包)使用org.osgi.framework.system.packages屬性將父類載入器中任何相關的包匯出為系統包。由於將託管JDK重新打包成一個捆綁包不是一個可行的選項,可以使用此設定使系統捆綁包(或ID為0的捆綁包)自行匯出這些包。大多數OSGi實現已經使用此屬性匯出所有公共JDK包(基於檢測到的JDK版本)。下面是Equinox配置檔案中Java 1.6的片段。

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頭中的特殊宿主符號名稱和額外屬性。這告訴框架該捆綁包不僅僅是一個普通的片段,而是一個擴充套件捆綁包。一旦附加,相關的擴充套件清單指令就會與系統捆綁包(其宿主)的指令合併。

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

解決方案 A' basically is a variant of A (hence the name) - instead of using system properties, one can use fragment bundles to extend the system bundle which can be more convenient in some cases. It's worth pointing out that the extension bundles might perform loading using the Java boot class path, an optional mechanism defined by the specification which is not required for compliant implementations. However, at the moment, none of the OSGi frameworks that I have tried, implement this feature.

兩種解決方案的主要優點是包在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.*包看到了這一點。這允許使用者建立“隱含”包,這些包將始終由框架父類載入器載入,即使捆綁包沒有提供正確的匯入。

類載入委託 類載入委託

此選項主要用於適應各種角落情況,尤其是在JDK類中,這些類期望父類載入委託始終發生,或者假設系統中的每個類載入器都擁有對整個啟動路徑的完全訪問許可權。包sun.*com.sun.是兩個最常見的例子(正如我已經提到的),因此一些OSGi實現(例如Equinox)預設啟用它們。

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

順便說一句,Spring DM在其整合測試框架(AbstractConfigurableOsgiTests#getBootDelegationPackages())

哪個解決方案更好?

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

關於父類載入器的說明

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

連結

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

P.S. 本條目沒有程式碼列表,但程式碼愛好者可以從此處獲取圖定義。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

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

檢視所有