Excelエクスポートにおける一時ファイル削除エラーの解決

問題現象と初期調査

システムが一定期間稼働後、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-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)
...

プロジェクト情報とログ分析から、一時フォルダの削除が原因であると特定されました:

  1. Excelエクスポート機能はEasyExcelライブラリを使用しており、内部実装はApache POIのSXSSFWorkbookを利用
  2. 例外は一時ファイル作成時に発生し、パスは/tmpディレクトリ配下のUUIDを含むサブディレクトリを指している
  3. サーバー上で該当パスを確認すると、UUIDディレクトリとpoifilesサブディレクトリが存在しない

根本原因の分析

SXSSFWorkbookはApache POIが提供するメモリ使用量を抑えたExcel書き込み方式で、大部分のデータを一時ファイルに書き込みます。これらの一時ファイルはデフォルトでJavaシステムプロパティjava.io.tmpdirで指定されたディレクトリ(通常はOSの/tmp)に保存されます。

POIとEasyExcelの一時ファイル管理メカニズム

POIのDefaultTempFileCreationStrategy:

POIはDefaultTempFileCreationStrategyで一時ファイルの作成を管理します。主要なロジックは以下の通り:

public class PoiTempFileManager {
    private static final String TEMP_FOLDER_NAME = "poifiles";
    private volatile File tempDirectory;
    
    private void initializeTempDirectory() throws IOException {
        if (tempDirectory == null) {
            synchronized (this) {
                if (tempDirectory == null) {
                    String systemTempDir = System.getProperty("java.io.tmpdir");
                    if (systemTempDir == null) {
                        throw new IOException("一時ディレクトリが定義されていません");
                    }
                    Path directoryPath = Paths.get(systemTempDir, TEMP_FOLDER_NAME);
                    tempDirectory = Files.createDirectories(directoryPath).toFile();
                }
            }
        }
    }
    
    public File createTemporaryFile(String namePrefix, String extension) throws IOException {
        initializeTempDirectory();
        File newTempFile = Files.createTempFile(tempDirectory.toPath(), namePrefix, extension).toFile();
        if (System.getProperty("poi.delete.temp.files") != null) {
            newTempFile.deleteOnExit();
        }
        return newTempFile;
    }
}

このコードは、POIがjava.io.tmpdirディレクトリ配下にpoifilesサブディレクトリを作成し、その中に一時Excelデータファイルを生成することを示しています。一度tempDirectory変数が初期化されると、POIは再度poifilesディレクトリの作成を試みません。

EasyExcelの拡張:

EasyExcelは複数アプリケーション間での権限や競合問題を防ぐため、POIを拡張してUUIDを一時ディレクトリのプレフィックスとして使用します:

public class EnhancedTempFileHandler {
    private static String customTempPath = 
        System.getProperty("java.io.tmpdir") + File.separator + 
        UUID.randomUUID().toString() + File.separator;
    
    public static void setupCustomTempDirectory() {
        File enhancedTempDir = new File(customTempPath + "poifiles");
        createDirectoryStructure(enhancedTempDir);
        TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(enhancedTempDir));
    }
}

これにより、EasyExcelは/tmp/{UUID}/poifilesのようなパス配下に一時ファイルを作成します。

OSの/tmpディレクトリ自動クリーンアップ

多くのLinuxディストリビューション(CentOS 7以降など)はsystemd-tmpfilesサービスを使用してシステム一時ディレクトリを管理・クリーンアップします。/usr/lib/tmpfiles.d/tmp.confファイルには以下のようなルールが定義されています:

# 一時ディレクトリのクリーンアップ設定
q /tmp 1777 root root 10d
q /var/tmp 1777 root root 30d

q /tmp 1777 root root 10dは、/tmpディレクトリ配下のファイルとディレクトリが10日間アクセスまたは変更されない場合、自動的に削除されることを意味します。

問題の根本原因

上記の分析を総合すると、問題の根本原因は以下になります:

  1. Javaアプリケーション(EasyExcel/POI経由)が/tmpディレクトリ配下に{UUID}/poifiles一時ディレクトリとファイルを作成
  2. これらの一時ディレクトリとファイルが一定期間(例:10日間)アクセスまたは変更されない場合、systemd-tmpfilesサービスが自動的に削除
  3. しかし、Javaアプリケーション内のPOI DefaultTempFileCreationStrategyは削除済みディレクトリへの参照を保持したまま(tempDirectory変数が初期化済み)、後続のExcelエクスポート操作で再度ディレクトリを作成しようとしない
  4. アプリケーションが存在しないディレクトリ内に新規一時ファイルを作成しようとすると、java.nio.file.NoSuchFileExceptionがスローされる

解決策

1. java.io.tmpdirシステムプロパティの変更

一時ディレクトリをシステム自動クリーンアップの対象外となるカスタムパスに設定します。

専用ディレクトリの作成と権限設定:

sudo mkdir /opt/application/temp
sudo chmod 755 /opt/application/temp

Java起動コマンドへのパラメータ追加:

java -Djava.io.tmpdir=/opt/application/temp -jar application.jar

プログラム初期化時の設定(起動スクリプト変更不可の場合):

// アプリケーション起動時、可能な限り早期に設定
System.setProperty("java.io.tmpdir", "/opt/application/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 ResilientTempFileStrategy {
    
    private static final String APPLICATION_TEMP_BASE = "/opt/application/temp";
    
    public static void configureTempFileManagement() {
        File baseTempDir = new File(APPLICATION_TEMP_BASE);
        if (!baseTempDir.exists()) {
            baseTempDir.mkdirs();
        }
        
        TempFile.setTempFileCreationStrategy(new TempFileCreationStrategy() {
            private volatile File activePoiDirectory;
            
            @Override
            public File createTempFile(String filePrefix, String fileSuffix) throws IOException {
                verifyPoiDirectory();
                return Files.createTempFile(activePoiDirectory.toPath(), filePrefix, fileSuffix).toFile();
            }
            
            @Override
            public File createTempDirectory(String directoryPrefix) throws IOException {
                verifyPoiDirectory();
                return Files.createTempDirectory(activePoiDirectory.toPath(), directoryPrefix).toFile();
            }
            
            private void verifyPoiDirectory() throws IOException {
                if (activePoiDirectory == null || !activePoiDirectory.exists()) {
                    synchronized (this) {
                        if (activePoiDirectory == null || !activePoiDirectory.exists()) {
                            String sessionId = UUID.randomUUID().toString();
                            Path fullTempPath = Paths.get(APPLICATION_TEMP_BASE, sessionId, 
                                DefaultTempFileCreationStrategy.POIFILES);
                            activePoiDirectory = Files.createDirectories(fullTempPath).toFile();
                        }
                    }
                }
            }
        });
        
        System.out.println("カスタム一時ファイル管理ストラテジーを初期化しました: " + APPLICATION_TEMP_BASE);
    }
    
    public static void demonstrateUsage() {
        configureTempFileManagement();
        
        try (SXSSFWorkbook excelWorkbook = new SXSSFWorkbook(100)) {
            excelWorkbook.createSheet("サンプルシート");
            System.out.println("Excelファイルの作成に成功しました");
        } catch (IOException error) {
            error.printStackTrace();
        }
    }
}

説明:

  • configureTempFileManagement()メソッドはアプリケーション起動時に呼び出し、POI操作の前にストラテジーを設定
  • カスタムストラテジーは一時ファイル作成前に親ディレクトリ(UUIDとpoifilesを含む)の存在を保証
  • システムによるクリーンアップが発生した場合、自動的に再作成
  • 基本一時ディレクトリを/tmpからAPPLICATION_TEMP_BASEに変更し、システムクリーンアップを回避

タグ: Apache POI EasyExcel SXSSFWorkbook 一時ファイル管理 Java

9月29日 09:00 投稿