Apusic中的類裝載(classloader)機制

本文轉自Apusic官方文檔,詳細內容請訪問http://infocenter.apusic.com 。

 

配置Classloader

JavaEE 規範定義了一個打包機制的框架,用來把JavaEE應用的各個部分組織在一起。不同的應用服務器廠商可以自由的設計自己的類裝載層次來裝載應用中的類和資 源。因此開發者必須非常清楚類和資源應該放置在什麼位置對於JavaEE應用纔是可用的。理解Apusic應用服務器的類裝載體系結構能夠幫助 JavaEE應用的開發者設計高效和可移植應用打包結構。本章先介紹類裝載的基本概念,然後討論了Apusic應用服務器的類裝載層次是如何設計的。

Classloader的基本概念

Classloader 在運行期會以父/子的層次結構存在,每個Classloader的實例都持有其父Classloader的引用,而父Classloader並不持有子 Classloader的引用,從而形成一條單向鏈,當一個類裝載請求被提交到某個Classloader時,其默認的類裝載過程如下:

  • 檢查這個類有沒有被裝載過,如果已經裝載過,則直接返回;

  • 調用父Classloader去裝載類,如果裝載成功直接返回;

  • 調用自身的裝載類的方法,如果裝載成功直接返回;

  • 上述所有步驟都沒有成功裝載到類,拋出ClassNotFoundException;

每一層次的Classloader都重複上述動作。

簡 單說,當Classloader鏈上的某一Classloader收到類裝載請求時,會按順序向上詢問其所有父節點,直至最頂端 (BootstrapClassLoader),任何一個節點成功受理了此請求,則返回,如果所有父節點都不能受理,這時候才由被請求的 Classloader自身來裝載這個類,如果仍然不能裝載,則拋出異常。

類裝載的方式

類裝載的方式主要有兩種:顯式的和隱式的。

  • 顯式類裝載

    發生在使用以下方法調用進行裝載類的時候:

    • ClassLoader.loadClass()(使用指定的Classloader進行裝載)

    • Class.forName()(使用當前類的Caller Classloader進行裝載)

      當調用上述方法的時候,指定的Class(以類名爲參數)由Classloader裝入。這兩個方法的行爲有輕微的區別,Class.forName()在類裝載完成後,會對類進行初始化,而ClassLoader.loadClass()只負責裝載類。

  • 隱式類裝載

    發生在由於引用、實例化或繼承導致需要裝載類的時候。隱式類裝載是在幕後啓動的,JVM會解析必要的引用並裝載類。

    類的裝載通常組合了顯式和隱式兩種方式。例如,Classloader可能先顯式地裝載一個類,然後再隱式地裝載它引用的其它類。

  • 類裝載發生的時間

    從 類裝載方式的描述中我們可以看到,只有在顯式的調用方法或者實例化、引用、繼承一個類時,類才真正被裝載。由此,我們可以知道,import並不會導致類 裝載,以及,在一個類實例化之前,調用它的靜態方法,會導致這個類和它的父類、實現的接口和相關的靜態成員的類會被裝載,而它的成員變量的類卻不會被裝載

一個基本的Classloader的層次結構

上 圖顯示了一個基本的Classloader的層次結構。在給定層次上的Classloader不能引用任何層次低於它的Classloader,另外,它 的子Classloader裝載的類對於其是不可見的。在上圖中,如果Foo.class是由ClassLoaderB裝載的,並且Foo.class依 賴於Bar.class,那麼Bar.class必須由ClassLoaderA或B裝載。如果Bar.class只是對ClassLoaderC和D可 見,那麼將會發生ClassNotFoundException或者NoClassDefFoundError異常。

如果 Bar.class分別對於兩個平級的Classloader可見(例如C和D),但對於它們的父Classloader不可見,那麼當類裝載請求發送到 這兩個Classloader時,每一個Classloader會裝載自己版本的類。ClassLoaderC裝載的Bar.class的實例將不兼容於 ClassLoaderD裝載的Bar.class的實例。如果對Classloader的層次結構不瞭解,試圖使用由ClassLoaderC裝載的類 去造型一個ClassLoaderD裝載的Bar.class的實例,則會發生造型失敗(ClassCastException)。

基本的Classloader

最基本的Classloader是Bootstrap Classloader和System Classloader(也有人稱之爲AppClassLoader),只要寫過java程序,都會用到這兩個Classloader。

  • Bootstrap Classloader

    這 個Classloader裝載Java虛擬機提供的基本運行時刻類($JAVA_HOME/jre/lib),還包括放置在系統擴展目錄($ JAVA_HOME/jre/lib/ext)內的JAR文件中的類。這個Classloader是java程序最頂層的Classloader,只有它 沒有父Classloader。如果你將一個自己寫的類或第三方jar包放進$JAVA_HOME/jre/lib/ext目錄中,那麼它將被 Bootstrap Classloader裝載。

  • System Classloader

    System Classloader通常負責裝載系統環境變量CLASSPATH中設置的類。由System Classloader裝載的類對於Apusic服務器內部的類和部署在Apusic服務器上的J2EE應用(通常打包成ear)都是可見的。% APUSIC_HOME%/lib目錄下的jar文件是Apusic應用服務器的核心類,一般把這些jar文件都加在系統CLASSPATH中。另外,一 些公用類也可以加在系統CLASSPATH中,如JDBC驅動程序等。

自定義Classloader

在 編寫應用代碼的時候,常常有需要動態加載類和資源,比如顯式的調用classLoader.loadClass(“ClassName”),雖然直接使用 ClassLoader.getSystemClassLoader(),可以得到SystemlassLoader來完成這項任務。但是,由於 System Classloader是JVM創建的Classloader,它的職責有限,只適合於普通的java應用程序,在很多複雜場景中不能滿足需求,比如在應 用服務器中。這時候就需要自行實現一個Classloader的子類,實現特定的行爲。Apusic應用服務器中就定義了若干個特有的 Classloader,負責裝載部署在Apusic中的JavaEE應用中的類,這裏並不試圖去描述如何實現一個自定義的Classloader,但本 章第二部分將詳細描述Apusic自定義的Classloader的行爲。

Caller Classloader和線程上下文Classloader

動態加載資源時,往往有三種Classloader可選擇:System Classloader、Caller Classloader、當前線程的上下文Classloader。System Classloader前面已經描述過了,下面我們看看什麼是Caller Classloader、當前線程的上下文Classloader。

  • Caller Classloader

    Caller Classloader指的是當前所在的類裝載時使用的Classloader,它可能是System Classloader,也可能是一個自定義的Classloader,這裏,我們都稱之爲Caller Classloader。我們可以通過getClass().getClassLoader()來得到Caller Classloader。例如,存在A類,是被AClassLoader所加載,A.class.getClassLoader()爲AClassLoader的實例,它就是A.class的Caller Classloader。

    如果在A類中使用new關鍵字,或者Class.forName(String className)和Class.getResource(String resourceName)方法,那麼這時也是使用Caller Classloader來裝載類和資源。比如在A類中初始化B類:

    /**
      * A.java
    */
    ...
    public void foo() {
        B b = new B();
        b.setName("b");
    }
    

    那麼,B類由當前Classloader,也就是AClassloader裝載。同樣的,修改上述的foo方法,其實現改爲:

    Class clazz = Class.forName("foo.B");

    最終獲取到的clazz,也是由AClassLoader所裝載。

    那麼,如何使用指定的Classloader去完成類和資源的裝載呢?或者說,當需要去實例化一個Caller Classloader和它的父Classloader都不能裝載的類時,怎麼辦呢?

    一 個很典型的例子是JAXP,當使用xerces的SAX實現時,我們首先需要通過rt.jar中的 javax.xml.parsers.SAXParserFactory.getInstance()得到xercesImpl.jar中的 org.apache.xerces.jaxp.SAXParserFactoryImpl的實例。由於JAXP的框架接口的class位於 JAVA_HOME/lib/rt.jar中,由Bootstrap Classloader裝載,處於Classloader層次結構中的最頂層,而xercesImpl.jar由低層的Classloader裝載,也就 是說SAXParserFactoryImpl是在SAXParserFactory中實例化的,如前所述,使用SAXParserFactory的 Caller Classloader(這裏是Bootstrap Classloader)是完成不了這個任務的。

    這時,我們就需要了解一下線程上下文Classloader了。

  • 線程上下文Classloader

    每個線程都有一個關聯的上下文Classloader。如果使用new Thread()方式生成新的線程,新線程將繼承其父線程的上下文Classloader。如果程序對線程上下文Classloader沒有任何改動的話,程序中所有的線程將都使用System Classloader作爲上下文Classloader。

    當 使用Thread.currentThread().setContextClassLoader(classloader)時,線程上下文 Classloader就變成了指定的Classloader了。此時,在本線程的任意一處地方,調用Thread.currentThread(). getContextClassLoader(),都可以得到前面設置的Classloader。

    回到JAXP的例子,假設 xercesImpl.jar只有AClassLoader能裝載,現在A.class內部要使用JAXP,但是A.class卻不是由 AClassLoader或者它的子Classloader裝載的,那麼在A.class中,應該這樣寫才能正確得到xercesImpl的實現:

    AClassLoader aClassLoader = new AClassLoader(parent);
    Thread.currentThread().setContextClassLoader(aClassLoader);
    SAXParserFactory factory = SAXParserFactory.getInstance();
    ...
    

    JAXP這時就可以通過線程上下文Classloader裝載xercesImpl的實現類了,當然,還有一個前提是在配製文件或啓動參數中指定了使用xerces作爲JAXP的實現。下面是JAXP中的代碼片斷:

    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    …
    Class providerClass = cl.loadClass(className);
    …
    

JVM中類的唯一性

JVM 爲每一個Classloader維護一個唯一標識。在一個JVM裏(對應一個Java進程),可以由不同的Classloader裝載多個同名的類(指包 名和類名都完全相同,下同),爲了唯一地標識被不同Classloader裝載的類,JVM會在被裝載的類名前加上裝載該類的Classloader的標 識。

Apusic的Classloader體系

在上一節,我們瞭解了基本的Classloader層次結構模型,知道了Bootstrap Classloader、System Classloader的職責,還知道可以通過自定義Classloader來完成特定的裝載任務,除此之外,我們還了解了什麼是Caller Classloader和線程上下文Classloader。下面,我們就可以根據這些基本的Classloader概念去看看Apusic Classloader體系是長什麼樣的了。

JavaEE應用對Classloader的要求

Apusic 應用服務器本身運行需要的類都在CLASSPATH中,由System Classloader加載。在上一節中,我們提到Apusic應用服務器中定義了若干個專有的Classloader,負責裝載部署在Apusic中的 JavaEE應用中的類和資源。Apusic爲何要額外的去自定義Classloader呢?把應用需要的類和資源都放在CLASSPATH中, System Classloader不也可以加載這些類嗎?要回答這些問題,我們先考慮一下下面兩個簡單的需求:

  • 不同的應用中,可能有同名的資源文件或類,它們在各自應用中有不同的行爲或語義。

  • 應用發生變化的時候,例如改了Jsp或者JavaBean,在不重啓服務器甚至不重啓應用的情況下,需要立即看到修改的效果。

我們前面提到過在一個JVM中一個類的唯一標識,當不能改變類的包名和類名的情況下,除非 Classloader的實例發生變化,纔有可能實現對一個類的再次加載。顯然,在只有System Classloader的情況下,無法滿足上面兩個簡單的需求。這是因爲在運行期,我們無法重新創建System Classloader的實例,也沒辦法讓它裝載一個已經裝載過的類

對 於第一個需求,我們可以對不同的應用中的類和資源進行隔離加載,這就需要爲每個應用使用不同的Classloader實例;對於第二個需求,當Jsp或 JavaBean發生變化時,我們需要把原來裝載Jsp的Classloader銷燬掉,創建一個新的Classloader實例,並讓它去裝載修改後的 類,因此,要專門定義一個Classloader去負責裝載Jsp、JavaBean,使得在重新創建Classloader時,受影響的範圍儘可能的 小。

Apusic的Classloader和它們的層次結構

Apusic爲裝載JavaEE應用中的類定義了EJBClassLoader和ServletClassLoader這兩個主要的Classloader。假設一個JavaEE應用的結構如下:

  • EJBClassLoader

    每 個JavaEE應用都有一個EJBClassLoader,用於裝載EJB module和公共類。上圖中的ejbjarA.jar、ejbjarB.jar、util.jar以及app.ear我們可以看成是一個jar文件,也 可以看成是一個目錄,它們裏邊的類和文件都由同一個EJBClassLoader實例裝載,因此,同一個JavaEE應用中的EJB module和公共類是相互可見的。

    不同的應用,其EJBClassLoader實例也不同,且每個EJBClassLoader實例間是平級關係,所以不同應用中的類是相互不可見的。

  • ServletClassLoader

    在Apusic應用服務器中,每個Web module都有一個ServletClassLoader,用於裝載Web module中的類和資源文件。所以,每個JavaEE應用中都可能有一個或多個ServletClassLoader,例如上圖表示的JavaEE應用就有兩個ServletClassLoader,它們是平級關係,所以Web module中的類相互不可見。對於ServletClassLoader,還有一些特殊的行爲,將在下一節介紹。

  • 層次結構

    通過以上的介紹,我們可以知道,Apusic應用服務器啓動後,假設其中部署了兩個應用,分別是appA.ear和appB.ear,那麼其Classloader層次結構可表現爲:

    其 中,我們可以看到,EJBClassLoader是ServletClassLoader的父,由ejbClassLoaderA裝載的類和文件,對於 servletClassLoaderA和servletClassLoaderB裝載的類都是可見的。也就是說,同一個應用中的任意Web Module的類(即位於WEB-INF/classes、WEB-INF/lib中的類),都可以使用ejb jar或util jar中的類。

    但對於上圖中ejbClassLoaderA裝載的類,servletClassLoaderC是看不見的,它們屬於不同的應用。

ServletClassLoader的特性

在 前面幾節,我們提到過Apusic對於Web module中的類,包括jsp(最終被應用服務器解析成servlet並編譯成Java類)、WEB-INF/classes和WEB-INF/lib 裏邊的class和資源文件,專門定義一個ServletClassLoader進行加載是了滿足類似開發期中類的動態加載、不同Module間類的隔離 等的需要。Apusic應用服務器在Classloader體系中做了充分的考慮以降低Web應用開發的複雜性及提升應用服務器的易用性。下面將介紹 Apusic的ServletClassLoader的行爲特性:

類的動態加載

在Apusic檢測到jsp或WEB-INF/classes目錄下的類的更新後,會重新加載修改過的類。對於用戶來說,不需要做任何事情,在修改完後馬上調用該類就可以看到剛剛做的更新。

考 慮到運行期和開發期的要求不同,運行期類和資源文件不會頻繁更新,因此,在運行期,不需要頻繁檢測類文件是否已經更新,可通過配置apusic.conf 中的ServletReloadCheckInterval屬性值來修改檢測時間。當值小於”0”時,不檢測。此值默認是3,即每3秒中檢測一次。

ServletClassLoader的多層結構

ServletClassLoader是一層殼,根據配置的不同策略,委託給不同的Classloader執行裝載任務。Servlet Classloader的裝載行爲有兩種策略,可通過配置進行指定,配置有兩種方式:

  • 在web.xml中增加Context Parameter

    <context-param>
        <param-name>com.apusic.web.ServletClassLoaderDelegate</param-name>
        <param-value>Separated</param-value>
    </context-param>

    這樣的配置有效範圍只有當前應用。如果修改的是$DOMAIN_HOME/config/web.xml下的配置,則適用所有應用。

  • 通過VM參數指定

    -Dcom.apusic.web.ServletClassLoaderDelegate=Separated

    這種系統屬性配置,所有的應用都生效。

ServletClassLoader的兩種裝載策略分別通過com.apusic.web.ServletClassLoaderDelegate的兩個值來指定:

  • Composite

    默 認值,表示ServletClassLoader的行爲委託給了兩層Classloader,一層叫CompositeClassLoader,它的父 Classloader是EJBClassLoader,它負責WEB-INF/lib和WEB-INF/classes目錄下的類和資源的裝載,其中, 如果在WEB-INF/lib和WEB-INF/classes下有同名的類或資源,WEB-INF/classes下的類將被優先裝載;另一層是 JSPClassLoader,它的父Classloader是CompositeClassLoader,它負責裝載解析編譯後的JSP。

  • Separated

    表 示ServletClassLoader的行爲委託給了三層Classloader,跟上一種策略不同的是WEB-INF/lib下類和WEB- INF/classes目錄下的類和資源由不同的Classloader裝載,前者叫StaticClassLoader,它的父是 EJBClassLoader;後者我們稱爲ReloadableClassLoader,它的父是StaticClassLoader,子是 JSPClassLoader。根據前面對Classloader父子關係的描述,我們可以知道,WEB-INF/lib下的類看不見WEB- INF/classes下的類,而WEB-INF/classes下的類可以看見WEB-INF/lib下的類。考慮到客戶應用中,資源文件一般放在 WEB-INF/classes目錄中,因此,如果在WEB-INF/lib和WEB-INF/classes下有同名的資源文件,仍然是WEB- INF/classes下的資源優先裝載。

如果客戶應用系統中,WEB-INF/lib下的類會引用WEB-INF/classes下的類或資源,或者認爲WEB-INF/classes下的類應該優先於WEB-INF/lib下的類裝載,我們建議使用Composite,即默認的策略。

如 果考慮到在開發期WEB-INF/lib下的類或文件不會頻繁更新,爲了避免檢測範圍太大而導致的檢測時間過長,不掃描WEB-INF/lib下的更新 (即此目錄下的類只被裝載一次,如果有更新,則需要重啓應用才能生效),或者認爲WEB-INF/lib下的類應該優先於WEB-INF/classes 下的類裝載時,可採用Separated策略。

Session中對象的類動態裝載

如果session中保存的對象實例的類發生了更改,且類的簽名未發生變化,那麼對象實例的類型信息將被標識爲新裝載的類,從Session中取出對象後,它的行爲按更新後的類執行。但如果類的簽名發生了變化,那麼此session中的對象實例將被丟棄。

類裝載的Web優先策略

在默認情況下,ServletClassLoader遵循大多數Classloader的裝載行爲,如“Classloader的基本概念 ” 一節描述的那樣,會按順序向上詢問其所有父節點裝載,如果父沒裝載到,纔會由自身進行加載。這種默認的Java類裝載機制有時也會碰到麻煩,比如WEB- INF/classes中有某個類,在系統Classpath中有這個類的另一個版本,Classloader默認的裝載行爲決定了系統 Classpath中的類會被優先加載。如果我們期望WEB-INF/classes中的類要優先加載,Apusic的Servlet Classloader提供了機會,可以通過配置系統屬性或者在web.xml中增加Context Parameter來達到此目的:

  • 在web.xml中增加Context Parameter

    <context-param>
        <param-name>apusic.prefer.war.classes</param-name>
        <param-value>true</param-value>
    </context-param>

    這樣的配置有效範圍只有當前應用。如果修改的是$DOMAIN_HOME/config/web.xml下的配置,則適用所有應用。

  • 通過VM參數指定

    -Dapusic.prefer.war.classes=true

    這種系統屬性配置,所有的應用都生效。

類裝載查看服務

Apusic應用服務器提供了類裝載查看服務,通過此服務,可以查找指定的類是由哪一層的Classloader裝載的,類文件路徑等信息,從而可以協助排查一些跟類裝載相關的問題。類裝載查看服務的相關配置段如下:

...
<SERVICE
    CLASS="com.apusic.util.ClassLoaderViewer"
    >
</SERVICE>
...

可以通過Admin Console上提供的類加載器來訪問類裝載查看服務,如何使用類加載器請參考Admin Console文檔。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章