最近在使用一些方法获取当前代码的运行路径的时候,发现代码中使用的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 自定义了不少东西来解决这些复杂的状况,后续有机会详解,简单来讲