摘要 通過構建一個能夠把Java類裝載隔離到一個指定的jar文件中的類裝載組件容器框架,你可以確保運行時刻會裝載你期望的組件版本。
Java的類裝載框架強有力且具有靈活性。它允許應用程序存取類庫而不必鏈接到靜態的"include"文件。代之的是,它能夠從指定位置裝載包含庫類和資源的檔案文件,例如由CLASSPATH環境變量所定義的目錄和網絡位置。由系統來動態地解析對類和資源的運行時刻參考,從而簡化了更新和版本發行。然而,每一個庫都有其自己的依賴性集合-並且由開發者和發布人員來保證他們的應用程序適當地參考正確的版本。遺憾的是,默認的類裝載系統和特定依賴性的結合可能並且確實會導致錯誤、系統崩潰甚至於更糟糕的情況發生。
本文中,我將向你建議一個實現類裝載的容器框架,從而解決這些問題。
一、Java Classpath
Java根據環境屬性/變量CLASSPATH來指定運行時刻用來查找類和其它資源的路徑。你可以通過設置CLASSPATH環境變量或使用Java命令行選項--classpath來定義CLASSPATH屬性。
典型地,一個Java運行時刻以下面順序查找和加載類:
1. 在bootstrap類列表中的類-這些是體現Java平台的類,例如在rt.jar中的類。
2. 出現在擴展類列表中的類-這些類使用擴展機制框架來擴展Java平台,使用位於運行時刻環境的/lib/ext目錄下的檔案文件(.jar,.zip,等等。)。
3. 用戶類-這些類不使用-classpath命令行選項或CLASSPATH環境變量標識的擴展機制架構。
二、檔案與Classpath
一個檔案.jar或.zip文件可以包括一個manifest文件-它們包含能夠用於提供檔案信息,設置檔案屬性,等等的入口。這個manifest文件還可以通過包括一個名為Class-Path的入口(它包含一個檔案和目錄列表)來擴展classpath。JDK 1.3中引入了Class-Path manifest入口用於指定可選的據需要可以加載的jar文件和目錄。下面是一個Class-Path入口的例子:
Class-Path: mystuff/utils.jar
mystuff/logging.jar mylib/
Java提供了一種可擴展模型用於指定裝載類的位置和文件列表。然而,由此也引發了一些問題,例如,一個不同版本的庫可能存在於classpath中-這超出一個執行類所期望的結果。
三、Classpath版本沖突
在Java中,一個類的運行時刻標識是由通過其完全限定名字來定義的(在類名之前的包名,有時被作為FQN),所有這些都添加到裝載類的相關裝載器的ID。這樣以來,由多個類加載器加載的一個類的每一個實例都將被當作是Java運行時刻的一個單獨的實體。這意味著,運行時刻能夠在任何時間裝載同一個類的多個版本。這是一種非常有力和相當靈活的特征;然而,如果一位開發人員不認真地使用的話,某些副作用可能會令他疑惑不解。
可以設想,你在開發一個企業應用程序-它使用類似語義從多種源存取數據,例如一個文件系統和一個數據庫。許多這種類型的系統都暴露一個數據存取層-通過抽象類似數據源的數據存取對象(DAO)。現在,設想你裝載一個新版本的一個數據庫DAO,使用一種略微不同的API來滿足一個DAO客戶端的新特征的要求-但是你仍然需要舊式的DAO以便適合於其它還沒有為這種新的API准備好的客戶端。在典型的運行時刻環境下,這種新的DAO將簡單地替換舊的版本並且所有的新實例都將從新版本中創建。然而,如果在不停止運行時刻環境的前提下發生更新,那麼任何已經存在的舊DAO的實例將與該新DAO的任何實例一起駐留於內存中-當創建這些新實例時。這已經足已令人疑惑了。更為糟糕的是,一位DAO客戶期望創建一個舊版本的DAO的實例,但是實際上得到一個具有已改變的API的新版本的實例。正如你所見,這可能會帶來一些有趣的挑戰。
為了確保穩定性和安全性,調用代碼必須能夠指明它想使用的類的正確版本。為此,你可以創建一個類加載器,組件容器模型並且使用一些簡單的類加載技術。
四、檔案與組件
因為檔案文件(jar文件,zip文件,等等)與Java類加載機制和發布工具之間具有相當松的耦合性,所以它們是一種用作自定義組件容器的自然的候選。一個Java組件在一個檔案文件中的打包與發布的成功依賴於:
· 能夠指定要實例化一個組件的哪個版本的開發者
· 裝載組件的輔助類的正確版本-根據與該組件在同一個jar文件中發現的信息。
這使得組件的開發者和消費者能夠完全控制實際創建和使用每一個組件的相應版本。
在下面的幾節中,我將討論一下有關於定義組件和組件命名空間的概念。
五、共享輔助資源
最大的問題之一是,當使用標准類加載器在Java中處理共享庫時,所有的類都被加載到一個命名空間中。這使得在任何給定時刻很難使用相同庫的不同版本。你所需要的是,一個組件能夠定義它自己的命名空間-該組件及其所有輔助庫將會裝載到其中的。
因為在Java中,一個類的運行時刻標識是使用類的完全限定名和其加載器的ID來定義的,所以一個命名空間已經相應於每一個類加載器存在。因此,你可以使用類加載器來構建一個組件容器,由它來定義一個組件及其依賴對象的一個命名空間。
例如,如果我有一個命名為"com.jeffhanson.components.HelloWorld"的類,我想運行它的兩個版本,那麼解決方案是,使用一個類裝載器創建HelloWorld類的一個版本的一個實例,而使用另一個類裝載器創建另一個版本的HelloWorld類。圖1展示了這一概念。
圖1.使用多個類裝載器:由於Java命名慣例特征的影響,使用不同的類裝載器將定義不同的命名空間。
正如我將在本文中所要展示的,使用兩個不同的類裝載器來實例化一個類的技術實際上創建了一個虛擬的命名空間。然而,我實際上剛好創建了同一個版本的類的多個實例。
為了便於加載和實例化同一個類的多個版本,我將展示(在下面的幾節中)一個組件-容器框架-它基於類裝載器命名空間機制以允許裝載同一個類的不同版本。
六、利用Classloader命名空間
你可以把組件容器框架實現為一個容器實體-負責加載在jar或zip檔案中定義的組件以及該組件需要的輔助類。這個框架的創建目標是:
1. 允許開發者指定實例化一個組件的哪個版本。
2. 基於與組件在同一個jar文件中找到的信息為每個組件裝載正確的輔助類。
3. 跨組件共享輔助類和檔案。
你將需要一個配置文件來定義組件及其相應的輔助文件,正如下列示例所展示的:
<?xml version="1.0"?>
<component name="com.jeffhanson.components.HelloWorld">
<component-archive>
HelloWorldComponentV1.jar
</component-archive>
<ancillary-resources>
<ancillary-resource>
log4j-1.2.12.jar
</ancillary-resource>
<ancillary-resource>
concurrent-1.3.4.jar
</ancillary-resource>
</ancillary-resources>
</component>
你可以把上面例子中的元素與下面例子中的元素進行比較。唯一的改變是組件檔案元素的值。這個組件元素值定義了包含每一個版本組件的檔案的名字。
<?xml version="1.0"?>
<component name=
"com.jeffhanson.components.HelloWorld">
<component-archive>
HelloWorldComponentV2.jar
</component-archive>
<ancillary-resources>
<ancillary-resource>
log4j-1.2.12.jar
</ancillary-resource>
<ancillary-resource>
concurrent-1.3.4.jar
</ancillary-resource>
</ancillary-resources>
</component>
為了確保框架僅從指定位置加載類,你必須創建一個新的擴展URLClassLoader的ClassLoader。重載loadClass方法以防止到它的調用傳播到默認的類裝載器的父級-並因此從標准classpath中加載類。這樣以來,就可以把類搜索限定到提供給類裝載器的URL並且讓你把特定jar文件位置提供給裝載組件的類裝載器。
下列代碼展示了組件的類裝載機制:
package com.jeffhanson.components;
import java.net.URL;
import java.net.URLClassLoader;
public class RestrictedURLClassLoader
extends URLClassLoader {
public RestrictedURLClassLoader( URL[] urls) { super(urls, null);)
}
圖2.組件容器框架類關系:該圖展示了存在於組件容器框架中的類之間的關系。
public Class loadClass(String name)
throws ClassNotFoundException
{
Class cls = super.loadClass(name);
if (cls == null)
{
throw new ClassNotFoundException("Restricted ClassLoader" + " is unable to find class: " + name);
}
return cls;
}
}
這個受限制的類裝載器由組件容器使用來裝載組件和任何指定的輔助類。
該組件容器使用當前線程的上下文類裝載器來查找該組件的URL。然後,這個URL被加入到受限制的類裝載器並且用於實例化該組件。然後,該組件類被組件容器緩沖以便於後面的調用。列表1展示了該組件容器的代碼,圖2展示了在組件容器框架中的類之間的關系。
七、裝載特定類版本
現在,你可以使用該容器和受限制的類裝載器來從指定檔案中裝載包含版本信息的類的組件。
列表2展示了如何實例化組件容器的實例並且使用配置文件名初始化它們-針對兩種版本的HelloWorld組件。然後,每個組件版本被裝載和實例化-使用ComponentContainer類的createComponent方法。
對於每一個實例化的組件對象的調用將產生每個組件的期望版本相應的結果。
圖3.組件序列圖:該圖展示了組件容器框架創建一個組件的序列。
圖3中的序列圖展示了框架用於裝載和創建一個組件的步驟。
注意,在實例化一個默認類裝載器之前,對於RestrictedURLClassLoader類的調用終止,從而把類搜索限制為提供給RestrictedURLClassLoader實例的URL。
八、小結
總之,你已經從本文中看到了怎樣構建一個類裝載組件容器框架。這樣以來,便利了在一個自包含上下文中定義、版本化和創建Java組件。以這種方式來利用Java的類裝載能力可以把類裝載約束到指定位置,從而讓你同時裝載不同版本的類-在同一個運行JVM中創建的和使用的類。