1. 概述
在 Java 应用中读取资源文件时,经常会遇到 FileNotFoundException
。问题的根源在于:项目打包后,资源文件的物理路径和源码中的目录结构完全不同。
比如开发时文件在 src/main/resources
下,运行时却可能被打包进 JAR 的根目录。直接用 FileReader
按路径读取,打包后必然踩坑 ❌。
本文带你搞清楚为什么会出现这个问题,并给出 ✅ 正确、通用的资源加载方式,确保无论是在 IDE 中运行还是打包成 JAR 都能正常工作。
2. 直接读取文件的陷阱
假设我们的应用在启动时需要读取一个配置文件:
try (FileReader fileReader = new FileReader("src/main/resources/input.txt");
BufferedReader reader = new BufferedReader(fileReader)) {
String contents = reader.lines()
.collect(Collectors.joining(System.lineSeparator()));
}
这段代码在 IDE 里运行是 ✅ 正常的。原因很简单:
- IDE 默认以项目根目录作为当前工作目录(current working directory)
- 所以
src/main/resources/input.txt
路径真实存在,能顺利读取
⚠️ 但一旦用 Maven 打成 JAR 包:
java -jar core-java-io2.jar
立马报错:
Exception in thread "main" java.io.FileNotFoundException:
src/main/resources/input.txt (No such file or directory)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at java.io.FileReader.<init>(FileReader.java:58)
at com.baeldung.resource.MyResourceLoader.loadResourceWithReader(MyResourceLoader.java:14)
at com.baeldung.resource.MyResourceLoader.main(MyResourceLoader.java:37)
原因也很直接:JAR 包里的文件不是独立存在的,不能当普通文件用路径访问。
3. 源码结构 vs 打包后结构
我们来看源码和打包后的差异。
源码结构
src/
main/
resources/
input.txt
java/
com/baeldung/resource/MyResourceLoader.java
打包后的 JAR 内容
META-INF/MANIFEST.MF
com/baeldung/resource/MyResourceLoader.class
input.txt
META-INF/maven/com.baeldung/core-java-io-files/pom.xml
...
注意到没?input.txt
被直接拷贝到了 JAR 的根目录下,和 com/
包同级。
所以即使你改成 /input.txt
,依然读不到 ❌。因为:
✅ 资源文件是打包在 JAR 内部的,不是文件系统上的独立文件
❌ 不能用File
、FileReader
这类基于文件系统的 API 访问
4. 正确做法:使用 Classpath 加载资源
正确姿势是:通过类加载器从 classpath 加载资源,这才是通用、可靠的方式。
try (InputStream inputStream = getClass().getResourceAsStream("/input.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String contents = reader.lines()
.collect(Collectors.joining(System.lineSeparator()));
}
关键点解析
getResourceAsStream()
是从 classpath 查找资源- 路径前加
/
表示从 classpath 的根目录开始查找(绝对路径) - JAR 包本身就在 classpath 中,所以可以直接读到根目录下的
input.txt
为什么在 IDE 和 JAR 中都能工作?
环境 | classpath 包含的内容 |
---|---|
IDE | src/main/resources 、target/classes 等 |
JAR | JAR 包本身(根目录即 classpath 根) |
✅ 无论哪种环境,input.txt
都在 classpath 根路径下,因此 getResourceAsStream("/input.txt")
都能成功。
其他写法对比
// ✅ 推荐:从 classpath 根开始(带 /)
getClass().getResourceAsStream("/input.txt")
// ⚠️ 相对路径:从当前类所在包开始查找(不推荐,易出错)
getClass().getResourceAsStream("input.txt")
// ✅ 也可以用 ClassLoader(更底层,但等效)
getClass().getClassLoader().getResourceAsStream("input.txt") // 注意:这里不能加 /
💡 小贴士:ClassLoader 的路径是相对于 classpath 根的,所以**不要加
/
**,否则会找不到。
5. 总结
- ❌ 不要用
FileReader
直接读src/main/resources
下的文件,打包后必报FileNotFoundException
- ✅ 应使用
getResourceAsStream()
从 classpath 加载资源 - ✅ 路径带
/
表示从 classpath 根开始,推荐统一使用这种写法,清晰不易错 - ✅ 该方式在 IDE 和 JAR 环境下都可靠,是标准实践
示例代码已托管至 GitHub:https://github.com/baeldung-tutorials/core-java-modules/tree/master/core-java-io