最近遇到一個問題: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 文檔和源碼中可以看出:
Class.getResourceAsStream
代理給了加載該 class 的 ClassLoader 去實現,調用ClassLoader.getResourceAsStream
;- 如果該類的 ClassLoader 爲 null,說明該 class 一個系統 class,所以委託給
ClassLoader.getSystemResourceAsStream
。
通過源碼的分析,可以看出來加載資源的動作和該類的類加載器有關,所以下面我們需要介紹什麼是類加載器。
二、類加載器(ClassLoader)
我們都知道 Java 文件被運行,第一步,需要通過 javac
編譯器編譯爲 class 文件;第二步,JVM 運行 class 文件,實現跨平臺。而 JVM 虛擬機第一步肯定是 加載 class 文件,所以,類加載器實現的就是(來自《深入理解Java虛擬機》):
通過一個類的全限定名來獲取描述此類的二進制字節流
類加載器有幾個重要的特性:
- 每個類加載器都有自己的預定義的搜索範圍,用來加載 class 文件;
- 每個類和加載它的類加載器共同確定了這個類的唯一性,也就是說如果一個 class 文件被不同的類加載器加載到了 JVM 中,那麼這兩個類就是不同的類,雖然他們都來自同一份 class 文件;
- 雙親委派模型。
2.1 雙親委派模型
- 所有的類加載器都是有層級結構的,每個類加載器都有一個父類類加載器(通過組合實現,而不是繼承),除了啓動類加載器(Bootstrap ClassLoader);
- 當一個類加載器接收到一個類加載請求時,首先將這個請求委派給它的父加載器去加載,所以每個類加載請求最終都會傳遞到頂層的啓動類加載器,如果父加載器無法加載時,子類加載器纔會去嘗試自己去加載;
通過雙親委派模型就實現了類加載器的三個特性:
- 委派(delegation):子類加載器委派給父類加載器加載;
- 可見性(visibility):子類加載器可訪問父類加載器加載的類,父類不能訪問子類加載器加載的類;
- 唯一性(uniqueness):可保證每個類只被加載一次,比如
Object
類是被 Bootstrap ClassLoader 加載的,因爲有了雙親委派模型,所有的 Object 類加載請求都委派到了 Bootstrap ClassLoader,所以保證了只被加載一次。
以上就是類加載器的一些特性,那麼在 Java 中類加載器是如何實現的呢?
2.2 Java 中的類加載器
從 JVM 虛擬機的角度來看,只存在兩種不同的類加載器:
- 啓動類加載器(Bootstrap ClassLoader),是虛擬機自身的一部分;
- 所有其他的類加載器,獨立於虛擬機外部,都繼承自抽象類
java.lang.ClassLoader
。
而絕大多數 Java 應用都會用到如下 3 中系統提供的類加載器:
- 啓動類加載器(Bootstrap/Primordial/NULL ClassLoader):頂層的類加載器,沒有父類加載器。負責加載 /lib 目錄下的,或則被 -Xbootclasspath 參數所指定路徑中的,並被 JVM 識別的(僅按文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄也不會被加載)類庫加載到虛擬機內存中。所有被 Bootstrap classloader 加載的類,它的
Class.getClassLoader
方法返回的都是null
,所以也稱作 NULL ClassLoader。 - 擴展類加載器(Extension CLassLoader):由
sun.misc.Launcher$ExtClassLoader
實現,負責加載<JAVA_HOME>/lib/ext
目錄下,或被java.ext.dirs
系統變量所指定的目錄下的所有類庫; - 應用程序類加載器(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等)。
- Bootstrap 和 Extension:和前面介紹 Java 系統類加載器一樣,這裏不再贅述;
- 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
- Common:通過該類加載器加載的類庫可被 Tomcat 和所有的 Web 應用共同使用。該類加載器的搜索位置是通過 $CATALINA_BASE/conf/catalina.properties 文件中的
common.loader
屬性指定的,默認包括如下位置:
$CATALINA_BASE/lib
下未打包的類和資源;$CATALINA_BASE/lib
下的 jar 包;$CATALINA_HOME/lib
下未打包的類和資源;$CATALINA_HOME/lib
下的 jar 包。
- 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);
}
現在,我們來分析整個加載過程:
- 獲取該類的類加載器,判斷是否爲 null,爲 null 說明是 JRE 相關的類(此處是
Object
,所以類加載器是 null),那麼委派給ClassLoader.getSystemResourceAsStream(String)
; - 獲取 System 類加載器,通過 System 類加載器去加載文件;
現在,我們知道問題就出在這個 System 類加載器上:
- 服務器:通過設置 CLASSPATH 變量,所以 System 類加載器能夠找到
ss.properties
; - 本地:本地環境下,
ss.properties
最終是放在 Web 應用下的/WEB-INF/classes
文件夾下,不能被 System 類加載器獲取到,所以加載失敗。
解決方案:通過 getClass().getResouceAsStream(String)
去加載資源,這樣首先就在 WebappX 類加載器中去尋找資源,所以無論如何都能找到。
六、總結
通過解決一個文件加載的問題,學習了 Java 應用的類加載器和 Tomcat 的類加載器架構,瞭解了加載的底層原理,很有成就感。
在解決該問題的過程中,有幾點小心得:
- 出現問題時,從源頭找問題,比如從源碼去看加載的邏輯;
- 學會看源碼,可能很多問題,一看源碼就解決了;
- 多 Google,看資料時融匯貫通,比如解決這個問題時,最主要的幫助來自於:1、源碼;2、《深入理解 Java 虛擬機》;3、Tomcat ClassLoader 的文檔。
七、參考
- 《深入理解 Java 虛擬機》
- Class Loader HOW-TO