類加載器與 Class.getResourceAsStream 問題解決

最近遇到一個問題:tomcat 服務器中通過 Object.class.getResourceAsStream("ss.properties") 加載 webapps 下某 webapp 中某個文件,服務器上可以正常加載,本地運行卻不能正確加載,提示找不到文件

在接下來排查問題的過程中,發現服務器和本地的配置不同:

  • 服務器:通過 tomcat 的 bin/setclasspath.sh 腳本將 ss.properties 所在的目錄設置爲了 classpath;
  • 本地:將 ss.properties 放置在了 webapps/webapp1/WEB-INF/classes 文件夾下,也就是 WAR 文件格式規定的 classpath 目錄之一。

爲什麼這樣配置的不同,就導致了文件不能被正確加載。

一、源碼分析

爲了找出原因,我們第一步看 Object.class.getResourceAsStream("ss.properties") 的源碼:

// Class.java
public InputStream getResourceAsStream(String name) {
    name = resolveName(name);
    ClassLoader cl = getClassLoader0();// 獲取加載該Class的ClassLoader
    if (cl==null) {
        // A system class.
        return ClassLoader.getSystemResourceAsStream(name);
    }
    return cl.getResourceAsStream(name);
}

從 javadoc 文檔和源碼中可以看出:

  1. Class.getResourceAsStream 代理給了加載該 class 的 ClassLoader 去實現,調用 ClassLoader.getResourceAsStream
  2. 如果該類的 ClassLoader 爲 null,說明該 class 一個系統 class,所以委託給 ClassLoader.getSystemResourceAsStream

通過源碼的分析,可以看出來加載資源的動作和該類的類加載器有關,所以下面我們需要介紹什麼是類加載器。

二、類加載器(ClassLoader)

我們都知道 Java 文件被運行,第一步,需要通過 javac 編譯器編譯爲 class 文件;第二步,JVM 運行 class 文件,實現跨平臺。而 JVM 虛擬機第一步肯定是 加載 class 文件,所以,類加載器實現的就是(來自《深入理解Java虛擬機》):

通過一個類的全限定名來獲取描述此類的二進制字節流

類加載器有幾個重要的特性:

  1. 每個類加載器都有自己的預定義的搜索範圍,用來加載 class 文件;
  2. 每個類和加載它的類加載器共同確定了這個類的唯一性,也就是說如果一個 class 文件被不同的類加載器加載到了 JVM 中,那麼這兩個類就是不同的類,雖然他們都來自同一份 class 文件;
  3. 雙親委派模型。

2.1 雙親委派模型

  1. 所有的類加載器都是有層級結構的,每個類加載器都有一個父類類加載器(通過組合實現,而不是繼承),除了啓動類加載器(Bootstrap ClassLoader)
  2. 當一個類加載器接收到一個類加載請求時,首先將這個請求委派給它的父加載器去加載,所以每個類加載請求最終都會傳遞到頂層的啓動類加載器,如果父加載器無法加載時,子類加載器纔會去嘗試自己去加載;

通過雙親委派模型就實現了類加載器的三個特性:

  1. 委派(delegation):子類加載器委派給父類加載器加載;
  2. 可見性(visibility):子類加載器可訪問父類加載器加載的類,父類不能訪問子類加載器加載的類;
  3. 唯一性(uniqueness):可保證每個類只被加載一次,比如 Object 類是被 Bootstrap ClassLoader 加載的,因爲有了雙親委派模型,所有的 Object 類加載請求都委派到了 Bootstrap ClassLoader,所以保證了只被加載一次。

以上就是類加載器的一些特性,那麼在 Java 中類加載器是如何實現的呢?

2.2 Java 中的類加載器

從 JVM 虛擬機的角度來看,只存在兩種不同的類加載器:

  1. 啓動類加載器(Bootstrap ClassLoader),是虛擬機自身的一部分;
  2. 所有其他的類加載器,獨立於虛擬機外部,都繼承自抽象類 java.lang.ClassLoader

而絕大多數 Java 應用都會用到如下 3 中系統提供的類加載器:

  1. 啓動類加載器(Bootstrap/Primordial/NULL ClassLoader):頂層的類加載器,沒有父類加載器。負責加載 /lib 目錄下的,或則被 -Xbootclasspath 參數所指定路徑中的,並被 JVM 識別的(僅按文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄也不會被加載)類庫加載到虛擬機內存中。所有被 Bootstrap classloader 加載的類,它的 Class.getClassLoader 方法返回的都是 null,所以也稱作 NULL ClassLoader。
  2. 擴展類加載器(Extension CLassLoader):由 sun.misc.Launcher$ExtClassLoader 實現,負責加載 <JAVA_HOME>/lib/ext 目錄下,或被 java.ext.dirs 系統變量所指定的目錄下的所有類庫;
  3. 應用程序類加載器(Application/System ClassLoader):由 sun.misc.Launcher$AppClassLoader 實現。它是 ClassLoader.getSystemClassLoader() 方法的默認返回值,所以也稱爲系統類加載器(System ClassLoader)。它負責加載 classpath 下所指定的類庫,如果應用程序沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

如下,就是 Java 程序中的類加載器層級結構圖:

類加載器

以上,我們介紹了 Java 系統的類加載器,但是我們的應用是運行在 tomcat 中的,那麼我們當然也應該探究 tomcat 是如何加載類的。

三、Tomcat 類加載器架構

根據 Class Loader HOW-TO 中的描述,tomcat7 主要有如下的類加載器層級:

      Bootstrap
          |
      Extension
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ...

從圖中可以看出,除了系統類加載器(Bootstrap、Extension、System),tomcat 還自定義了自己的類加載器(Common、Webapp等)。

  1. BootstrapExtension:和前面介紹 Java 系統類加載器一樣,這裏不再贅述;
  2. System:從 CLASSPATH 系統變量指定的目錄中加載類庫。該加載器加載的類對 tomcat 本身和 web 應用都可見。但是,標準的 tomcat 啓動腳本($CATALINA_HOME/bin/catalina.sh or %CATALINA_HOME%\bin\catalina.bat)都會忽略系統變量 CLASSPATH 的值,而會使用如下的類庫來創建 System 類加載器(setclasspath 腳本設置的 CLASSPATH 變量對 tomcat 有用):
    • $CATALINA_HOME/bin/bootstrap.jar
    • $CATALINA_BASE/bin/tomcat-juli.jar 或 $CATALINA_HOME/bin/tomcat-juli.jar
    • $CATALINA_HOME/bin/commons-daemon.jar
  3. Common:通過該類加載器加載的類庫可被 Tomcat 和所有的 Web 應用共同使用。該類加載器的搜索位置是通過 $CATALINA_BASE/conf/catalina.properties 文件中的 common.loader 屬性指定的,默認包括如下位置:
    • $CATALINA_BASE/lib 下未打包的類和資源;
    • $CATALINA_BASE/lib 下的 jar 包;
    • $CATALINA_HOME/lib 下未打包的類和資源;
    • $CATALINA_HOME/lib 下的 jar 包。
  4. WebappX:每個 Web 應用自己的類加載器,能夠加載 /WEB-INF/classes/WEB-INF/lib 下的類和資源。能夠被此 Web 應用使用,但對其他 Web 應用不可見。

對於 WebappX 類加載器,它並不是雙親委派模型的。當 WebappX 類接收到一個類加載請求時,它會先嚐試自己去加載,自己不能加載時,再委派給父類加載器。但是,例外就是JRE 相關的類不能被覆蓋。除了,WebappX 類加載器,其他的類加載器都符合通常的雙親委派模型

四、解決 Object.class.getResourceAsStream 問題

有了以上有關類加載器的知識,現在應該能夠解決爲什麼配置不同,導致文件不能被正確加載的問題了。

第一步,看源碼:

// Class.java
public InputStream getResourceAsStream(String name) {
    name = resolveName(name);
    ClassLoader cl = getClassLoader0();// 獲取加載該Class的ClassLoader
    if (cl==null) {
        // A system class.
        return ClassLoader.getSystemResourceAsStream(name);
    }
    return cl.getResourceAsStream(name);
}

// ClassLoader.java
public static InputStream getSystemResourceAsStream(String name) {
    URL url = getSystemResource(name);
    try {
        return url != null ? url.openStream() : null;
    } catch (IOException e) {
        return null;
    }
}

public static URL getSystemResource(String name) {
    ClassLoader system = getSystemClassLoader();// 獲取 System ClassLoader
    if (system == null) {
        return getBootstrapResource(name);
    }
    return system.getResource(name);
}

現在,我們來分析整個加載過程:

  1. 獲取該類的類加載器,判斷是否爲 null,爲 null 說明是 JRE 相關的類(此處是 Object,所以類加載器是 null),那麼委派給 ClassLoader.getSystemResourceAsStream(String)
  2. 獲取 System 類加載器,通過 System 類加載器去加載文件;

現在,我們知道問題就出在這個 System 類加載器上:

  • 服務器:通過設置 CLASSPATH 變量,所以 System 類加載器能夠找到 ss.properties
  • 本地:本地環境下,ss.properties 最終是放在 Web 應用下的 /WEB-INF/classes 文件夾下,不能被 System 類加載器獲取到,所以加載失敗。

解決方案:通過 getClass().getResouceAsStream(String) 去加載資源,這樣首先就在 WebappX 類加載器中去尋找資源,所以無論如何都能找到。

六、總結

通過解決一個文件加載的問題,學習了 Java 應用的類加載器和 Tomcat 的類加載器架構,瞭解了加載的底層原理,很有成就感。

在解決該問題的過程中,有幾點小心得:

  1. 出現問題時,從源頭找問題,比如從源碼去看加載的邏輯;
  2. 學會看源碼,可能很多問題,一看源碼就解決了;
  3. 多 Google,看資料時融匯貫通,比如解決這個問題時,最主要的幫助來自於:1、源碼;2、《深入理解 Java 虛擬機》;3、Tomcat ClassLoader 的文檔。

七、參考

  1. 《深入理解 Java 虛擬機》
  2. Class Loader HOW-TO
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章