最近在使用一些方法獲取當前代碼的運行路徑的時候,發現代碼中使用的this.getClass().getClassloader().getResource("").getPath()
有時候好使,有時候則是NPE(空指針),緣由就是有時候this.getClass().getClassloader().getResource("")
會返回空,那麼爲何是這樣呢?java
先想象一下,咱們平時如何啓動一個 Java 應用?git
值得一提的是 spring-boot 和 fat-jar 都是經過java -jar your.jar
的方式啓動,之因此換分爲兩類,是由於在 spring boot中類加載器(LaunchedURLClassLoader
)是被從新定義過的,能夠隨意加載 nested jars,而 fat-jar 目前都仍是簡單實現了 classloader. 這裏咱們主要用兩個比較有表明性的例子經過IDEmain
方法啓動和經過 fat-jar 啓動spring
package com.example.test;
import java.net.URL;
/** * @author lican */
public class FooTest {
public static void main(String[] args) {
ClassLoader classLoader = FooTest.class.getClassLoader();
System.out.println(classLoader);
URL resource = classLoader.getResource("");
System.out.println(resource);
}
}
複製代碼
結果bootstrap
sun.misc.Launcher$AppClassLoader@18b4aac2
file:/Users/lican/git/test/target/test-classes/
複製代碼
package com.test.fastjar.fatjartest;
import java.net.URL;
public class FatJarTestApplication {
public static void main(String[] args) throws Exception {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(contextClassLoader);
URL resource = contextClassLoader.getResource("");
System.out.println(resource);
}
}
複製代碼
用mvn clean install -DskipTests
進行打包,在命令行進行啓動緩存
java -jar target/fat-jar-test-0.0.1-SNAPSHOT-jar-with-dependencies.jar
複製代碼
執行結果:tomcat
jdk.internal.loader.ClassLoaders$AppClassLoader@4f8e5cde
null
複製代碼
可見ClassLoader.getResource("")
在某些狀況下並不能如願獲取項目執行的根路徑,那麼這裏面的緣由是什麼?是否有通用的方法能夠避免這些問題呢?固然有.bash
首先咱們分下一下 jdk 關於這一段的源碼或許就比較清楚了. 咱們調用 getResource("") 首先會到java.lang.ClassLoader#getResource
服務器
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;
}
複製代碼
這裏若是咱們用的是 main 方法啓動,那麼當前的 classloader 就是AppClassloader
,parent 就是ExtClassloader
, 這裏不管從 parent 仍是 bootstrapResource
都沒法找到相對應的資源(經過 debug), 那麼這個返回值確定是從 findResource(name)
中得到.app
可是 getResource 方法確實這樣的ide
protected URL findResource(String name) {
return null;
}
複製代碼
顯然被子類覆寫了,查看一下實現的子類,因爲 AppClassloader 繼承自 URLClassloader 因此目光聚焦在這裏
這裏是java.net.URLClassLoader#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;
}
複製代碼
大概能夠看明白,這裏最終是ucp.findResource(name, true);
在查找資源 定位到sun.misc.URLClassPath#findResource
public URL findResource(String name, boolean check) {
Loader loader;
int[] cache = getLookupCache(name);
for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
URL url = loader.findResource(name, check);
if (url != null) {
return url;
}
}
return null;
}
複製代碼
就是URL url = loader.findResource(name, check);
這裏在加載. 可是這個loader
是個什麼鬼?它又是從哪裏加載的咱們查找的 name 呢?
Loader
是URLClassPath
裏面的一個靜態內部類 sun.misc.URLClassPath.Loader
總共有兩個子類
從名稱上面看FileLoader
就是加載文件的 loader,JarLoader
就是加載 jar 包的 loader.最終的 findResource 會找到各自loader 的 findResource 進行查找. 在分析這兩個 loader 以前,咱們先看看這兩個 loader 是怎樣產生的? sun.misc.URLClassPath#getLoader(java.net.URL)
/* * Returns the Loader for the specified base URL. */
private Loader getLoader(final URL url) throws IOException {
try {
return java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Loader>() {
public Loader run() throws IOException {
String file = url.getFile();
if (file != null && file.endsWith("/")) {
if ("file".equals(url.getProtocol())) {
return new FileLoader(url);
} else {
return new Loader(url);
}
} else {
return new JarLoader(url, jarHandler, lmap, acc);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException)pae.getException();
}
}
複製代碼
須要說明的是,這裏的參數 url 是從 classpath 中 pop 出來的,循環 pop, 直到所有查詢完成. 那麼咱們在 IDE 的 main
方法運行時,他的 classpath之一其實就是file:/Users/lican/git/test/target/test-classes/
而在用 jar 包運行的時候, classpath 之一是運行的 jar 包,好比 /Users/lican/git/fat-jar-test/target/fat-jar-test-0.0.1-SNAPSHOT-jar-with-dependencies.jar
,因爲這兩個 classpath 得不一樣致使了一個走向了 FileLoader
, 一個走向了JarLoader
, 最終的緣由就定位到了這兩個 loader 得 getResource 的不一樣之處.
Resource getResource(final String name, boolean check) {
final URL url;
try {
URL normalizedBase = new URL(getBaseURL(), ".");
url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));
if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
// requested resource had ../..'s in path
return null;
}
if (check)
URLClassPath.check(url);
final File file;
if (name.indexOf("..") != -1) {
file = (new File(dir, name.replace('/', File.separatorChar)))
.getCanonicalFile();
if ( !((file.getPath()).startsWith(dir.getPath())) ) {
/* outside of base dir */
return null;
}
} else {
file = new File(dir, name.replace('/', File.separatorChar));
}
if (file.exists()) {
return new Resource() {
public String getName() { return name; };
public URL getURL() { return url; };
public URL getCodeSourceURL() { return getBaseURL(); };
public InputStream getInputStream() throws IOException { return new FileInputStream(file); };
public int getContentLength() throws IOException { return (int)file.length(); };
};
}
} catch (Exception e) {
return null;
}
return null;
}
複製代碼
這裏的 dir 就傳進來的 classpath:file:/Users/lican/git/test/target/test-classes/
因此到了這一行file = new File(dir, name.replace('/', File.separatorChar));
即便進來的是空字符串(""),由於自己是一個目錄,因此 file 是存在的,因此下面的 exists 判斷城裏,最後返回了這個文件夾的 url 資源回去.因而拿到了根目錄.
/* * Returns the JAR Resource for the specified name. */
Resource getResource(final String name, boolean check) {
if (metaIndex != null) {
if (!metaIndex.mayContain(name)) {
return null;
}
}
try {
ensureOpen();
} catch (IOException e) {
throw new InternalError(e);
}
final JarEntry entry = jar.getJarEntry(name);
if (entry != null)
return checkResource(name, check, entry);
if (index == null)
return null;
HashSet<String> visited = new HashSet<String>();
return getResource(name, check, visited);
}
複製代碼
首先會從 jar 包裏面去找""的資源,對於final JarEntry entry = jar.getJarEntry(name);
顯然是拿不到的,這裏確定會返回 null, 程序會繼續向下走到return getResource(name, check, visited);
,咱們看看這裏面的實現.
Resource getResource(final String name, boolean check, Set<String> visited) {
Resource res;
String[] jarFiles;
int count = 0;
LinkedList<String> jarFilesList = null;
/* If there no jar files in the index that can potential contain * this resource then return immediately. */
if((jarFilesList = index.get(name)) == null)
return null;
do {
...
複製代碼
if((jarFilesList = index.get(name)) == null)
這一步其實就永遠是 null 了(index就是一個文件名稱和 jar 包的一對多映射關係),由於 index 裏面不會緩存""爲 key 的東西.因此經過 jar 包去拿跟路徑永遠返回 null.
至此,咱們就明白了爲何經過this.getClass().getClassloader().getResource("")
有時候拿獲得,有時候拿不到的緣由了,那麼有什麼辦法能夠解決嗎?
看過上面的實現,其實解決方案就比較明確了,使final JarEntry entry = jar.getJarEntry(name);
返回不爲空那麼咱們即可以拿到路徑了,這裏咱們用了一個變通的方法.實現以下,能夠在任何狀況下拿到路徑,好比當前的工具類是InstanceInfoUtils,那麼
private static String getRuntimePath() {
String classPath = InstanceInfoUtils.class.getName().replaceAll("\\.", "/") + ".class";
URL resource = InstanceInfoUtils.class.getClassLoader().getResource(classPath);
if (resource == null) {
return null;
}
String urlString = resource.toString();
int insidePathIndex = urlString.indexOf('!');
boolean isInJar = insidePathIndex > -1;
if (isInJar) {
urlString = urlString.substring(urlString.indexOf("file:"), insidePathIndex);
return urlString;
}
return urlString.substring(urlString.indexOf("file:"), urlString.length() - classPath.length());
}
複製代碼
驗證上述 fat-jar 的例子,返回結果爲
file:/Users/lican/git/fat-jar-test/target/fat-jar-test-0.0.1-SNAPSHOT-jar-with-dependencies.jar
複製代碼
符合指望.
爲何 spring boot能夠拿到呢? spring boot 自定義了不少東西來解決這些複雜的狀況,後續有機會詳解,簡單來講