隨筆 - 1, 文章 - 44, 評(píng)論 - 2, 引用 - 0
          數(shù)據(jù)加載中……

          通過ClassLoader管理組件依賴

          通過ClassLoader管理組件依賴

          作者:Don Schwarz

          譯者:xMatrix


          版權(quán)聲明:任何獲得Matrix授權(quán)的網(wǎng)站,轉(zhuǎn)載時(shí)請務(wù)必以超鏈接形式標(biāo)明文章原始出處和作者信息及本聲明
          作者: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
          關(guān)鍵詞: ClassLoader

          Java的類加載機(jī)制是非常強(qiáng)大的。你可以利用外部第三方的組件而不需要頭文件或靜態(tài)連接。你只需要簡單的把組件的JAR文件放到classpath下的目錄中。運(yùn)行時(shí)引用完全是動(dòng)態(tài)處理的。但如果這些第三方組件有自己的依賴關(guān)系時(shí)會(huì)怎么樣呢?通常這需要開發(fā)人員自己解決所有需要的相應(yīng)版本的組件集,并且確認(rèn)他們被加到classpath中。

          JAR清單文件
          實(shí)際上你不需要這樣做,Java的類加載機(jī)制可以更優(yōu)雅地解決這個(gè)問題。一種方案是需要每一個(gè)組件的作者在JAR清單中定義內(nèi)部組件的依賴關(guān)系。這里清單是指一個(gè)被包含在JAR中的定義文件元數(shù)據(jù)的文本文件(META-INF/MANIFEST.MF)。最常用的屬性是Main-Class,定義了通過java –jar方式定位哪個(gè)類會(huì)被調(diào)用。然而,還有一個(gè)不那么有名的屬性Class-Path可以用來定義他所依賴的其他JAR。Java缺省的ClassLoader會(huì)檢查這些屬性并且自動(dòng)附加這些特定的依賴到classpath中。

          讓我們來看一個(gè)例子??紤]一個(gè)實(shí)現(xiàn)交通模擬的Java應(yīng)用,他由三個(gè)JAR組成:
          ·simulator-ui.jar:基于Swing的視圖來顯示模擬的過程。
          ·simulator.jar:用來表示模擬狀態(tài)的數(shù)據(jù)對(duì)象和實(shí)現(xiàn)模擬的控制類。
          ·rule-engine.jar:常用的第三方規(guī)則引擎被用來建立模擬規(guī)則的模型。
          simulator-ui.jar依賴simulator.jar,而simulator.jar依賴rule-engine.jar。

          而通常執(zhí)行這個(gè)應(yīng)用的方法如下:
          $ java -classpath
          ?? simulator-ui.jar:simulator.jar:rule-engine.jar
          ?? com.oreilly.simulator.ui.Main


          編者注:上面的命令行應(yīng)該在同一行鍵入;只是由于網(wǎng)頁布局的限制看起來好像是多行。

          但我們也可以在JAR的清單文件中定義這些信息,simulator-ui的MANIFEST.MF如下:

          Main-Class: com.oreilly.simulator.ui.Main
          Class-Path: simulator.jar


          而simulator的MANIFEST.MF包含:
          Class-Path: rule-engine.jar

          rule-engine.jar或者沒有清單文件,或者清單文件為空。
          現(xiàn)在我們可以這樣做:
          $ java -jar simulator-ui.jar

          Java會(huì)自動(dòng)解析清單的入口來取得主類及修改classpath,甚至可以確定simulator-ui.jar的路徑和解釋所有與這個(gè)路徑相關(guān)的Class-Path屬性,所以我們可以簡單按照下面的方式之一來做:
          $ java -jar ../simulator-ui.jar
          $ java -jar /home/don/build/simulator-ui.jar


          依賴沖突

          Java的Class-Path屬性的實(shí)現(xiàn)相對(duì)于手工定義整個(gè)classpath是一個(gè)大的改善。然而,兩種方式都有自己的限制。一個(gè)重要的限制就是你只能加載組件的一個(gè)特定版本。這看起來是很顯然的因?yàn)樵S多編程環(huán)境都有這個(gè)限制。但是在大的包含多個(gè)第三方依賴的多JAR項(xiàng)目中依賴沖突是很常見的。

          例如,你正在開發(fā)一個(gè)通過查詢多個(gè)搜索引擎并比較他們的結(jié)果的搜索引擎。Google和Amazon的Alexa都支持使用SOAP作為通訊機(jī)制的網(wǎng)絡(luò)服務(wù)API,也都提供了相應(yīng)的Java類庫方便訪問這些API。讓我們假設(shè)你的JAR- metasearch.jar,依賴于google.jar和amazon.jar,而他們都依賴于公共的soap.jar。
          現(xiàn)在是沒有問題,但如果將來SOAP協(xié)議或API發(fā)生改變時(shí)會(huì)怎么樣呢?很可能這兩個(gè)搜索引擎不會(huì)選擇同時(shí)升級(jí)??赡茉谀骋惶炷阍L問Amazon時(shí)需要SOAP1.x版本而訪問Google時(shí)需要SOAP2.x版本,而這兩個(gè)版本的SOAP并不能在同一個(gè)進(jìn)程空間中共存。在這里,我們可能包含下面的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


          上面正確地描述了依賴關(guān)系,但這里并沒有包含什么魔法--這樣設(shè)置并不會(huì)像我們期望地那樣工作。如果soap-v1.jar和soap-v2.jar定義了許多相同的類,我們肯定這是會(huì)出問題的。
          $ java -jar metasearch.jar
          SOAP v1: remotely invoking searchAmazon
          SOAP v1: remotely invoking searchGoogle


          你可以看到,soap-v1.jar被首先加在classpath中,因此實(shí)際上也只有他會(huì)被使用。上面的例子等價(jià)于:
          $ java -classpath
          ?? metasearch.jar:amazon.jar:google.jar:soap-v1.jar:soap-v2.jar
          ?? # WRONG!


          編者注:上面的命令行應(yīng)該在同一行鍵入;只是由于網(wǎng)頁布局的限制看起來好像是多行。

          有趣的是如果Yahoo也發(fā)布了一個(gè)網(wǎng)絡(luò)服務(wù)API,而他看起來并沒有依賴于現(xiàn)有的SOAP/XML-RPC類庫。在較小的項(xiàng)目中,組件依賴沖突常被用來作為在你只要手工包裝方案或者只需要一兩個(gè)類時(shí)而不使用讓你不使用全量組件(如集合類庫)的原因之一。手工包裝方案有他的用處,但使用已有的組件是更普遍的方式。而且復(fù)制其他組件的類到你的代碼庫永遠(yuǎn)不是一個(gè)好主意。實(shí)際上你已經(jīng)與組件的開發(fā)產(chǎn)生分岐而且沒有機(jī)會(huì)在有問題修復(fù)或安全升級(jí)時(shí)合并他。

          許多大的項(xiàng)目,如主要的商業(yè)組件,已經(jīng)采用將他們使用的整個(gè)組件構(gòu)建到他們的JAR內(nèi)部。為了這么做,他們改變了包名使其唯一(如com/acme/foobar/org/freeware/utility),而且直接在他們的JAR中包含類。這樣做的好處是可以防止在這些組件中多個(gè)版本的沖突,但這也是有代價(jià)的。這么做對(duì)開發(fā)人員來說完全隱藏了對(duì)第三方的依賴。但如果這種方式大規(guī)模的應(yīng)用,將會(huì)導(dǎo)致效率的降低(包括JAR文件的大小和加載多個(gè)JAR版本到進(jìn)程中的效率降低)。這種方式的問題在于如果兩個(gè)組件依賴于同一個(gè)版本的第三方組件時(shí),就沒有協(xié)調(diào)機(jī)制來確定共享的組件只被加載一次。這個(gè)問題我們會(huì)在下一節(jié)進(jìn)行研究。除了效率的降低外,很可能你這種綁定第三方軟件的方式會(huì)與那些軟件的許可協(xié)議沖突。

          另一種解決這個(gè)問題的方式是每一個(gè)組件的開發(fā)人員顯式的在他的包名中編碼一個(gè)版本號(hào)。Sun的javac代碼就采用這個(gè)方式—一個(gè)com.sun.tools.javac.Main類會(huì)簡單地轉(zhuǎn)發(fā)給com.sun.tools.javac.v8.Maino。每次一個(gè)新的Java版本發(fā)布,這個(gè)代碼的包名就改變一次。這就允許一個(gè)組件的多個(gè)發(fā)布版本可以共存在同一個(gè)類加載器中并且這使得版本的選擇是顯式的。但這也不是一個(gè)非常好的解決方案,因?yàn)榛蛘呖蛻粜枰獪?zhǔn)確知道他們計(jì)劃使用的版本而且必須改變他們的代碼來轉(zhuǎn)換到新的版本,或者他們必須依賴于一個(gè)包裝類來轉(zhuǎn)發(fā)方案調(diào)用給最新的版本(在這種情況下,這些包裝類就會(huì)承受我們上面提到的相同問題)。

          加載多個(gè)發(fā)布版本
          這里我們遇到的問題在大多數(shù)項(xiàng)目中也存在,所有的類都會(huì)被加載到一個(gè)全局命名空間。如果每一個(gè)組件有自己的命名空間而且他會(huì)加載所有他依賴的組件到這個(gè)命名空間而不影響進(jìn)程的其他部分,那又會(huì)怎么樣呢?實(shí)際上我們可以在Java中這么做!類名不需要是唯一的,只要類名和其所對(duì)應(yīng)的ClassLoader的組合是唯一的就可以了。這意味著ClassLoader類似于命名空間,而如果我們可以加載每一個(gè)組件在自己的ClassLoader中,他就可以控制如何滿足依賴。他可以代理類定位給其他的包含他的依賴組件所需要的特定版本的ClassLoader。如圖1。

          image
          Figure 1. Decentralized class loaders

          然而這個(gè)架構(gòu)并不比綁定每一個(gè)依賴的JAR在自己的JAR中好多少。我們需要的是一個(gè)可以確保每一個(gè)組件版本僅被一個(gè)類加載器加載的中央集權(quán)。圖2中的架構(gòu)可以確定每一個(gè)組件版本僅被加載一次。


          Figure 2. Class loaders with mediator

          為了實(shí)現(xiàn)這種方式,我們需要?jiǎng)?chuàng)建兩個(gè)不同類型的類加載器。每一個(gè)ComponentClassLoader需要擴(kuò)展Java的URLClassLoader來提供需要的邏輯來從一個(gè)JAR中獲取.class文件。當(dāng)然他也會(huì)執(zhí)行兩個(gè)其他的任務(wù)。在創(chuàng)建的時(shí)候,他會(huì)獲取JAR清單文件并定位一個(gè)新屬性Restricted-Class-Path。不像Sun提供的Class-Path屬性,這個(gè)屬性暗示特定的JAR應(yīng)該只對(duì)這個(gè)組件有效。
          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;
          ??}
          }


          當(dāng)一個(gè)請求要求加載一個(gè)在特定JAR中不存在的類時(shí),他會(huì)顯式的調(diào)用MasterClassLoader并傳遞他的JAR依賴列表作為參數(shù)而不是簡單的轉(zhuǎn)發(fā)給父類加載器。然后MasterClassLoader將每一個(gè)特定依賴請求轉(zhuǎn)發(fā)給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);
          ??}
          }


          這種方法有許多有用的特性。最重要的是我們現(xiàn)在可以滿足原始的依賴圖而不需要修改代碼(理論上是這樣的,但還需要看一下面給出的警告)。他減少了組件間的耦合,因?yàn)槊恳粋€(gè)組件可以依賴于他所需要的組件版本,而不需要強(qiáng)制其他組件升級(jí)或降級(jí)版本來滿足他。

          另一個(gè)優(yōu)點(diǎn)是這種技術(shù)增加了透明性。每一個(gè)組件的運(yùn)行時(shí)依賴被顯式地列出來了,而且這是強(qiáng)制的。即使使用Class-Path清單屬性,你也不能確信你沒有誤匹配一個(gè)依賴。考慮一下當(dāng)你的組件使用commons-log組件時(shí),后來使用log4j來做日志處理。你可能有其他組件依賴log4j但沒有定義這個(gè)依賴。因?yàn)樗呀?jīng)被加在classpath,你也不會(huì)檢查到這個(gè)問題,但如果有一天你用其他的日志處理代替了log4j,你就有問題了。相反,如果使用Restricted-Class-Path而你沒有列出log4j作為依賴,你會(huì)得到一個(gè)ClassNotFoundException異常。

          重寫系統(tǒng)類加載器
          現(xiàn)在我們已經(jīng)有了一個(gè)類加載器可以實(shí)現(xiàn)我們新的版本策略,我們需要通過某種方式來安裝了。如果我們的代碼會(huì)被嵌入在應(yīng)用服務(wù)器中或其他類型的解釋器,那么解釋器的代碼可以編程創(chuàng)建新的類加載器并使用他來加載我們的代碼。通過這種方式,一個(gè)服務(wù)器進(jìn)程可以通過定義在請求中定義需要的版本來執(zhí)行多個(gè)代碼版本。但如果我們只想在普通的Java應(yīng)用中使用時(shí)需要怎么做呢?

          一種主觀的方式是使用Java1.5的-javaagent命令行參數(shù)。這樣我們可以在加載我們應(yīng)用的主類之前初始化特定的JAR(這稱為代理)。不幸地是,代理類被與加載主類的同一個(gè)類加載器加載(系統(tǒng)類加載器),因此在這時(shí)安裝我們的自定義類加載器已經(jīng)太遲了,因?yàn)槲覀兊拇淼姆椒ㄒ呀?jīng)執(zhí)行了。

          另一種方式是創(chuàng)建一個(gè)“引導(dǎo)”主類來建立類加載器并且使用他來定位我們實(shí)際的主類并執(zhí)行主方法。這種方式很簡單,但去掉了一些Java的好的用法如-classpath和-jar選項(xiàng)并且需要我們自己調(diào)用主方法。

          實(shí)際上,我們可以重寫java.system.class.loader系統(tǒng)屬性來使我們的類加載器作為系統(tǒng)類加載器被初始化。這樣做的話,我們會(huì)創(chuàng)建第三個(gè)類加載器WrapperClassLoader作為系統(tǒng)類加載器的代替。他的父類會(huì)是引導(dǎo)類加載器,他包含Java運(yùn)行時(shí)類庫(rt.jar)。在初始化的時(shí)候,他會(huì)讀取java.library.path系統(tǒng)屬性并且為每一個(gè)特定的JAR創(chuàng)建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;
          }


          現(xiàn)在我們可以像下面那樣運(yùn)行我們的搜索引擎了:
          $ 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)


          小結(jié)
          在最后版本中,我們實(shí)際上進(jìn)行了超過原始需求的更多研究。除了在一個(gè)靜態(tài)字段中嵌入版本號(hào)之外,我們現(xiàn)在可以從屬性文件中獲取了。這意味著可以通過我們的類加載器加載資源文件,而且必須包含與實(shí)際類加載類似的邏輯。我們也可以修改一下soap-v2.jar的API,從 public Object invokeMethod (String name, Object[] args)
          到public Object invokeMethod (String name, Object[] args,????????????????????boolean newFlag)

          這看起來有些奇怪,但這意味站如果我們將剛才運(yùn)行的源程序放在同一個(gè)目錄下,我們可能不能編譯他。如果我們嘗試用同一版本的soap.jar同時(shí)構(gòu)建google 和amazon,其中一個(gè)的方法標(biāo)識(shí)可能不匹配。如果我們用兩個(gè)soap.jar的版本,又會(huì)得到重復(fù)類錯(cuò)誤。但是,我們可以分別編譯google.jar和amazon.jar,而不需要考慮他們是否使用兼容的soap.jar版本,而且我們可以在同一進(jìn)程中用不同的類加載器運(yùn)行他們。

          考慮一下,如果將這種技術(shù)運(yùn)用在一個(gè)在構(gòu)建時(shí)管理組件依賴的構(gòu)建工具(如Maven),你將不會(huì)遇到缺少依賴或JAR沖突的問題了。


          關(guān)于作者:Don Schwarz是一家專注于元編程和語言集成的大投資銀行的Java開發(fā)人員。

          資源
          ·onjava.com:onjava.com
          ·Matrix-Java開發(fā)者社區(qū):http://www.matrix.org.cn/

          posted on 2006-05-29 15:08 ASONG 閱讀(282) 評(píng)論(0)  編輯  收藏 所屬分類: JAVA

          主站蜘蛛池模板: 会理县| 铜梁县| 阳曲县| 托里县| 沁源县| 昌江| 梁平县| 乌海市| 井研县| 乐亭县| 即墨市| 新乡县| 民乐县| 陈巴尔虎旗| 石嘴山市| 汤原县| 天台县| 博爱县| 兖州市| 巴彦县| 通化市| 东安县| 嘉祥县| 邢台县| 高州市| 安泽县| 富顺县| 罗平县| 龙海市| 台江县| 石首市| 航空| 东源县| 皋兰县| 德惠市| 贺州市| 三台县| 白朗县| 鄯善县| 广宗县| 女性|