Spring 源码深度剖析:ResourceLoader 的资源加载与路径解析策略

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() 返回用于加载类路径资源的 ClassLoader
  • CLASSPATH_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 "教会"新的协议。

工作原理:责任链模式

ProtocolResolvergetResource() 流程中最先被调用------比内置检查都靠前:

复制代码
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:

  1. 提取根目录和子模式rootDirPath = "classpath*:org/app/config/"subPattern = "**/*.properties"
  2. 获取根目录下所有条目 :调用 findAllClassPathResources("org/app/config/"),这里使用 ClassLoader.getResources() 返回所有 jar 和目录中该路径下的 URL
  3. 逐个条目匹配:对每个根目录资源,判断是文件系统还是 jar,分别调用对应的匹配方法
  4. Ant 风格匹配 :在文件系统场景下,递归遍历目录,用 AntPathMatcher.match() 判断每个文件的完整路径是否满足子模式
  5. 收集结果 :所有匹配的文件被包装为 FileSystemResourceUrlResource 返回

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 模式匹配

实战:实现一个配置文件自动发现工具

以下展示了一个结合 ResourceLoaderResourcePatternResolver 的实际工具:

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 ← 推荐

核心要点回顾

  1. ResourceLoader.getResource() 根据当前 ApplicationContext 类型和 location 前缀自动选择 Resource 实现
  2. ProtocolResolver 是 Spring 4.3 新增的扩展点,允许自定义前缀处理逻辑
  3. classpath*: 前缀触发 ResourcePatternResolver 的多条目扫描行为
  4. PathMatchingResourcePatternResolver 使用 AntPathMatcher 实现 Ant 风格通配符匹配
  5. AbstractApplicationContext 自身既是一个 ResourceLoader(继承自 DefaultResourceLoader),也是一个 ResourcePatternResolver(通过内部持有的 PathMatchingResourcePatternResolver 委托)
  6. 路径分隔符在内部统一被规范化为 /,以确保跨平台兼容性(Windows 的 \ 会被转换)

Spring Boot 中的对应机制

Spring Boot 深度集成 ResourceLoaderResourcePatternResolver,在多个关键场景中发挥作用:

  1. 自动配置类扫描@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 包中的自动配置类。

  2. spring-boot-starter-data-jpa 等 Starter 的实体扫描 :JPA 的 @EntityScan 和 MyBatis 的 @MapperScan 底层使用 PathMatchingResourcePatternResolverclasspath*: 语法来扫描所有 classpath 条目下的实体类和映射文件。

  3. 配置文件优先级加载 :Spring Boot 的 ConfigFileApplicationListener 通过 ResourceLoader 加载 application.propertiesapplication.yml,并按照特定顺序(jar 包内 → 外部 → 命令行指定)加载不同位置的配置文件。

  4. spring.config.additional-location 支持 :通过 ResourceLoader.getResource() 加载外部配置目录中的文件,使得运维人员可以在不修改 jar 包的情况下覆盖配置。

  5. Spring Boot DevTools 的类路径监控 :DevTools 使用 ResourcePatternResolver 监控 classpath 下资源的变化,实现热重载。

  6. 自定义 ProtocolResolver :在 Spring Boot 中,可以通过 ApplicationContext 获取 DefaultResourceLoader 并注册自定义 ProtocolResolver,例如实现 consul:nacos: 等配置中心协议的支持。

最佳实践与避坑指南

  1. classpath:classpath*: 的区别classpath:config.xml 只返回第一个匹配的资源(通常是按 classpath 顺序最先找到的 jar),而 classpath*:config.xml 返回所有 classpath 条目中匹配的资源。当需要收集多个 jar 包中的同名配置文件时,必须使用 classpath*:

  2. classpath*: 的通配符性能陷阱classpath*:org/springframework/**/*.class 这样的模式会扫描所有 jar 包,在大型应用中可能非常耗时。建议尽可能缩小扫描范围,例如指定具体的包路径。

  3. ProtocolResolver 的注册时机ProtocolResolver 必须在 getResource() 被调用之前注册。如果在 ApplicationContext 已经刷新后注册,可能导致某些资源加载失败。建议在 ApplicationContextInitializerEnvironmentPostProcessor 中注册。

  4. 路径中的空格和特殊字符ResourceLoader.getResource() 接收的 location 字符串可能包含空格和特殊字符。对于 file: 协议的路径,建议使用 URLEncoder 编码或使用 java.io.File 构造路径后通过 toURI().toURL() 转换。

  5. AntPathMatcher 的匹配规则** 匹配多级目录,* 只匹配单级目录,? 匹配单个字符。注意 classpath*:config/**/*.xmlclasspath*:config/*.xml 的区别------前者递归匹配所有子目录,后者只匹配一级子目录。

  6. 跨平台路径分隔符 :在 Windows 上使用 FileSystemResource 时,如果 pattern 中包含 \PathMatchingResourcePatternResolver 会将其规范化为 /,但 File.separator 在 Windows 上仍然是 \。建议始终使用 / 作为路径分隔符。

  7. getResource() 不保证资源存在ResourceLoader.getResource() 返回的 Resource 对象不代表资源一定存在,需要通过 resource.exists() 判断。这是 Spring 的延迟加载设计------资源路径在调用 getInputStream() 时才真正验证。

相关推荐
他们叫我秃子1 小时前
前端开发转 Go 全栈(五):终于遇到熟人了,Go 的闭包和高阶函数原来这么像 JavaScript
前端·后端·go
SomeB1oody1 小时前
【RustyML入门】2.6. 线性判别分析
开发语言·后端·机器学习·rust·教程
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十七):AI时代对组件封装的新理解、agInput 统一入口,动态表单预备
前端·后端·go
Yao8061 小时前
SpringAI + Ollama + DeepSeek + RAG 搭建 AI 航线规划系统
后端
大勇前进1 小时前
SQL Server 分页查询多种写法对比:哪种性能最高?
后端
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十六):附件管理、增加根据文件后缀生成 SVG 文件图标的接口
前端·后端·go
前端Hardy1 小时前
Vue 终于杀进终端界!这个开源项目让 CLI 开发像写网页一样简单
前端·javascript·后端
程序员雷欧1 小时前
环形缓冲区深度解析:从基础原理到Disruptor源码的全面剖析
java
xiaoqiMikko1 小时前
Dependabot 面板全绿,不代表你的 Tomcat 没洞
java·spring boot
老孙讲技术2 小时前
周界枪响了、电梯脸却进了物业群:一个 callback,怎么把 alarm 和 faceAnalysis 拆成两条值班线?
后端·物联网