Spring 的 Resource 和 ResourceLoader 是什么?classpath: 和 classpath*: 有什么区别?
简化版
Resource 是 Spring 对「一个资源」的统一抽象——不管这个资源是文件系统里的文件、classpath 下的类路径资源、jar 包里的条目、还是一个网络 URL,都用统一的 Resource 接口来表示和读取(getInputStream()、exists()、getFile() 等)。ResourceLoader 是「加载资源的入口」——给它一个「位置字符串」(如 classpath:config.xml、file:/data/a.txt、https://...),它根据前缀返回对应类型的 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: |
| ServletContextResource | Web 应用根目录下的资源 | — |
| 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:vsclasspath*:是高频考点,务必分清: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.getResources,classpath*: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()**」。