1.前言
為什麼要寫這篇文章?身為Java程式設計師你有沒有過每次需要讀取 ClassPath 下的資原始檔的時候,都要去百度一下,然後看到下面的這種答案:
Thread.currentThread().getContextClassLoader().getResource("ss.properties").getPath();
亦或是:
Object.class.getResourceAsStream("ss.properties");
你複製貼上一下然後放到自己的專案裡執行,還真跑起來了。但是當打成 jar 包作為其它專案的依賴時,或者打成 war 包被 Tomcat 載入時,你還能保證你的resources 資原始檔被讀取到嗎?答案是不能的。
其中的原因如何而又如何解決,究竟怎樣才能寫出萬無一失根本不用擔心任何環境的程式碼?箇中原委,請聽我一一道來。
2.再看類載入機制
看到這個標題你也許會有些意外,不是說的讀取ClassPath下的檔案嗎?為什麼要講類載入機制。
其實你有沒有想過,ClassPath下的資原始檔標準存放的是什麼?顧名思義,是 .class 類檔案。為什麼我們的類可以被正確載入到Java虛擬機器(JVM),而自己新增的資原始檔卻載入失敗呢?歸根結底是你沒有理解類載入機制,也就無法做到舉一反三。
類載入機制與類載入器
程式設計師將原始碼寫入.Java檔案中,經過(javac)編譯,生成.class二進位制檔案。虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。
從巨集觀上理解了類載入機制後,接下來就要從細節上說一說類載入器,以及類載入器的工作原理。
類載入器,顧名思義,是載入類的器件。JVM只存在兩種不同的類載入器:啟動類載入器(Bootstrap ClassLoader),使用C++實現,是虛擬機器自身的一部分。另一種是所有其他的類載入器,使用JAVA實現,獨立於JVM,並且全部繼承自抽象類java.lang.ClassLoader。包括擴充套件類載入器、應用程式類載入器。
它我們在寫程式碼時,總是會new很多物件,我們之所以可以new出物件,是因為該物件對應的類已經被JVM載入為Class類的物件例項。這句話有點繞,我用程式碼展示一下:
Obj obj = new Obj(); //Obj物件例項
Class o = obj.getClass(); //Obj類是Class類的物件例項
在JVM中,一般情況下,我們的類的類例項是唯一的,這得益於類載入機制的雙親委派模型。
如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都是應該傳送到頂層的啟動類載入器中,只有當父類載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
3.類也是一種Resource
言歸正傳,通過上述對類載入機制的學習,我們可以得出這樣的一個結論:一個類檔案是由某個類載入器負責載入到JVM中的,且只會有一個類載入器去載入。反過來說,由一個類例項就可以獲取到載入它到JVM中的那個類載入器。
用程式碼闡述我的上段話如下所示:
Obj obj = new Obj();
ClassLoader classLoader = obj.getClass().getClassLoader();
跟著我的思路繼續走,該類載入器之所以可以載入這個類,是因為這個類在該類載入器的搜尋範圍內。類載入器既然可以載入這個類檔案,那麼也可以載入該類檔案同級目錄下的所有資原始檔。
所以,我們要想確保可以讀取到某個資原始檔,只需呼叫和該資原始檔在同一目錄下的類的Class物件的getClassLoader()方法獲取該類載入器即可。
舉個例子,我們有一個properties檔案和Obj.class在同一個目錄下, 那我們讀取該properties檔案的最正確的方式就是通過Obj.class.getClassLoader().getResourceAsStream()
方法。
4.一個錯誤的例子
為了印證上面的結論,先看下 Object.class.getResourceAsStream()
的原始碼:
// Class.java
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
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負責載入的,類也是一種resources檔案。
但通過Object.class.getResourceAsStream()
不一定可以搜尋到指定的資原始檔,原因就在於前面說過的類載入器的搜尋範圍,所以這種方式並不推薦使用。
5.結語
關於如何正確讀取ClassPath下的資原始檔相信你已經掌握了正確姿勢。
我是薛勤,我們們下期見!關注我,帶你領略更多程式設計技能!