Spring Boot 打包后如何读取 JAR 包同级目录的外部文件?

20 阅读4分钟

背景与痛点

在平时的项目开发中,我们经常会遇到这样的需求:项目中有一个 template 文件夹(比如存放 Word、Excel 模板,或者 Freemarker/HTML 模板),我们希望在代码中去读取它。

如果是直接放在 src/main/resources 下,打包后文件会被打进 JAR 包内部。这就带来一个问题:如果生产环境下需要修改模板内容,就必须重新打包发布,非常不方便。

因此,更优雅的做法是:template 文件夹放在项目根目录,并在打包后,让它与 JAR 包处于同级目录。代码去读取这个外部文件夹。

本文将手把手教你如何实现这个需求,并避开路径解析中的常见“坑”。


避坑指南:为什么不建议直接用 ./template

很多开发者直觉上会写出这样的代码:

File templateDir = new File("./template");

结论:能用,但有非常大的隐患!

在 Java 中,./ 代表的是当前工作目录(Current Working Directory),等同于 System.getProperty("user.dir")。它的致命弱点在于:它依赖于“你在哪里敲的启动命令”,而不是“JAR 包在哪里”。

  • 场景 1(正常):cd 到了 JAR 包所在的目录,然后执行 java -jar app.jar。此时 ./ 就是 JAR 包所在目录,读取成功。
  • 场景 2(报错): 你的 JAR 包在 /app/my-project/ 下,但你在系统根目录启动项目:
    cd /
    java -jar /app/my-project/app.jar
    
    此时,Java 认为的 ./ 是根目录 /!程序会去寻找 /template,结果自然是找不到文件
  • 场景 3(报错): 将 Spring Boot 配置为 Linux 的 Systemd 服务启动时,如果没有严格配置 WorkingDirectory,默认路径也会导致找不到文件。

为了代码的健壮性,我们需要使用绝对路径来定位 JAR 包!


核心方案:精准定位 JAR 包所在目录

方法 1:Spring Boot 的极简写法(强烈推荐)

如果你使用的是 Spring Boot 项目,Spring 官方提供了一个非常贴心的类 ApplicationHome。它可以自动判断当前是处于 IDE 环境还是 JAR 包运行环境,并牢牢锁定真实的根目录。

import org.springframework.boot.system.ApplicationHome;
import java.io.File;

public class TemplateReader {
    
    public void readTemplates() {
        // 获取当前程序所在的根目录 (IDE中是项目根目录,打包后是 jar 包所在目录)
        ApplicationHome home = new ApplicationHome(getClass());
        File rootDir = home.getDir();
        
        // 拼接出 template 目录的绝对路径
        File templateDir = new File(rootDir, "template");
        
        if (templateDir.exists() && templateDir.isDirectory()) {
            System.out.println("成功找到 template 目录: " + templateDir.getAbsolutePath());
            File[] files = templateDir.listFiles();
            if (files != null) {
                for (File file : files) {
                    System.out.println("读取到文件: " + file.getName());
                    // 在这里处理你的文件逻辑...
                }
            }
        } else {
            System.out.println("未找到 template 目录,请检查路径: " + templateDir.getAbsolutePath());
        }
    }
}

方法 2:原生 Java 通用写法(适用于非 Spring 项目)

如果你使用的是普通的 Java Maven 项目,可以通过 ProtectionDomain 获取当前类的实际运行物理路径,然后反推根目录:

import java.io.File;
import java.net.URLDecoder;

public class NativeTemplateReader {

    private static File getTemplateDir() {
        try {
            // 获取当前类的代码源路径
            String path = NativeTemplateReader.class.getProtectionDomain().getCodeSource().getLocation().getPath();
            path = URLDecoder.decode(path, "UTF-8"); // 防止路径中的中文或空格被 URLEncode
            File file = new File(path);

            File rootDir;
            if (file.isFile() && path.endsWith(".jar")) {
                // 环境:打包后的 jar 运行,根目录就是 jar 包所在的父目录
                rootDir = file.getParentFile();
            } else {
                // 环境:IDE (如 IDEA) 中运行,退回项目根目录
                rootDir = new File(System.getProperty("user.dir")); 
            }

            return new File(rootDir, "template");

        } catch (Exception e) {
            // 兜底方案
            return new File(System.getProperty("user.dir"), "template");
        }
    }
}

工程化进阶:让 Maven 打包时自动复制文件夹

代码写好了,但每次执行 mvn clean package 打包后,我们还需要手动把根目录的 template 文件夹复制到 target 目录下,和 jar 包放在一起,太繁琐了!

我们可以通过配置 maven-resources-plugin 插件,让 Maven 在打包时自动完成复制

1. 规范的项目结构

首先确保你的项目目录长这样:

my-project/
 ├── src/
 │    └── main/java/...
 ├── template/          <-- 放在项目根目录下,与 src 同级
 │    ├── test1.txt
 │    └── test2.html
 └── pom.xml

2. 修改 pom.xml

<build><plugins> 节点下添加以下配置:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.2.0</version>
    <executions>
        <execution>
            <id>copy-template-folder</id>
            <!-- 在打包(package)阶段执行 -->
            <phase>package</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <!-- 复制的目标位置:打包后的 target 目录下的 template 文件夹 -->
                <outputDirectory>${project.build.directory}/template</outputDirectory>
                <resources>
                    <resource>
                        <!-- 复制的源位置:项目根目录下的 template 文件夹 -->
                        <directory>${project.basedir}/template</directory>
                        <filtering>false</filtering>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

最终效果验证

  1. 开发阶段:直接在 IDEA 中运行,代码会精准读取项目根目录下的 template 文件夹。
  2. 打包阶段:执行 mvn clean package。你会发现 target 目录下不仅有 xxx.jar,旁边还自动多出了一个 template 文件夹。
  3. 部署阶段:把 target 目录下的 jar 包和 template 文件夹一起丢到服务器上。无论你用什么姿势(跨目录、写脚本、systemd)启动项目,代码都能稳稳地读取到外部的模板文件。
  4. 后期运维:需要改模板?直接替换服务器上 template 里的文件即可,无需重启!无需重新打包!

总结

  • 读取外部同级文件,不要直接用 ./,容易因为启动目录不同而产生 BUG。
  • Spring Boot 项目无脑使用 ApplicationHome
  • 结合 Maven maven-resources-plugin 插件,可以实现丝滑的自动化打包体验。

希望这篇指南能帮你彻底解决 Java 读取外部配置文件/目录的烦恼!如果觉得有帮助,欢迎点赞收藏!