Java POIでSXSSFWorkbookを使ってOutOfMemoryを回避する方法
JavaのPOIでSXSSFWorkbookを使ってOutOfMemoryを回避する方法です。
JavaのPOIを使ってExcelファイル出力をするのはよくある仕様ですね。今回は30列で15000行を出力しようとしたところ、11000行を出力した辺りでOutOfMemoryとなり、処理が停止しました。
そもそもJavaで大量データのExcel出力なんかするなって声が聞こえてきそうですが、まあシステム要求がそうならなんとかしてみたくなるのもエンジニアって生きモノなんでしょうね。
調べたところXSSFWorkbookではなくSXSSFWorkbookを使うと処理が軽くなり、メモリ使用量が抑えられるとのこと。
ここでは JavaのPOIでSXSSFWorkbookを使ってOutOfMemoryを回避する方法 を紹介します。
目次
検証したPOIのバージョン
- POI 3.14
- POI 4.12
POI 4系とはFontやStyleの設定方法が違いますが、今回のサンプルソースはどちらでも動きます。
SXSSFWorkbookの注意点
XSSFとSXSSFの大きな違いは、セルの読み込み(getRow)ができないってこと。テンプレートファイルを用意しておいて、そのセルに値を書き込むってのは通例だと思うのですが、これができない。
つまり、先に行に値や書式が入っている場合にはエラー(IllegalArgumentException)となってしまうんです。
これは自由度がかなり低いと言わざるを得ないですね。
また、SXSSFWorkbookはメモリを使わない代わりにテンポラリーファイルを作成します。処理が終わったらテンポラリーファイルを削除するメソッド(dispose)を呼び出す必要があります。
SXSSFWorkbookでExcel出力する
SXSSFWorkbookではセルの読み込みができませんが、SXSSFはXSSFベースで構築されています。そこで一旦XSSFで読み込んで、それをベースとしてセルコピーしながらExcelファイルに出力していく方法をとります。
テンプレートファイルの読み込み
XSSFとSXSSFでファイルを読み込みます。XSSFWorkbookで読み込んだものをSXSSFWorkbookのコンストラクタに渡してやればテンプレートファイルを開けます。
XSSFWorkbook original = new XSSFWorkbook(new FileInputStream(テンプレートファイルパス)); // XSSF
XSSFSheet tmpSheet = original.getSheet("Temp"); // XSSF
Row baseRow = tmpSheet.getRow(tmpSheet.getLastRowNum()); // XSSF
SXSSFWorkbook workbook = new SXSSFWorkbook(original); // SXSSF
SXSSFSheet listSheet = workbook.getSheet("List"); // SXSSF
ここでは「Temp」というシートを設けて、そこにセル書式を設定しておきます。データを貼り付けるのは「List」というシートとしています。
行を追加してセルをコピー
次に「List」シートに行を追加して、「Temp」シートの列分セルをコピーします。
// 行を追加
SXSSFRow newRow = listSheet.createRow(startRowNum++);
// 列分処理
for ( int i = 0; i < baseRow.getLastCellNum(); i++ ) {
// ベースとなるセルを取得
Cell originCell = baseRow.getCell(i);
// 貼り付け先のセルを作成
SXSSFCell newCell = newRow.createCell(i);
//セルの値をコピー
switch (originCell.getCellType()) {
case XSSFCell.CELL_TYPE_BLANK:
newCell.setCellValue(originCell.getStringCellValue());
break;
case XSSFCell.CELL_TYPE_BOOLEAN:
newCell.setCellValue(originCell.getBooleanCellValue());
break;
case XSSFCell.CELL_TYPE_ERROR:
newCell.setCellErrorValue(originCell.getErrorCellValue());
break;
case XSSFCell.CELL_TYPE_FORMULA:
newCell.setCellFormula(originCell.getCellFormula());
break;
case XSSFCell.CELL_TYPE_NUMERIC:
newCell.setCellValue(originCell.getNumericCellValue());
break;
case XSSFCell.CELL_TYPE_STRING:
newCell.setCellValue(originCell.getRichStringCellValue());
break;
}
// セルスタイルのコピー
newCell.setCellStyle(originCell.getCellStyle());
}
ワークブックを保存する
最後にワークブックを保存します。保存したらクローズして、ワーク用のテンポラリーファイル削除しましょう。
// ワークブックを保存
workbook.write(new FileOutputStream(new File(保存ファイルパス)));
// クローズ
workbook.close();
original.close();
// ワーク用のテンポラリーファイル削除
workbook.dispose();
テストする
適当な件数をループさせてテストしてみると、、
おおおー、コピーされているー!!
大量データをExcel出力する
では、早速30列で15000行を出力してみようと思います。下図のように各シートを列30まで増やして準備します。
データ作成用クラスを作る
@Setter
@Getter
public class ColItem implements Serializable {
private String col1;
private String col2;
private String col3;
・・・省略・・・
private String col29;
private String col30;
}
Lombokを使ってgetter・setterを省略しています。
30列15000行を出力する
今回のテストソース全文です。
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.Test;
import dto.ColItem;
public class PoiSXSSFTest {
@Test
public void makeEcelFileForSXSSFTest() throws Exception {
// データ作成
List<ColItem> datas = new ArrayList<ColItem>();
for (int i = 0; i < 15000; i++) {
ColItem colItem = new ColItem();
for (int j = 0; j < 30; j++) {
try {
PropertyDescriptor prop = new PropertyDescriptor("col" + String.valueOf(j + 1), colItem.getClass());
Method setter = prop.getWriteMethod();
setter.invoke(colItem, "ColData" + String.valueOf(j + 1));
} catch (IntrospectionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
break;
}
}
datas.add(colItem);
}
// ファイルパス設定
String dir = Thread.currentThread().getContextClassLoader().getResource("").getPath();
System.out.println(dir);
String templatePath = dir + "Book1.xlsx";
String resultPath = dir + "result_Book1.xlsx";
// テンプレートファイル取得
XSSFWorkbook original = new XSSFWorkbook(new FileInputStream(templatePath)); // XSSF
XSSFSheet tmpSheet = original.getSheet("Temp"); // XSSF
Row baseRow = tmpSheet.getRow(tmpSheet.getLastRowNum()); // XSSF
SXSSFWorkbook workbook = new SXSSFWorkbook(original); // SXSSF
SXSSFSheet listSheet = workbook.getSheet("List"); // SXSSF
int startRowNum = 1;
// データ処理
for ( ColItem data : datas ) {
// 行を追加
SXSSFRow newRow = listSheet.createRow(startRowNum++);
// 列分処理
for ( int i = 0; i < baseRow.getLastCellNum(); i++ ) {
// ベースとなるセルを取得
Cell originCell = baseRow.getCell(i);
// 貼り付け先のセルを作成
SXSSFCell newCell = newRow.createCell(i);
//セルの値をコピー
switch (originCell.getCellType()) {
case XSSFCell.CELL_TYPE_BLANK:
newCell.setCellValue(originCell.getStringCellValue());
break;
case XSSFCell.CELL_TYPE_BOOLEAN:
newCell.setCellValue(originCell.getBooleanCellValue());
break;
case XSSFCell.CELL_TYPE_ERROR:
newCell.setCellErrorValue(originCell.getErrorCellValue());
break;
case XSSFCell.CELL_TYPE_FORMULA:
newCell.setCellFormula(originCell.getCellFormula());
break;
case XSSFCell.CELL_TYPE_NUMERIC:
newCell.setCellValue(originCell.getNumericCellValue());
break;
case XSSFCell.CELL_TYPE_STRING:
newCell.setCellValue(originCell.getRichStringCellValue());
break;
}
// セルスタイルのコピー
newCell.setCellStyle(originCell.getCellStyle());
}
// データ貼り付け
int colNum = 0;
newRow.getCell(colNum++).setCellValue(data.getCol1());
newRow.getCell(colNum++).setCellValue(data.getCol2());
newRow.getCell(colNum++).setCellValue(data.getCol3());
newRow.getCell(colNum++).setCellValue(data.getCol4());
newRow.getCell(colNum++).setCellValue(data.getCol5());
newRow.getCell(colNum++).setCellValue(data.getCol6());
newRow.getCell(colNum++).setCellValue(data.getCol7());
newRow.getCell(colNum++).setCellValue(data.getCol8());
newRow.getCell(colNum++).setCellValue(data.getCol9());
newRow.getCell(colNum++).setCellValue(data.getCol10());
newRow.getCell(colNum++).setCellValue(data.getCol11());
newRow.getCell(colNum++).setCellValue(data.getCol12());
newRow.getCell(colNum++).setCellValue(data.getCol13());
newRow.getCell(colNum++).setCellValue(data.getCol14());
newRow.getCell(colNum++).setCellValue(data.getCol15());
newRow.getCell(colNum++).setCellValue(data.getCol16());
newRow.getCell(colNum++).setCellValue(data.getCol17());
newRow.getCell(colNum++).setCellValue(data.getCol18());
newRow.getCell(colNum++).setCellValue(data.getCol19());
newRow.getCell(colNum++).setCellValue(data.getCol20());
newRow.getCell(colNum++).setCellValue(data.getCol21());
newRow.getCell(colNum++).setCellValue(data.getCol22());
newRow.getCell(colNum++).setCellValue(data.getCol23());
newRow.getCell(colNum++).setCellValue(data.getCol24());
newRow.getCell(colNum++).setCellValue(data.getCol25());
newRow.getCell(colNum++).setCellValue(data.getCol26());
newRow.getCell(colNum++).setCellValue(data.getCol27());
newRow.getCell(colNum++).setCellValue(data.getCol28());
newRow.getCell(colNum++).setCellValue(data.getCol29());
newRow.getCell(colNum++).setCellValue(data.getCol30());
}
// ワークブックを保存
workbook.write(new FileOutputStream(new File(resultPath)));
// クローズ
workbook.close();
original.close();
// ワーク用のテンポラリーファイル削除
workbook.dispose();
}
}
実行すると、、、
おおおー、OutOfMemoryが発生せずにExcel出力できたー!!
ちなに出力速度ですが「プロセッサ: Intel Corew i3-7130U CPU @ 2.70GHz、メモリ: 4.00GB 」のマシンスペックで 11秒ほど でした。
まとめ
JavaのPOIでSXSSFWorkbookを使ってOutOfMemoryを回避する方法を紹介しました。
個人的に、これはどうしてもって時に使う手段にしたいですね。やっぱりPOIはコーディング量も多いし、扱いは正直しんどい。
ちなみに30列/5000行くらいだったらJETTが便利だったんで興味がある方は読んでみてください。
おつかれさまでした。