類加載器與Web容器


關於類加載器中已經介紹了Jvm的類加載機制,然而對於運行在Java EE容器中的Web應用來說,類加載器的實現方式與一般的Java應用有所不同。不同的Web容器的實現方式也會有所不同。

Tomcat中的類加載機制

在Apache Tomcat 中,為了提高系統的靈活性,引入了commonLoader、sharedLoader、catalinaLoader;為了支持和分隔多個web應用,使用了WebappClassLoader。下面以Tomcat7.0的類加載器結構圖總體來看大致如下:

   Bootstrap
      |
   System
      |
   Common
   /     \
Webapp1   Webapp2 ...
  • Tomcat中的系統類加載器。Tomcat也是一個Java應用,他也是在最初系統提供的幾層類加載器環境下運行起來的。那么Tomcat的一些最基本的類,也和其它簡單Java應用一樣,是通過系統的類加載器來加載的,比如默認配置下的tomcat/bin目錄下的bootstrap.jar、tomcat-juli.jar、commons-daemon.jar這幾個jar包中的類。
  • Tomcat的Common Loader。Common Loader是Tomcat在系統類加載器之上建立起來的,其父loader是系統類加載器。Common Loader負責上面幾個jar包外的Tomcat的大部分java類,通常情況下是tomcat/lib下的所有jar包。
  • Webapp Class Loader。這個類加載器可以說是Tomcat種最重要的Class Loader,它創造了各個Web app空間。在實現上,它打破了系統默認規則,或者說是打破了java.lang.ClassLoader邏輯中的“雙親委派”模式,提供了一套自定義的類加載流程。默認情況下,對於一個未加載過的類,WebappClassLoader會先讓系統加載java.lang.Object等Java本身的基礎類,如果不是基礎類則優先在當前Web app范圍內查找並加載,如果沒加載到,再交給common loader走標准的雙親委派模式加載。

那么commonLoader、sharedLoader、catalinaLoader這三個loader各有什么作用呢?在tomcat/conf目錄的catalina.properties中有common.loader、server.loader、shared.loader的配置,這分別對應着commonLoader、catalinaLoader和sharedLoader。我們可以看到,默認情況下,serverl.loader和shared.loader的配置是空的,這意味着此兩者在運行時和commonLoader相同。實際上,在Tomcat5.5之后的版本中,做了簡化,只有commonLoader具備實際意義。而在5.5及之前的版本中,三者各不相同,各有分工。這里我們就不做考慮了。

絕大多數情況下,Web 應用的開發人員不需要考慮與類加載器相關的細節。下面給出幾條簡單的原則:

  • 每個 Web 應用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
  • 多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面。
  • 當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。

總結

每個 Web 應用都有一個對應的類加載器實例。其加載過程如下圖:

   Bootstrap
      |
   System
      |
   Common
   /     \
Webapp1   Webapp2 ...
    |
java.lang.Object等Java基礎類
    |
待加載類

在一個web應用中,當需要獲取class實例的時候,可以直接獲取當前類的類加載器或者獲取當前線程中的類加載器。
使用當前類的類加載器是缺省規則:

Class.forName(String)

通過獲取當前線程中的類加載器,代碼如下:

Class.forName(className, isInitialized, Thread.currentThread().getContextClassLoader());

附錄:

關於何時使用線程的上下文加載器?

這是一個很常見的問題,但答案卻很難回答。這個問題通常在需要動態加載類和資源的系統編程時會遇到。總的說來動態加載資源時,往往需要從三種類加載器里選擇:系統或程序的類加載器、當前類加載器、以及當前線程的上下文類加載器。在程序中應該使用何種類加載器呢?
  系統類加載器通常不會使用。此類加載器處理啟動應用程序時classpath指定的類,可以通過ClassLoader.getSystemClassLoader()來獲得。所有的ClassLoader.getSystemXXX()接口也是通過這個類加載器加載的。一般不要顯式調用這些方法,應該讓其他類加載器代理到系統類加載器上。由於系統類加載器是JVM最后創建的類加載器,這樣代碼只會適應於簡單命令行啟動的程序。一旦代碼移植到EJB、Web應用或者Java Web Start應用程序中,程序肯定不能正確執行。
  因此一般只有兩種選擇,當前類加載器和線程上下文類加載器。當前類加載器是指當前方法所在類的加載器。這個類加載器是運行時類解析使用的加載器,Class.forName(String)和Class.getResource(String)也使用該類加載器。代碼中X.class的寫法使用的類加載器也是這個類加載器。
  線程上下文類加載器在Java 2(J2SE)時引入。每個線程都有一個關聯的上下文類加載器。如果你使用new Thread()方式生成新的線程,新線程將繼承其父線程的上下文類加載器。如果程序對線程上下文類加載器沒有任何改動的話,程序中所有的線程將都使用系統類加載器作為上下文類加載器。Web應用和Java企業級應用中,應用服務器經常要使用復雜的類加載器結構來實現JNDI(Java命名和目錄接口)、線程池、組件熱部署等功能,因此理解這一點尤其重要。
  為什么要引入線程的上下文類加載器?將它引入J2SE並不是純粹的噱頭,由於Sun沒有提供充分的文檔解釋說明這一點,這使許多開發者很糊塗。實際上,上下文類加載器為同樣在J2SE中引入的類加載代理機制提供了后門。通常JVM中的類加載器是按照層次結構組織的,目的是每個類加載器(除了啟動整個JVM的原初類加載器)都有一個父類加載器。當類加載請求到來時,類加載器通常首先將請求代理給父類加載器。只有當父類加載器失敗后,它才試圖按照自己的算法查找並定義當前類。
  有時這種模式並不能總是奏效。這通常發生在JVM核心代碼必須動態加載由應用程序動態提供的資源時。拿JNDI為例,它的核心是由JRE核心類(rt.jar)實現的。但這些核心JNDI類必須能加載由第三方廠商提供的JNDI實現。這種情況下調用父類加載器(原初類加載器)來加載只有其子類加載器可見的類,這種代理機制就會失效。解決辦法就是讓核心JNDI類使用線程上下文類加載器,從而有效的打通類加載器層次結構,逆着代理機制的方向使用類加載器。
  順便提一下,XML解析API(JAXP)也是使用此種機制。當JAXP還是J2SE擴展時,XML解析器使用當前類加載器方法來加載解析器實現。但當JAXP成為J2SE核心代碼后,類加載機制就換成了使用線程上下文加載器,這和JNDI的原因相似。
  好了,現在我們明白了問題的關鍵:這兩種選擇不可能適應所有情況。一些人認為線程上下文類加載器應成為新的標准。但這在不同JVM線程共享數據來溝通時,就會使類加載器的結構亂七八糟。除非所有線程都使用同一個上下文類加載器。而且,使用當前類加載器已成為缺省規則,它們廣泛應用在類聲明、Class.forName等情景中。即使你想盡可能只使用上下文類加載器,總是有這樣那樣的代碼不是你所能控制的。這些代碼都使用代理到當前類加載器的模式。混雜使用代理模式是很危險的。
  更為糟糕的是,某些應用服務器將當前類加載器和上下文類加器分別設置成不同的ClassLoader實例。雖然它們擁有相同的類路徑,但是它們之間並不存在父子代理關系。想想這為什么可怕:記住加載並定義某個類的類加載器是虛擬機內部標識該類的組成部分,如果當前類加載器加載類X並接着執行它,如JNDI查找類型為Y的數據,上下文類加載器能夠加載並定義Y,這個Y的定義和當前類加載器加載的相同名稱的類就不是同一個,使用隱式類型轉換就會造成異常。
  這種混亂的狀況還將在Java中存在很長時間。在J2SE中還包括以下的功能使用不同的類加載器:
  (1)JNDI使用線程上下文類加載器。
  (2)Class.getResource()和Class.forName()使用當前類加載器。
  (3)JAXP使用上下文類加載器。
  (4)java.util.ResourceBundle使用調用者的當前類加載器。
  (5)URL協議處理器使用java.protocol.handler.pkgs系統屬性並只使用系統類加載器。
  (6)Java序列化API缺省使用調用者當前的類加載器。
  這些類加載器非常混亂,沒有在J2SE文檔中給以清晰明確的說明。
  該如何選擇類加載器?
  如若代碼是限於某些特定框架,這些框架有着特定加載規則,則不要做任何改動,讓框架開發者來保證其工作(比如應用服務器提供商,盡管他們並不能總是做對)。如在Web應用和EJB中,要使用Class.gerResource來加載資源。
  在其他情況下,我們可以自己來選擇最合適的類加載器。可以使用策略模式來設計選擇機制。其思想是將“總是使用上下文類加載器”或者“總是使用當前類加載器”的決策同具體實現邏輯分離開。往往設計之初是很難預測何種類加載策略是合適的,該設計能夠讓你可以后來修改類加載策略。

以上回答來自深入理解Java類加載器,感興趣的可以去看看,里面還給了一個類加載策略的實現。

參考
深入探討 Java 類加載器
Tomcat7源碼分析(上)——啟動過程和類加載器
深入理解Java類加載器


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM