項目中經常會使用 properties 文件定義一些配置變量,相應的就需要寫一個類來加載此配置。
常用的方式是使用 class 或者 classLoader 對象的getResourceAsStream 來加載properties文件。
eg:
GlobalConfig.class.getResourceAsStream("/properties/globalConfig.properties")
GlobalConfig.class.getClassLoader().getResourceAsStream("properties/globalConfig.properties")
用Class或者ClassLoader 讀取的區別是什麼呢?相信眼尖的讀者已經看出來了,就是指定的路徑差了個 /
。
路徑是否有 “/” 到底有什麼影響,能不能使用 GlobalConfig.class.getResourceAsStream(“properties/globalConfig.properties”)
或者 GlobalConfig.class.getClassLoader().getResourceAsStream("/properties/globalConfig.properties") 來讀取配置呢?
帶着種種疑問,下面就來探究一下這裏面有什麼貓膩。
測試環境:sping-boot項目(jar包)
項目結構:
深入源碼探究:
- Class類的 getResourceAsStream方法 有啥貓膩?
查看Class類getResourceAsStream方法的源碼,如下:
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
//...
return cl.getResourceAsStream(name);
}
哦呵,看出來了吧,Class的getResourceAsStream方法調用的是ClassLoader的getResourceAsStream,Class這傢伙真是夠懶的。不過呢,調用之前也不是什麼都不做,在調用前對name做了一些操作,即resolveName(name)。看來也不是真懶。
我們看看 resolveName 方法做了什麼呢。
/**
* Add a package name prefix if the name is not absolute
* Remove leading "/" if name is absolute
*/
private String resolveName(String name) {
//...
if (!name.startsWith("/")) { // name不是以 "/" 開頭
Class<?> c = this;
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName(); // 當前類的全限定名。eg:com.markix.config.GlobalConfig
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/') +"/"+name;
}
} else { // name以 "/" 開頭
name = name.substring(1);
}
return name;
}
其實,resolveName方法上的註釋已說明一切,硬翻譯一波:如果name不是絕對的,則添加包名前綴;如果name是絕對的,則刪除最前面的"/"。
通過代碼我們也能得出此結論,當name以‘/’開頭,則執行 name = name.substring(1);
也就是截取掉開頭的’/’。當name不是以“/”開頭,則獲取當前類class對象的name(即類的全路徑名,包括包名),再截取包名替換成路徑形式拼接到name的前綴。
舉個栗子直觀描述上面說的一坨東西:
-
絕對路徑:GlobalConfig.class.getResourceAsStream("/properties/config.properties")
name 經過 resolveName 方法從/properties/config.properties
變成properties/config.properties
進而調用 ClassLoader類的getResourceAsStream(“properties/config.properties”) -
相對路徑:GlobalConfig類class.getResourceAsStream(“properties/config.properties”)
name 經過 resolveName 方法從原本的properties/config.properties
變成com/markix/config/properties/config.properties
進而調用 ClassLoader類的getResourceAsStream(“com/markix/config/properties/config.properties”)
通過上述分析,得出結論:
- 調用
類名.class.getResourceAsStream("/路徑")
等價於調用類名.class.getClassLoader().getResourceAsStream("路徑")
- 調用
類名.class.getResourceAsStream("路徑")
等價於調用類名.class.getClassLoader().getResourceAsStream("類路徑 + 路徑")
過渡一句,class的getResourceAsStream本質就是調用classLoader的getResourceAsStream,下面探究下classLoader的getResourceAsStream。
- ClassLoader類的getResourceAsStream方法 有啥貓膩?
查看ClassLoader類getResourceAsStream方法的源碼,呃,遇到難題了,有多個實現類重寫了getResourceAsStream,到底是哪個類?
這裏關乎Java類加載器的知識,不瞭解的請先自覺補姿勢。博主直接拋結論啦,一般我們應用類運行都是使用 AppClassLoader 加載的,其繼承自 URLClassLoader,所以我們查看URLClassLoader的getResourceAsStream方法,如下:
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
if (url == null) {
return null;
}
URLConnection urlc = url.openConnection();
InputStream is = urlc.getInputStream();
//...
return is;
} catch (IOException e) {
return null;
}
}
核心就是調用了 getResource 方法,接着獲取流就返回了。繼續看getResource方法,在ClassLoader類中:
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
和類加載類似,採用雙親委託。如果有parent,就先調用父加載器的方法。
我們的資源在我們項目中,其實最終調用的是findResource方法進行查找,代碼如下:
public URL findResource(final String name) {
/*
* The same restriction to finding classes applies to resources
*/
URL url = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
return ucp.findResource(name, true);
}
}, acc);
return url != null ? ucp.checkURL(url) : null;
}
點到爲止哈哈,有興趣自行debug哈(再深入編不下去了哈哈)。通過debug調試,總結一下結論。
結論:
- 調用
類名.class.getClassLoader().getResourceAsStream("/路徑")
總是返回 null。’/’ 不可訪問。 - 調用
類名.class.getClassLoader().getResourceAsStream("路徑")
,會在運行環境的所有加載目錄(包括jar)中查找路徑。
舉個栗子:
我的項目存放在 E:\WorkSpace\IDEA\spring-boot-demo,編譯目錄爲 E:\WorkSpace\IDEA\spring-boot-demo\target\classes\。我是直接在IDEA運行的,運行時會加載編譯目錄的內容,當調用
GlobalConfig.class.getClassLoader().getResourceAsStream(“properties/globalConfig.properties”) 時,也就會在 E:\WorkSpace\IDEA\spring-boot-demo\target\classes\ 目錄查找一下 properties\globalConfig.properties 文件是否存在,找不到就去其他目錄找,找得到就返回了。
另:
在tomcat容器環境中,調用類名.class.getClassLoader().getResourceAsStream("/路徑")
並不是返回null。原因在於tomcat重寫了ClassLoader機制,war項目運行時並不是使用AppClassLoader加載,而是使用tomcat自定義的WebappClassLoader類,其父類WebappClassLoaderBase重寫了getResourceAsStream方法,對路徑做了特殊處理,最終實現了調用 類名.class.getClassLoader().getResourceAsStream("/路徑")
等價於調用 類名.class.getClassLoader().getResourceAsStream("路徑")
。
詳見 WebappClassLoaderBase的getResourceAsStream方法。
所以,還是推薦使用如下形式:
GlobalConfig.class.getResourceAsStream("/properties/globalConfig.properties")
// 或者
GlobalConfig.class.getClassLoader().getResourceAsStream("properties/globalConfig.properties")
end