SpringBoot读取resource目录文件六种方法详解与避坑指南

发布时间:2026/10/3 4:12:57
SpringBoot读取resource目录文件六种方法详解与避坑指南 先说个我见过很多次的场景项目在 IDE 里跑得好好的File一读一个准结果一打包成 JAR 部署到服务器马上报FileNotFoundException或者中文文件读出来全变乱码。如果你是 SpringBoot 新手或者写工具类时总是拿捏不准 resource 目录下的文件到底该用哪种方式读这篇就是给你准备的。我会把六种方法全部拆开讲一遍每种都带上原理、代码和坑点最后附上真实项目里的排查经验。先说清楚一件事resource 目录下的文件最终会被 Maven/Gradle 构建时复制到target/classes也就是 classpath 的根路径里。也就是说读取 resource 文件本质上就是读取 classpath 下的资源。这个理解到位了后面每一种方法你就都能看透了。1. 为什么读取 resource 目录是个高频需求1.1 资源文件在 SpringBoot 里的存储与打包机制SpringBoot 项目里我们常把配置、模板、静态文本、SQL 脚本、证书文件等放在src/main/resources目录下。这些文件在开发阶段是一个真实的目录我们可以用常规的File方式访问但一旦执行mvn package打成 JAR整个resources目录和classes目录会被压缩进一个类似 ZIP 的包里此时文件已经不是操作系统上的真实文件了而是 JAR 包内部的“条目”。这就是很多人第一次踩坑的根源开发环境和工作环境之间的资源存在形式不一致。所以选择一个正确的读取方式核心目的只有一个——让代码在两种环境下都能稳定运行。而判断一种方法好不好你只需要问自己两个问题这个方法拿到的流InputStream是不是从 JAR 包内部也能打开这个方法对路径的书写方式是否有严格限制我会不会写错1.2 六种方法的选型总览先把这六种方法亮出来后面逐个拆解方法核心类/工具支持通配符打包 JAR 后可用推荐程度方法一ClassPathResource否是推荐方法二ResourceLoader否是推荐方法三Class.getResourceAsStream否是推荐方法四ClassLoader.getResourceAsStream否是推荐方法五ResourceUtils.getFile否否不推荐方法六PathMatchingResourcePatternResolver是是视场景从表里能直接看出除了方法五之外其他五种在 JAR 包内都能正常工作。方法五之所以很多人还在用是因为早期教程大量传播但它只适合开发环境调试我建议你直接避坑。2. 六种读取方法逐一拆解2.1 方法一ClassPathResourceSpring 框架自带的ClassPathResource是读取 classpath 资源最直接的方式。它的原理很简单内部持有一个路径字符串在getInputStream()时通过类加载器去定位资源因为用的是类加载器机制所以 JAR 包内部也能正常读取。直接上代码假设我们有一个文件放在src/main/resources/config/test.txtimport org.springframework.core.io.ClassPathResource; import org.springframework.util.StreamUtils; import java.nio.charset.StandardCharsets; public String readByClassPathResource() throws IOException { ClassPathResource resource new ClassPathResource(config/test.txt); try (InputStream is resource.getInputStream()) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } }这里有几个关键细节ClassPathResource的路径参数不能以/开头也不要加classpath:前缀直接写相对于 classpath 根的路径。写成/config/test.txt反而会去尝试从文件系统根目录定位导致读不到。如果文件就在 resources 根目录下直接写new ClassPathResource(test.txt)就行。官方文档说明new ClassPathResource(...)默认从线程上下文的类加载器加载大多数情况下没问题如果遇到特殊容器环境找不到可以改用重载构造方法new ClassPathResource(path, 具体的ClassLoader)。这个方法没什么学习成本也是我个人在业务代码里最常用的方式之一。因为它和 Spring 的Resource抽象绑定后面如果想把文件来源从 classpath 切换到本地磁盘file:只需要改动路径协议调用方代码几乎不用变。2.2 方法二ResourceLoaderResourceLoader是 Spring 框架定义的资源加载器接口它统一了classpath:、file:、http:等不同协议的资源访问方式。在 SpringBoot 容器里我们可以直接注入已经自动配置好的ResourceLoader实例。代码示例import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; Service public class FileReadService { private final ResourceLoader resourceLoader; public FileReadService(ResourceLoader resourceLoader) { this.resourceLoader resourceLoader; } public String readByResourceLoader() throws IOException { Resource resource resourceLoader.getResource(classpath:config/test.txt); try (InputStream is resource.getInputStream()) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } } }注意点getResource()方法传参时必须带上classpath:前缀否则它会把字符串当作文件系统路径去解析这是和方法一最大的差异。这里用了构造器注入在 SpringBoot 1.x 里也可以直接Autowired ResourceLoader resourceLoader;都是一样的。ResourceLoader按名字匹配时只返回第一个命中的资源。如果 classpath 中存在多个同名文件比如不同 JAR 里各有一个application.properties它返回的是最先找到的那一个。如果说方法一是“点对点”的定向读取方法二就是“协议感知”的通用读取。你想把资源来源做成可配置的比如从classpath:切换成file:/data/用这种方法就非常方便只需改配置文件里的前缀就行。2.3 方法三Class.getResourceAsStream这个方法来自纯 JDK不依赖 Spring所以哪怕是一个普通的 Java 工具类里也能用。不过它有个容易写错的地方路径解析规则跟当前类所在的包密切相关。直接看代码public String readByClassGetResourceAsStream() throws IOException { // 注意前导 / 表示从 classpath 根路径开始查找 try (InputStream is this.getClass().getResourceAsStream(/config/test.txt)) { if (is null) { throw new IOException(resource not found); } return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } }这里要重点解释路径规则因为这个方法我见过太多人写错如果路径以/开头比如/config/test.txt它是从 classpath 根路径查找推荐这种写法。如果路径不以/开头比如config/test.txt它会自动在“当前类所在包目录”前面拼接路径后再去查找。也就是说如果你的类在com.example.demo包下实际查找路径是com/example/demo/config/test.txt这大概率不是你要找的文件。我个人的习惯是用getClass()还是类名.class都行但千万别忘了以/开头。如果实在担心当前类继承关系复杂导致getClass()返回的是子类信息可以直接写FileReadUtils.class.getResourceAsStream(...)一样的效果但语义更明确。2.4 方法四ClassLoader.getResourceAsStream既然看到了Class.getResourceAsStream就不得不提它的底层实现ClassLoader.getResourceAsStream。这个方法绕过了类的包路径逻辑永远从 classpath 根路径开始查找所以路径规则比方法三简单得多不能以/开头。代码示例public String readByClassLoaderGetResourceAsStream() throws IOException { ClassLoader cl Thread.currentThread().getContextClassLoader(); try (InputStream is cl.getResourceAsStream(config/test.txt)) { if (is null) { throw new IOException(resource not found); } return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } }几个实践要点这里我使用了Thread.currentThread().getContextClassLoader()而不是this.getClass().getClassLoader()。在大多数 Web 容器中线程上下文类加载器能正确处理各个应用之间的隔离避免出现类加载器视角不同导致资源找不到的问题。如果你在静态方法里写没有this可用老老实实用线程上下文类加载器就好了。这个方法返回的InputStream同样可以直接从 JAR 包里读取不用担心打包问题。为什么Class.getResourceAsStream和方法四看起来都是读 resource还要分成两个方法关键是它们的路径表达规则完全不同一个交给人来记忆容易错一个交给规则但不用记。实际开发中我更推荐固定使用方法四因为“以根路径为基准不带斜杠”的规则只此一条不容易踩坑。2.5 方法五ResourceUtils.getFile不推荐但要会辨别ResourceUtils.getFile是 Spring 早期提供的一个工具类核心作用是加上了classpath:前缀然后尝试将资源路径转换为真实的文件系统路径最终返回一个File。代码写起来非常简单import org.springframework.util.ResourceUtils; public String readByResourceUtils() throws IOException { File file ResourceUtils.getFile(classpath:config/test.txt); try (InputStream is new FileInputStream(file)) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } }但你注意看这个实现逻辑它要把classpath:资源转换成真实文件路径。开发环境里 resource 文件是真实存在的文件所以能成功一旦打包成 JAR资源变成了 JAR 包内部的条目不再位于原生文件系统getFile()就会直接抛出java.io.FileNotFoundException: classpath resource [config/test.txt] cannot be resolved to absolute file path because it does not reside in the file system这正是“开发环境正常、生产环境爆炸”的经典案例。所以我的态度很明确这个方法只用于本地调试永远不要进生产代码。如果有人给你提的代码评审意见里出现ResourceUtils.getFile你完全有理由让他改掉。为什么我仍然把它列为六种方法之一因为你在旧项目里一定会见到它。了解它的原理和局限性你才能快速判断历史代码里为什么会有这种写法以及改造成哪种方式最适合。2.6 方法六PathMatchingResourcePatternResolver前面几种方法都只能读一个文件碰到“读取 resource 批次下的多个文件”这种需求就抓瞎了。PathMatchingResourcePatternResolver是 Spring 用来支持通配符匹配的资源解析器底层基于 AntPathMatcher支持*、**、?等表达式。代码示例import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; public ListString readAllByPattern() throws IOException { PathMatchingResourcePatternResolver resolver new PathMatchingResourcePatternResolver(); Resource[] resources resolver.getResources(classpath:config/*.txt); ListString contents new ArrayList(); for (Resource resource : resources) { try (InputStream is resource.getInputStream()) { contents.add(StreamUtils.copyToString(is, StandardCharsets.UTF_8)); } } return contents; }getResources()支持的几种常见模式模式匹配含义classpath:config/*.txt匹配 config 目录下所有 txt 文件classpath:config/**/*.txt递归匹配 config 下所有子目录内的 txtclasspath*:config/*.txt匹配所有类路径包括依赖 JAR 内的同名资源第六种方法的核心价值是批量。比如你要读取i18n目录下所有 properties或者加载模板引擎下的所有 markdown 文件用getResources(classpath:templates/**/*.md)一行搞定。不过它也有一点注意classpath:和classpath*:的差异。前缀classpath*:表示扫描所有 classpath 中包含该路径的 JAR/目录适合做框架集成接口普通业务场景用单星号classpath:就够了匹配范围更可控避免扫描到依赖包里的同名资源。3. 实操过程与核心环节实现3.1 测试环境与目录准备为了让你能直接复现我给出一套完整的测试结构。假设你的 SpringBoot 项目名是demo按下面目录准备一个文件src/main/resources/ ├── application.properties └── config/ └── test.txtconfig/test.txt内容随意我推荐放一行纯英文加一行中文方便测试编码比如hello resource 你好资源文件Maven 项目不需要额外配置SpringBoot 的 spring-boot-starter-parent 已经默认将src/main/resources作为资源目录。如果你用的是 Gradle也是一样的src/main/resources也会被自动处理。3.2 完整测试代码与运行结果我写一个统一的测试类把六种方法放在同一个类里方便你直接对比import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.Resource; import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; public class ResourceReadDemo { // 方法一ClassPathResource public String method1() throws Exception { ClassPathResource resource new ClassPathResource(config/test.txt); try (InputStream is resource.getInputStream()) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } } // 方法二ResourceLoader由 Spring 容器注入 public String method2(ResourceLoader resourceLoader) throws Exception { Resource resource resourceLoader.getResource(classpath:config/test.txt); try (InputStream is resource.getInputStream()) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } } // 方法三Class.getResourceAsStream public String method3() throws Exception { try (InputStream is this.getClass().getResourceAsStream(/config/test.txt)) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } } // 方法四ClassLoader.getResourceAsStream public String method4() throws Exception { ClassLoader cl Thread.currentThread().getContextClassLoader(); try (InputStream is cl.getResourceAsStream(config/test.txt)) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } } // 方法五ResourceUtils.getFile仅开发环境可用 public String method5() throws Exception { File file ResourceUtils.getFile(classpath:config/test.txt); try (InputStream is new FileInputStream(file)) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } } // 方法六PathMatchingResourcePatternResolver public ListString method6() throws Exception { PathMatchingResourcePatternResolver resolver new PathMatchingResourcePatternResolver(); Resource[] resources resolver.getResources(classpath:config/*.txt); ListString result new ArrayList(); for (Resource resource : resources) { try (InputStream is resource.getInputStream()) { result.add(StreamUtils.copyToString(is, StandardCharsets.UTF_8)); } } return result; } }这个类你可以在任意一个 SpringBoot 的SpringBootTest测试方法里调用也可以直接放在工具类里调用method1、method3、method4、method6它们不依赖 Spring 注入。method2需要 Spring 容器的ResourceLoadermethod5只能在 IDE 里跑通。我在本机跑一次的结果都一样内容输出为hello resource 你好资源文件五个方法在开发环境全部正常唯独要注意的是编码。如果你发现输出中文乱码或者第一个中文字符变成?基本可以断定是读取时没有显式指定UTF-8。这里我给所有人的建议是凡是把字节流转字符串一律显式传StandardCharsets.UTF_8不要依赖平台默认编码否则部署到 Linux 服务器大概率会出现中文乱码。3.3 打包成 JAR 后的差异实测现在来看最关键的一步。执行mvn clean package -DskipTests然后分别运行 JAR 包里的同一个方法。你会发现method5ResourceUtils.getFile直接抛异常报错信息就是我前面写的那句cannot be resolved to absolute file path。method1、method3、method4、method6正常输出文件内容。method2正常因为ResourceLoader在运行时依然通过 classpath 机制定位资源。为什么会有这个差异我用一个生活类比例子解释JAR 包就像一个压缩行李箱里面的衣服文件在行李箱里叠得整整齐齐但你不能用“拉开衣橱门”的动作去拿里面的衣服你得用“拉链开口”类加载器去取。File类的本质是操作系统路径访问它不认识压缩包里的东西而InputStream是流式访问压缩包内部也可以打开输入流。这个差异就是整个 SpringBoot 读取 resource 文件的核心分界线。理解了它你就不会被各种方法表面的 API 差异迷惑。4. 常见问题与排查技巧4.1 问题速查表我在实际工作中收集了一些高频问题整理成速查表遇到问题可以直接对照排查现象可能原因解决方案开发环境能读JAR 包运行报 FileNotFoundException使用了 File 方式如 ResourceUtils.getFile换成 InputStream 系列方式中文乱码没有指定编码或用了平台默认编码统一用 StandardCharsets.UTF_8方法三读取不到文件路径没以/开头被拼接了包路径以/开头或改方法四方法四读取不到文件路径以/开头了把根路径写错去掉开头斜杠修改 resources 文件后 IDEA 里读不到最新内容target/classes 未更新或 IDE 缓存手动 Rebuild或mvn clean compile通配符读不到子目录文件*只匹配一层没递归用**如classpath:config/**/*.txt读到了其他 JAR 包里的同名资源使用了classpath*或同名文件冲突使用单classpath:或明确校验内容这里额外说明一下 IDE 缓存问题。IDEA 里改了resources下的文件有时候运行后读到的还是旧内容多半是构建输出目录target/classes没有同步。遇到这种情况不需要慌执行一次Build - Rebuild Project或者用mvn clean compile强制重新复制资源基本就解决了。4.2 我的踩坑经验与选型建议最后分享几个真实场景下的选型建议都是我自己的实践经验。第一个经验是大多数业务代码一句ClassPathResource就够了。它不像ResourceLoader那样需要注入也不像Class.getResourceAsStream那样有路径前缀歧义语义最直观JAR 包内都能稳定运行。我在团队内部定的代码规约就是读单个资源优先用 ClassPathResource静态工具类里因为拿不到 Spring 容器就退一步用ClassLoader.getResourceAsStream。第二个经验是批量读取多个资源时一定优先考虑 PathMatchingResourcePatternResolver。比如做一个多语言资源加载、或者批量扫描模板文件用通配符一次搞定不仅代码简洁而且不会出现数组下标越界这种手动拼路径引起的低级错误。如果你在框架代码里需要跨多个 JAR 扫描资源就配合classpath*:前缀使用但必须意识到匹配范围变大记得在循环里跳过你不关心的资源。第三个经验也是我最想强调的不要在代码里依赖“开发环境”和“运行环境”的差异。有些人觉得既然本地跑得通那生产大概也没问题于是把ResourceUtils.getFile的代码带上线结果翻车。这类问题的特点是平时不炸一到大流量或新环境部署就炸排查成本极高。规避的方式就一条凡是读 classpath 资源一律走流式 API禁止在业务代码里调用File对象去访问 classpath 资源。如果你现在正在接手遗留项目看到类似new File(ResourceUtils.getURL(classpath:xxx).toURI())这种写法别犹豫改成我文中的方法一或方法四一劳永逸。个人平时还有个小小的操作习惯在写工具类时返回的InputStream一律由调用方关闭工具类内部只负责打开流不负责close()。这样既保持了职责单一也避免在工具方法里把流关掉后导致上层逻辑报Stream closed的错。资源读取看起来是个不起眼的功能但稍微用错一个路径前缀就能让你排查一整天。希望这篇文章能帮你把这个小功能彻底吃透以后碰到类似需求直接照着选型就行。