【Java】OOM回避!EasyExcelで100万件データを安全にエクスポートするベストプラクティス
業務システム開発において「データのエクスポート(Excelダウンロード)」は避けて通れない機能です。 しかし、データ量が10万、100万と膨れ上がると、従来のApache POIでは以下のような問題が頻発します。
OOM (OutOfMemoryError):全データをメモリに展開してアプリが落ちる
タイムアウト:処理が重すぎて画面が固まる
リソース枯渇:CPU/IOが専有され、他のユーザーに迷惑がかかる
今回は、Alibaba製のライブラリ**「EasyExcel」**の「ページング検索 + 分割書き込み」機能を活用して、これらの問題を解決する方法を紹介します。コピペで使えるユーティリティクラスも用意しました。
1. なぜエクスポート機能は「死ぬ」のか?

解決策:EasyExcelの「ページング書き込み」
核心となる考え方は**「分割して処理する(Divide and Conquer)」**です。
ページング検索:DBから少しずつデータを取得する(例:2,000件ずつ)
分割書き込み:取得した分だけExcelに書き込む
メモリ解放:【重要】書き終わったListを即座に破棄する
ループ:全データが終わるまで繰り返す
2. 【基本】ページング書き込みの実装(10万件〜)
まずは、ページング書き込みのロジックをカプセル化したヘルパークラスを作成します。これを使えば、どんなEntityでも汎用的に処理できます。
コアクラス:PageWriteExcelHelper.java
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import java.io.OutputStream;
import java.util.List;
/**
* 【核心】ページング書き込みヘルパー - OOM対策の要
*/
public class PageWriteExcelHelper<T> {
// 🎯 重要なインターフェース:ページング取得ロジック(呼び出し元で実装)
public interface PageQuerySupplier<T> {
List<T> getPage(int pageNum, int pageSize); // 何ページ目? サイズは?
}
/**
* ページング書き込み実行
* @param outputStream 出力ストリーム(HttpServletResponseなど)
* @param head データモデルClass (例: User.class)
* @param pageSize 【重要】1回あたりの処理件数 (推奨 1000~5000)
* @param totalCount 総データ件数
* @param supplier データ取得ロジック
*/
public static <T> void writeByPage(OutputStream outputStream,
Class<T> head,
int pageSize,
int totalCount,
PageQuerySupplier<T> supplier) {
// 🔧 1. ExcelWriterの初期化
ExcelWriter excelWriter = EasyExcel.write(outputStream, head).build();
WriteSheet writeSheet = EasyExcel.writerSheet("Sheet1").build();
try {
// 📐 2. 総ページ数の計算
int totalPage = totalCount > 0 ? (int) Math.ceil((double) totalCount / pageSize) : 1;
// 🔁 3. ループ処理:取得 -> 書き込み -> 解放
for (int pageNum = 1; pageNum <= totalPage; pageNum++) {
// 🚚 3.1 データ取得(コールバック実行)
List<T> pageData = supplier.getPage(pageNum, pageSize);
// ✍️ 3.2 Excelへの書き込み
excelWriter.write(pageData, writeSheet);
// 🗑️ 3.3 【最重要】メモリを即時解放!
pageData.clear();
}
} finally {
// 🔒 4. 【必須】リソース解放(メモリリーク防止)
if (excelWriter != null) {
excelWriter.finish();
}
}
}
}
3. 【応用】即戦力ユーティリティ(ExcelExporter)
さらに使いやすくするために、レスポンスヘッダーの設定や例外処理を含めたラッパークラスを作ります。コントローラーからはこれを呼び出すだけです。
ExcelExporter.java
import com.alibaba.excel.EasyExcel;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
/**
* 【即戦力】EasyExcel拡張ユーティリティ
*/
public class ExcelExporter {
// ============== 【1. 大量データ用(ページング)】 ==============
public static <T> void exportByPage(HttpServletResponse response,
String fileName, // ファイル名
Class<T> dataModel, // データクラス
int pageSize, // ページサイズ
int totalCount, // 総件数
PageWriteExcelHelper.PageQuerySupplier<T> pageSupplier) {
setupResponse(response, fileName);
try (OutputStream out = response.getOutputStream()) {
// ヘルパーに委譲
PageWriteExcelHelper.writeByPage(out, dataModel, pageSize, totalCount, pageSupplier);
} catch (Exception e) {
throw new RuntimeException("エクスポート失敗: " + e.getMessage(), e);
}
}
// ============== 【2. 小規模データ用(一括)】 ==============
public static <T> void exportSimple(HttpServletResponse response,
String fileName,
String sheetName,
Class<T> dataModel,
List<T> dataList) {
setupResponse(response, fileName);
try (OutputStream out = response.getOutputStream()) {
EasyExcel.write(out, dataModel).sheet(sheetName).doWrite(dataList);
} catch (Exception e) {
throw new RuntimeException("エクスポート失敗", e);
}
}
// ============== 【共通:レスポンスヘッダー設定】 ==============
private static void setupResponse(HttpServletResponse response, String fileName) {
try {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
// 日本語ファイル名の文字化け対策
String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
} catch (Exception e) {
throw new RuntimeException("Header設定失敗", e);
}
}
}
Controllerでの使用例
これだけで、メモリ使用量を一定に保ったまま100万件のデータが出力できます。
@GetMapping("/export/users/large")
public void exportLargeUserList(HttpServletResponse response) {
// 1. 総件数を取得
int total = userService.countTotalUsers();
// 2. エクスポート実行
ExcelExporter.exportByPage(
response,
"全ユーザーリスト",
User.class,
3000, // 1回あたり3000件処理
total, // 総件数
// Lambdaでページング検索ロジックを渡す
(pageNum, pageSize) -> userService.findByPage(pageNum, pageSize)
);
}
4. さらなる最適化テクニック
動的ページサイズの計算 固定値(3000件など)ではなく、1行あたりのバイト数とJVMの空きメモリから逆算して、最適なページサイズを動的に決定するとさらに安定します。
非同期処理(Async) 50万件を超えると、どうしても物理的な書き込み時間がかかります(数十秒〜分単位)。 ユーザーを待たせないために、@Async でバックグラウンド処理し、進捗状況(%)を別APIでポーリングする設計が推奨されます。
まとめ
1万件以下:一括書き込みでOK(早くて簡単)。
1万〜50万件:今回紹介した「ページング書き込み」が必須。
50万件以上:「ページング書き込み」+「非同期処理(タスク化)」を検討。
EasyExcelのページング機構を使えば、OOMの恐怖から解放されます。ぜひプロジェクトに取り入れてみてください。
