背景与痛点
在平时的项目开发中,我们经常会遇到这样的需求:项目中有一个 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/下,但你在系统根目录启动项目:此时,Java 认为的cd / java -jar /app/my-project/app.jar./是根目录/!程序会去寻找/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>
最终效果验证
- 开发阶段:直接在 IDEA 中运行,代码会精准读取项目根目录下的
template文件夹。 - 打包阶段:执行
mvn clean package。你会发现target目录下不仅有xxx.jar,旁边还自动多出了一个template文件夹。 - 部署阶段:把
target目录下的 jar 包和template文件夹一起丢到服务器上。无论你用什么姿势(跨目录、写脚本、systemd)启动项目,代码都能稳稳地读取到外部的模板文件。 - 后期运维:需要改模板?直接替换服务器上
template里的文件即可,无需重启!无需重新打包!
总结
- 读取外部同级文件,不要直接用
./,容易因为启动目录不同而产生 BUG。 - Spring Boot 项目无脑使用
ApplicationHome。 - 结合 Maven
maven-resources-plugin插件,可以实现丝滑的自动化打包体验。
希望这篇指南能帮你彻底解决 Java 读取外部配置文件/目录的烦恼!如果觉得有帮助,欢迎点赞收藏!