Java 正確地從類路徑中獲取資源
Java 可通過(guò)以下幾種方法來(lái)訪問(wèn)資源:
- Class 的 getResource 方法
- ClassLoader 的 getResource 方法
- ClassLoader 的 getSystemResource 靜態(tài)方法
在使用中,Class 可通過(guò)直接引用類的 class 屬性而獲得,或是通過(guò)實(shí)例的 getClass() 方法來(lái)獲得。獲取 ClassLoader 的方式則比較多,常見(jiàn)以下幾種:
- 調(diào)用 Class 的 getClassLoader 方法,如:getClass().getClassLoader()
- 由當(dāng)前線程獲取 ClassLoader:Thread.currentThread().getContextClassLoader()
- 獲取系統(tǒng) ClassLoader: ClassLoader.getSystemClassLoader()
不過(guò),若是對(duì) Java 的 ClassLoader 概念不太了解,最好還是盡量避免使用它。
Class.getResource 與 ClassLoader.getResource 的區(qū)別
這兩種方式,都接受一個(gè)字符串形式的路徑表達(dá)式,即資源名,并返回找到的資源的 URL。兩種方式都可用來(lái)定位資源,在網(wǎng)絡(luò)上流傳的文章中,兩者都是常見(jiàn)的。實(shí)際上,Class 的 getResource 方法也調(diào)用了 ClassLoader 的 getResource 方法,但兩者有著很大的不同,不了解這兩種方法的區(qū)別,就容易造成隱患。隱患經(jīng)常比編寫(xiě)時(shí)就出錯(cuò)要可怕得多,因?yàn)樗谝欢▓?chǎng)合下是正常的,不容易被發(fā)現(xiàn)。
兩者最大的區(qū)別,是從哪里開(kāi)始尋找資源。ClassLoader 并不關(guān)心當(dāng)前類的包名路徑,它永遠(yuǎn)以 classpath 為基點(diǎn)來(lái)定位資源。而 Class.getResource 則不同,如果資源名是絕對(duì)路徑(以"/"開(kāi)頭),它會(huì)將開(kāi)頭的"/"去除,然后調(diào)用 ClassLoader 的 getResource 方法來(lái)尋找資源;如果資源名是相對(duì)路徑,它會(huì)在當(dāng)前的包路徑下面尋找資源。
舉例來(lái)說(shuō),假設(shè)我們有一個(gè)類:test.App (包名為 test),并且在 test 包下有一個(gè)與類名同名的 js 文件,名為 App.js。如果用 ClassLoader 來(lái)獲取這個(gè) js 文件,應(yīng)該這樣寫(xiě):
App.class.getClassLoader().getResource("test/App.js");
如果用 Class 的 getResource 方法,則有兩種寫(xiě)法:
- 使用相對(duì)路徑:
App.class.getResource("App.js");
- 使用絕對(duì)路徑:
App.class.getResource("/test/App.js");
從上面的例子,可以看出兩者之間巨大的區(qū)別。有些人從網(wǎng)絡(luò)上復(fù)制類似的代碼,看看不能正確運(yùn)行,就開(kāi)始嘗試在資源名前加上 "/",或是去掉開(kāi)頭的 "/",試成功了,便算完工,這絕非正道。
Class 與 ClassLoader 的 getResource 方法還有其它一些不同,對(duì) Class 的 getResource 方法來(lái)說(shuō),若傳入的是相對(duì)路徑,它還會(huì)嘗試做包名與路徑名的轉(zhuǎn)換。查看 Class.getResource 方法的源碼,可以看到它首先對(duì)資源名調(diào)用了 resolveName 方法,然后再調(diào)用 ClassLoader 的 getResource 方法來(lái)完成資源的定位。
測(cè)試代碼
作為演示,我寫(xiě)了以下代碼來(lái)展示 Class 與 ClassLoader 的 getResource 方法的輸出:
/** * Copyright (c) 2014 Chen Zhiqiang <chenzhiqiang@mail.com>. Released under the MIT license. */ package test; import java.net.URL; import java.util.Enumeration; /** * Tests for the use of {@link Class#getResource(String)} and * {@link ClassLoader#getResource(String)}. * * @author Chen Zhiqiang <chenzhiqiang@mail.com> */ public class ClassResourceTest { Class<ClassResourceTest> cls = ClassResourceTest.class; ClassLoader ldr = cls.getClassLoader(); // Thread.currentThread().getContextClassLoader() 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(""); 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(""); ClassResourceTest t = new ClassResourceTest(); t.testForResource(); } }
編譯上述代碼,看看不同資源路徑的輸出結(jié)果。
打包為 Jar 包后的變化
現(xiàn)在,將上述代碼編譯后的結(jié)果打包成 Jar 文件,假設(shè)是 test.jar ,然后從這個(gè) jar 包中運(yùn)行上述代碼,再看看輸出結(jié)果,比較下與上面的輸出有什么變化:
java -classpath test.jar test.ClassResourceTest
值得注意的幾點(diǎn):
- Class.getResource("") 還有其它一些輸出,結(jié)果是 jar:file:/some_path/test.jar!/some_path,而在打包為 Jar 之前,它們的輸出形式是 file:/some_path...;
- Class.getResource("/") 為 null,而在打包之前,該輸出是 ClassResourceTest 的類路徑;
- ClassLoader.getResource("") 為 null,而在打包之前,該輸出是 ClassResourceTest 的類路徑;
- 調(diào)用 ClassLoader.getResource 方法時(shí),若資源名為絕對(duì)路徑,不管是否打包,其輸出結(jié)果為 null,至少在我這里是這樣。
錯(cuò)誤與陷阱
- 使用 Class.getResource("/") 或 ClassLoader.getResource("") 來(lái)當(dāng)作類路徑的根。
這是一種常見(jiàn)的錯(cuò)誤,并在網(wǎng)絡(luò)上廣為流傳。它們?cè)诖虬?Jar 包后,其結(jié)果會(huì)發(fā)生變化。
- 獲得 getResource 方法的輸出后,簡(jiǎn)單地對(duì)結(jié)果調(diào)用 getFile 或 getPath,并把它當(dāng)作文件路徑來(lái)處理。
資源有可能以文件和目錄的形式位于類路徑之中,但也可能打包進(jìn)了 Jar 包或 Zip 包,你不能假設(shè)你的代碼不會(huì)被打包。
- 將絕對(duì)路徑傳給 ClassLoader 的 getResource 方法。
網(wǎng)絡(luò)上有人說(shuō),對(duì)于 ClassLoader 的 getResource 方法來(lái)說(shuō),資源名是否以 "/" 開(kāi)頭是一樣的,然而,在我這里,ClassLoader 的 getResource 方法并不接受絕對(duì)路徑,其輸出結(jié)果為 null。
正確使用 getResource 方法
- 避免使用 Class.getResource("/") 或 ClassLoader.getResource("")。你應(yīng)該傳入一個(gè)確切的資源名,然后對(duì)輸出結(jié)果作計(jì)算。比如,如果你確實(shí)想獲取當(dāng)前類是從哪個(gè)類路徑起點(diǎn)上執(zhí)行的,以前面提到的 test.App 來(lái)說(shuō),可以調(diào)用 App.class.getResource(App.class.getSimpleName() + ".class")。如果所得結(jié)果不是 jar 協(xié)議的URL,說(shuō)明 class 文件沒(méi)有打包,將所得結(jié)果去除尾部的 "test/App.class",即可獲得 test.App 的類路徑的起點(diǎn);如果結(jié)果是 jar 協(xié)議的 URL,去除尾部的 "!/test/App.class",和前面的 "jar:",即是 test.App 所在的 jar 文件的 url。
- 如果要定位與某個(gè)類同一個(gè)包的資源,盡量使用那個(gè)類的getResource方法并使用相對(duì)路徑。如前文所述,要獲取與 test.App.class 同一個(gè)包下的 App.js 文件,應(yīng)使用 App.class.getResource("App.js") 。當(dāng)然,事無(wú)絕對(duì),用 ClassLoader.getResource("test/App.js") 也可以,這取決于你所面對(duì)的問(wèn)題是什么。
- 如果對(duì) ClassLoader 不太了解,那就盡量使用 Class 的 getResource 方法。
- 如果不理解或無(wú)法確定該傳給 Class.getResource 方法的相對(duì)路徑,那就以類路徑的頂層包路徑為參考起點(diǎn),總是傳給它以 "/" 開(kāi)頭的路徑吧。
- 不要假設(shè)你的調(diào)試環(huán)境就是最后的運(yùn)行環(huán)境。你的代碼可能不打包,也可能打包,你得考慮這些情況,不要埋坑。
getResources: 枚舉資源
Java 的 CLASSPATH 是一個(gè)路徑列表,因此,有可能在多個(gè)類路徑中出現(xiàn)同樣的資源名。如果要列舉它們,可以使用 ClassLoader 的 getResources 方法。
下面的代碼可以枚舉所有的 "META-INF/MANIFEST.MF",你還可以觀察到在類路徑中哪些 jar 文件包含有該資源:
import java.net.URL; import java.util.Enumeration; public class Test { public static void main(String[] args) throws Exception { ClassLoader ldr = Test.class.getClassLoader(); System.out.println("## Test for getResources(‘META-INF/MANIFEST.MF') ##"); Enumeration<URL> urls = ldr.getResources("META-INF/MANIFEST.MF"); while(urls.hasMoreElements()) System.out.println(urls.nextElement()); } }
實(shí)例
下面的代碼演示了如何正確獲取代碼的類路徑起點(diǎn):
/** * Copyright (c) 2014 Chen Zhiqiang <chenzhiqiang@mail.com>. Released under the MIT license. */ package test; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 演示如何獲取當(dāng)前類路徑的起點(diǎn) * * @author Chen Zhiqiang <chenzhiqiang@mail.com> */ public class AppDirTest { Classcls = AppDirTest.class; URL codeLocation = getCodeLocation(); /** * Get the code location. * * Return the classpath where the code run from. The return url will be: * file:/path/my-app/calsses/ or file:/path/my-app/my-app.jar * * @return URL */ public URL getCodeLocation() { if (codeLocation != null) return codeLocation; // Get code location using the CodeSource codeLocation = cls.getProtectionDomain().getCodeSource().getLocation(); if (codeLocation != null) return codeLocation; // If CodeSource didn't work, use {@link } Class.getResource instead. URL r = cls.getResource(""); synchronized (r) { String s = r.toString(); Pattern jar_re = Pattern.compile("jar:\\s?(.*)!/.*"); Matcher m = jar_re.matcher(s); if (m.find()) { // the code is run from a jar file. s = m.group(1); } else { String p = cls.getPackage().getName().replace('.', '/'); s = s.substring(0, s.lastIndexOf(p)); } try { codeLocation = new URL(s); } catch (MalformedURLException e) { throw new RuntimeException(e); } } return codeLocation; } /** * Get the class path root where the program startup, if run in a jar, * return the jar file's parent path. * * @return */ public String getAppDir() { File f = new File(getCodeLocation().getPath()); return f.isFile() ? f.getParent() : f.getPath(); } public static void main(String[] args) { AppDirTest t = new AppDirTest(); System.out.println("code location: " + t.getCodeLocation()); System.out.println("app dir: " + t.getAppDir()); } }
以上就是Java 正確地從類路徑中獲取資源的詳細(xì)內(nèi)容,更多關(guān)于Java 從類路徑中獲取資源的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Java 配置log4j日志文件路徑 (附-獲取當(dāng)前類路徑的多種操作)
- 基于java類路徑classpath和包的實(shí)例講解
- Java 讀取類路徑下的資源文件實(shí)現(xiàn)代碼
- Java中獲取類路徑classpath的簡(jiǎn)單方法(推薦)
- 關(guān)于idea中Java Web項(xiàng)目的訪問(wèn)路徑問(wèn)題
- JAVA獲取當(dāng)前項(xiàng)目和文件所在路徑的實(shí)例代碼
- Java中的通用路徑轉(zhuǎn)義符介紹
- java下載url路徑包含中文需要轉(zhuǎn)義的操作
- IDEA 打開(kāi)java文件對(duì)應(yīng)的class路徑的操作步驟
- Java在指定路徑上創(chuàng)建文件提示不存在解決方法
- linux中java獲取路徑的實(shí)例代碼
相關(guān)文章
SpringBoot 在測(cè)試時(shí)如何指定包的掃描范圍
這篇文章主要介紹了SpringBoot 在測(cè)試時(shí)如何指定包的掃描范圍,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11SpringBoot+Dubbo+Seata分布式事務(wù)實(shí)戰(zhàn)詳解
這篇文章主要介紹了SpringBoot+Dubbo+Seata分布式事務(wù)實(shí)戰(zhàn)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07Spring @Transactional注解的聲明式事務(wù)簡(jiǎn)化業(yè)務(wù)邏輯中的事務(wù)管理
這篇文章主要為大家介紹了Spring @Transactional注解的聲明式事務(wù)簡(jiǎn)化業(yè)務(wù)邏輯中的事務(wù)管理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10Spring事務(wù)執(zhí)行流程及如何創(chuàng)建事務(wù)
這篇文章主要介紹了Spring事務(wù)執(zhí)行流程及如何創(chuàng)建事務(wù),幫助大家更好的理解和學(xué)習(xí)使用spring框架,感興趣的朋友可以了解下2021-03-03SpringCloud?OpenFeign?服務(wù)調(diào)用傳遞?token的場(chǎng)景分析
這篇文章主要介紹了SpringCloud?OpenFeign?服務(wù)調(diào)用傳遞?token的場(chǎng)景分析,本篇文章簡(jiǎn)單介紹?OpenFeign?調(diào)用傳遞?header?,以及多線程環(huán)境下可能會(huì)出現(xiàn)的問(wèn)題,其中涉及到?ThreadLocal?的相關(guān)知識(shí),需要的朋友可以參考下2022-07-07SpringCloud如何搭建一個(gè)多模塊項(xiàng)目
這篇文章主要介紹了SpringCloud如何搭建一個(gè)多模塊項(xiàng)目,記錄下使用SpringCloud創(chuàng)建多模塊項(xiàng)目,一步一步記錄搭建的過(guò)程,感興趣的可以了解一下2021-05-05解決SpringMVC、tomcat、Intellij idea、ajax中文亂碼問(wèn)題
這篇文章主要介紹了解決SpringMVC、tomcat、Intellij idea、ajax中文亂碼問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09Javaweb項(xiàng)目session超時(shí)解決方案
這篇文章主要介紹了Javaweb項(xiàng)目session超時(shí)解決方案,關(guān)于解決方案分類比較明確,內(nèi)容詳細(xì),需要的朋友可以參考下。2017-09-09