這個(gè)問(wèn)題經(jīng)常出現在編寫(xiě)框架代碼 , 需要動(dòng)態(tài)加載很多類(lèi)和資源的時(shí)候 . 通常當你需要動(dòng)態(tài)加載資源的時(shí)候 , 你至少有三個(gè) ClassLoader 可以選擇 :

²        系統類(lèi)加載器或叫作應用類(lèi)加載器 (system classloader or application classloader)

²        當前類(lèi)加載器

²        當前線(xiàn)程類(lèi)加載器

上面的問(wèn)題指的是最后一種類(lèi)加載器 . 哪種類(lèi)加載器是正確的選擇呢 ?

第一種選擇可以很容易地排除 : 系統類(lèi)加載器 (system classloader). 這個(gè)類(lèi)加載器處理 -classpath 下的類(lèi)加載工作 , 可以通過(guò) ClassLoader.getSystemClassLoader() 方法調用 . ClassLoader 下所有的 getSystemXXX() 的靜態(tài)方法都是通過(guò)這個(gè)方法定義的 . 在你的代碼中 , 你應該盡量少地調用這個(gè)方法 , 以其它的類(lèi)加載器作為代理 . 否則你的代碼將只能工作在簡(jiǎn)單的命令行應用中 , 這個(gè)時(shí)候系統類(lèi)加載器 (system classloader) JVM 最后創(chuàng )建的類(lèi)加載器 . 一但你把代碼移到 EJB, Web 應用或 Java Web Start 應用中 , 一定會(huì )出問(wèn)題 .

      所以我們來(lái)看第二種選擇 : 當前上下文環(huán)境下的類(lèi)加載器 . 根據定義 , 當前類(lèi)加載器就是你當前方法所屬的類(lèi)的加載器 . 在運行時(shí)類(lèi)之間動(dòng)態(tài)聯(lián)編 , 及調用 Class.forName,() Class.getResource() 等類(lèi)似方法時(shí) , 這個(gè)類(lèi)加載器會(huì )被隱含地使用 . It is also used by syntactic constructs like X.class class literals.

    線(xiàn)程上下文類(lèi)型加載器是在Java 2平臺上被引入的. 每一個(gè)線(xiàn)程都有一個(gè)類(lèi)加載器與之對應(除非這個(gè)線(xiàn)程是被本地代碼創(chuàng )建的). 這個(gè)類(lèi)加載器是通過(guò)Thread.setContextClassLoaser()方法設置的. 如果你不在線(xiàn)程構造后調用這個(gè)方法, 這個(gè)線(xiàn)程將從它的父線(xiàn)程中繼承相應的上下文類(lèi)加載器. 如果在整個(gè)應用中你不做任何特殊設置, 所有的線(xiàn)程將都以系統類(lèi)加載器(system classloader)作為自己的線(xiàn)程上下文類(lèi)加載器. 自從WebJ2EE應用服務(wù)器使用成熟的類(lèi)加載器機制來(lái)實(shí)現諸如JNDI, 線(xiàn)程池, 組件熱部署等功能以來(lái), 這種在整個(gè)應用中不做任何線(xiàn)程類(lèi)加載器設置的情況就很少了.

    為什么線(xiàn)程上下文類(lèi)加載器存在于如此重要的位置呢? 這個(gè)概念在J2SE中的引入并不引人注目. 很多開(kāi)發(fā)人員對這一概念迷惑的原因是Sun公司在這方面缺乏適當的指引和文檔.

    事實(shí)上, 上下文類(lèi)加載器提供了類(lèi)加載機制的后門(mén), 這一點(diǎn)也在J2SE中被引入了. 通常, JVM中的所有類(lèi)加載器被組織成了有繼承層次的結構, 每一個(gè)類(lèi)加載器(除了引導JVM的原始類(lèi)加載器)都有一個(gè)父加載器. 每當被請示加載類(lèi)時(shí), 類(lèi)加載器都會(huì )首先請求其父類(lèi)加載器, 只有當父類(lèi)加載器不能加載時(shí), 才會(huì )自己進(jìn)行類(lèi)加載.

   有時(shí)候這種類(lèi)加載的順序安排不能正常工作, (此處的意思是:正常情況下都是從子類(lèi)加載器到根類(lèi)加載器請求,萬(wàn)一有根類(lèi)里需要加載子類(lèi)時(shí),這種順序就不能滿(mǎn)足要求,就要有一條反向的通道,即得到子類(lèi)加載器,這樣就用到了thread context classloader,因為通過(guò)thread.getcontextclassloader()可以得到子類(lèi)加載器).通常當必須動(dòng)態(tài)加載應用程序開(kāi)發(fā)人員提供的資源的時(shí)候. JNDI為例: 它的內容(J2SE1.3開(kāi)始)就在rt.jar中的引導類(lèi)中實(shí)現了, 但是這些JNDI核心類(lèi)需要動(dòng)態(tài)加載由獨立廠(chǎng)商實(shí)現并部署在應用程序的classpath下的JNDI提供者. 這種情況就要求一個(gè)父classloader(本例, 就是引導類(lèi)加載器)去加載對于它其中一個(gè)子classloader(本例, 系統類(lèi)加載器)可見(jiàn)的類(lèi). 這時(shí)通常的類(lèi)加載代理機制不能實(shí)現這個(gè)要求. 解決的辦法(workaround)就是, JNDI核心類(lèi)使用當前線(xiàn)程上下文的類(lèi)加載器, 這樣, 就基本的類(lèi)加載代理機制的相反方向建立了一條有效的途徑.

    另外, 上面一段可能讓你想起一些其它的事情: XML解析Java API(JAXP). 是的, JAXP只是J2SE的擴展進(jìn), 它很自然地用當前類(lèi)加載器來(lái)引導解析器的實(shí)現. 而當JAXP被加入到J2SE1.4的核心類(lèi)庫中時(shí), 它的類(lèi)加載也就改成了用當前線(xiàn)程類(lèi)加載器, JNDI的情況完全類(lèi)似(也使很多程序員很迷惑). 明白為什么我說(shuō)來(lái)自Sun的指導很缺乏了吧?

   在以上的介紹之后, 我們來(lái)看關(guān)鍵問(wèn)題: 這兩種選擇(當前類(lèi)加載器和當前線(xiàn)程類(lèi)加載器)都不是在所有環(huán)境下都適用. 有些人認為當前線(xiàn)程類(lèi)加載器應該成為新的標準策略. 但是, 如果這樣, 當多個(gè)線(xiàn)程通過(guò)共享數據進(jìn)行交互的時(shí), 將會(huì )呈現出一幅極其復雜的類(lèi)加載的畫(huà)面, 除非它們全部使用了同一個(gè)上下文的類(lèi)加載器. 進(jìn)一步說(shuō), 在某些遺留下來(lái)的解決方案中, 委派到當前類(lèi)加載器的方法已經(jīng)是標準. 比如對Class.forName(String)的直接調用(這也是我為什么推薦盡量避免對這個(gè)方法進(jìn)行調用的原因). 即使你努力去只調用上下文相關(guān)的類(lèi)加載器, 仍然會(huì )有一些代碼會(huì )不由你控制. 這種不受控制的類(lèi)加載委派機制是混入是很危險的.

    更嚴重的問(wèn)題, 某些應用服務(wù)器把環(huán)境上下文及當前類(lèi)加載器設置到不同的類(lèi)加載器實(shí)例上, 而這些類(lèi)加載器有相同的類(lèi)路徑但卻沒(méi)有委派機制中的父子關(guān)系. 想想這為什么十分可怕. 要知道類(lèi)加載器定義并加載的類(lèi)實(shí)例會(huì )帶有一個(gè)JVM內部的ID. 如果當前類(lèi)加載器加載一個(gè)類(lèi)X的實(shí)例, 這個(gè)實(shí)例調用JNDI查找類(lèi)Y的實(shí)例, 些時(shí)的上下文的類(lèi)加載器也可以定義了加載類(lèi)Y實(shí)例. 這個(gè)類(lèi)Y的定義就與當前類(lèi)加載器看到的類(lèi)Y的定義不同. 如果進(jìn)行強制類(lèi)型轉換, 則產(chǎn)生異常.

   這種混亂的情況還將在Java中存在一段時(shí)間. 對于那些需要動(dòng)態(tài)加載資源的J2SEAPI, 我們來(lái)猜想它們的類(lèi)加策略. 例如:

Ø         JNDI 使用線(xiàn)程上下文類(lèi)加載器

Ø         Class.getResource() Class.forName()使用當前類(lèi)加載器

Ø         JAXP(J2SE 1.4 及之后)使用線(xiàn)程上下文類(lèi)加載器

Ø         java.util.ResourceBundle 使用調用者的當前類(lèi)加載器

Ø         URL protocol handlers specified via java.protocol.handler.pkgs system property are looked up in the bootstrap and system classloaders only

Ø         Java 序列化API默認使用調用者當前的類(lèi)加載器

這些類(lèi)及資源的加載策略問(wèn)題, 肯定是J2SE領(lǐng)域中文檔最及說(shuō)明最缺乏的部分了.