天天看點

《深入了解OSGi:Equinox原理、應用與最佳實踐》一2.5 OSGi的類加載架構

osgi為java平台提供了動态子產品化的特性,但是它并沒有對java的底層實作如類庫和java虛拟機等進行修改,osgi實作的子產品間引用與隔離、子產品的動态啟用與停用的關鍵在于它擴充的類加載架構。

osgi的類加載架構并未遵循java所推薦的雙親委派模型(parents delegation model),它的類加載器通過嚴謹定義的規則從bundle的一個子集中加載類。除了fragment bundle外,每一個被正确解析的bundle都有一個獨立的類加載器支援,這些類加載器之間互相協作形成了一個類加載的代理網絡架構,是以osgi中采用的是網狀的類加載架構,而不是java傳統的樹狀類加載架構,如圖2-14所示。

《深入了解OSGi:Equinox原理、應用與最佳實踐》一2.5 OSGi的類加載架構

在osgi中,類加載器可以劃分為3類。

父類加載器:由java平台直接提供,最典型的場景包括啟動類加載器(bootstrap classloader)、擴充類加載器(extension classloader)和應用程式類加載器(application classloader)。在一些特殊場景中(如将osgi内嵌入一個web中間件)還會有更多的加載器組成。它們用于加載以“java.*”開頭的類以及在父類委派清單中聲明為要委派給父類加載器加載的類。

bundle類加載器:每個bundle都有自己獨立的類加載器,用于加載本bundle中的類和資源。當一個bundle去請求加載另一個bundle導出的package中的類時,要把加載請求委派給導出類的那個bundle的加載器處理,而無法自己去加載其他bundle的類。

其他加載器:譬如線程上下文類加載器、架構類加載器等。它們并非osgi規範中專門定義的,但是為了實作友善,在許多osgi架構中都會使用。例如架構類加載器,osgi架構實作一般會将這個獨立的架構類加載器用于加載架構實作的類和關鍵的服務接口類。

不同類加載器所能完成的(無論是自己完成加載,還是委派給其他類加載器來加載)加載請求的範圍構成了該bundle的類名稱空間(class name space)。在同一個類名稱空間中,類必須是一緻的,也就是說不會存在完全重名的兩個類。但是在整個osgi的子產品層,允許多個相同名稱的類同時存在,因為osgi子產品層是由多個bundle的類名稱空間組成的。單獨一個bundle的類名稱空間由如下内容組成:

父類加載器提供的類(以java.*開頭的類以及在委派名單中列明的類);

導入的package(import-package);

導入的bundle(require-bundle);

本bundle的classpath(私有package,bundle-classpath);

附加的fragment bundle(fragment-attachment);

動态導入的package(dynamicimport-package)。

下面将介紹bundle中各種類的加載過程,涉及類加載器,以及類加載的優先級次序。

osgi架構必須将以java.*開頭的package交給父類加載器代理,這一點是無須設定且不可改動的。除此之外,osgi架構也允許使用者通過系統參數“org.osgi.framework.bootdelegation”自行指定一些package委派給父類加載器加載,這個參數被稱為“父類委派清單”(boot delegation list)。它的值應為一系列的包名,用逗号分隔,支援通配符,例如:

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

如果org.osgi.framework.bootdelegation的參數值如以上代碼中所示,那麼以sun.和com.sun.開頭的類也會委派給父類加載器去加載。這個設定在特定場景下很有用。

例如某個部署在web中間件上的osgi應用需要使用jdbc通路資料庫,與大多數應用一樣,通路資料庫的connection是由應用伺服器的jndi提供的,這時候就應當把jdbc驅動設定為由父類加載器加載,而不是由osgi中的某個bundle包提供。因為web中間件通常會帶有連接配接池實作,為了實作事務控制和連接配接監視等功能,從jndi中查到的datasource是被中間件伺服器包裝過的,并非直接由原生的jdbc驅動所提供。為了保證中間件伺服器中一些需要把connection、statement、resultset等從接口轉型為具體實作類的代碼(大多數是操作大字段的代碼)能正常執行,必須保證中間件伺服器和osgi應用所使用的jdbc驅動是同一個—不僅是同一個檔案,還要是由同一個類加載器加載的,這樣才能保證轉型成功。

以java.開頭的package是預設被隐式導出的,在所有bundle中無需導入便可以直接使用,并且osgi規範明确禁止在bundle中導入或導出以java.開頭的package。與前面提到的父類委派清單類似,osgi也定義了添加隐式導出package的參數“org.osgi.framework.system.packages”。這個參數使用标準的export-package文法描述,例如:

org.osgi.framework.system.packages=javax.crypto.interfaces

這裡定義的package将由系統bundle(id為0的bundle)導出,由父類加載器加載。這樣導出的package與普通的導出方式沒有太大差別,可以帶有屬性和版本号,也可以使用uses參數描述依賴。

osgi架構為每一個bundle(不包括fragment bundle)生成了一個bundle類加載器的執行個體,這些類加載器負責處理其他bundle委派的加載請求,根據中繼資料資訊确定這些加載請求的類是否與該bundle的導出清單相符合,然後對合法的加載請求進行響應,傳回該bundle的類供其他bundle使用。

bundle-classpath這個中繼資料标記與bundle類加載器密切相關,它描述了bundle加載器的classpath範圍,即bundle加載器應該到哪裡去查找類。

bundle-classpath标記有預設值“.”,它代表該bundle的根目錄,或者說代表該bundle的jar檔案。如果不在中繼資料資訊中顯式定義這個标記,那麼bundle類加載器就在整個bundle的範圍内查找類。但是要注意,在這種預設配置下,如果bundle存在其他jar檔案,類加載器隻能把它當作一個普通資源來讀取,而無法查找到這些jar檔案内部包含的類。例如,在bundle中有如下路徑:

bundle:

log4j.jar

bundle類加載器可以通路到example.class,但是無法通路到logger.class,最多隻能把log4j.jar當作與圖檔、音頻等類似的二進制資源整體提供出去。

要讀取到logger.class,必須設定bundle-classpath标記為:

bundle-classpath: lib/log4j.jar,.

注意不要遺漏了後面的“,.”,這裡有兩個classpath路徑,它們之間使用逗号分隔,如果沒有了後面的“.”,那麼bundle類加載器就隻能處理log4j.jar中的類而無法處理本bundle的example.class了。

如果bundle-classpath标記的值是多個classpath路徑,那麼它們之間還有優先級關系,例如下面這個定義:

bundle-classpath: required.jar,optional.jar,default.jar

該定義中required.jar是必須出現在bundle中的類和資源;optional.jar是某個可選的jar包,其中存放着可選的類和資源;default.jar中存放着optional.jar不可用時這些類和資源的預設值,如果optional.jar中有可用的内容便會對其覆寫。

如果一個bundle被另一個fragment bundle附加,那麼bundle-classpath也會相應疊加,例如下面定義:

bundle a:

bundle b:

bundle-classpath: fragment.jar

fragment-host: bundlea

此時bundle a的bundle類加載器能搜尋到的classpath依次為:required.jar、optional.jar、default.jar、fragment.jar。

bundle類加載器收到類加載請求時,會優先委托給導入包的其他bundle類加載器處理,隻有其他導入包的bundle類加載器都無法處理時才會嘗試自己處理。讀者可以通俗地了解為“import-package”和“require-bundle”的優先級高于“bundle-classpath”,如果能在前者中找到所需的類,後者就不會起作用。這條規則讀起來不複雜,但初接觸osgi的朋友在實際編碼時候可能會對此有些不習慣,例如下面這個例子:

在bundle a、b中都有package p,兩者的package p中都存在有類classa。同時,bundle b還導入了bundle a中的package p。在這個前提下,假設bundle a中有下列代碼:

classa ana = new classa();

這時候classa用的都是bundle a中的類,符合一般思維習慣。但是如果bundle b中有同樣的代碼,所使用的classa依然是bundle a中的類,即使bundle b自己的classpath中也有這類classa,甚至與調用classa的代碼檔案存在于同一個目錄下緊緊相鄰的就是classa,都不會被使用,這就不符合一般的思維習慣了,如圖2-15所示。

《深入了解OSGi:Equinox原理、應用與最佳實踐》一2.5 OSGi的類加載架構

這裡假設bundle a導出的p中存在classa這個類,這樣bundle b的classa就無法派上用場。如果情況更極端一些,bundle a導出的p不存在classa這個類,那bundle b的classa依然不會被使用,而會直接收到classnotfoundexception異常,異常資訊類似如下所示:

caused by: java.lang.classnotfoundexception: p.classa

loader.java:107)

對于在bundle中發生的加載請求而言,目前bundle的bundle類加載器是使用到的類的初始類加載器(initiating classloader,它表示加載請求最先發送到的類加載器),而哪個類加載器是定義類加載器(defining classloader,它表示加載請求被不斷委派後,最終執行加載動作的類加載器)則要根據osgi類加載順序來判定。在類型強制轉換和類型比較(譬如instanceof操作)時了解類加載順序很重要,因為即使是同一個類檔案,由不同定義類加載器加載所形成的類在java虛拟機中也是完全獨立且不可互相轉型的。

在osgi中還可能使用到其他的類加載器,比如osgi實作架構中一般都會有架構類加載器(framework classloader)。osgi架構為每個bundle建立bundle類加載器的執行個體,而osgi架構自身的代碼——至少涉及osgi架構啟動的代碼就沒法使用bundle類加載器來加載,是以需要一個專門的架構類加載器來完成這個任務。這個架構類加載器是各個osgi實作架構自己定義的,有時候可能直接使用java平台提供的應用程式類加載器(application classloader)。這個架構類加載器還可能同時充當父類加載器的角色,比如在equinox架構中就可以選擇是使用啟動類加載器、擴充類加載器、應用程式類加載器還是使用架構類加載器來作為父類加載器。

另外一個在osgi中比較常見的類加載器是線程上下文類加載器(thread contextclassloaser),這個類加載器并不是在osgi中才出現的,它在普通的java應用中有廣泛應用。這個類加載器可以通過java.lang.thread類的setcontextclassloaser()方法進行設定,如果建立線程時未設定,那麼它将會從父線程中繼承一個;如果在應用程式的全局範圍内都沒有設定過,那麼這個類加載器就預設是應用程式類加載器。有了線程上下文類加載器,就可以做一些“舞弊”的事情,例如直接加載沒有經過導入和導出的類,或者讓由架構類加載器加載的osgi架構代碼在運作期得以通路一些系統bundle中的類。

osgi中其他的類加載器與具體實作密切相關,後面我們将會在确定具體osgi實作架構和具體上下文的場景下再進行介紹,此處不再贅述。

2.5.4 類加載順序

當一個bundle類加載器遇到需要加載某個類或查找某個資源的請求時,搜尋過程必須按以下指定步驟執行:

1)如果類或資源在以java.*開頭的package中,那麼這個請求需要委派給父類加載器;否則,繼續下一個步驟搜尋。如果将這個請求委派給父類加載器後發現類或資源不存在,那麼搜尋終止并宣告這次類加載請求失敗。

2)如果類或資源在父類委派清單(org.osgi.framework. bootdelegation)所列明的package中,那麼這個請求也将委派給父類加載器。如果将這個請求委派給父類加載器後,發現類或資源不存在,那麼搜尋将跳轉到一個步驟。

3)如果類或資源在import-package标記描述的package中,那麼請求将委派給導出這個包的bundle的類加載器,否則搜尋過程将跳轉到下一個步驟。如果将這個請求委派給bundle類加載器後,發現類或資源不存在,那麼搜尋終止并宣告這次類加載請求失敗。

4)如果類或資源在require-bundle導入的一個或多個bundle的包中,這個請求将按照require-bundle指定的bundle清單順序逐一委派給對應bundle的類加載器,由于被委派的加載器也會按照這裡描述的搜尋過程查找類,是以整個搜尋過程就構成了深度優先的搜尋政策。如果所有被委派的bundle類加載器都沒有找到類或資源,那麼搜尋将轉到下一個步驟。

5)搜尋bundle内部的classpath。如果類或資源沒有找到,那麼這個搜尋将轉到下一個步驟。

6)搜尋每個附加的fragment bundle的classpath。搜尋順序将按這些fragment bundle的id升序搜尋。如果這個類或資源沒有找到,那麼搜尋轉到下一個步驟。

7)如果類或資源在某個bundle已聲明導出的package中,或者包含在已聲明導入(import-package或require-bundle)的package中,那麼這次搜尋過程将以沒有找到指定的類或資源而終止。

8)如果類或資源在某個使用dynamicimport-package聲明導入的package中,那麼将嘗試在運作時動态導入這個package。如果在某個導出該package的bundle中找到需要加載的類,那麼後面的類加載過程将按照步驟3)處理。

9)如果可以确定找到一個合适的完成動态導入的bundle,那麼這個請求将委派給該bundle的類加載器。如果無法找到任何合适的bundle來完成動态導入,那麼搜尋終止并宣告此次類加載請求失敗。當将動态導入委派給另一個bundle 類加載器時,類加載請求将按照步驟3)處理。

上述加載過程如圖2-16所示。

《深入了解OSGi:Equinox原理、應用與最佳實踐》一2.5 OSGi的類加載架構
《深入了解OSGi:Equinox原理、應用與最佳實踐》一2.5 OSGi的類加載架構