Class和ClassLoader的getResource方法對比

程式設計師自由之路發表於2021-05-20

最近在看寫Spring的原始碼,裡面有好多地方都用到了Class和ClassLoader類的getResource方法來載入資原始檔。之前對這兩個類的這個方法一知半解,概念也很模糊,這邊做下整理,加深理解。

PS:本部落格主要參考了Java中如何正確地從類路徑中獲取資源,但是為了加強理解記憶自己還是將其中的重點按照自己的風格排版記錄了下,可以點選連結檢視原文。

訪問資源的主要方式

在Java中,通常可以通過以下方式來訪問資源:

  • Class的getResource方法;
  • ClassLoader的getResource方法;
  • ClassLoader的getResources方法; //獲取批量資源
  • ClassLoader的getSystemResource; //靜態方法

在使用中,Class可通過直接引用類的class屬性而獲得,或是通過例項的 getClass() 方法來獲得。獲取ClassLoader的方式則比較多,常見以下幾種:

  • 呼叫Class的getClassLoader方法,如:getClass().getClassLoader()
  • 由當前執行緒獲取ClassLoader:Thread.currentThread().getContextClassLoader()
  • 獲取系統ClassLoader: ClassLoader.getSystemClassLoader()

Class.getResource 與 ClassLoader.getResource 的區別

public class ClassLoaderAndClassContrast {

public static void main(String[] args) throws Exception {
	Class<ClassLoaderAndClassContrast> aClass = ClassLoaderAndClassContrast.class;
	ClassLoader classLoader = aClass.getClassLoader();

	URL resource = classLoader.getResource("cookies.properties");
	URL resource1 = aClass.getResource("dir/cookies.properties");
	if(resource!=null){
		byte[] bytes = FileCopyUtils.copyToByteArray(resource.openStream());
	    System.out.println(new String(bytes));
	}
}
}

兩者最大的區別,是從哪裡開始尋找資源。

  • ClassLoader並不關心當前類的包名路徑,它永遠以classpath為基點來定位資源。需要注意的是在用ClassLoader載入資源時,路徑不要以"/"開頭,所有以"/"開頭的路徑都返回null;
  • Class.getResource如果資源名是絕對路徑(以"/"開頭),那麼會以classpath為基準路徑去載入資源,如果不以"/"開頭,那麼以這個類的Class檔案所在的路徑為基準路徑去載入資源。

在實際開發過程中建議使用Class.getResource這個方法,但是如果你想獲取批量資源,那麼就必須使用到ClassLoader的getResources()方法。

public class ClassLoaderAndClassContrast {

    Class<ClassLoaderAndClassContrast> cls = ClassLoaderAndClassContrast.class;
    ClassLoader ldr = cls.getClassLoader();

    public static void println(Object s) {
        System.out.println(s);
    }

    void showResource(String name) {
        println("## Test resource for: “" + name + "” ##");
        println(String.format("ClassLoader#getResource(\"%s\")=%s", name, ldr.getResource(name)));
        println(String.format("Class#getResource(\"%s\")=%s", name, cls.getResource(name)));
    }

    public final void testForResource() throws Exception {
        showResource("");
        //對於Class來,返回這個類所在的路徑
        showResource("/");
        showResource(cls.getSimpleName() + ".class");
        String n = cls.getName().replace('.', '/') + ".class";
        showResource(n);
        showResource("/" + n);
        showResource("java/lang/Object.class");
        showResource("/java/lang/Object.class");
    }

    public static void main(String[] args) throws Exception {
        println("java.class.path: " + System.getProperty("java.class.path"));
        println("user.dir: " + System.getProperty("user.dir"));
        println("");
        ClassLoaderAndClassContrast t = new ClassLoaderAndClassContrast();
        t.testForResource();
    }
}

正確使用getResource方法

  • 避免使用 Class.getResource("/") 或 ClassLoader.getResource("")。你應該傳入一個確切的資源名,然後對輸出結果作計算。比如,如果你確實想獲取當前類是從哪個類路徑起點上執行的,以前面提到的test.App來說,可以呼叫 App.class.getResource(App.class.getSimpleName() + ".class")。如果所得結果不是 jar 協議的URL,說明 class 檔案沒有打包,將所得結果去除尾部的 "test/App.class",即可獲得 test.App 的類路徑的起點;如果結果是 jar 協議的 URL,去除尾部的 "!/test/App.class",和前面的 "jar:",即是 test.App 所在的 jar 檔案的url。
  • 如果要定位與某個類同一個包的資源,儘量使用那個類的getResource方法並使用相對路徑。如前文所述,要獲取與 test.App.class 同一個包下的 App.js 檔案,應使用 App.class.getResource("App.js") 。當然,事無絕對,用 ClassLoader.getResource("test/App.js") 也可以,這取決於你所面對的問題是什麼。
  • 如果對ClassLoader不太瞭解,那就儘量使用Class的getResource方法。
  • 如果不理解或無法確定該傳給Class.getResource 方法的相對路徑,那就以類路徑的頂層包路徑為參考起點,總是傳給它以 "/" 開頭的路徑吧。
  • 不要假設你的除錯環境就是最後的執行環境。你的程式碼可能不打包,也可能打包,你得考慮這些情況,不要埋坑。

獲取批量資源

使用classLoader的getResources方法可以獲得批量資源

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> resources = classLoader.getResources("META-INF/MANIFEST.MF");

Spring的ResourceLoader

在Spring框架中ResourceLoader和ResourcePatternResolver介面封裝了獲取資源的方法,我們可以直接拿來使用。ResourceUtils這個類中提供了很多判斷資源型別的工具方法,可以直接使用。

//前面三種的寫法效果是一樣的,必須從classpath基準目錄開始寫精確的匹配路徑
//如果想匹配多個資源,需要以classpath*/打頭,然後配合萬用字元使用
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.spring.xml");

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/beans.spring.xml");

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:/beans.spring.xml");

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath*:/**/beans.spring.xml");
  • ResourceLoader 介面

  • ResourcePatternResolver 介面 --- 重點

給出一個例子

String txt = "";
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("templates/layout/email.html");
Resource resource = resources[0];
//獲得檔案流,因為在jar檔案中,不能直接通過檔案資源路徑拿到檔案,但是可以在jar包中拿到檔案流
InputStream stream = resource.getInputStream();
StringBuilder buffer = new StringBuilder();
byte[] bytes = new byte[1024];
try {
    for (int n; (n = stream.read(bytes)) != -1; ) {
        buffer.append(new String(bytes, 0, n));
    }
} catch (IOException e) {
    e.printStackTrace();
}
txt = buffer.toString();
  • ResourceUtils

  • ResourcePatternUtils

相關文章