Resource / ResourceLoader 资源加载族
概述
Spring 的 Resource 体系是对底层资源的统一抽象,屏蔽了类路径、文件系统、URL、ServletContext 等不同资源来源的差异。ResourceLoader 和 ResourcePatternResolver 提供了资源定位和加载的能力。这一体系是 Spring Boot 自动配置扫描、配置文件加载、类路径探测等所有资源操作的底层基础。
本文将深入拆解 Resource / ResourceLoader 资源加载族的 10 个关键细节,涵盖 Resource 接口方法、ClassPathResource 搜索、多种 Resource 实现对比、路径匹配模式、资源工具类、编码处理、Properties 加载、Spring Boot 启动扫描等核心内容。
本文基于 Spring Framework 6.1.6 源码分析。
1. Resource 接口的 6 个核心方法
Resource 接口是 Spring 资源抽象的核心,继承自 InputStreamSource。
public interface InputStreamSource {
// 获取 InputStream(每次调用返回新的流)
InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
// ① 资源是否存在
boolean exists();
// ② 资源是否可读(内容非空且可读取)
default boolean isReadable() {
return exists();
}
// ③ 资源是否已打开(是否为只能读取一次的流式资源)
default boolean isOpen() {
return false;
}
// ④ 获取资源的 URL 引用
URL getURL() throws IOException;
// ⑤ 获取资源的 File 引用(仅限文件系统资源)
File getFile() throws IOException;
// ⑥ 获取资源的描述信息(用于日志和错误提示)
String getDescription();
}6 个核心方法的作用:
| 方法 | 返回值 | 说明 | 典型实现 |
|---|---|---|---|
getInputStream() | InputStream | 获取资源内容流 | 所有实现 |
exists() | boolean | 判断资源是否存在 | File.exists()、URLConnection.getContentLength() |
isReadable() | boolean | 内容非空且可读 | exists() + 长度判断 |
isOpen() | boolean | 是否只能读取一次 | true — InputStreamResource;false — 其他 |
getURL() | URL | 获取资源 URL | File.toURI().toURL()、ClassLoader.getResource() |
getFile() | File | 获取文件引用 | new File(url.getFile())(仅限文件系统) |
getDescription() | String | 人类可读描述 | "class path resource [config.yml]" |
示例对比:
// ClassPathResource
Resource resource = new ClassPathResource("application.yml");
resource.exists(); // true / false
resource.getDescription(); // "class path resource [application.yml]"
resource.getInputStream(); // ClassLoader.getResourceAsStream()
// resource.getFile() // 可能不支持(JAR 包内资源)
// FileSystemResource
Resource fileResource = new FileSystemResource("/etc/config/app.yml");
fileResource.getFile(); // File("/etc/config/app.yml")
fileResource.getDescription(); // "file [/etc/config/app.yml]"2. ClassPathResource 的类路径搜索
ClassPathResource 是最常用的 Resource 实现,用于从 classpath 中加载资源。
public class ClassPathResource extends AbstractResource {
private final String path; // 资源路径
private final ClassLoader classLoader; // 类加载器
private final Class<?> clazz; // 相对类(可选)
public ClassPathResource(String path) {
this(path, (ClassLoader) null);
}
public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
this.path = StringUtils.cleanPath(path);
this.classLoader = classLoader != null ? classLoader
: ClassUtils.getDefaultClassLoader(); // 使用默认类加载器
this.clazz = null;
}
// 关键:类路径资源的查找实现
@Override
public URL getURL() throws IOException {
URL url = resolveURL();
if (url == null) {
// 资源不存在 → 抛出 FileNotFoundException
throw new FileNotFoundException(getDescription() + " 找不到");
}
return url;
}
// 按优先级搜索类路径
private URL resolveURL() {
if (this.clazz != null) {
// 方式 1:从指定类的相对路径查找
return this.clazz.getResource(this.path);
}
if (this.classLoader != null) {
// 方式 2:从 ClassLoader 查找(推荐,支持 classpath*: 多 jar)
return this.classLoader.getResource(this.path);
}
// 方式 3:从系统 ClassLoader 查找(兜底)
return ClassLoader.getSystemResource(this.path);
}
@Override
public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {
// 方式 1
is = this.clazz.getResourceAsStream(this.path);
} else if (this.classLoader != null) {
// 方式 2
is = this.classLoader.getResourceAsStream(this.path);
} else {
// 方式 3
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " 找不到");
}
return is;
}
}类加载器查找优先级:
ClassPathResource("application.yml")
↓
① ClassUtils.getDefaultClassLoader()
↓
├── Thread.currentThread().getContextClassLoader() ← 线程上下文类加载器(优先)
├── ClassUtils.class.getClassLoader() ← 框架类加载器
└── ClassLoader.getSystemClassLoader() ← 系统类加载器(兜底)
↓
② classLoader.getResource("application.yml")
↓
├── 项目根目录 config/application.yml
├── src/main/resources/application.yml
├── JAR 包中的 BOOT-INF/classes/application.yml
└── Spring Boot 自动配置 jar 中的 application.ymlJAR 包内的资源访问:
// Spring Boot Fat Jar 中,ClassPathResource 通过 LaunchedURLClassLoader 访问
// 路径格式:BOOT-INF/classes/application.yml
// 实际 URL:jar:file:/app/app.jar!/BOOT-INF/classes!/application.yml
ClassPathResource resource = new ClassPathResource("application.yml");
// → classLoader.getResource("application.yml")
// → LaunchedURLClassLoader → BootLoader → NestedJarFile
// → 从 BOOT-INF/classes/application.yml 读取3. FileSystemResource vs ClassPathResource
两种最常用的 Resource 实现,针对不同的资源来源。
// FileSystemResource:文件系统资源
public class FileSystemResource extends AbstractResource {
private final File file; // 文件引用
private final String path; // 文件路径
@Override
public InputStream getInputStream() throws IOException {
// 使用 FileInputStream 读取文件
return Files.newInputStream(this.file.toPath());
}
@Override
public File getFile() {
return this.file; // 直接返回 File 对象
}
@Override
public long contentLength() throws IOException {
// 使用 NIO Files.size() 获取文件大小
return Files.size(this.file.toPath());
}
@Override
public long lastModified() throws IOException {
// 获取文件最后修改时间
return Files.getLastModifiedTime(this.file.toPath()).toMillis();
}
@Override
public boolean exists() {
return this.file.exists(); // File.exists() 检查
}
}
// ClassPathResource:类路径资源
public class ClassPathResource extends AbstractResource {
private final String path;
private final ClassLoader classLoader;
@Override
public InputStream getInputStream() throws IOException {
// 使用 ClassLoader.getResourceAsStream() 从 classpath 加载
InputStream is = this.classLoader.getResourceAsStream(this.path);
if (is == null) {
throw new FileNotFoundException(getDescription() + " 找不到");
}
return is;
}
@Override
public boolean exists() {
URL url = resolveURL();
return url != null; // URL 不为 null 表示存在
}
}完整对比:
| 特性 | FileSystemResource | ClassPathResource |
|---|---|---|
| 底层实现 | FileInputStream / FileChannel | ClassLoader.getResourceAsStream() |
支持 getFile() | ✅ 直接返回 File 对象 | ❌ JAR 包内不支持(抛出异常) |
支持 getURL() | ✅ File.toURI().toURL() | ✅ ClassLoader.getResource() |
| 支持修改操作 | ✅ getOutputStream()、lastModified() | ❌ 只读 |
| 路径前缀 | 无(绝对/相对路径) | 无(相对 classpath) |
| JAR 包内资源 | ❌ 不支持 | ✅ 支持 |
| Fat Jar 支持 | ❌ | ✅(通过 LaunchedURLClassLoader) |
| 性能 | FileChannel 零拷贝更优 | 依赖 ClassLoader 实现 |
使用选择:
// 配置文件:优先 ClassPathResource(兼容 JAR 包运行)
Resource config = new ClassPathResource("application.yml");
// 运行时动态文件:使用 FileSystemResource
Resource logFile = new FileSystemResource("/var/log/myapp/spring.log");
// 不确定来源时:让 ResourceLoader 自动判断
Resource auto = resourceLoader.getResource("/var/log/myapp/spring.log"); // FileSystemResource
Resource auto2 = resourceLoader.getResource("classpath:application.yml"); // ClassPathResource4. PathMatchingResourcePatternResolver.getResources() 的 3 种模式
PathMatchingResourcePatternResolver 是 Spring 中最常用的资源模式解析器,支持多种模式匹配。
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
private final ResourceLoader resourceLoader;
private PathMatcher pathMatcher = new AntPathMatcher(); // 默认使用 Ant 风格匹配
@Override
public Resource[] getResources(String locationPattern) throws IOException {
// 1. 判断是否是 classpath*: 模式
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// classpath*: 多 jar 扫描
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
return findPathMatchingResources(locationPattern);
}
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
// 2. 判断是否是 file: 前缀
if (locationPattern.startsWith("file:")) {
URL url = new URL(locationPattern);
return findResourcesByUrl(url, locationPattern);
}
// 3. 普通路径
if (getPathMatcher().isPattern(locationPattern)) {
return findPathMatchingResources(locationPattern);
}
// 直接返回单个资源
return new Resource[]{getResourceLoader().getResource(locationPattern)};
}
}3 种模式示例:
| 模式 | 前缀/格式 | 说明 | 示例 |
|---|---|---|---|
classpath*: | 多 JAR 扫描 | 搜索所有 classpath 根路径的匹配资源 | classpath*:META-INF/spring/*.imports |
file: | 文件系统 | 直接搜索本地文件系统 | file:/etc/config/**/*.yml |
**/ Ant 风格 | 路径通配符 | 支持 *、**、? 通配符 | classpath:com/example/**/*.class |
findPathMatchingResources() 的实现:
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
// 1. 确定根目录(去掉通配符部分)
String rootDirPath = determineRootDir(locationPattern);
// 2. 获取根目录下的所有资源
Resource[] rootDirResources = getResources(rootDirPath);
// 3. 遍历每个根目录,匹配子资源
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
// 将根目录转换为文件系统路径
File rootDir = rootDirResource.getFile();
// 4. 递归遍历目录,匹配 Ant 风格模式
doRetrieveMatchingFiles(rootDir, subPattern, result);
}
return result.toArray(new Resource[0]);
}classpath*: vs classpath::
// classpath: — 只返回第一个匹配的资源
Resource resource = new ClassPathResource("application.yml");
// → classLoader.getResource("application.yml")
// → 返回 classpath 中的第一个 application.yml
// classpath*: — 返回所有匹配的资源
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:META-INF/spring/*.imports");
// → classLoader.getResources("META-INF/spring/")
// → ClassLoader.getResources() 返回所有 JAR 中匹配的 URL
// → 遍历每个 URL,匹配 *.imports 模式
// → 返回所有 JAR 中 META-INF/spring/*.imports 文件5. ResourceUtils.isUrl() 的协议检测
ResourceUtils 提供了一系列资源协议检测的静态工具方法。
public abstract class ResourceUtils {
// 协议前缀常量
public static final String CLASSPATH_URL_PREFIX = "classpath:";
public static final String FILE_URL_PREFIX = "file:";
public static final String JAR_URL_PREFIX = "jar:";
public static final String URL_PROTOCOL_FILE = "file";
public static final String URL_PROTOCOL_JAR = "jar";
public static final String URL_PROTOCOL_ZIP = "zip";
public static final String URL_PROTOCOL_VFSZIP = "vfszip";
public static final String URL_PROTOCOL_VFS = "vfs";
public static final String URL_PROTOCOL_WSJAR = "wsjar";
// 判断是否是 URL(含 classpath: 虚拟协议)
public static boolean isUrl(String resourceLocation) {
if (resourceLocation == null) return false;
// 检查是否以已知协议前缀开头
if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) return true;
try {
// 尝试解析为 URL
new URL(resourceLocation);
return true;
} catch (MalformedURLException ex) {
return false;
}
}
// 获取 URL 的 JarFile(从 jar:file:... 协议中解包)
public static JarFile getJarFile(URL url) throws IOException {
// 处理 jar: 协议 → 提取 !/ 分隔符之前的路径
String urlFile = url.getFile();
int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR); // "!/"
if (separatorIndex != -1) {
String jarFileUrl = urlFile.substring(0, separatorIndex);
return new JarFile(new URL(jarFileUrl).getFile());
}
return new JarFile(urlFile);
}
// 判断是否是 JAR URL
public static boolean isJarURL(URL url) {
String protocol = url.getProtocol();
return URL_PROTOCOL_JAR.equals(protocol)
|| URL_PROTOCOL_ZIP.equals(protocol)
|| URL_PROTOCOL_VFSZIP.equals(protocol)
|| URL_PROTOCOL_WSJAR.equals(protocol);
}
// 处理 JBoss VFS URL
public static boolean isVfsUrl(URL url) {
return URL_PROTOCOL_VFS.equals(url.getProtocol());
}
}协议检测应用:
ResourceUtils.isUrl("classpath:application.yml"); // true
ResourceUtils.isUrl("file:/etc/config/app.yml"); // true
ResourceUtils.isUrl("http://example.com/config.yml"); // true
ResourceUtils.isUrl("application.yml"); // false(无协议前缀)
ResourceUtils.isJarURL(new URL("jar:file:///app.jar!/config.yml")); // true
ResourceUtils.isJarURL(new URL("file:///etc/config.yml")); // false6. EncodedResource 的编码处理
EncodedResource 是对 Resource 的装饰器,为其添加字符编码信息。
public class EncodedResource implements InputStreamSource {
private final Resource resource; // 被装饰的原始资源
private final String encoding; // 字符编码(如 UTF-8)
private final Charset charset; // 字符集
public EncodedResource(Resource resource, @Nullable String encoding) {
this.resource = resource;
this.encoding = encoding;
this.charset = (encoding != null) ? Charset.forName(encoding) : null;
}
// 获取 Reader(带编码)
public Reader getReader() throws IOException {
if (this.charset != null) {
// 使用指定编码创建 InputStreamReader
return new InputStreamReader(this.resource.getInputStream(), this.charset);
}
// 无编码 → 使用默认编码
return new InputStreamReader(this.resource.getInputStream());
}
@Override
public InputStream getInputStream() throws IOException {
// 底层流的获取委托给原始 Resource
return this.resource.getInputStream();
}
}编码处理在配置加载中的应用:
// PropertiesLoaderUtils 中读取编码的属性文件
public abstract class PropertiesLoaderUtils {
public static Properties loadProperties(EncodedResource resource) throws IOException {
Properties props = new Properties();
fillProperties(props, resource);
return props;
}
static void fillProperties(Properties props, EncodedResource resource) throws IOException {
// 1. 获取 Reader(可能是带编码的)
Reader reader = resource.getReader();
// 2. 根据文件扩展名决定解析方式
String filename = resource.getResource().getFilename();
if (filename != null && filename.endsWith(".xml")) {
// XML 格式:Properties.loadFromXML()
props.loadFromXML(reader);
} else {
// .properties 格式:Properties.load(reader)
props.load(reader);
}
reader.close();
}
}使用场景:
// Spring Boot 加载 application.properties 时指定 UTF-8 编码
Resource resource = new ClassPathResource("application.properties");
EncodedResource encodedResource = new EncodedResource(resource, "UTF-8");
// 读取配置
Reader reader = encodedResource.getReader(); // InputStreamReader with UTF-8
Properties props = PropertiesLoaderUtils.loadProperties(encodedResource);7. PropertiesLoaderUtils.fillProperties() 加载
PropertiesLoaderUtils 是 Spring 内部通用的属性文件加载工具类。
public abstract class PropertiesLoaderUtils {
// 从 Resource 加载 Properties
public static Properties loadProperties(Resource resource) throws IOException {
Properties props = new Properties();
fillProperties(props, resource);
return props;
}
// 从多个 Resource 加载 Properties(后面的覆盖前面的)
public static Properties loadProperties(Resource... resources) throws IOException {
Properties props = new Properties();
for (Resource resource : resources) {
fillProperties(props, resource);
}
return props;
}
// 填充 Properties
static void fillProperties(Properties props, Resource resource) throws IOException {
// 1. 创建 EncodedResource(使用 UTF-8 编码读取)
EncodedResource encodedResource = new EncodedResource(resource, "UTF-8");
// 2. 获取 InputStream
InputStream is = encodedResource.getInputStream();
if (is == null) {
throw new IllegalArgumentException("无法打开资源 [" + resource + "]");
}
try {
// 3. 逐行解析(Properties 的 load 方法)
props.load(is);
// Properties.load() 的解析规则:
// - key=value 或 key:value
// - 空行和 #/! 开头的行为注释
// - 反斜杠 \ 续行
// - ISO 8859-1 编码,不支持中文字符时用 \uXXXX 转义
} finally {
is.close();
}
}
}Properties.load() 逐行解析流程:
Properties.load(is)
↓
LineReader.readLine() 逐行读取
↓
① 跳过空行和注释行(# / !)
↓
② 解析 key(遇到 = / : / 空格 结束)
↓
③ 解析 value(剩余部分,去除首尾空格)
↓
④ 处理续行(结尾反斜杠 \)
↓
⑤ 处理 Unicode 转义(\uXXXX → 实际字符)
↓
⑥ props.put(key, value)
↓
继续下一行...Spring Boot 中的应用:
// Spring Boot 加载 application.properties 的过程
Resource resource = new ClassPathResource("application.properties");
if (resource.exists()) {
Properties props = PropertiesLoaderUtils.loadProperties(resource);
// 然后通过 Environment 绑定到 PropertySource 链
// propertySource = new PropertiesPropertySource("application.properties", props);
}8. Spring Boot 启动时扫描 META-INF/spring/ 的资源加载
Spring Boot 在启动时通过 PathMatchingResourcePatternResolver 扫描 META-INF/spring/ 目录下的特定文件,实现自动配置的 SPI 发现。
// Spring Boot 3.x 中加载 AutoConfiguration.imports
class ImportCandidates {
static List<String> load(Class<?> annotation, ClassLoader classLoader) {
// 1. 构建资源路径:classpath*:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
String location = String.format("META-INF/spring/%s.imports", annotation.getName());
// 2. 使用 PathMatchingResourcePatternResolver 扫描
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader);
try {
// 3. 获取所有匹配的资源
Resource[] resources = resolver.getResources("classpath*:" + location);
for (Resource resource : resources) {
// 4. 读取每个资源文件的内容
readCandidateClasses(resource);
}
} catch (IOException ex) {
throw new IllegalStateException("无法加载自动配置类", ex);
}
}
}Spring Boot 中扫描的 META-INF/spring/ 文件:
| 文件名 | 说明 | 用途 |
|---|---|---|
*.imports | 自动配置类列表 | AutoConfiguration.imports、ApplicationListener.imports |
org.springframework.boot.autoconfigure.AutoConfiguration.imports | 自动配置类列表 | 加载所有 @AutoConfiguration 类 |
org.springframework.boot.SpringApplicationRunListener.imports | RunListener 列表 | 加载启动监听器 |
org.springframework.boot.BootstrapRegistryInitializer.imports | Bootstrap 初始化器 | 加载 Bootstrap 初始化器 |
扫描示例:
// AutoConfiguration.imports 位置
// spring-boot-autoconfigure-3.2.5.jar
// └── META-INF/spring/
// └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
// 文件内容(部分):
// org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
// org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
// org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
// org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
// ...
// 扫描结果:
// PathMatchingResourcePatternResolver.getResources(
// "classpath*:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports")
// → 返回所有 JAR 包中该文件的 Resource 数组
// → 逐个读取 → 合并 → 去重 → 排序ClassLoader.getResources() vs ClassLoader.getResource():
// getResource() — 返回第一个匹配
URL url = classLoader.getResource("META-INF/spring/xxx.imports");
// 只返回 classpath 中出现的第一个
// getResources() — 返回所有匹配(classpath*: 核心)
Enumeration<URL> urls = classLoader.getResources("META-INF/spring/xxx.imports");
// 从所有 JAR 和 classpath 目录中查找
// 返回所有匹配的 URL9. AbstractResource 模板方法的子类实现
AbstractResource 是 Resource 体系的骨架实现,应用了模板方法模式。
public abstract class AbstractResource implements Resource {
// 模板方法:exists() 的默认实现
@Override
public boolean exists() {
// 方式 1:尝试通过 getFile() 检查
try {
File file = getFile();
return file.exists();
} catch (IOException ex) {
// 不支持 getFile()(如 JAR 包内资源)
}
// 方式 2:尝试通过 getInputStream() 检查
try {
InputStream is = getInputStream();
is.close();
return true; // 能打开流 → 资源存在
} catch (IOException ex) {
return false; // 无法打开流 → 资源不存在
}
}
// 模板方法:isReadable() 的默认实现
@Override
public boolean isReadable() {
return exists(); // 默认行为:存在即可读
}
// 子类必须实现
@Override
public abstract String getDescription();
// toString() 模板方法
@Override
public String toString() {
return getDescription();
}
}子类覆盖示例:
// FileSystemResource 覆盖了更多方法
public class FileSystemResource extends AbstractResource {
private final File file;
@Override
public boolean exists() {
// 直接使用 File.exists(),避免打开流
return this.file.exists();
}
@Override
public long contentLength() throws IOException {
// 使用 NIO Files.size(),高效获取大小
return Files.size(this.file.toPath());
}
@Override
public long lastModified() throws IOException {
// 获取文件最后修改时间
return Files.getLastModifiedTime(this.file.toPath()).toMillis();
}
@Override
public String getDescription() {
return "file [" + this.file.getAbsolutePath() + "]";
}
}
// ClassPathResource 也覆盖了部分方法
public class ClassPathResource extends AbstractResource {
@Override
public boolean exists() {
// 通过 ClassLoader.getResource() 检查(轻量)
return resolveURL() != null;
}
@Override
public String getDescription() {
StringBuilder builder = new StringBuilder("class path resource [");
builder.append(this.path).append("]");
return builder.toString();
}
}AbstractResource vs 子类的模板方法模式:
AbstractResource(骨架)
├── exists() ← 模板方法(默认通过 getInputStream() 检测,子类可优化)
├── isReadable() ← 默认 = exists()
├── isOpen() ← 默认 = false
└── getDescription() ← abstract(子类必须实现)
子类优化示例:
├── FileSystemResource.exists() → file.exists()(高效)
├── FileSystemResource.contentLength() → Files.size()(高效)
├── ClassPathResource.exists() → resolveURL() != null(轻量)
└── ClassPathResource.getDescription() → "class path resource [...]"10. VfsResource 对 JBoss VFS 的兼容
VfsResource 是 Spring 为兼容 JBoss/WildFly VFS(Virtual File System)提供的 Resource 实现。
public class VfsResource extends AbstractResource {
private final Object virtualFile; // org.jboss.vfs.VirtualFile 实例
public VfsResource(Object virtualFile) {
this.virtualFile = virtualFile;
}
@Override
public InputStream getInputStream() throws IOException {
// 通过 VfsUtils 反射调用 VirtualFile.openStream()
return VfsUtils.getInputStream(this.virtualFile);
}
@Override
public boolean exists() {
// 反射调用 VirtualFile.exists()
return VfsUtils.exists(this.virtualFile);
}
@Override
public boolean isReadable() {
// 反射调用 VirtualFile.isReadable()
return VfsUtils.isReadable(this.virtualFile);
}
@Override
public URL getURL() throws IOException {
// 反射调用 VirtualFile.toURL()
return VfsUtils.getURL(this.virtualFile);
}
@Override
public File getFile() throws IOException {
// VFS 不保证支持 getFile()(可能抛出异常)
return VfsUtils.getFile(this.virtualFile);
}
@Override
public long contentLength() throws IOException {
// 反射调用 VirtualFile.getContentLength()
return VfsUtils.getSize(this.virtualFile);
}
@Override
public long lastModified() throws IOException {
return VfsUtils.getLastModified(this.virtualFile);
}
@Override
public String getDescription() {
return "VFS resource [" + this.virtualFile + "]";
}
}VfsUtils 反射工具:
public abstract class VfsUtils {
// VirtualFile 类名(JBoss VFS 接口)
private static final String VIRTUAL_FILE_CLASS = "org.jboss.vfs.VirtualFile";
// VFS 方法的反射句柄
private static final Method GET_LAST_MODIFIED;
private static final Method GET_CHILD;
private static final Method GET_ATTACHMENT;
// ... 其他方法
static {
try {
// 获取 VirtualFile 类
Class<?> virtualFileClass = ClassUtils.forName(VIRTUAL_FILE_CLASS,
VfsUtils.class.getClassLoader());
// 获取各方法的反射引用
GET_LAST_MODIFIED = virtualFileClass.getMethod("getLastModified");
GET_CHILD = virtualFileClass.getMethod("getChild", String.class);
// ...
} catch (Exception ex) {
throw new IllegalStateException("无法初始化 VFS 工具类", ex);
}
}
// 反射调用 VirtualFile.getInputStream()
public static InputStream getInputStream(Object vfsResource) throws IOException {
// VFS 3.0+ 使用 VirtualFile.openStream()
return (InputStream) invokeMethod(GET_INPUT_STREAM, vfsResource);
}
// 反射调用 VirtualFile.exists()
public static boolean exists(Object vfsResource) {
try {
return (boolean) invokeMethod(GET_EXISTS, vfsResource);
} catch (Exception ex) {
return false;
}
}
// 反射调用 VirtualFile.getURL()
public static URL getURL(Object vfsResource) throws IOException {
return (URL) invokeMethod(GET_URL, vfsResource);
}
}触发条件:
// DefaultResourceLoader 中检测 VFS
public class DefaultResourceLoader implements ResourceLoader {
@Override
public Resource getResource(String location) {
// 检查是否是 VFS URL
if (location.startsWith("vfs:")) {
try {
// 使用 VfsResourceLoader 加载
return new VfsResource(VfsUtils.getVirtualFile(location));
} catch (Exception ex) {
throw new IOException("无法解析 VFS 资源: " + location, ex);
}
}
// 其他协议...
}
}适用场景:
| 容器 | VFS 支持 | 说明 |
|---|---|---|
| JBoss AS / WildFly | ✅ VfsResource | VFS 实现,不支持 File |
| Tomcat / Jetty | ❌ 不使用 VFS | 标准文件系统 |
| Spring Boot Fat Jar | ❌ 不使用 VFS | LaunchedURLClassLoader + NestedJarFile |
| Undertow | ❌ 不使用 VFS | 标准文件系统 |
Spring Boot 与 VFS 的关系:Spring Boot 应用的嵌入式容器使用标准文件系统,不会遇到 JBoss VFS。VfsResource 主要用于将 Spring Boot 应用部署到 JBoss/WildFly 等传统应用服务器时的兼容场景。
总结
Resource / ResourceLoader 资源加载族的 10 个细节点总结如下:
| # | 细节点 | 核心类/机制 |
|---|---|---|
| ① | Resource 接口的 6 个核心方法 | getInputStream() / exists() / isReadable() / isOpen() / getURL() / getFile() |
| ② | ClassPathResource 的类路径搜索 | ClassLoader.getResource(path) → URL → InputStream,按线程/框架/系统类加载器顺序 |
| ③ | FileSystemResource vs ClassPathResource | 前者使用 FileInputStream 直接操作文件系统,后者使用 ClassLoader.getResourceAsStream() 支持 JAR |
| ④ | PathMatchingResourcePatternResolver.getResources() 的 3 种模式 | classpath*: 多 JAR 扫描 / file: 文件系统 / **/ Ant 风格通配符匹配 |
| ⑤ | ResourceUtils.isUrl() 的协议检测 | file: / jar: / classpath: / http: 等协议前缀,支持 isJarURL() / isVfsUrl() |
| ⑥ | EncodedResource 的编码处理 | getReader() → new InputStreamReader(inputStream, charset),装饰器模式 |
| ⑦ | PropertiesLoaderUtils.fillProperties() 加载 | EncodedResource.getReader() → Properties.load(reader) → 逐行解析 |
| ⑧ | Spring Boot 启动时扫描 META-INF/spring/ | PathMatchingResourcePatternResolver.getResources("classpath*:META-INF/spring/*.imports") |
| ⑨ | AbstractResource 模板方法的子类实现 | exists() 默认调用 getInputStream() 检查,子类可优化(如 FileSystemResource 用 File.exists()) |
| ⑩ | VfsResource 对 JBoss VFS 的兼容 | VirtualFile 包装 → VfsUtils 反射调用 → getInputStream() / exists() 等 |