首頁 > 軟體

解決使用this.getClass().getResource()獲取檔案時遇到的坑

2022-12-29 14:00:29

使用this.getClass().getResource()獲取檔案時遇到的坑

最近在工作中遇到需要讀取組態檔,然後第一想法就是將檔案放到專案的resources目錄下,

然後使用:

String fileName = "config/zh.md"
String path = this.getClass().getResource("/").getPath()  + fileName;
System.out.println(path);// D:/example/exam01/target/classes/config/zh.md

在IDE工具中開發及Debug時一切都正常,但是打成Jar包釋出到線上時就會出現java.io.FileNotFoundException

java.io.FileNotFoundException: file:/usr/local/exam01-1.0-SNAPSHOT.jar!/BOOT-INF/classes!/config/zh.md (No such file or directory)

錯誤資訊也已經很明顯了,就是因為檔案不存在,但是在IDE中是可以正常執行了,那為什麼打成jar包放到伺服器中就不行了呢?

仔細檢查報錯路徑發現在磁碟確實不存在這樣一條路徑,因為路徑從 .../exam01-1.0-SNAPSHOT.jar/...開始,後面的檔案路徑都是打到Jar包中的,磁碟沒有後面 .../BOOT-INF/classes!/config/zh.md這樣的目錄;

在Jar包中的檔案在磁碟是沒有實際路徑的,所以這時候通過 this.getClass()..getResource() 無法獲取檔案。

解決方式一

直接將需要的檔案上傳到伺服器指定的資料夾下,如果把檔案路徑寫死,就太low了,也不符合編碼規範。

而且存在各種隱患例如:不同的環境釋出到不同的伺服器上,開發一個伺服器,測試一個伺服器,生產一個伺服器,每個伺服器中都要上傳一份;如果誤刪或者遷移專案忘記遷移這個檔案就麻煩了;

解決方式二

可以通過 this.getClass()..getResourceAsStream("/config/zh.md") 能夠正常獲取到檔案流。

xxx.class.getResource("")xxx.class.getClassLoader().getResource("")

上面問題已經解決了,我們看下xxx.class.getResource("") 和 **xxx.class.getClassLoader().getResource("")**的區別;

1.其實

class.getResource("/") == class.getClassLoader().getResource("");

Class.getResource和ClassLoader.getResource本質上是一樣的,都是使用ClassLoader.getResource載入資源的。

對於Class.getResource:

先獲取檔案的路徑path,不以’/‘開頭時,預設是從此類所在的包下取資源;path以’/'開頭時,則是從專案的ClassPath根下獲取資源。

對於ClassLoader.getResource:

同樣先獲取檔案的路徑,path不以’/'開頭時,首先通過雙親委派機制,使用的逐級向上委託的形式載入的,最後發現雙親沒有載入到檔案,最後通過當前類載入classpath根下資原始檔。

對於getResource("/"),’/'表示Boot ClassLoader中的載入範圍,因為這個類載入器是C++實現的,所以載入範圍為null。

2.以上兩種方法返回的都是 java.net.URL物件

如果需要得到相應的String型別,可以用以下方法:

xxx.class.getResource("").getPath();

xxx.class.getResource("").getFile();

或者通過

InputStream input = getClass().getClassLoader().getResourceAsStream("config\config.properties");

獲取IO流;

3.類載入器ClassLoader

我們都知道 Java 檔案被執行,第一步,需要通過 javac 編譯器編譯為 class 檔案;第二步,JVM 執行 class 檔案,實現跨平臺。

而 JVM 虛擬機器器第一步肯定是 載入 class 檔案,所以,類載入器實現的就是(來自《深入理解Java虛擬機器器》):

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

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

  • 每個類載入器都有自己的預定義的搜尋範圍,用來載入 class 檔案;
  • 每個類和載入它的類載入器共同確定了這個類的唯一性,也就是說如果一個 class 檔案被不同的類載入器載入到了 JVM 中, 那麼這兩個類就是不同的類,雖然他們都來自同一份 class 檔案;

3.1 雙親委派模型

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

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

  • 委派(delegation):子類載入器委派給父類別載入器載入;
  • 可見性(visibility):子類載入器可存取父類別載入器載入的類,父類別不能存取子類載入器載入的類;
  • 唯一性(uniqueness):可保證每個類只被載入一次,比如 Object 類是被 Bootstrap ClassLoader 載入的,因為有了雙親委派模型,所有的 Object 類載入請求都委派到了 Bootstrap ClassLoader,所以保證了只被載入一次。

3.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 程式中的類載入器層級結構圖:

總結

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com