首页/新闻资讯/正文详情

Spring Boot读取resources目录文件的9种方式与避坑指南

发布时间:2026/9/26 15:43:41 来源:云帆数科 栏目:资讯中心
Spring Boot读取resources目录文件的9种方式与避坑指南
在 Spring Boot 项目里读resources目录下的文件这问题看着基础但几乎每个项目都会在这上面踩一两个坑。尤其是从开发环境切换到打好的 jar 包运行时明明开发时跑得好好的文件读取代码突然就给你抛个FileNotFoundException或者路径直接变了。今天就把我这些年实际用过的 9 种读取方式一次性整理清楚每种方式适合什么场景、有什么坑、底层原理是什么掰开揉碎讲明白。1. 先想清楚resources目录到底是怎么回事1.1 为什么 resources 读取问题总是困扰开发者很多新手刚接触 Spring Boot 时习惯性地把resources目录当成“文件系统里一个普通的文件夹”用new File(src/main/resources/xxx.txt)这样直截了当的方式去读文件。在 IDE 里点 Run 跑起来的时候偶尔还真能跑通但一旦用mvn package打成 jar 再执行九成九就会报错。原因很简单resources目录在你的工程里叫src/main/resources但经过 Maven 或 Gradle 编译打包后它的内容全部被搬到了target/classes开发环境或者 jar 包的根路径下。所谓“resources”根本不是一个固定的磁盘路径而是一个“类路径classpath”上的约定目录。更关键的是jar 本身是一个压缩文件里面的文件并不是操作系统上真实存在的文件系统路径你没法用File类直接去“操作”它。1.2 路径获取的底层差异类路径 vs 文件系统先说两个基础概念后面所有方式都离不开它们概念说明通俗类比类路径classpathJVM 搜索类和资源文件的根路径集合你把一堆书放在几个书架上找书时要按书架来找文件系统路径操作系统里真实存在的盘符或绝对路径书的精确房间号锁在柜子里那种类路径的核心优势在于它屏蔽了代码运行在哪种包、哪个环境里的差异。你只要知道“资源在类路径的哪个位置”就能让 JVM 用统一的查找机制去定位它。ClassLoader.getResourceAsStream()就是走类路径的标准方式这也是第 1 种和第 2 种方式最通用的根本原因。1.3 决定用哪种读取方式的两个关键条件实际业务里我判断用什么方式读文件只看两个问题代码是运行在 IDE /java -jar/ 外部 Tomcatwar 包里还是运行在 Web 容器如 Tomcat的 Servlet 上下文里如果是最后一种就要考虑ServletContext的方式。我最需要的是“读字节流”还是“拿到 File 对象”做进一步操作想清楚这两个问题这 9 种方式就会变得很好理解因为它们本质上是在不同环境、不同需求下的不同答案。2. 九种读取方式逐个拆解2.1 ClassLoader.getResourceAsStream()最推荐的通用方案this.getClass().getClassLoader().getResourceAsStream(files/abc.txt)这是我在实战中使用频率最高、也最推荐大家优先考虑的方式。它的原理是拿当前类的 ClassLoader 去类路径根目录下查找资源注意路径不能以/开头因为类加载器的视角里类路径的根目录就是“原点”。这种方式最大的优点有三个打包成 jar 后依然有效jar 内部的资源会被 classloader 自动处理不需要关心当前运行环境是 IDE、普通 Java 进程还是 Spring Boot 的嵌入式容器天然返回InputStream适合读取配置文件、模板文件、证书等场景。我有一个印象很深的经验在处理包含国际化消息的 properties 文件时用这个方式读出来的流可以直接丢给ResourceBundle或Properties.load(),非常顺滑。但有一个限制如果资源体积很大比如是一个几十 MB 的模型文件用这种方式拿到流后不能随机读取只能从头到尾读。想跳转就得把流先全部读进内存或落到临时文件后面的文章里我会提到具体场景的处理办法。2.2 Class.getResourceAsStream()和 ClassLoader 的区别在哪this.getClass().getResourceAsStream(/files/abc.txt)这种方式和 2.1 的区别主要在“路径语义”上。用Class来获取时如果路径以/开头表示从类路径根目录查找如果路径不以/开头则以当前类所在的包名对应的目录作为相对路径基准。举个例子如果类在com.example.util包里那么getResourceAsStream(data.txt)会去找com/example/util/data.txt加了/才表示找类路径根部的data.txt。这个细节就是新手最常见的坑在类路径根目录放了一个data.txt却用不带斜杠的方式去取结果null然后一脸茫然。我的建议很简单如果你记不住这些花哨规则就统一用 2.1 的ClassLoader方式路径固定从类路径根开始写既清晰又不容易踩坑。但Class.getResourceAsStream有一种场景很好用——你想访问“和某个类源码同目录”的资源。比如把资源放在某个包的目录下用相对路径就显得特别自然。2.3 ClassPathResource 配合 ResourceLoaderSpring 生态的标配既然咱们用的是 Spring Boot那么 Spring 自己提供的ClassPathResource与ResourceLoader就是绕不开的一对优质方案。用法非常简单Autowired private ResourceLoader resourceLoader; public void read() throws IOException { Resource resource resourceLoader.getResource(classpath:files/abc.txt); InputStream inputStream resource.getInputStream(); }ResourceLoader接口在 Spring 容器里无处不在注入进来直接就能用。它的好处是能统一处理classpath:、file:、http://等多种前缀的资源定位返回的Resource对象还能通过getFilename()、exists()、lastModified()等方法方便地拿到资源元信息。ClassPathResource还有几个独特优势第一它对不同容器的 classloader 做了兼容处理在 Spring Boot 的嵌套 jar 场景下表现稳定第二它天然支持classpath*:这种通配模式比如加载所有 jar 里的META-INF/xxx.properties第三配合 Spring 的PropertySource做配置解析时流程非常标准。我在做多模块项目时就特别依赖这种方式去加载依赖 jar 里提供的模板文件。2.4 ResourceUtils.getFile开发环境好使打包后翻车看到这个名字你可能会觉得这不是 Spring 提供的工具类吗怎么还能有问题没错ResourceUtils.getFile(classpath:files/abc.txt)确实能拿到File对象但前提是你的 classpath 资源对应的是一个真实磁盘文件。在 IDE 里跑、或者用java -cp按普通目录方式运行时classpath:files/abc.txt对应的本质是target/classes/files/abc.txt这是一个真实文件所以 File 能正常拿到。但如果是java -jar app.jar这样的 Spring Boot 可执行 jar 运行方式资源在 jar 包内部Spring 虽然能定位到 URL但再用getFile()去“假装”它是一个磁盘路径就会直接报错。很多老项目里写的“Spring 官方示例”动不动就ResourceUtils.getFile然后拿到File去做FileReader、FileInputStream这种代码在开发环境跑得贼溜一到生产用 jar 部署就炸。我自己的结论是这个方式能不用就不用除非你能百分之百确定运行环境不是 jar/war 包否则就老老实实用流式 API。如果你确实需要File对象来做一些底层操作比如引入 native 库时需要绝对路径那可以把 stream 的内容复制到一个临时文件里再用File配合临时文件操作。2.5 外部化配置让配置文件游离于 resources 之外上面 4 种方式解决的都是“从 classpath 内读文件”但还有一类非常常见的需求是我要运维在不重新打包的情况下修改配置或替换文件这时再把人家的数据塞进 jar 里反而是累赘。Spring Boot 本身支持非常完善的外部化配置你可以用spring.config.additional-location指定额外的配置文件目录也可以用application.properties里的spring.web.resources.static-locations扩展静态资源的搜索路径。我实际项目中做文件存储时最常用的一种做法是在启动命令里用-Dexternal.config.path/data/app/config/或者 Spring 占位符方式注入外部目录然后在代码里用System.getProperty拼接路径读取。这样就算 jar 更新了外部数据文件依然保持独立读写都走真实文件系统。严格来说这不是“从 resources 读取”的正统方式但它解决的问题恰恰是“resources 里的文件不可随意修改”的痛点。如果说前几种是“从包里找”那么这一种就是“让包外的东西变成 resources 的替代品”。2.6 ServletContext.getResourceAsStreamweb 环境专属方案如果项目不是用 Spring Boot 内置容器而是将 war 包丢到独立 Tomcat 里运行比如老牌企业项目那ServletContext就变成了一个重要的资源入口。servletContext.getResourceAsStream(/WEB-INF/classes/files/abc.txt)能直接读取到部署后真实路径下的 classpath 资源。这背后是 Servlet 容器对 web 应用目录结构的管理/WEB-INF/classes对应编译后的 classpath 根目录/WEB-INF/lib下则是对应的依赖 jar。这个方案的好处是它能拿到相对 web 应用上下文直接可读的输入流缺点也很明显一旦脱离 Servlet 容器比如单元测试、命令行启动ServletContext就不存在了代码耦合很重。在我维护的遗留项目里凡是只能用ServletContext读取的资源我都会单独抽一个接口隔离出来避免处处直接依赖。如果你正在处理一个老的单体应用这一点值得在改造时特别注意。2.7 Files.newInputStream(Paths.get())直接走文件系统这种方式谈不上优雅但实用。你只要知道资源在磁盘上的绝对路径可以用Files.newInputStream(Paths.get(绝对路径))直接读取。但问题来了jar 包内没有真实磁盘路径所以这种方式只适用于两种情况——外部化配置目录下的文件或者开发环境中你明确知道target/classes的绝对路径。前者更常见。我在临时脚本、数据处理的小工具里用得比较多比如定时任务需要读取磁盘上一个 CSV 文件那就没必要把 CSV 打进 jar直接把服务器上的绝对路径配在配置中心然后用Paths.get()去读取。这种做法简单粗暴能避免很多 classpath 资源加载的意外。但它有一个先天缺陷可移植性差。你的代码一旦脱离了那台服务器路径就废了。所以项目里我通常会做一个“路径解析器”配置项支持classpath:、file:、裸路径三种格式内部统一解析成Resource返回这样既保持了代码风格统一又给了部署方式足够的灵活性。2.8 编译期把资源复制成绝对路径目录靠构建工具这种思路其实不是“读取方式”而是“资源放置策略”但在实际工作中非常管用。以 Maven 为例你可以通过maven-resources-plugin的配置把某些目录额外复制到指定位置或者通过spring-boot-maven-plugin的配置把部分文件排除出 jar、放到运行目录里。还有一种常见操作是利用maven-dependency-plugin把依赖里的资源 unpack 到本地目录然后代码里直接按这个目录的绝对路径去读。有次做 Python 和 Java 混合系统时我需要把一份算法模型文件放到服务运行的同级目录下同时又想保持项目结构整洁。最后就是用 Maven 的 resource 复制功能把files/model/xgb.model直接复制到target/输出目录并起一个约定名字代码里通过System.getProperty(user.dir)拼出相对路径去读取。这种方式牺牲了一点“可移植性”换来了“永远是一个真实文件”的确定性。在离线部署、边缘计算盒子里尤其好用。2.9 枚举 Jar 条目暴力读取遗留系统的兜底绝招如果说前 8 种都是“知道路径再读”这最后一种在少数特殊场景里能救你命。有些老系统会直接把资源打进 jar 的深层路径或者资源文件名带动态版本号单纯用getResourceAsStream(files/abc.*.txt)根本没法匹配。这时候可以使用JarFile枚举 jar 条目JarEntry来找到真正符合通配条件的资源。关键代码大概长这样try (JarFile jarFile new JarFile(jarPath)) { EnumerationJarEntry entries jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry entries.nextElement(); if (entry.getName().startsWith(files/) entry.getName().endsWith(.txt)) { // 用 jarFile.getInputStream(entry) 读取 } } }但这里有一个 Spring Boot 用户必须特别小心的点Spring Boot 的 fat jar 采用嵌套结构外层的 BOOT-INF/lib 里的依赖 jar 和 BOOT-INF/classes 下的资源不是普通 JarFile 能直接枚举的。直接用new JarFile(application.jar)枚举得到的是嵌套 jar 的“外壳”里面的条目用传统方式访问不到。正确的解法是用org.springframework.boot.loader.jar.JarFileSpring Boot 自带的 loader 重写了 jar 读取逻辑来枚举嵌套 jar 内容或者直接通过java.util.jar.JarURLConnection配合jar:file:...!/BOOT-INF/classes/...这种 URL 来读取。前一种属于“换零件”后一种属于“手动拆包装”。总之除非真的遇到“路径可变、无法静态匹配”的硬需求否则不推荐一开始就用这种枚举方式复杂度完全不值当。3. 代码演示一个 demo 跑通全部核心场景3.1 准备一个标准的 Spring Boot 工程结构咱们不搞纸上谈兵直接搭一个最简单的工程来验证上面几种方式。先看结构spring-boot-resources-demo ├── src/main/java/com/example/demo │ └── DemoApplication.java ├── src/main/resources │ ├── application.yml │ └── files │ └── hello.txt └── pom.xmlhello.txt内容随便写一行“hello resources”。pom 里就一个spring-boot-starter-web依赖避免引入太多干扰项。这个结构就是日常开发最常见的标准结构。3.2 在 IDE 里运行的表现在 IDE 里直接 RunDemoApplicationSpring Boot 会从target/classes加载所有 resources 资源。我在一个 Controller 里写了几个接口来模拟每种方式RestController public class ResourceController { GetMapping(/read1) public String read1() throws IOException { try (InputStream in this.getClass().getClassLoader() .getResourceAsStream(files/hello.txt)) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } } }IDE 里测试下来2.1/2.3/2.4 都能正常拿到内容2.7 只要能拼对target/classes/files/hello.txt的绝对路径也能成功。这个阶段大家都能跑真正翻车的是下面两种运行模式。3.3 打包成 jar 后运行的表现执行mvn package生成 jar 后运行java -jar demo.jar。此时如果还保留 2.4ResourceUtils.getFile的代码会抛 FileNotFoundException如果用了 2.1 或 2.3 的流式写法一切正常如果 2.7 用绝对路径则必然报错因为 jar 里没有真实文件路径如果 2.9 用传统JarFile枚举得到的也将是混乱的结构。这里你就能直观感受到流式读取是正道File 化读取是风险。Java 的classloader天生能“无差别”处理 jar 内部的资源而你非要把它“还原”成 File 对象这就是逆流而上。3.4 war 部署时的表现把打包方式改成 war 并部署到独立 Tomcat 时情况又有变化。如果容器允许你可以通过ServletContext.getResourceAsStream(/WEB-INF/classes/files/hello.txt)读取。但要注意classpath 根部还是对应/WEB-INF/classes/所以路径写法和 2.1 差不多只是多了个 web 前缀。而 2.4 在这个场景下多半也会失败因为 Tomcat 解压 war 后确实存在真实路径但路径拼接和序贯路径差异带来的坑并不少。总体建议除非你自己的 web 应用有强制要求否则应对多环境部署流式方式始终是最省心的。4. 实战中踩过的坑与排查技巧4.1 getResource 和 getResourceAsStream 路径斜杠的区别这两个方法一个返回 URL一个返回流核心机制一样但路径“带不带斜杠”往往就是成功和失败的差别。Class.getResource带不带斜杠影响基准ClassLoader.getResource一律不带斜杠。我见过太多同学把classloader.getResource(/files/abc.txt)写成带斜杠结果取不到。记住一个口诀用 ClassLoader 就从根开始不写斜杠用 Class 就写斜杠表示根、不写斜杠表示当前类所在包目录。4.2 jar 包内永远不要用 File这个我自己踩过最深的一次坑写了一个读取数据库证书的工具类用new FileInputStream(path)开发环境跑得好好的上线后一直报“找不到文件”。排查了半天才发现jar 内根本没有这个路径。之后我养成了一个习惯凡是资源类文件一律流式读取。只有确实要传给一些只接受 File 对象的第三方库比如某些 native 库的load()方法才考虑把流落盘到临时文件再转 File。4.3 中文文件名被 URL 编码的坑如果resources目录里的文件名包含中文或空格通过getResource拿 URL 时可能拿到的是%e4%b8%ad%e6%96%87这样的编码形式。直接把这个 URL 转成 File 或路径去拼字符串百分百会出问题。解决办法也很简单尽量用英文文件名要么就用URLDecoder.decode(url.getPath(), UTF-8)去还原但还原时注意 URL 里可能混号这种场景用URLDecoder本身就会埋雷。所以最好还是从命名上规避别给生产环境找麻烦。4.4 IDEA 里能跑打包后不行这真是技术群里被问烂了的问题开发环境都是直接从target/classes读的但打成 jar 后 classloader 的行为被 Spring Boot 的 fat jar 机制接管了。它的BOOT-INF/classes和普通 jar 的根路径不同导致部分依赖 File 方式的代码直接崩掉。我排查这个问题的固定流程是这样先用-jar方式启动并打印System.getProperty(java.class.path)和this.getClass().getClassLoader().getResource()的 URL确认资源在打包后的具体位置如果确认在jar:file...BOOT-INF/classes就需要把所有 File 化代码替换成流式代码。这个流程走一遍百分之九十的同类问题都能定位。4.5 零散但重要的资源读取细节如果需要把target/classes下的文件标注为“资源”记得在 maven 的resources配置里加上non-filtered或列明扩展名否则一些 .txt 会被 filter 造成内容变化比如某些占位符。spring.config.location和spring.config.additional-location的顺序问题是很多配置装载异常的原因additional-location 追加在默认位置之后。使用ClassPathResource时可以用resource.createRelative(xxx)来按相对路径创建新的 Resource这在资源目录模块化打包时很省事。如果同一份资源存在多个同名文件不同 jar 里默认的getResource只会返回第一个命中的classpath*:则能拿到所有匹配项。读取大文件时用InputStream.transferTo()/readAllBytes()都要小心内存最好用固定缓冲区的循环读法。5. 选型与个人体会说了这么多最后分享下我在实际项目里的选择逻辑场景首选方式备用方式读取配置、模板、脚本2.1ClassLoader.getResourceAsStream2.3ClassPathResourceWeb 应用内需要文件元信息2.3ClassPathResource2.1遗留 war 包项目2.6ServletContext.getResourceAsStream2.3服务器磁盘上灵活可改文件2.5 外部化配置 2.7 绝对路径2.3无法静态匹配路径的 jar 资源2.9 枚举 Jar 条目2.1 配合通配符处理必须拿到 File 对象2.1/2.3 临时文件落盘2.4 仅限非 jar 场景我的真实经验就是先默认用流式方式读明确知道要 File 或动态资源再针对性地换方案。这个顺序能帮你避开 90% 以上的资源读取问题。如果你正在做一个需要多环境部署的 Spring Boot 项目强烈建议所有读取入口统一到一个“资源服务类”里内部封装好classpath:和file:的处理。运维要改路径时只改配置运维要换文件时只换文件代码一行不用动。这也是我做完几个中大型项目后最大的体会技术方案本身不难难的是让它在各种运行环境下都保持稳定可预期。希望这 9 种方式能让你在遇到具体问题时心里有个清晰的清单一眼就能选出最合适的那个。

相关推荐

机房精密空调环境监控低成本方案:温湿度漏水告警实战
机房精密空调环境监控低成本方案:温湿度漏水告警实战

机房半夜被叫起来抢修这事,干运维的朋友多少都经历过。最怕的不是空调坏,而是你压根不知道它什么时候坏的——等监控平台上温度曲线飙到40℃才发现,设备间里早就跟桑拿房一样了。精密空调这东西跟家用空调完全是两码事,家用机顶多… · 2026/9/26 15:43:41

2026芯片IP方案全景:从授权模式到选型避坑指南
2026芯片IP方案全景:从授权模式到选型避坑指南

2026 年芯片设计圈有个很有意思的现象:大家见面聊的不再是"我们准备流片哪个工艺",而是"这套 SoC 的 IP 方案定了没有"。不管是做 AI 推理芯片、车规 MCU,还是搞 Chiplet 集成,IP 选型的优先级已经悄悄排到了… · 2026/9/26 15:43:35

用Python计算空气清新剂安全用量与通风时间:从TVOC模型到代码实现
用Python计算空气清新剂安全用量与通风时间:从TVOC模型到代码实现

去年冬天,我朋友在12平米的卧室里连按了三次空气清新剂,然后关窗睡觉,第二天嗓子干疼得像吞了砂纸。市面上的空气清新剂包装上都写着“请勿过量使用”,但“过量”到底是多少,几乎没人会告诉你。我花了一个周末写了个Py… · 2026/9/26 15:43:34

Claude Code模板实战:从上下文工程到高效AI编程工作流
Claude Code模板实战:从上下文工程到高效AI编程工作流

最近 Claude Code 在开发者圈子里已经成了绕不开的话题。命令行里跑一个 AI 编程助手,帮你看代码、改代码、执行命令,这种体验确实比来回复制粘贴要痛快得多。但我发现身边很多朋友装上 Claude Code 之后,用了几次就放在那里吃灰,… · 2026/9/26 17:29:31

MySQL zip包安装全流程详解:从解压到配置服务与报错排查
MySQL zip包安装全流程详解:从解压到配置服务与报错排查

说实话,我第一次用zip压缩包装MySQL的时候,完全没意识到这件事和用msi安装包那个“下一步下一步”是两种物种。身边有个人丢了个压缩包过来,说“你解压之后配置一下就能用了”,结果我对着一个没有data目录的文件夹愣了半天&#x… · 2026/9/26 17:29:31

荣耀远航计划:主题精品共创激励的底层逻辑与避坑实操
荣耀远航计划:主题精品共创激励的底层逻辑与避坑实操

"荣耀远航计划"最近在创作者圈子里出现的频率越来越高。我第一次看到这个计划,是朋友转来的一张活动海报,当时第一反应是:又是一个刷量投稿的激励活动吧。后来仔细把"主题精品共创激励更新"这几个字拆开读了一遍&#xf… · 2026/9/26 17:29:31

荣耀远航计划主题精品共创激励更新全解析
荣耀远航计划主题精品共创激励更新全解析

看到"荣耀远航计划丨主题精品共创激励更新"这个标题,做过内容创作或平台运营的朋友应该马上能反应过来:这又是一档平台级的创作者激励项目,而且重点在最后四个字——"激励更新"。也就是说,平台不是新开一个活… · 2026/9/26 17:29:31

Kafka实战:体育赛事实时数据管道与架构设计
Kafka实战:体育赛事实时数据管道与架构设计

如果你用手机App看过大型体育赛事,大概率已经体验过Kafka在背后提供支撑的场景:比分近乎实时同步、进球后几秒钟推送弹出来、球员命中率随每一次出手立刻刷新。这些年我在体育数据服务公司负责实时比赛数据分析,从第一版用数据库轮询做推送&a… · 2026/9/26 17:29:31

Claude Code 模板化实战:从提示词工程到高效AI编程工作流
Claude Code 模板化实战:从提示词工程到高效AI编程工作流

看到 claude-code-templates 这个项目标题,我第一反应是:终于有人把 Claude Code 的提示词当成“一等公民”来对待了。用过 Claude Code 的朋友应该都有同感——这工具本身能力很强,但每次让它干活,都要现场写一大段需求说明。写得… · 2026/9/26 17:29:22

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码