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 内部的,不是文件系统上的独立文件
❌ 不能用 FileFileReader 这类基于文件系统的 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/resourcestarget/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


原始标题:How to Avoid the Java FileNotFoundException When Loading Resources | Baeldung