Spring 源码深度剖析:ResourceLoader 的资源加载与路径解析策略
从 Resource 到 ResourceLoader 的自然演进
上一篇文章详细拆解了 Resource 接口及其七大实现类,它们解决了"如何描述一种资源"的问题。但在真实项目中,我们通常不希望手动判断资源类型然后去 new ClassPathResource() 或 new FileSystemResource()------我们需要一个工厂层,它能够根据给定的位置字符串,智能地选择最合适的 Resource 实现并返回。
这正是一篇将要分析的 ResourceLoader 接口的设计定位:它是资源定位字符串与具体 Resource 实例之间的桥梁,是"按名称加载"这一语义的抽象入口。
知识体系全景图
在深入之前,先梳理 ResourceLoader 涉及的完整知识体系:
ResourceLoader 知识体系
│
├── 核心接口
│ ├── ResourceLoader (资源定位)
│ │ ├── getResource(String) → 根据位置字符串返回 Resource
│ │ └── getClassLoader()
│ │
│ ├── ResourcePatternResolver extends ResourceLoader (批量扫描)
│ │ └── getResources(String) → 返回 Resource[] 数组
│ │
│ └── ProtocolResolver (自定义协议扩展 SPI)
│ └── resolve(String, ResourceLoader) → Resource or null
│
├── 默认实现
│ ├── DefaultResourceLoader (基础前缀路由)
│ │ ├── classpath: → ClassPathResource
│ │ ├── http:/https: → UrlResource
│ │ ├── file: → FileUrlResource
│ │ └── 无前缀 → 上下文相关
│ │
│ └── PathMatchingResourcePatternResolver (Ant 通配符匹配)
│ ├── classpath*: → 跨 jar 扫描
│ ├── classpath: → 单 classpath 扫描
│ └── file: → 文件系统通配符匹配
│
├── ApplicationContext 体系中的角色
│ ├── AbstractApplicationContext → ResourceLoader
│ ├── AbstractRefreshableConfigApplicationContext → ResourcePatternResolver
│ └── WebApplicationContext → ServletContextResourceLoader
│
└── Spring Boot 中的应用
├── 自动配置扫描 (classpath*:META-INF/spring.factories)
├── 配置文件加载 (application.properties)
├── 静态资源映射 (classpath:/static/)
└── 自定义 ProtocolResolver (配置中心)
核心契约:ResourceLoader 接口
ResourceLoader 的契约相当简洁,仅包含两个方法:
java
package com.example.spring.resourceloader.demo;
public interface ResourceLoader {
String CLASSPATH_URL_PREFIX = "classpath:";
Resource getResource(String location);
ClassLoader getClassLoader();
}
getResource(String)接收一个位置描述字符串,返回对应的 Resource 对象getClassLoader()返回用于加载类路径资源的 ClassLoaderCLASSPATH_URL_PREFIX是一个常量,值为"classpath:",用作显式类路径前缀
上下文敏感的默认行为
当调用方不传入任何前缀时,ResourceLoader 的默认行为取决于其所在的 ApplicationContext 类型。这种"上下文敏感"的设计使得同一个 getResource("config/app.properties") 调用在不同环境下会自动返回恰当的资源类型:
java
package com.example.spring.resourceloader.demo;
public class ContextAwareResourceLoadingDemo {
public static void main(String[] args) {
demonstrateContextSensitiveLoading();
}
static void demonstrateContextSensitiveLoading() {
// 假设存在以下三种 ApplicationContext,它们分别是:
// ClassPathXmlApplicationContext ctx1 = ...;
// ctx1.getResource("config/app.properties")
// → 返回 ClassPathResource(因为上下文本身是类路径导向的)
// FileSystemXmlApplicationContext ctx2 = ...;
// ctx2.getResource("config/app.properties")
// → 返回 FileSystemResource(因为上下文基于文件系统)
// WebApplicationContext ctx3 = ...;
// ctx3.getResource("config/app.properties")
// → 返回 ServletContextResource(因为上下文位于 Web 容器中)
System.out.println("不同 ApplicationContext 下,同一个路径字符串");
System.out.println("会解析为不同 Resource 子类型的实例");
}
}
前缀覆盖机制
如果你想在特定上下文中强制指定资源类型,只需添加相应前缀:
| 前缀 | 示例 | 解析结果 |
|---|---|---|
classpath: |
classpath:com/myapp/config.xml |
ClassPathResource |
file: |
file:///data/config.xml |
FileUrlResource(一种 UrlResource 的子类) |
http: / https: |
http://myserver/logo.png |
UrlResource |
| (无前缀) | /data/config.xml |
取决于当前 ApplicationContext 的类型 |
扩展:ResourcePatternResolver 与 Ant 风格通配符
单个资源加载在很多场景下是不够的。当需要一次性定位某目录下所有匹配特定模式的 .xml 文件时,ResourcePatternResolver 接口登场了:
java
package com.example.spring.resourceloader.demo;
import java.io.IOException;
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
classpath*: 与 classpath: 的内部实现差异
这是理解 Spring 资源加载最关键的细节之一。classpath*: 和 classpath: 在底层使用了完全不同的 ClassLoader API:
| 特征 | classpath: |
classpath*: |
|---|---|---|
| 底层 API | ClassLoader.getResource() |
ClassLoader.getResources() |
| 返回数量 | 单个 URL(第一个匹配) | 多个 URL(所有匹配) |
| 扫描机制 | 不扫描,直接定位 | 需枚举所有 classpath 条目 |
| 通配符支持 | 仅结合 PathMatchingResourcePatternResolver | 结合 PathMatchingResourcePatternResolver |
| 性能 | 快(直接查询) | 较慢(需要遍历所有条目) |
| 典型场景 | 加载唯一配置文件 | 跨 jar 收集同名文件 |
java
// 底层对比
public class ClasspathStarVsSingleDemo {
public static void main(String[] args) throws Exception {
// === classpath: 的底层实现 ===
// ClassLoader.getResource() 按 classpath 顺序返回第一个匹配
URL singleUrl = Thread.currentThread()
.getContextClassLoader().getResource("META-INF/spring.factories");
System.out.println("classpath: 返回: " + singleUrl);
// === classpath*: 的底层实现 ===
// ClassLoader.getResources() 扫描所有 classpath 条目
Enumeration<URL> allUrls = Thread.currentThread()
.getContextClassLoader().getResources("META-INF/spring.factories");
System.out.println("classpath*: 返回:");
while (allUrls.hasMoreElements()) {
System.out.println(" - " + allUrls.nextElement());
}
}
}
通配符模式匹配的执行流程详解
PathMatchingResourcePatternResolver 处理 classpath*:org/example/**/*.class 的完整流程如下:
getResources("classpath*:org/example/**/*.class")
│
├── 步骤1: 判定前缀为 classpath*: 且包含通配符
│
├── 步骤2: determineRootDir()
│ └── 找到最后一个非通配符路径段
│ └── rootDir = "classpath*:org/example/"
│ └── subPattern = "**/*.class"
│
├── 步骤3: getResources(rootDir) 递归调用
│ └── ClassLoader.getResources("org/example/")
│ ├── jar:file://app.jar!/org/example/
│ ├── jar:file://lib/lib1.jar!/org/example/
│ └── file:/classes/org/example/
│
├── 步骤4: 对每个 rootDirResource,判断是 Jar 还是文件系统
│ ├── Jar URL → doFindPathMatchingJarResources()
│ │ └── JarFile.getJarEntry() → 遍历 jar 内所有条目
│ │ └── AntPathMatcher.match("**/*.class", entryName)
│ └── 文件系统 → doFindPathMatchingFileResources()
│ └── 递归遍历目录树
│ └── AntPathMatcher.match() 逐文件判断
│
└── 步骤5: 合并结果 → 返回 Resource[]
DefaultResourceLoader 源码级剖析
DefaultResourceLoader 是 Spring 中 ResourceLoader 的唯一直接实现,其他 ApplicationContext 都是它的子类。让我们完整重现代码来看清楚它的解析逻辑:
java
package com.example.spring.resourceloader.impl;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
public class DefaultResourceLoader implements ResourceLoader {
private ClassLoader classLoader;
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);
public DefaultResourceLoader() {
this.classLoader = Thread.currentThread().getContextClassLoader();
}
public DefaultResourceLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public ClassLoader getClassLoader() {
return this.classLoader != null
? this.classLoader
: Thread.currentThread().getContextClassLoader();
}
public void addProtocolResolver(ProtocolResolver resolver) {
this.protocolResolvers.add(resolver);
}
@Override
public Resource getResource(String location) {
// 第一步:遍历所有注册的 ProtocolResolver,看有没有能处理这个 location 的
for (ProtocolResolver resolver : this.protocolResolvers) {
Resource resource = resolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
// 第二步:根据 location 的前缀或格式决定 Resource 类型
if (location.startsWith("/")) {
return getResourceByPath(location);
} else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(
location.substring(CLASSPATH_URL_PREFIX.length()),
getClassLoader()
);
} else {
try {
URL url = new URL(location);
if (url.getProtocol().equals("file")) {
return new FileUrlResource(url);
}
return new UrlResource(url);
} catch (MalformedURLException ex) {
return getResourceByPath(location);
}
}
}
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
// ---- ProtocolResolver 扩展点 ----
public interface ProtocolResolver {
Resource resolve(String location, ResourceLoader resourceLoader);
}
}
// ---- 辅助类 ----
class ClassPathContextResource extends ClassPathResource {
ClassPathContextResource(String path, ClassLoader cl) {
super(path, cl);
}
}
class FileUrlResource extends UrlResource {
FileUrlResource(URL url) {
super(url);
}
}
解析决策树
getResource() 方法的处理流程可以用下面的决策树清晰表达:
getResource(location)
│
├── ① 遍历 protocolResolvers
│ └── 若某 resolver 返回非 null → 直接返回该 Resource
│
├── ② location 以 "/" 开头
│ └── 调用 getResourceByPath() → 返回 ClassPathContextResource
│
├── ③ location 以 "classpath:" 开头
│ └── 截掉前缀,构造 ClassPathResource
│
├── ④ location 能被解析为 URL
│ ├── 协议为 "file" → FileUrlResource
│ └── 其他协议 → UrlResource
│
└── ⑤ URL 解析失败
└── 兜底调用 getResourceByPath() → 返回 ClassPathContextResource
ResourceLoader 接口体系与继承层级
ResourceLoader 不是只有一个------它是一个接口,有多个实现类。 但你在应用中通常只接触到一个实例,因为 ApplicationContext(容器)本身就实现了 ResourceLoader:
ResourceLoader (接口)
└── ResourcePatternResolver (子接口,新增 classpath* 支持)
└── ApplicationContext (子接口,容器本身!)
├── ClassPathXmlApplicationContext
├── AnnotationConfigApplicationContext
└── SpringApplication.run() 返回的 ApplicationContext
当你写 @Autowired private ResourceLoader resourceLoader; 时,注入进来的其实就是 ApplicationContext 本身。主要实现类:
| 实现类 | 用途 |
|---|---|
DefaultResourceLoader |
标准实现,ApplicationContext 的默认基类 |
ServletContextResourceLoader |
覆写 getResourceByPath(),让 / 开头路径走 ServletContext |
FileSystemResourceLoader |
覆写 getResourceByPath(),让路径走文件系统 |
PathMatchingResourcePatternResolver |
实现 ResourcePatternResolver,支持 classpath*: 和通配符 |
准确说:应用运行时只有一个 ResourceLoader 实例(就是你的 ApplicationContext),但其背后有一套类继承体系来支持不同环境下的差异化行为。
但某些情况下也可能存在多个 ResourceLoader 实例:
| 情况 | 数量 | 说明 |
|---|---|---|
| 标准 Spring Boot 应用 | 1 个 | 就是 ApplicationContext 本身 |
| 传统 Spring MVC(父子容器,web.xml 配置) | 2 个 | Root ApplicationContext + DispatcherServlet ApplicationContext,各自都是独立的 ResourceLoader |
手动 new DefaultResourceLoader() |
1 + N 个 | 容器 1 个 + 你手动创建的 N 个 |
| 容器层级嵌套 | N 个 | 每个层级的容器都是独立的 ResourceLoader |
Spring Boot 启动过程中,SpringApplication.run() 也会在准备环境阶段创建临时的 DefaultResourceLoader 来读取引导配置,容器正式创建后即被丢弃。
ProtocolResolver SPI 扩展点
为什么需要 ProtocolResolver?
回顾 DefaultResourceLoader.getResource() 的解析流程,它只内置了有限的协议识别:
getResource("xxx")
├── "/" 开头 → getResourceByPath()
├── "classpath:" 前缀 → ClassPathResource
├── 合法 URL(http/file/ftp) → UrlResource
└── 都不匹配 → 当作 classpath 路径
问题来了:如果你的资源不在 classpath 里,也不是标准 URL 协议,怎么办? 比如自研配置中心的 apollo://db-config/jdbc.url------DefaultResourceLoader 不认识这个前缀。
这就是 ProtocolResolver 要解决的问题:给 DefaultResourceLoader "教会"新的协议。
工作原理:责任链模式
ProtocolResolver 在 getResource() 流程中最先被调用------比内置检查都靠前:
getResource("apollo://db-config/jdbc.url")
│
├── ★ 第 1 步:遍历 ProtocolResolver 链
│ ├── ResolverA.resolve("apollo://...") → 不认识,返回 null
│ ├── ResolverB.resolve("apollo://...") → 认识!→ 返回 Resource ✓
│ └── [后续步骤不再执行]
│
├── 第 2 步:检查 "/" 前缀 ← 没走到这里
├── 第 3 步:检查 "classpath:" 前缀 ← 没走到这里
└── ...
规则: 返回 Resource → 直接使用;返回 null → 交给下一个 Resolver 或内置流程。
可以类比为:DefaultResourceLoader 是一个翻译官,默认只会翻译几门语言。ProtocolResolver 就是给它附加的翻译手册。
接口定义与基本示例
Spring 4.3 引入了 addProtocolResolver() 方法:
java
package org.springframework.core.io;
@FunctionalInterface
public interface ProtocolResolver {
Resource resolve(String location, ResourceLoader resourceLoader);
}
java
// ===== 自定义 ProtocolResolver 示例:支持 myconfig:// 协议 =====
public class MyConfigProtocolResolver implements ProtocolResolver {
private static final String PREFIX = "myconfig://";
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
if (!location.startsWith(PREFIX)) {
return null; // 不处理,交给下一个
}
String configPath = location.substring(PREFIX.length());
byte[] configData = ConfigCenterClient.fetch(configPath);
return new ByteArrayResource(configData, "config:" + configPath);
}
}
// 注册到 DefaultResourceLoader
DefaultResourceLoader loader = new DefaultResourceLoader();
loader.addProtocolResolver(new MyConfigProtocolResolver());
Resource resource = loader.getResource("myconfig://app/db-config");
ProtocolResolver 注册时机与 Spring Boot 集成
方式一:手动注册(太晚了,不推荐)
java
ConfigurableApplicationContext context = SpringApplication.run(MyApp.class, args);
DefaultResourceLoader loader = (DefaultResourceLoader) context.getResourceLoader();
loader.addProtocolResolver(new MyConfigProtocolResolver());
// 问题:此时容器已启动完毕,所有配置和 Bean 都已加载完成,Resolver 注册太晚了
方式二:通过 ApplicationContextInitializer 注册(推荐)
ApplicationContextInitializer 在 ApplicationContext 创建之后、refresh() 执行之前被调用,正是注册 ProtocolResolver 的最佳时机。
第一步------编写实现类:
java
public class ProtocolResolverInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
if (context instanceof DefaultResourceLoader) {
((DefaultResourceLoader) context)
.addProtocolResolver(new MyConfigProtocolResolver());
}
}
}
第二步------在 src/main/resources/META-INF/spring.factories 中注册:
org.springframework.context.ApplicationContextInitializer=\
com.example.config.ProtocolResolverInitializer
整个时序:
Spring Boot 启动
→ 创建 ApplicationContext
→ 扫描 spring.factories → 发现 ProtocolResolverInitializer → 执行 initialize()
★ 在这里注册了 ProtocolResolver
→ refresh() 开始
→ 加载 application.yml ← 此时可以识别自定义协议了
→ 扫描 @ComponentScan
→ Singleton Bean 初始化
→ 容器启动完成
生产级示例:对接 Apollo 配置中心
java
public class ApolloProtocolResolver implements ProtocolResolver {
private static final String PREFIX = "apollo://";
private final Config apolloConfig;
public ApolloProtocolResolver(String namespace) {
this.apolloConfig = ConfigService.getConfig(namespace);
}
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
if (!location.startsWith(PREFIX)) {
return null;
}
String propertyKey = location.substring(PREFIX.length());
String value = apolloConfig.getProperty(propertyKey, null);
if (value == null) {
return null;
}
return new ByteArrayResource(
value.getBytes(StandardCharsets.UTF_8),
"apollo:" + propertyKey);
}
}
PathMatchingResourcePatternResolver 详解
当需要 Ant 风格的通配符匹配时(如 classpath*:org/springframework/**/*.class),PathMatchingResourcePatternResolver 是核心执行引擎。
java
package com.example.spring.resourceloader.impl;
import java.io.*;
import java.util.*;
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
private final ResourceLoader resourceLoader;
private PathMatcher pathMatcher = new AntPathMatcher();
public PathMatchingResourcePatternResolver() {
this.resourceLoader = new DefaultResourceLoader();
}
public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public PathMatchingResourcePatternResolver(ClassLoader classLoader) {
this.resourceLoader = new DefaultResourceLoader(classLoader);
}
public ResourceLoader getResourceLoader() {
return this.resourceLoader;
}
public PathMatcher getPathMatcher() {
return this.pathMatcher;
}
public void setPathMatcher(PathMatcher pathMatcher) {
this.pathMatcher = pathMatcher;
}
@Override
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
}
@Override
public Resource[] getResources(String locationPattern) throws IOException {
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
String pathWithoutPrefix = locationPattern.substring(
CLASSPATH_ALL_URL_PREFIX.length());
if (getPathMatcher().isPattern(pathWithoutPrefix)) {
// classpath*:...且包含通配符 → 递归匹配
return findPathMatchingResources(locationPattern);
} else {
// classpath*:...但无通配符 → 找出所有同名资源
return findAllClassPathResources(pathWithoutPrefix);
}
} else {
int prefixEnd = locationPattern.indexOf(":") + 1;
if (getPathMatcher().isPattern(
locationPattern.substring(prefixEnd))) {
return findPathMatchingResources(locationPattern);
} else {
return new Resource[] {
getResourceLoader().getResource(locationPattern)
};
}
}
}
// ---- 核心:查找所有 classpath 下匹配通配符模式的资源 ----
protected Resource[] findPathMatchingResources(String locationPattern)
throws IOException {
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
// 先找 rootDir 下所有 Resource(就是去掉通配符部分的目录)
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirURL = rootDirResource.getURL();
if (ResourceUtils.isJarURL(rootDirURL)) {
result.addAll(doFindPathMatchingJarResources(
rootDirResource, rootDirURL, subPattern));
} else {
result.addAll(doFindPathMatchingFileResources(
rootDirResource, subPattern));
}
}
return result.toArray(new Resource[0]);
}
// ---- 文件系统目录下的通配符匹配 ----
protected Set<Resource> doFindPathMatchingFileResources(
Resource rootDirResource, String subPattern) throws IOException {
File rootDir;
try {
rootDir = rootDirResource.getFile().getAbsoluteFile();
} catch (IOException ex) {
return Collections.emptySet();
}
return doFindMatchingFileSystemResources(rootDir, subPattern);
}
protected Set<Resource> doFindMatchingFileSystemResources(
File rootDir, String subPattern) throws IOException {
// 用 AntPathMatcher 找出所有匹配的文件
Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
for (File file : matchingFiles) {
result.add(new FileSystemResource(file));
}
return result;
}
// ---- 递归文件匹配核心算法 ----
protected Set<File> retrieveMatchingFiles(File rootDir, String pattern)
throws IOException {
if (!rootDir.exists() || !rootDir.isDirectory() || !rootDir.canRead()) {
return Collections.emptySet();
}
// 构建完整路径模式(统一使用 "/")
String fullPattern = rootDir.getAbsolutePath()
.replace(File.separatorChar, '/');
if (!pattern.startsWith("/")) {
fullPattern += "/";
}
fullPattern += pattern.replace(File.separatorChar, '/');
Set<File> result = new LinkedHashSet<>(8);
doRetrieveMatchingFiles(fullPattern, rootDir, result);
return result;
}
protected void doRetrieveMatchingFiles(String fullPattern, File dir,
Set<File> result) {
File[] dirContents = dir.listFiles();
if (dirContents == null) return;
for (File content : dirContents) {
String currPath = content.getAbsolutePath()
.replace(File.separatorChar, '/');
if (content.isDirectory()
&& getPathMatcher().matchStart(fullPattern, currPath + "/")) {
doRetrieveMatchingFiles(fullPattern, content, result);
}
if (getPathMatcher().match(fullPattern, currPath)) {
result.add(content);
}
}
}
// ---- 查找所有 classpath 条目中的同名资源 ----
protected Resource[] findAllClassPathResources(String location)
throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
Set<Resource> result = doFindAllClassPathResources(path);
return result.toArray(new Resource[0]);
}
protected Set<Resource> doFindAllClassPathResources(String path)
throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getResourceLoader().getClassLoader();
Enumeration<URL> resourceUrls = (cl != null
? cl.getResources(path)
: ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(new UrlResource(url));
}
if ("".equals(path)) {
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
private void addAllClassLoaderJarRoots(ClassLoader cl,
Set<Resource> result) {
// 处理空路径------需要把所有 jar 的根目录也加入结果集
// 此处简化,实际实现涉及 ClassLoader 层次遍历
}
private String determineRootDir(String location) {
int prefixEnd = location.indexOf(":") + 1;
int rootDirEnd = location.length();
while (rootDirEnd > prefixEnd
&& getPathMatcher().isPattern(
location.substring(prefixEnd, rootDirEnd))) {
rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
}
if (rootDirEnd == 0) {
rootDirEnd = prefixEnd;
}
return location.substring(0, rootDirEnd);
}
private Resource resolveRootDirResource(Resource original) {
return original;
}
private Set<Resource> doFindPathMatchingJarResources(
Resource rootDirResource, URL rootDirURL, String subPattern) {
// Jar 文件中通配符查找(此处简化)
return Collections.emptySet();
}
}
路径模式匹配的执行流程
对于 classpath*:org/app/config/**/*.properties 这样的 pattern:
- 提取根目录和子模式 :
rootDirPath = "classpath*:org/app/config/",subPattern = "**/*.properties" - 获取根目录下所有条目 :调用
findAllClassPathResources("org/app/config/"),这里使用ClassLoader.getResources()返回所有 jar 和目录中该路径下的 URL - 逐个条目匹配:对每个根目录资源,判断是文件系统还是 jar,分别调用对应的匹配方法
- Ant 风格匹配 :在文件系统场景下,递归遍历目录,用
AntPathMatcher.match()判断每个文件的完整路径是否满足子模式 - 收集结果 :所有匹配的文件被包装为
FileSystemResource或UrlResource返回
AbstractApplicationContext:同时是 ResourceLoader 也是 ResourcePatternResolver
AbstractApplicationContext 继承 DefaultResourceLoader 并持有 PathMatchingResourcePatternResolver,因此一个 ApplicationContext 实例同时扮演着两种角色:
java
package com.example.spring.resourceloader.demo;
public class ApplicationContextResourceLoadingDemo {
public static void main(String[] args) {
// 在实际项目中,ApplicationContext 同时支持两种接口:
// 作为 ResourceLoader:
// Resource single = ctx.getResource("classpath:app.properties");
// 作为 ResourcePatternResolver:
// Resource[] all = ctx.getResources("classpath*:META-INF/spring.factories");
System.out.println("AbstractApplicationContext 的设计:");
System.out.println(" - 继承 DefaultResourceLoader(实现 ResourceLoader)");
System.out.println(" - 通过 ConfigurableApplicationContext 间接实现 ResourcePatternResolver");
System.out.println(" - 内部将 getResources() 委托给 PathMatchingResourcePatternResolver");
}
}
委托模式的源码分析
java
// AbstractApplicationContext 中关于资源加载的核心源码
public abstract class AbstractApplicationContext
extends DefaultResourceLoader // ← 继承 ResourceLoader 能力
implements ConfigurableApplicationContext {
// 持有 ResourcePatternResolver 委托实例
private ResourcePatternResolver resourcePatternResolver;
public AbstractApplicationContext() {
// 构造时创建委托对象,传入 this 作为 ResourceLoader
this.resourcePatternResolver =
new PathMatchingResourcePatternResolver(this);
}
// 覆盖父类的 getResourceByPath ------ 实现上下文敏感的默认行为
@Override
protected Resource getResourceByPath(String path) {
// 默认实现返回 ClassPathContextResource
// WebApplicationContext 会重写为 ServletContextResource
return new ClassPathContextResource(path, getClassLoader());
}
// 资源批量查找(委托模式)
@Override
public Resource[] getResources(String locationPattern) throws IOException {
// 完全委托给 PathMatchingResourcePatternResolver
return this.resourcePatternResolver.getResources(locationPattern);
}
}
不同 ApplicationContext 的 getResourceByPath 实现
| ApplicationContext 类型 | getResourceByPath() 返回类型 | 默认行为(无前缀时) |
|---|---|---|
ClassPathXmlApplicationContext |
ClassPathContextResource |
从 classpath 根目录查找 |
FileSystemXmlApplicationContext |
FileSystemContextResource |
从文件系统当前路径查找 |
AnnotationConfigApplicationContext |
ClassPathContextResource |
从 classpath 根目录查找 |
GenericWebApplicationContext |
ServletContextResource |
从 Web 应用根目录查找 |
AnnotationConfigServletWebServerApplicationContext |
ServletContextResource |
从 Web 应用根目录查找 |
关键设计模式总结
ApplicationContext 的资源加载双重角色
│
├── 角色1: ResourceLoader (继承 DefaultResourceLoader)
│ ├── getResource("classpath:config.xml") → ClassPathResource
│ ├── getResource("file:/etc/config.xml") → FileUrlResource
│ └── getResource("http://host/config") → UrlResource
│
├── 角色2: ResourcePatternResolver (委托)
│ ├── getResources("classpath*:META-INF/*.xml") → Resource[]
│ ├── getResources("classpath:config/**/*.xml") → Resource[]
│ └── getResources("file:/etc/**/*.xml") → Resource[]
│
└── 模板方法模式
└── getResourceByPath() 由子类覆写
├── Default: ClassPathContextResource
└── WebApplication: ServletContextResource
性能考量
| 操作 | 性能 | 说明 |
|---|---|---|
getResource() + classpath: |
O(1) | 直接通过 ClassLoader 查询,极快 |
getResource() + file: |
O(1) | 直接构造 UrlResource/FileSystemResource |
getResources() + classpath*: + 无通配符 |
O(n) (n=classpath 条目数) | 遍历所有 classpath 条目但无模式匹配 |
getResources() + classpath*:**/*.class |
O(n×m) (n=条目数, m=jar 内文件数) | 最慢------遍历所有 jar 的全部条目 + Ant 模式匹配 |
实战:实现一个配置文件自动发现工具
以下展示了一个结合 ResourceLoader 和 ResourcePatternResolver 的实际工具:
java
package com.example.spring.resourceloader.demo;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class ConfigurationAutoDiscoverer {
private final ResourcePatternResolver patternResolver;
private final ResourceLoader resourceLoader;
public ConfigurationAutoDiscoverer() {
DefaultResourceLoader loader = new DefaultResourceLoader();
this.resourceLoader = loader;
this.patternResolver = new PathMatchingResourcePatternResolver(loader);
}
/**
* 从多个位置加载配置,按优先级合并
*/
public Properties loadMergedConfiguration(String baseName) throws IOException {
Properties merged = new Properties();
// 优先级1:classpath 根目录(高优先级,因为更具体)
Resource classpathRes = resourceLoader.getResource(
"classpath:" + baseName);
if (classpathRes.exists()) {
loadProperties(classpathRes, merged);
System.out.println("[加载] classpath:" + baseName);
}
// 优先级2:classpath 下所有匹配 pattern 的文件(作为默认值)
Resource[] allMatches = patternResolver.getResources(
"classpath*:config/**/" + baseName);
for (Resource res : allMatches) {
loadProperties(res, merged);
System.out.println("[加载] " + res.getDescription());
}
// 优先级3:外部文件系统路径
String externalPath = System.getProperty("app.config.dir", "/etc/myapp");
Resource externalRes = resourceLoader.getResource(
"file:" + externalPath + "/" + baseName);
if (externalRes.exists()) {
loadProperties(externalRes, merged);
System.out.println("[加载] file:" + externalPath + "/" + baseName);
}
return merged;
}
private void loadProperties(Resource resource, Properties target)
throws IOException {
try (InputStream is = resource.getInputStream();
Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Properties props = new Properties();
props.load(reader);
target.putAll(props);
}
}
public static void main(String[] args) throws IOException {
ConfigurationAutoDiscoverer discoverer = new ConfigurationAutoDiscoverer();
Properties config = discoverer.loadMergedConfiguration("application.properties");
System.out.println("\n最终合并配置:");
config.forEach((k, v) -> System.out.println(" " + k + " = " + v));
}
}
ResourceLoaderAware ------ 业务代码如何获取 ResourceLoader
前面讲的都是 Spring 内部怎样用 ResourceLoader 去加载配置文件。但你的业务 Bean 也可能需要自己加载资源------读取模板、导出 CSV、动态加载 SQL 脚本等。问题在于:你的 Bean 只是一个 @Component,它怎么拿到容器的 ResourceLoader?Spring 提供了三种方式。
方式一:实现 ResourceLoaderAware 接口(传统回调)
ResourceLoaderAware 属于 Spring 的 Aware 回调体系 ------Bean 实现该接口后,Spring 会在初始化时自动调用 setResourceLoader() 注入:
java
package org.springframework.context;
public interface ResourceLoaderAware extends Aware {
void setResourceLoader(ResourceLoader resourceLoader);
}
后台原理: 容器通过 ApplicationContextAwareProcessor(一个 BeanPostProcessor)检查每个 Bean,发现实现了 ResourceLoaderAware 就回调:
Bean 生命周期:
1. 实例化 → 2. @Autowired 注入 → 3. Aware 回调(★这里) → 4. @PostConstruct → 5. 就绪
java
@Component
public class ConfigLoader implements ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override // ★ 由 ApplicationContextAwareProcessor 自动调用
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public String loadConfig(String path) throws IOException {
Resource resource = resourceLoader.getResource(path);
try (InputStream is = resource.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
}
方式二:@Autowired 注入(更简洁)
ApplicationContext 本身实现了 ResourceLoader,因此可以直接注入:
java
@Component
public class SimpleConfigService {
@Autowired // 等价于实现 ResourceLoaderAware + setResourceLoader()
private ResourceLoader resourceLoader;
public void doSomething() {
Resource res = resourceLoader.getResource("classpath:data.csv");
}
}
| 对比 | ResourceLoaderAware | @Autowired |
|---|---|---|
| 侵入性 | 需实现接口 | 不需要 |
| 代码量 | setter + 字段 | 一个字段 + 注解 |
| 本质 | 回调模式 | 依赖注入 |
结论:能用 @Autowired 就不需要 ResourceLoaderAware。 后者是 Spring 2.5 之前的历史产物。
方式三:@Value 直接注入 Resource(最简单)
如果只需要加载固定位置 的资源,连 ResourceLoader 都不需要拿:
java
@Component
public class StaticConfigService {
@Value("classpath:default-config.properties") // Spring 自动解析为 ClassPathResource
private Resource defaultConfig;
// 支持所有格式: file:, http:, classpath: 等
public String readConfig() throws IOException {
try (InputStream is = defaultConfig.getInputStream()) {
return StreamUtils.copyToString(is, StandardCharsets.UTF_8);
}
}
}
后台原理: Spring 的 ResourceEditor(PropertyEditor)检测到 @Value 目标是 Resource 类型时,自动调用 ResourceLoader.getResource() 完成转换。
三种方式选择策略
资源路径是固定的吗?
├── 是 → @Value 注入 Resource ← 最简洁
└── 否 → @Autowired 注入 ResourceLoader ← 推荐
核心要点回顾
ResourceLoader.getResource()根据当前 ApplicationContext 类型和 location 前缀自动选择 Resource 实现ProtocolResolver是 Spring 4.3 新增的扩展点,允许自定义前缀处理逻辑classpath*:前缀触发ResourcePatternResolver的多条目扫描行为PathMatchingResourcePatternResolver使用AntPathMatcher实现 Ant 风格通配符匹配AbstractApplicationContext自身既是一个ResourceLoader(继承自DefaultResourceLoader),也是一个ResourcePatternResolver(通过内部持有的PathMatchingResourcePatternResolver委托)- 路径分隔符在内部统一被规范化为
/,以确保跨平台兼容性(Windows 的\会被转换)
Spring Boot 中的对应机制
Spring Boot 深度集成 ResourceLoader 和 ResourcePatternResolver,在多个关键场景中发挥作用:
-
自动配置类扫描 :
@SpringBootApplication注解中的@EnableAutoConfiguration通过SpringFactoriesLoader加载META-INF/spring.factories(或 Spring Boot 3.x 的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports),底层使用ResourcePatternResolver.getResources("classpath*:META-INF/spring.factories")来扫描所有 jar 包中的自动配置类。 -
spring-boot-starter-data-jpa等 Starter 的实体扫描 :JPA 的@EntityScan和 MyBatis 的@MapperScan底层使用PathMatchingResourcePatternResolver的classpath*:语法来扫描所有 classpath 条目下的实体类和映射文件。 -
配置文件优先级加载 :Spring Boot 的
ConfigFileApplicationListener通过ResourceLoader加载application.properties和application.yml,并按照特定顺序(jar 包内 → 外部 → 命令行指定)加载不同位置的配置文件。 -
spring.config.additional-location支持 :通过ResourceLoader.getResource()加载外部配置目录中的文件,使得运维人员可以在不修改 jar 包的情况下覆盖配置。 -
Spring Boot DevTools 的类路径监控 :DevTools 使用
ResourcePatternResolver监控 classpath 下资源的变化,实现热重载。 -
自定义 ProtocolResolver :在 Spring Boot 中,可以通过
ApplicationContext获取DefaultResourceLoader并注册自定义ProtocolResolver,例如实现consul:、nacos:等配置中心协议的支持。
最佳实践与避坑指南
-
classpath:与classpath*:的区别 :classpath:config.xml只返回第一个匹配的资源(通常是按 classpath 顺序最先找到的 jar),而classpath*:config.xml返回所有 classpath 条目中匹配的资源。当需要收集多个 jar 包中的同名配置文件时,必须使用classpath*:。 -
classpath*:的通配符性能陷阱 :classpath*:org/springframework/**/*.class这样的模式会扫描所有 jar 包,在大型应用中可能非常耗时。建议尽可能缩小扫描范围,例如指定具体的包路径。 -
ProtocolResolver的注册时机 :ProtocolResolver必须在getResource()被调用之前注册。如果在 ApplicationContext 已经刷新后注册,可能导致某些资源加载失败。建议在ApplicationContextInitializer或EnvironmentPostProcessor中注册。 -
路径中的空格和特殊字符 :
ResourceLoader.getResource()接收的 location 字符串可能包含空格和特殊字符。对于file:协议的路径,建议使用URLEncoder编码或使用java.io.File构造路径后通过toURI().toURL()转换。 -
AntPathMatcher的匹配规则 :**匹配多级目录,*只匹配单级目录,?匹配单个字符。注意classpath*:config/**/*.xml和classpath*:config/*.xml的区别------前者递归匹配所有子目录,后者只匹配一级子目录。 -
跨平台路径分隔符 :在 Windows 上使用
FileSystemResource时,如果 pattern 中包含\,PathMatchingResourcePatternResolver会将其规范化为/,但File.separator在 Windows 上仍然是\。建议始终使用/作为路径分隔符。 -
getResource()不保证资源存在 :ResourceLoader.getResource()返回的Resource对象不代表资源一定存在,需要通过resource.exists()判断。这是 Spring 的延迟加载设计------资源路径在调用getInputStream()时才真正验证。