1. 程式人生 > >Java Gradle專案中的資源正確獲取

Java Gradle專案中的資源正確獲取

引言

一個Java Gradle專案會涉及到資源的訪問. 一般情況下會將當前專案所需的資原始檔全部放置於resources資料夾下, 無論是main檔案下的source code 還是test資料夾下的test code. 都或多或少的涉及到獲取resources資料夾下的資源. 本文主要目的就是詳細的總結一下如何獲取resources資料夾下的資源.

兩個getResource方法

來看一個簡單的Java Gradle專案(稱呼其為simpleresource)的專案結構

simpleresource

首先這個project執行build之後會在根目錄下建立一個out目錄, 這個目錄存放所有的編譯結果(class檔案以及資原始檔). 如上圖所示production資料夾對應的是source code而test資料夾對應的是test code.
所有的資源都會儲存在resources資料夾內部. 當程式執行時獲取的資源就是這個resources資料夾下的資源.
我們使用最多的獲取資源的方法有兩個 Class.getResourceClassLoader.getResource 但是這兩個方法傳遞引數與結果不同, 下面詳細分析一下這兩個方法引數以及返回值.
先看 ClassLoader
中的 getResource 方法. 只需要獲取類載入器物件即可(獲取方式不再贅述). 先看這個方法的API文件相關的描述:

Finds the resource with the given name. A resource is some data (images, audio, text, etc) that can be accessed by class code in a way that is independent of the location of the code.
The name of a resource is a '/'-separated path name
that identifies the resource. This method will first search the parent class loader for the resource; if the parent is null the path of the class loader built-in to the virtual machine is searched. That failing, this method will invoke findResource(String) to find the resource.

從這個描述中可以得知提供資源的路徑(我理解的是相對路徑), 正常情況下該方法會返回資源完整的URL. 傳遞的引數有一個重要的注意事項, 就是傳遞的引數不能夠以/ 開始, 這也是我為什麼稱呼這個引數為資源的相路徑. 舉個例子

URL test = this.getClass().getClassLoader().getResource("/");

執行上述程式碼返回的結果是:

null

參考simpleresource的專案結構, 正確獲取 com.mainres 下的檔案的正確做法是:

String name = "com/mainres/testmain.txt";
URL test = this.getClass().getClassLoader().getResource(name);

結果為:

rightpath

如果在表示資源路徑的字串中加上 / 那麼獲取到的URL依然為null

String name = "/com/mainres/testmain.txt";
URL test = this.getClass().getClassLoader().getResource(name);

null

宗上所述, 使用類載入器獲取資源的方式傳遞的引數為資源相對路徑(相對於resources資料夾的路徑), 既然是相對路徑自然引數 不能夠/ 開始.
有一個問題需要注意, 當傳遞引數為空字串的時候, 得到路徑其實是classes資料夾的絕對路徑, 但當傳遞一個正確的資源路徑相對字串時, 得到路徑卻是resources資料夾下的資源路徑.

 String name = "";
 URL test = this.getClass().getClassLoader().getResource(name);

classespath

我的理解是本質上是通過此方法獲取的其實類載入器載入的class位元組碼目錄, 所以使用空字串會看到實際輸出的是classes資料夾絕對路徑, 當傳遞正確的資源路徑的時候, 程式碼層面做轉換, 轉而獲取與classes資料夾同級的resources資料夾下的資源.
再看 Class 中的 getResurce 方法
由於這個方法傳遞引數是否是以 / 開頭會產生不同的結果, 且使用這個方法也比較容易和 ClassLoader 中的 getResource 方法搞混淆, 所以本文多次強調傳遞的引數是否以 / 開始.
首先看傳遞引數為 ""/ 的兩種情況得到的結果:
使用空字串:

String name = "";
URL test = this.getClass().getResource(name);

執行結果:

classpath

使用 /

String name = "/";
URL test = this.getClass().getResource(name);

執行結果為:

classpath

最大的區別是使用空字串 "" 獲取的路徑是相對於包的目錄, 而使用 / 獲取的路徑是類載入器載入class檔案的目錄, 這個和 ClassLoadergetResource 方法傳遞 "" 字串的結果是一樣的. 所以如果要正確的獲取到資原始檔, 那麼使用 ClassgetResource 方法如下:

String name = "/com/mainres/testmain.txt";
URL test = this.getClass().getResource(name);

執行結果:

classpath

所以綜上所述, 一個簡單的防止兩個方法傳遞引數搞混淆的記憶方式就是使用 ClassgetResource 方法需要加 / 而使用 ClassLoadergetResource 方法不要加 /.
其實參考 Class 類中的 getResource 方法:

 public java.net.URL getResource(String name) {
        name = resolveName(name);
        ClassLoader cl = getClassLoader0();
        if (cl==null) {
            // A system class.
            return ClassLoader.getSystemResource(name);
        }
        return cl.getResource(name);
    }

本質上講它也是呼叫ClassLoader 中的getResource 方法. 其中resolveName 這個方法對傳遞的引數做了轉換.

private String resolveName(String name) {
        if (name == null) {
            return name;
        }
        if (!name.startsWith("/")) {
            Class<?> c = this;
            while (c.isArray()) {
                c = c.getComponentType();
            }
            String baseName = c.getName();
            int index = baseName.lastIndexOf('.');
            if (index != -1) {
                name = baseName.substring(0, index).replace('.', '/')
                    +"/"+name;
            }
        } else {
            name = name.substring(1);
        }
        return name;
    }

當傳遞的引數帶有/ 時候, resolveName 會將/ 去除後的字串返回, 最後呼叫ClassLoader 中的 getResource 方法.

小結

本文對比了一下ClassClassLoader 中的getResource 方法的差異,如果單純從資源的獲取角度來看最終呼叫的都是ClassLoader 中的getResource 方法. 簡單記憶即是使用ClassgetResource 方法資源路徑需要加/ 而使用ClassLoader 中的getResource 方法則不需要加/.