讀取ClassPath下resource檔案的正確姿勢

薛勤發表於2019-07-11

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下的資原始檔相信你已經掌握了正確姿勢。

我是薛勤,我們們下期見!關注我,帶你領略更多程式設計技能!

相關文章