通過ClassLoader管理組件依賴
作者:Don Schwarz
譯者:xMatrix
版權聲明:任何獲得Matrix授權的網站,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
作者:Don Schwarz;
xMatrix原文地址:
http://www.onjava.com/pub/a/onjava/2005/04/13/dependencies.html中文地址:
http://www.matrix.org.cn/resource/article/43/43918_ClassLoader.html關鍵詞: ClassLoader
Java的類加載機制是非常強大的。你可以利用外部第三方的組件而不需要頭文件或靜態連接。你只需要簡單的把組件的JAR文件放到classpath下的目錄中。運行時引用完全是動態處理的。但如果這些第三方組件有自己的依賴關系時會怎么樣呢?通常這需要開發人員自己解決所有需要的相應版本的組件集,并且確認他們被加到classpath中。
JAR清單文件實際上你不需要這樣做,Java的類加載機制可以更優雅地解決這個問題。一種方案是需要每一個組件的作者在JAR清單中定義內部組件的依賴關系。這里清單是指一個被包含在JAR中的定義文件元數據的文本文件(META-INF/MANIFEST.MF)。最常用的屬性是Main-Class,定義了通過java –jar方式定位哪個類會被調用。然而,還有一個不那么有名的屬性Class-Path可以用來定義他所依賴的其他JAR。Java缺省的ClassLoader會檢查這些屬性并且自動附加這些特定的依賴到classpath中。
讓我們來看一個例子。考慮一個實現交通模擬的Java應用,他由三個JAR組成:
·simulator-ui.jar:基于Swing的視圖來顯示模擬的過程。
·simulator.jar:用來表示模擬狀態的數據對象和實現模擬的控制類。
·rule-engine.jar:常用的第三方規則引擎被用來建立模擬規則的模型。
simulator-ui.jar依賴simulator.jar,而simulator.jar依賴rule-engine.jar。
而通常執行這個應用的方法如下:
$ java -classpath
?? simulator-ui.jar:simulator.jar:rule-engine.jar
?? com.oreilly.simulator.ui.Main編者注:上面的命令行應該在同一行鍵入;只是由于網頁布局的限制看起來好像是多行。
但我們也可以在JAR的清單文件中定義這些信息,simulator-ui的MANIFEST.MF如下:
Main-Class: com.oreilly.simulator.ui.Main
Class-Path: simulator.jar而simulator的MANIFEST.MF包含:
Class-Path: rule-engine.jarrule-engine.jar或者沒有清單文件,或者清單文件為空。
現在我們可以這樣做:
$ java -jar simulator-ui.jarJava會自動解析清單的入口來取得主類及修改classpath,甚至可以確定simulator-ui.jar的路徑和解釋所有與這個路徑相關的Class-Path屬性,所以我們可以簡單按照下面的方式之一來做:
$ java -jar ../simulator-ui.jar
$ java -jar /home/don/build/simulator-ui.jar依賴沖突Java的Class-Path屬性的實現相對于手工定義整個classpath是一個大的改善。然而,兩種方式都有自己的限制。一個重要的限制就是你只能加載組件的一個特定版本。這看起來是很顯然的因為許多編程環境都有這個限制。但是在大的包含多個第三方依賴的多JAR項目中依賴沖突是很常見的。
例如,你正在開發一個通過查詢多個搜索引擎并比較他們的結果的搜索引擎。Google和Amazon的Alexa都支持使用SOAP作為通訊機制的網絡服務API,也都提供了相應的Java類庫方便訪問這些API。讓我們假設你的JAR- metasearch.jar,依賴于google.jar和amazon.jar,而他們都依賴于公共的soap.jar。
現在是沒有問題,但如果將來SOAP協議或API發生改變時會怎么樣呢?很可能這兩個搜索引擎不會選擇同時升級。可能在某一天你訪問Amazon時需要SOAP1.x版本而訪問Google時需要SOAP2.x版本,而這兩個版本的SOAP并不能在同一個進程空間中共存。在這里,我們可能包含下面的JAR依賴:
$ cat metasearch/META-INF/MANIFEST.MF
Main-Class: com.onjava.metasearch.Main
Class-Path: google.jar amazon.jar
$ cat amazon/META-INF/MANIFEST.MF
Class-Path: soap-v1.jar
$ cat google/META-INF/MANIFEST.MF
Class-Path: soap-v2.jar上面正確地描述了依賴關系,但這里并沒有包含什么魔法--這樣設置并不會像我們期望地那樣工作。如果soap-v1.jar和soap-v2.jar定義了許多相同的類,我們肯定這是會出問題的。
$ java -jar metasearch.jar
SOAP v1: remotely invoking searchAmazon
SOAP v1: remotely invoking searchGoogle你可以看到,soap-v1.jar被首先加在classpath中,因此實際上也只有他會被使用。上面的例子等價于:
$ java -classpath
?? metasearch.jar:amazon.jar:google.jar:soap-v1.jar:soap-v2.jar
?? # WRONG!編者注:上面的命令行應該在同一行鍵入;只是由于網頁布局的限制看起來好像是多行。
有趣的是如果Yahoo也發布了一個網絡服務API,而他看起來并沒有依賴于現有的SOAP/XML-RPC類庫。在較小的項目中,組件依賴沖突常被用來作為在你只要手工包裝方案或者只需要一兩個類時而不使用讓你不使用全量組件(如集合類庫)的原因之一。手工包裝方案有他的用處,但使用已有的組件是更普遍的方式。而且復制其他組件的類到你的代碼庫永遠不是一個好主意。實際上你已經與組件的開發產生分岐而且沒有機會在有問題修復或安全升級時合并他。
許多大的項目,如主要的商業組件,已經采用將他們使用的整個組件構建到他們的JAR內部。為了這么做,他們改變了包名使其唯一(如com/acme/foobar/org/freeware/utility),而且直接在他們的JAR中包含類。這樣做的好處是可以防止在這些組件中多個版本的沖突,但這也是有代價的。這么做對開發人員來說完全隱藏了對第三方的依賴。但如果這種方式大規模的應用,將會導致效率的降低(包括JAR文件的大小和加載多個JAR版本到進程中的效率降低)。這種方式的問題在于如果兩個組件依賴于同一個版本的第三方組件時,就沒有協調機制來確定共享的組件只被加載一次。這個問題我們會在下一節進行研究。除了效率的降低外,很可能你這種綁定第三方軟件的方式會與那些軟件的許可協議沖突。
另一種解決這個問題的方式是每一個組件的開發人員顯式的在他的包名中編碼一個版本號。Sun的javac代碼就采用這個方式—一個com.sun.tools.javac.Main類會簡單地轉發給com.sun.tools.javac.v8.Maino。每次一個新的Java版本發布,這個代碼的包名就改變一次。這就允許一個組件的多個發布版本可以共存在同一個類加載器中并且這使得版本的選擇是顯式的。但這也不是一個非常好的解決方案,因為或者客戶需要準確知道他們計劃使用的版本而且必須改變他們的代碼來轉換到新的版本,或者他們必須依賴于一個包裝類來轉發方案調用給最新的版本(在這種情況下,這些包裝類就會承受我們上面提到的相同問題)。
加載多個發布版本這里我們遇到的問題在大多數項目中也存在,所有的類都會被加載到一個全局命名空間。如果每一個組件有自己的命名空間而且他會加載所有他依賴的組件到這個命名空間而不影響進程的其他部分,那又會怎么樣呢?實際上我們可以在Java中這么做!類名不需要是唯一的,只要類名和其所對應的ClassLoader的組合是唯一的就可以了。這意味著ClassLoader類似于命名空間,而如果我們可以加載每一個組件在自己的ClassLoader中,他就可以控制如何滿足依賴。他可以代理類定位給其他的包含他的依賴組件所需要的特定版本的ClassLoader。如圖1。

Figure 1. Decentralized class loaders
然而這個架構并不比綁定每一個依賴的JAR在自己的JAR中好多少。我們需要的是一個可以確保每一個組件版本僅被一個類加載器加載的中央集權。圖2中的架構可以確定每一個組件版本僅被加載一次。

Figure 2. Class loaders with mediator
為了實現這種方式,我們需要創建兩個不同類型的類加載器。每一個ComponentClassLoader需要擴展Java的URLClassLoader來提供需要的邏輯來從一個JAR中獲取.class文件。當然他也會執行兩個其他的任務。在創建的時候,他會獲取JAR清單文件并定位一個新屬性Restricted-Class-Path。不像Sun提供的Class-Path屬性,這個屬性暗示特定的JAR應該只對這個組件有效。
public class ComponentClassLoader extends URLClassLoader {
??// ...
??public ComponentClassLoader (MasterClassLoader master, File file)
??{
????// ...
????JarFile jar = new JarFile(file);
????Manifest man = jar.getManifest();
????Attributes attr = man.getMainAttributes();
????List l = new ArrayList();
????String str = attr.getValue("Restricted-Class-Path");
????if (str != null) {
????????StringTokenizer tok = new StringTokenizer(str);
????????while (tok.hasMoreTokens()) {
????????????l.add(new File(file.getParentFile(),
?????????????????????????? tok.nextToken());
????????}
????}
????this.dependencies = l;
??}
??public Class loadClass (String name, boolean resolve)
????throws ClassNotFoundException
??{
????try {
??????// Try to load the class from our JAR.
??????return loadClassForComponent(name, resolve);
????} catch (ClassNotFoundException ex) {}
????// Couldn't find it -- let the master look for it
????// in another components.
????return master.loadClassForComponent(name,
?????????????????????????? resolve, dependencies);
??}
????
??public Class loadClassForComponent (String name,
?????????????????????????????????? boolean resolve)
????throws ClassNotFoundException
??{
????Class c = findLoadedClass(name);
????
????// Even if findLoadedClass returns a real class,
????// we might simply be its initiating ClassLoader.
????// Only return it if we're actually its defining
????// ClassLoader (as determined by Class.getClassLoader).
????//
????if (c == null || c.getClassLoader() != this) {
????????c = findClass(name);
????
????????if (resolve) {
????????????resolveClass(c);
????????}
????}
????return c;
??}
}
當一個請求要求加載一個在特定JAR中不存在的類時,他會顯式的調用MasterClassLoader并傳遞他的JAR依賴列表作為參數而不是簡單的轉發給父類加載器。然后MasterClassLoader將每一個特定依賴請求轉發給ComponentClassLoader
public class MasterClassLoader extends ClassLoader {
??// ...
??public Class loadClassForComponent (String name,
??????????????????????boolean resolve, List files)
????throws ClassNotFoundException
??{
????try {
??????return loadClass(name, resolve);
????} catch (ClassNotFoundException ex) {}
????for (Iterator i = files.iterator(); i.hasNext(); ) {
??????File f = (File)i.next();
??????try {
????????ComponentClassLoader ccl =
????????????getComponentClassLoader(f);
????????return ccl.loadClassForComponent(name, resolve);
??????} catch (Exception ex) {
????????// simplified for clarity
??????}
????}
????throw new ClassNotFoundException(name);
??}
}
這種方法有許多有用的特性。最重要的是我們現在可以滿足原始的依賴圖而不需要修改代碼(理論上是這樣的,但還需要看一下面給出的警告)。他減少了組件間的耦合,因為每一個組件可以依賴于他所需要的組件版本,而不需要強制其他組件升級或降級版本來滿足他。
另一個優點是這種技術增加了透明性。每一個組件的運行時依賴被顯式地列出來了,而且這是強制的。即使使用Class-Path清單屬性,你也不能確信你沒有誤匹配一個依賴。考慮一下當你的組件使用commons-log組件時,后來使用log4j來做日志處理。你可能有其他組件依賴log4j但沒有定義這個依賴。因為他已經被加在classpath,你也不會檢查到這個問題,但如果有一天你用其他的日志處理代替了log4j,你就有問題了。相反,如果使用Restricted-Class-Path而你沒有列出log4j作為依賴,你會得到一個ClassNotFoundException異常。
重寫系統類加載器現在我們已經有了一個類加載器可以實現我們新的版本策略,我們需要通過某種方式來安裝了。如果我們的代碼會被嵌入在應用服務器中或其他類型的解釋器,那么解釋器的代碼可以編程創建新的類加載器并使用他來加載我們的代碼。通過這種方式,一個服務器進程可以通過定義在請求中定義需要的版本來執行多個代碼版本。但如果我們只想在普通的Java應用中使用時需要怎么做呢?
一種主觀的方式是使用Java1.5的-javaagent命令行參數。這樣我們可以在加載我們應用的主類之前初始化特定的JAR(這稱為代理)。不幸地是,代理類被與加載主類的同一個類加載器加載(系統類加載器),因此在這時安裝我們的自定義類加載器已經太遲了,因為我們的代理的方法已經執行了。
另一種方式是創建一個“引導”主類來建立類加載器并且使用他來定位我們實際的主類并執行主方法。這種方式很簡單,但去掉了一些Java的好的用法如-classpath和-jar選項并且需要我們自己調用主方法。
實際上,我們可以重寫java.system.class.loader系統屬性來使我們的類加載器作為系統類加載器被初始化。這樣做的話,我們會創建第三個類加載器WrapperClassLoader作為系統類加載器的代替。他的父類會是引導類加載器,他包含Java運行時類庫(rt.jar)。在初始化的時候,他會讀取java.library.path系統屬性并且為每一個特定的JAR創建ComponentClassLoader。
public static List initClassLoaders (MasterClassLoader master)
??throws MalformedURLException, IOException
{
??List loaders = new ArrayList();
??String classpath =
????????????????System.getProperty("java.class.path");
??StringTokenizer tok = new StringTokenizer(classpath,
??????????????????????????????????File.pathSeparator);
??while (tok.hasMoreTokens()) {
????File file = new File(tok.nextToken());
????loaders.add(master.getComponentClassLoader(file));
??}
??return loaders;
}
現在我們可以像下面那樣運行我們的搜索引擎了:
$ java -Xbootclasspath/a:classloader.jar \
????-Djava.system.class.loader=
????????com.onjava.classloader.WrapperClassLoader \
????-jar metasearch.jar
SOAP v1: remotely invoking searchAmazon
SOAP v2: remotely invoking searchGoogle (with newFlag = true)小結在最后版本中,我們實際上進行了超過原始需求的更多研究。除了在一個靜態字段中嵌入版本號之外,我們現在可以從屬性文件中獲取了。這意味著可以通過我們的類加載器加載資源文件,而且必須包含與實際類加載類似的邏輯。我們也可以修改一下soap-v2.jar的API,從 public Object invokeMethod (String name, Object[] args)
到public Object invokeMethod (String name, Object[] args,????????????????????boolean newFlag)
這看起來有些奇怪,但這意味站如果我們將剛才運行的源程序放在同一個目錄下,我們可能不能編譯他。如果我們嘗試用同一版本的soap.jar同時構建google 和amazon,其中一個的方法標識可能不匹配。如果我們用兩個soap.jar的版本,又會得到重復類錯誤。但是,我們可以分別編譯google.jar和amazon.jar,而不需要考慮他們是否使用兼容的soap.jar版本,而且我們可以在同一進程中用不同的類加載器運行他們。
考慮一下,如果將這種技術運用在一個在構建時管理組件依賴的構建工具(如Maven),你將不會遇到缺少依賴或JAR沖突的問題了。
關于作者:Don Schwarz是一家專注于元編程和語言集成的大投資銀行的Java開發人員。
資源·onjava.com:
onjava.com·Matrix-Java開發者社區:
http://www.matrix.org.cn/