首頁 > 軟體

專案打包成jar後包無法讀取src/main/resources下檔案的解決

2022-04-02 13:00:47

一、專案場景

在專案中讀取檔案時, 使用new File() 出現的一個坑以及解決流程
這種問題不僅在本地檔案讀取時會遇到, 而且在下載專案下 (例如: src/main/resources目錄下) 的文字時, 也會遇到,

二、問題描述

發現問題

原來程式碼
該程式碼功能是利用 common.io 包下的FileUtils來讀取檔案, 放到一個字串中

String s = FileUtils.readFileToString(new File("src/main/resources/holiday.txt"), "utf-8");

這種路徑書寫方式 new File("src/main/resources/holiday.txt") , 在本地執行沒問題,
但是打包之後在伺服器中執行出現了問題. 下面是錯誤截圖

可以看到在伺服器中紀錄檔提示: java.io.FileNotFoundException: File 'holiday.txt' does not exist
即: 在打包後, 一開始設定的路徑src/main/resources下無法找到該檔案

分析問題

專案在打包之後, 位於 resource目錄下的檔案, 最常見的就是各種Spring組態檔就會打包在 BOOT-INF/classes 目錄下
而FIle 在按照原來的檔案路徑src/main/resources/holiday.txt'去尋找, 必然找不到檔案, 因此會報檔案找不到的異常

在定位問題的過程中發現, 這裡 提供了一個思路
就是SpringBoot中所有檔案都在jar包中,沒有一個實際的路徑,因此可以使用以下方式

    /**
     * 通過ClassPathResource類獲取,建議SpringBoot中使用
     * springboot專案中需要使用此種方法,因為jar包中沒有一個實際的路徑存放檔案
     *
     * @param fileName
     * @throws IOException
     */
    public void function6(String fileName) throws IOException {
        ClassPathResource classPathResource = new ClassPathResource(fileName);
        InputStream inputStream = classPathResource.getInputStream();
        getFileContent(inputStream);
    }

為什麼使用 ClassPathResource 後, 可以找到打包後的檔案路徑?

上面程式碼的核心就是: 範例化ClassPathResource 物件. 然後呼叫getInputStream 來獲取資原始檔

下面我們來分析這些程式碼
ClassPathResource 在範例化時, 會初始化類載入器 classLoader 並將專案所用到的所有路徑載入到類載入器 classLoader 中, 這些路徑包括: java執行環境的jar, Maven 專案中的jar, 以及當前專案打包後的jar等(如下圖)

classPathResource.getInputStream 在獲取資原始檔時, 因為上面我們初始化了一個classLoader.
所以classLoader不為空, 因此會執行 getResourceAsStream 方法, 我們來追一下這個方法

getResourceAsStream 方法中的getResource是實際的業務處理方法, 我們繼續深入

getResource 方法如下圖, 實際的功能就是遞迴呼叫自己, 去不斷遍歷 parent 下的路徑, 獲取對應的資原始檔
那麼 parent 又是誰呢? 我們繼續往下看

看到這裡我們豁然開朗, 這個神祕的 parent 就是類載入器classLoader!!!
因此getResource 方法就是去不斷遍歷我們在ClassPathResource範例化時, 建立的類載入器下面的路徑!!!(對應第1點)

三、解決方案

原來讀取檔案的程式碼如下

String s = FileUtils.readFileToString(new File("src/main/resources/holiday.txt"), "utf-8");

去檢視 File 的建構函式, 看能否通過 InputStream 來構造
從下圖看是不行的

方案一

並且我們發現 org.apache.commons.io沒有提供ClassPathResource 作為入參的讀取檔案的方法.
因此我們必須手寫讀取檔案的方法

手寫的程式碼如下
主要注意 Resource resource = new ClassPathResource(fileName); is = resource.getInputStream();

    /**
     * Java讀取txt檔案的內容
     *
     * @param fileName resources目錄下檔名稱(無需帶目錄)
     * @return 將每行作為一個單位放到list中
     */
    public static List<String> readTxtFile(String fileName) {
        List<String> listContent = new ArrayList<>();
        InputStream is = null;
        InputStreamReader isr = null;
        BufferedReader br = null;
        String encoding = "utf-8";
        try {
            Resource resource = new ClassPathResource(fileName);
            is = resource.getInputStream();
            isr = new InputStreamReader(is, encoding);
            br = new BufferedReader(isr);
            String lineTxt = null;
            while ((lineTxt = br.readLine()) != null) {
                listContent.add(lineTxt);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                br.close();
                isr.close();
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return listContent;
    }

方案二

這種方式對程式碼入侵較小, 核心還是利用 common.io 下的 FileUtils, 具體方法是
利用FileUtils將ClassPathResource.getInputStream 得到的輸入流複製到臨時檔案中, 然後讀取這個臨時檔案
這種方式缺點是: 需要建立臨時檔案, 如果待讀取檔案過大, 則重新建立檔案和複製操作會消耗一定的空間和時間, 影響效能

  //方式二 利用FileUtils將ClassPathResource.getInputStream 得到的輸入流複製到臨時檔案中
  Resource resource = new ClassPathResource("holiday.txt");
  InputStream inputStream = resource.getInputStream();
  File tempFile = File.createTempFile("temp", ".txt");
  FileUtils.copyInputStreamToFile(inputStream, tempFile);
  
  String s = FileUtils.readFileToString(tempFile, StandardCharsets.UTF_8);

意外出現

到這裡又出現了一個問題, 就是我用的測試專案因為在 maven 裡面指定了某些格式的檔案. 如下設定
因為指定了banner.txt 以及 xml 與 properties結尾的檔案作為資源被打包. 所以檔案 holiday.txt 執行後還是存取不到
有問題的pom.xml檔案如下

	<!-- 資源拷貝外掛,實現在打包時自動拷貝java目錄下以及resources目錄下的xml的組態檔 -->
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
				<includes>
					<include>**/*.xml</include>
					<include>**/*.properties</include>
					<include>**/banner.txt</include>
				</includes>
			</resource>
		</resources>

打包後資原始檔截圖如下, 從該圖中可以看到 holiday.txt 沒有被打包進來

程式執行之後的錯誤截圖

我們修改下指定打包的設定 <include>**/*.txt</include>
這樣設定後, 我們就可以將類路徑下的所有txt 檔案打包進行專案中了, 打包之後檔案位置如下圖
或者我們可以去除專案中下面的程式碼設定, 這樣做會預設打包 resources 下面的所有檔案

	<!-- 資源拷貝外掛,實現在打包時自動拷貝java目錄下以及resources目錄下的xml的組態檔 -->
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
				<includes>
					<include>**/*.xml</include>
					<include>**/*.properties</include>
					<include>**/*.txt</include>
				</includes>
			</resource>
		</resources>

修改pom檔案後, 重新打包後資原始檔(從這裡可以看到 holiday.txt 被打包進來 )

總結

在專案內的檔案的讀取/下載時, 由於本地路徑和專案打包後的路徑不同. 出現找不到檔案的情況,我們只需要例化ClassPathResource(檔名) 物件. 然後呼叫getInputStream 來獲取資原始檔.就能獲取任意環境下專案內的檔案

如果想打算使用其他方式來獲取resources 目錄下的檔案, 可以參見 這篇部落格 .核心和上面問題分析差不多, 基本上都是通過類載入器來獲取資原始檔的輸入流進而找到這個檔案

到此這篇關於專案打包成jar後包無法讀取src/main/resources下檔案的解決的文章就介紹到這了,更多相關jar無法讀取src/main/resources檔案內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


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