java中获取文件时,经常会因为搞不懂路径该怎么写,而导致出现各种获取文件失败的问题。首先对常见获取文件方式进行归类:
第一种,使用Java提供的IO类File、Path获取文件:
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Demo {
// 例如 idea下存在resources/test文件
public static void main(String[] args) {
// 使用File或Path进行获取文件时,主要收到相对路径和绝对路径的影响
String path = "src/main/resources/test";
File file = new File(path);
boolean exists = file.exists();
System.out.println(exists);
exists = Files.exists(Paths.get(path));
System.out.println(exists);
}
}
使用相对路径进行获取文件时,以System.getProperty("user.dir")
路径为起始路径,这样会存在idea与服务器运行时不一致问题。
第二种,使用ClassLoader获取资源文件:
import java.io.File;
import java.io.InputStream;
import java.net.URL;
public class Demo {
// 例如 idea下存在resources/test文件
public static void main(String[] args) {
String path = "test";
// 1 Class获取
URL classResource = Demo.class.getResource("/test"); // URL
InputStream classStream = Demo.class.getResourceAsStream("/test"); // InputStream
File classFile = new File(classResource.getPath());
// 2 ClassLoader获取
URL clResource = Demo.class.getClassLoader().getResource("test"); // URL
InputStream clStream = Demo.class.getClassLoader().getResourceAsStream("test"); // InputStream
File clFile = new File(clResource.getPath());
}
}
需要注意的是,使用ClassLoader相对路径获取时,起始路径是classpath(也约等于resources目录下),而使用class相对路径获取时,起始目录则是当前类的目录,如com.zsl0.Demo,使用Demo.class.getResource("test")
时,是获取com/zsl0/test文件,这则会导致获取resources/test
文件失败,而绝对路径则都是从classpath开始。
因此建议获取resources目录下的资源文件时,使用当前类.class.getClassLoader().getResourceAsStream()
方法进行获取资源文件。
为什么class.getResource()
方法获取的相对路径是从当前类开始的:
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {
// 省略...
public java.net.URL getResource(String name) {
name = resolveName(name); // <--- 将相对路径转化为当前类路径作为起始目录
ClassLoader cl = getClassLoader0();
if (cl == null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}
}
使用当前类的类加载器获取系统类加载器都是相同的,Demo.class.getClassLoader().getResource()
等同于ClassLoader.getSystemResource()
。
public abstract class ClassLoader {
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name); // 1. 这里会获取到上级的类加载
} else {
url = getBootstrapResource(name); // 2. 最终会执行到这里
}
if (url == null) {
url = findResource(name);
}
return url;
}
}
学过Java的类加载器应该知道,默认的三种类加载是继承关系(Bootstrap ClassLoader引导类加载器、Extension ClassLoader扩展类加载器、Application ClassLoader应用类加载器、Custom ClassLoader自定义类加载器),上面代码parent.getResource(name)
,也就是实现双亲委派机制的源头。
通过Debug得出最终会使用sun.misc.Launcher$ExtClassLoader
进行加载资源。
总结:
new File()
当前类.class.getClassLoader().getResourceAsStream()
或者ClassLoader.getSystemResourceAsStream()
SpringBoot提供工具ResourceUtils.getURL("classpath:test")
获取资源文件,当携带classpath:
前缀时,使用类加载器获取,其它情况使用URL获取:
public abstract class ResourceUtils {
/** Pseudo URL prefix for loading from the class path: "classpath:". */
public static final String CLASSPATH_URL_PREFIX = "classpath:";
// 省略其它代码...
public static URL getURL(String resourceLocation) throws FileNotFoundException {
Assert.notNull(resourceLocation, "Resource location must not be null");
if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { // 携带classpath:时进来
String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length());
ClassLoader cl = ClassUtils.getDefaultClassLoader();
URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path)); // 使用类加载获取资源文件
if (url == null) {
String description = "class path resource [" + path + "]";
throw new FileNotFoundException(description +
" cannot be resolved to URL because it does not exist");
}
return url;
}
try { // 其它情况
// try URL
return new URL(resourceLocation);
} catch (MalformedURLException ex) {
// no URL -> treat as file path
try {
return new File(resourceLocation).toURI().toURL();
} catch (MalformedURLException ex2) {
throw new FileNotFoundException("Resource location [" + resourceLocation +
"] is neither a URL not a well-formed file path");
}
}
}
}
从上面可以看到携带classpath:
时,会使用类加载器获取资源,否则使用URL(全局资源定位符,主要由Protocol协议、Destination目的地构成)获取。