记临时文件夹被删导致 Excel 导出失败
参考:0004
故障现象与初步分析
系统运行一段时间之后,出现了 Excel 导出失败的错误:
nested exception is java.lang.IllegalStateException: java.nio.file.NoSuchFileException: /tmp/b35f0633-5463-464c-a3ee-c927b2ec275e/poifiles/poi-sxssf-sheet3680958631078595506.xml] with root cause
java.nio.file.NoSuchFileException: /tmp/b35f0633-5463-464c-a3ee-c927b2ec275e/poifiles/poi-sxssf-sheet3680958631078595506.xml
at sun.nio.fs.UnixException.translateToIOException(UnixException.java:86)
at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102)
at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107)
at sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:214)
at java.nio.file.Files.newByteChannel(Files.java:361)
at java.nio.file.Files.createFile(Files.java:632)
at java.nio.file.TempFileHelper.create(TempFileHelper.java:138)
at java.nio.file.TempFileHelper.createTempFile(TempFileHelper.java:161)
at java.nio.file.Files.createTempFile(Files.java:852)
...
根据项目信息和日志等,基本可以确认是临时文件夹被删除导致报错:
- 服务使用的 Excel 导出组件为 EasyExcel,其底层实现是 Apache POI,使用了
SXSSFWorkbook
模型。 - 异常发生在尝试创建临时文件时,路径指向
/tmp
目录下的一个子目录,该子目录名称包含 UUID,其下还有poifiles
子目录。 - 在服务器上检查该路径,发现除了
/tmp
根目录,其下的 UUID 和poifiles
子目录均不存在。
根源分析
SXSSFWorkbook
是 Apache POI 提供的一种低内存占用的 Excel 写入方式,它通过将大部分数据写入临时文件,只在内存中保留少量行来避免内存溢出。这些临时文件默认存储在 Java 系统属性java.io.tmpdir
指定的目录下,通常是操作系统的/tmp
目录。
POI 与 EasyExcel 的临时文件管理机制
-
POI 的
DefaultTempFileCreationStrategy
:POI 通过
DefaultTempFileCreationStrategy
来管理临时文件的创建。其核心逻辑如下:public static final String POIFILES = "poifiles"; private volatile File dir; private void createPOIFilesDirectory() throws IOException { if (dir == null) { dirLock.lock(); try { if (dir == null) { String tmpDir = System.getProperty(JAVA_IO_TMPDIR); if (tmpDir == null) { throw new IOException("System's temporary directory not defined - set the -D" + JAVA_IO_TMPDIR + " jvm property!"); } Path dirPath = Paths.get(tmpDir, POIFILES); dir = Files.createDirectories(dirPath).toFile(); // 创建 /tmp/poifiles 目录 } } finally { dirLock.unlock(); } } } @Override public File createTempFile(String prefix, String suffix) throws IOException { createPOIFilesDirectory(); // 确保 /tmp/poifiles 存在 File newFile = Files.createTempFile(dir.toPath(), prefix, suffix).toFile(); // 在 /tmp/poifiles 下创建临时文件 if (System.getProperty(DELETE_FILES_ON_EXIT) != null) { newFile.deleteOnExit(); } return newFile; }
这段代码表明,POI 会在
java.io.tmpdir
目录下创建一个名为poifiles
的子目录,并在其中生成实际的临时 Excel 数据文件。一旦dir
变量被初始化,POI 就不会再次尝试创建poifiles
目录。 -
EasyExcel 的优化:
EasyExcel 为了防止多个应用共用一个临时文件夹可能存在的权限或冲突问题,在 POI 的基础上进一步优化,引入了 UUID 作为临时目录的前缀:
private static String tempFilePrefix = System.getProperty(TempFile.JAVA_IO_TMPDIR) + File.separator + UUID.randomUUID().toString() + File.separator; public static void createPoiFilesDirectory() { File poiFilesPathFile = new File(poiFilesPath); // 这里的 poiFilesPath 包含了 UUID createDirectory(poiFilesPathFile); // 创建 /tmp/{UUID}/poifiles 目录 TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(poiFilesPathFile)); }
这意味着 EasyExcel 会尝试在
/tmp/{UUID}/poifiles
这样的路径下创建临时文件。
操作系统对/tmp
目录的清理机制
大多数 Linux 发行版(如 CentOS 7+)使用systemd-tmpfiles
服务来管理和清理系统临时目录。通过查看/usr/lib/tmpfiles.d/tmp.conf
文件,可以发现类似以下规则:
# See tmpfiles.d(5) for details
# Clear tmp directories separately, to make them easier to override
q /tmp 1777 root root 10d
q /var/tmp 1777 root root 30d
其中,q /tmp 1777 root root 10d
表示/tmp
目录下的文件和目录,如果超过 10 天没有被访问或修改,就会被清理。
故障根源
综合上述分析,故障的根本原因在于:
- Java 应用(通过 EasyExcel/POI)在
/tmp
目录下创建了{UUID}/poifiles
这样的临时目录及其文件。 - 如果这些临时目录和文件在一段时间内(例如 10 天)没有被访问或修改,
systemd-tmpfiles
服务会自动将其清理删除。 - 然而,Java 应用中的 POI
DefaultTempFileCreationStrategy
可能仍然持有对已删除目录的引用(即dir
变量已初始化),在后续的 Excel 导出操作中,它不会再次尝试创建这个目录。 - 当应用再次尝试在该已不存在的目录中创建新的临时文件时,就会抛出
java.nio.file.NoSuchFileException
。
解决方案
针对该问题,推荐以下解决方法。
1. 更改java.io.tmpdir
系统属性
将临时目录设置为一个不会被系统自动清理的自定义路径,例如应用程序专属的数据目录。操作如下:
1、创建专属目录并设置权限:
sudo mkdir /path/to/your/app_temp
sudo chmod 777 /path/to/your/app_temp # 确保应用有读写权限
2、在 Java 启动命令中添加参数:
java -Djava.io.tmpdir=/path/to/your/app_temp -jar your_app.jar
3、在程序初始化时设置(如果无法修改启动脚本):
// 在应用程序启动时,尽早设置此属性
System.setProperty("java.io.tmpdir", "/path/to/your/app_temp");
注意:System.setProperty
必须在任何可能用到临时目录的代码(如SXSSFWorkbook
初始化)之前执行。
2. 调整 POI 临时文件创建策略(推荐的开发侧方案)
利用 POI 提供的扩展点,注册自定义的TempFileCreationStrategy
。每次使用临时目录前检查其是否存在,如果不存在则重新创建。这是对应用代码影响最小且最健壮的开发侧解决方案。
import org.apache.poi.util.DefaultTempFileCreationStrategy;
import org.apache.poi.util.TempFile;
import org.apache.poi.util.TempFileCreationStrategy;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
public class CustomTempFileStrategy {
// 自定义临时文件目录,避免使用系统/tmp
private static final String CUSTOM_TEMP_BASE_DIR = "/path/to/your/app_temp";
public static void initCustomTempFileStrategy() {
// 确保自定义的基础临时目录存在
File baseDir = new File(CUSTOM_TEMP_BASE_DIR);
if (!baseDir.exists()) {
baseDir.mkdirs();
}
TempFile.setTempFileCreationStrategy(new TempFileCreationStrategy() {
private volatile File currentPoiTempDir; // 存储 EasyExcel 生成的 UUID 目录下的 poifiles 目录
@Override
public File createTempFile(String prefix, String suffix) throws IOException {
ensurePoiTempDirExists();
return Files.createTempFile(currentPoiTempDir.toPath(), prefix, suffix).toFile();
}
@Override
public File createTempDirectory(String prefix) throws IOException {
ensurePoiTempDirExists();
return Files.createTempDirectory(currentPoiTempDir.toPath(), prefix).toFile();
}
private void ensurePoiTempDirExists() throws IOException {
if (currentPoiTempDir == null || !currentPoiTempDir.exists()) {
synchronized (this) { // 确保线程安全
if (currentPoiTempDir == null || !currentPoiTempDir.exists()) {
// 重新生成 UUID 目录,并确保其存在
String uuidDirName = UUID.randomUUID().toString();
Path appSpecificTempPath = Paths.get(CUSTOM_TEMP_BASE_DIR, uuidDirName, DefaultTempFileCreationStrategy.POIFILES);
currentPoiTempDir = Files.createDirectories(appSpecificTempPath).toFile();
}
}
}
}
});
// 验证是否已切换
System.out.println("POI Temporary directory strategy initialized. Base: " + CUSTOM_TEMP_BASE_DIR);
}
public static void main(String[] args) {
// 在应用程序启动时调用此方法
initCustomTempFileStrategy();
// 示例:使用 SXSSFWorkbook 进行导出
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
// ... 写入数据 ...
workbook.createSheet("Test Sheet");
System.out.println("Excel created successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
说明:
initCustomTempFileStrategy()
方法应在应用程序启动时调用,确保在任何 POI 操作之前设置好策略。- 自定义策略会确保每次创建临时文件前,其父目录(包含 UUID 和
poifiles
)都存在。如果被系统清理,它会重新创建。 - 同时,我们将基础临时目录从
/tmp
切换到了CUSTOM_TEMP_BASE_DIR
,进一步避免系统清理。