← 返回题目列表

Spring 的 Resource 和 ResourceLoader 是什么?classpath: 和 classpath*: 有什么区别?

中等 第 29 / 30 题 更新于 2026/07/28
ResourceResourceLoader资源加载classpath

简化版

Resource 是 Spring 对「一个资源」的统一抽象——不管这个资源是文件系统里的文件、classpath 下的类路径资源、jar 包里的条目、还是一个网络 URL,都用统一的 Resource 接口来表示和读取(getInputStream()exists()getFile() 等)。ResourceLoader 是「加载资源的入口」——给它一个「位置字符串」(如 classpath:config.xmlfile:/data/a.txthttps://...),它根据前缀返回对应类型的 Resource。这套抽象的价值:屏蔽「资源到底在哪、怎么读」的差异,代码统一用 resourceLoader.getResource(location) 就行。classpath:classpath*: 的区别(高频考点):classpath: 只找第一个匹配的资源(在类路径里找到一个就返回);classpath*: 会找所有 classpath 根下(包括所有 jar 包里)匹配的资源,返回多个——所以 classpath*: 常用于「扫描所有 jar 里符合模式的配置/类」(如 @ComponentScan、加载所有 META-INF/spring.factories)。ApplicationContext 本身就是一个 ResourceLoader。

详细版

Resource 的常见实现

实现类代表什么资源位置前缀
ClassPathResource类路径下的资源classpath:
FileSystemResource文件系统的文件file:
UrlResource一个 URL(http/ftp 等)http: / https: / ftp:
ServletContextResourceWeb 应用根目录下的资源
ByteArrayResource内存字节数组
InputStreamResource一个已有的输入流

Resource 接口的核心方法

public interface Resource {
    boolean exists();                    // 资源是否存在
    InputStream getInputStream();        // 读取内容(最核心)
    URL getURL();  URI getURI();
    File getFile();                      // 拿到 File(不是所有资源都支持)
    long contentLength();  long lastModified();
    String getFilename();
    Resource createRelative(String relativePath);  // 相对定位
}

ResourceLoader 按前缀返回不同 Resource

// ApplicationContext 就是 ResourceLoader
Resource r1 = ctx.getResource("classpath:app.properties");   // ClassPathResource
Resource r2 = ctx.getResource("file:/etc/config.txt");       // FileSystemResource
Resource r3 = ctx.getResource("https://example.com/a.json"); // UrlResource

// ResourcePatternResolver 支持通配符,返回多个
Resource[] rs = resolver.getResources("classpath*:META-INF/*.xml");

⚠️ classpath: vs classpath*: 是高频考点,务必分清classpath:foo.xml 只返回类路径里第一个找到的 foo.xml(多个 jar 都有的话,只拿一个,顺序不确定);classpath*:foo.xml 返回所有 classpath 根(含每个 jar)里的 foo.xml(可能多个)。所以「加载单个已知配置」用 classpath:;「聚合所有模块/jar 提供的同名配置」(如 Spring 的 spring.factories、MyBatis 的 *Mapper.xml、模块化的扩展点)必须用 classpath*:。再配合 **(任意子目录)、*(任意文件)等 Ant 风格通配符,classpath*:com/**/*.class 就能扫出所有 jar 里某包下的所有类——这正是 @ComponentScan 的底层。

完整版教学

一、Resource:统一的资源抽象

Resource 解决的是「资源来源五花八门,读取方式却想统一」:

一个"资源"可能在很多地方:
  - 文件系统:/data/config.txt
  - 类路径:classpath 下的 app.properties(可能在 jar 里)
  - 网络:https://example.com/data.json
  - 内存:一段字节数组

Java 原生怎么读?各不相同:
  文件 → new FileInputStream
  classpath → getClass().getResourceAsStream
  URL → url.openStream()
  → 代码要根据来源写不同逻辑,很乱

Spring 的 Resource:统一抽象
  不管资源在哪,都是一个 Resource
  都用 resource.getInputStream() 读、resource.exists() 判断存在
  → 屏蔽来源差异,代码统一

Resource 是「对一个资源的统一抽象」——不管资源在文件系统、classpath、网络还是内存,都用统一的 Resource 接口读取(getInputStream/exists/getFile)。它屏蔽了 Java 原生「不同来源不同读法」的差异。这是「面向接口编程」的典范——用统一抽象隔离底层差异。理解「Resource 是资源的统一抽象、屏蔽文件/classpath/网络/内存的来源差异、都用 getInputStream 读」,就理解了它的定位。

二、Resource 的实现家族

Resource 有一族实现,每种对应一类资源来源:

主要实现:
  ClassPathResource:类路径资源(可能在文件夹或 jar 里)
    → 最常用,读 classpath 下的配置、模板
  FileSystemResource:文件系统的文件
    → 读外部文件(绝对/相对路径)
  UrlResource:URL 指向的资源(http/https/ftp/file)
    → 读网络资源
  ServletContextResource:Web 应用根目录下的资源
  ByteArrayResource / InputStreamResource:内存数据/已有流

关键:它们都实现 Resource 接口
  → 调用方拿到的是 Resource,不用关心具体是哪个实现
  → 都能 getInputStream() 读取

Resource 有「一族实现」——ClassPathResource(类路径)、FileSystemResource(文件系统)、UrlResource(网络 URL)、ByteArrayResource(内存)等,每种对应一类来源。它们都实现 Resource 接口,所以调用方拿到的统一是 Resource,用统一方式读取。这是「策略模式/多态」的体现——同一接口、多种实现。理解「Resource 实现家族:ClassPathResource/FileSystemResource/UrlResource/ByteArrayResource,各对应一类来源、都实现统一接口」,就理解了资源抽象的实现层。

三、ResourceLoader:加载资源的入口

有了 Resource 实现,谁来「根据位置字符串创建对应的 Resource」?答案是 ResourceLoader

ResourceLoader 核心方法:
  Resource getResource(String location)

它根据 location 的"前缀"决定返回哪种 Resource:
  "classpath:app.xml"  → ClassPathResource
  "file:/data/a.txt"   → FileSystemResource
  "https://..."        → UrlResource
  "app.xml"(无前缀)   → 由具体 ResourceLoader 的默认策略决定

谁是 ResourceLoader?
  ApplicationContext 就实现了 ResourceLoader!
  所以可以直接:applicationContext.getResource("classpath:xxx")
  或注入 ResourceLoader 使用
  (不同的 ApplicationContext 默认策略不同:
   ClassPathXmlApplicationContext 默认当 classpath,
   FileSystemXmlApplicationContext 默认当文件系统)

ResourceLoader 是「加载资源的入口」——getResource(location) 根据位置前缀classpath:/file:/http:)返回对应类型的 Resource。关键是 ApplicationContext 本身就是 ResourceLoader,所以能直接 ctx.getResource(...) 或注入 ResourceLoader 使用。无前缀时由具体 ResourceLoader 的默认策略决定。理解「ResourceLoader 按前缀返回对应 Resource、ApplicationContext 就是 ResourceLoader、无前缀走默认策略」,就理解了资源加载的入口。

四、classpath: vs classpath*: 的本质区别

这是最高频的考点——两个前缀的本质区别:

classpath:foo.xml
  → 在类路径中找"第一个"匹配的 foo.xml
  → 找到一个就返回(如果多个 jar 都有,只返回一个,顺序不保证)
  → 用于"加载单个已知资源"

classpath*:foo.xml
  → 在"所有" classpath 根下查找 foo.xml
  → 遍历每个类路径根(每个目录、每个 jar),把所有的 foo.xml 都找出来
  → 可能返回多个 Resource
  → 用于"聚合所有来源的同名资源"

为什么需要 classpath*:模块化/可扩展
  很多框架约定:"每个模块在 META-INF 下放一个配置,我全加载"
  - Spring 的 spring.factories(每个 starter 一个,全加载)
  - MyBatis 各模块的 Mapper.xml
  - 各种 SPI 扩展点
  → 必须用 classpath*: 才能把所有 jar 里的都收集起来

classpath:classpath*: 的本质区别是「找一个 vs 找所有」:classpath: 找第一个匹配的就返回(加载单个已知资源);classpath*: 遍历所有 classpath 根(含每个 jar)把所有匹配的都找出来(聚合所有来源的同名资源)。classpath*: 是「模块化/可扩展」的基础——让每个模块/jar 提供同名配置,框架能全部收集(如 spring.factories、各模块 Mapper.xml)。理解「classpath: 找第一个 vs classpath*: 找所有 jar 里的、classpath*: 用于聚合各模块同名配置实现可扩展」,就掌握了这个高频考点。

五、通配符:Ant 风格路径匹配

classpath*: 配合通配符能实现强大的「批量资源匹配」:

Ant 风格通配符:
  ?   匹配一个字符
  *   匹配任意个字符(不跨目录)
  **  匹配任意层目录(跨目录)

例子(配合 ResourcePatternResolver.getResources):
  classpath*:META-INF/*.properties
    → 所有 jar 的 META-INF 下的所有 .properties
  classpath*:com/example/**/*.class
    → 所有 jar 里 com.example 包及子包下的所有 .class
    (★ 这正是 @ComponentScan 扫描的底层!)
  classpath:config/*.xml
    → 类路径 config 目录下的所有 xml(第一个 classpath 根)

需要用 ResourcePatternResolver(ResourceLoader 的扩展):
  Resource[] getResources(String pattern)  返回多个
  PathMatchingResourcePatternResolver 是标准实现

通配符让资源匹配从「精确路径」升级到「模式匹配」——?(一个字符)、*(任意字符不跨目录)、**(跨目录)。配合 classpath*:ResourcePatternResolver.getResourcesclasspath*:com/**/*.class 就能扫出所有 jar 里某包下的所有类——这正是 @ComponentScan 的底层(前面组件扫描题讲过)。理解「Ant 通配符 ?//** 、classpath:com/**/*.class 匹配所有 jar 里某包的类、是 @ComponentScan 的底层、用 ResourcePatternResolver」,就理解了资源模式匹配的威力。

六、实际应用与注意点

Resource 体系的实际应用和几个注意点:

典型应用:
  ① 读配置/模板/静态资源:
     @Value("classpath:template.html") Resource template;
     (@Value 能直接注入 Resource!)
  ② 加载所有模块的扩展配置:classpath*:
  ③ ResourceLoaderAware:让 Bean 拿到 ResourceLoader
  ④ 各种框架内部读文件都走 Resource

注意点:
  ① getFile() 不是万能的:
     jar 包里的资源没有独立的 File(它在 jar 内部)
     → 对 jar 里的资源调 getFile() 会失败
     → 应该用 getInputStream() 读(永远可用)
  ② classpath 资源可能在 jar 里,别假设它是文件系统路径
  ③ 相对定位用 createRelative,别手动拼路径

Resource 体系的实际应用:@Value("classpath:xxx") Resource 直接注入资源、用 classpath*: 加载所有模块的扩展配置、ResourceLoaderAware 拿到加载器。关键注意点getFile() 不是万能的——jar 包里的资源没有独立的 File(它在 jar 内部),对它调 getFile() 会失败,应该始终用 getInputStream()(永远可用)。这是很多「本地能跑、打成 jar 就读不到文件」问题的根源。理解「@Value 可注入 Resource、classpath*: 加载模块配置、getFile() 对 jar 内资源会失败要用 getInputStream()」,就掌握了 Resource 体系的实战要点。

记忆钩子:「Resource = 资源的统一抽象(文件/classpath/网络/内存都是 Resource,都用 getInputStream 读,屏蔽来源差异);实现家族:ClassPathResource/FileSystemResource/UrlResource/ByteArrayResource;ResourceLoader = 加载入口,按前缀(classpath:/file:/http:)返回对应 Resource,ApplicationContext 就是 ResourceLoader;★classpath:(找第一个,加载单个) vs classpath*:(找所有 jar 里的,聚合各模块同名配置实现可扩展如 spring.factories);Ant 通配符 ?/*/,classpath*:com//*.class 是 @ComponentScan 底层;坑:jar 里资源 getFile() 会失败,用 getInputStream()」

七、常见误区与追问

  • 误区:classpath: 和 classpath: 一样。* 本质区别是「找一个 vs 找所有」——classpath: 找第一个匹配的就返回(加载单个已知资源);classpath*: 遍历所有 classpath 根(含每个 jar)把所有匹配的都找出来(聚合各模块同名配置)。
  • 误区:Resource 一定能 getFile() 拿到 File。 不一定——jar 包里的资源、网络资源、内存资源没有独立的 File;对它们调 getFile() 会失败;应该始终用 getInputStream() 读取(所有 Resource 都支持),这是「打成 jar 后读不到文件」的常见原因。
  • 误区:classpath 资源就是文件系统里的文件。 classpath 资源可能在 jar 包内部(如依赖 jar 里的配置),此时它不对应文件系统的独立文件;不要假设 classpath 路径能当文件系统路径用。
  • 误区:ResourceLoader 要自己创建。 ApplicationContext 本身就实现了 ResourceLoader,直接用 ctx.getResource() 或注入 ResourceLoader / 实现 ResourceLoaderAware 即可;不用自己 new。
  • 追问:为什么加载 spring.factories 要用 classpath:?* 每个 starter/模块的 jar 里都有一个 META-INF/spring.factories,Spring 要把「所有 jar 里的」都收集起来合并处理;只有 classpath*: 才会遍历所有 classpath 根(含每个 jar)找出所有同名文件,classpath: 只会返回第一个。
  • 追问:@ComponentScan 底层和 Resource 有什么关系? @ComponentScan 把包路径转成 classpath*:com/example/**/.class 这样的模式,用 PathMatchingResourcePatternResolver.getResources 找出所有 jar 里该包下的所有 .class 资源,再用 ASM 读字节码判断注解——资源查找这一步正是靠 classpath: + Ant 通配符。
  • 追问:@Value 能注入什么资源相关的类型? 能直接注入 Resource(@Value("classpath:x.txt") Resource r)、Resource[](配合 classpath*: 通配符)、甚至 InputStream;靠类型转换体系把位置字符串转成 Resource——省去手动 getResource。

八、加强记忆

Resource 是 Spring 对「一个资源」的统一抽象——文件系统、classpath、网络 URL、内存字节数组都用统一的 Resource 接口表示和读取(getInputStream()/exists()/getFile()),屏蔽来源差异。实现家族:ClassPathResource(类路径)、FileSystemResource(文件)、UrlResource(网络)、ByteArrayResource(内存)等,都实现 Resource 接口。ResourceLoader 是加载入口——getResource(location)前缀classpath:/file:/http:)返回对应 Resource,ApplicationContext 本身就是 ResourceLoader高频考点 classpath: vs classpath*:classpath: 找第一个匹配的就返回(加载单个已知资源);classpath*: 遍历所有 classpath 根(含每个 jar)把所有匹配的都找出来(聚合各模块同名配置,如 spring.factories、各模块 Mapper.xml,是模块化/可扩展的基础)。配合 Ant 通配符?一字符、*任意字符不跨目录、**跨目录),classpath*:com/**/*.class 能扫出所有 jar 里某包下所有类——正是 @ComponentScan 的底层(用 PathMatchingResourcePatternResolver)。关键坑:jar 包里的资源没有独立 File,调 getFile() 会失败,始终用 getInputStream()@Value 能直接注入 Resource。一句话「*Resource 统一抽象资源(都用 getInputStream 读,屏蔽文件/classpath/网络差异),ResourceLoader 按前缀加载(ApplicationContext 就是);classpath:找第一个 vs classpath:找所有 jar 里的(聚合模块配置),配 Ant 通配符是 @ComponentScan 底层;jar 内资源 getFile() 会失败用 getInputStream()**」。