業務システムをやっていると「一覧をExcelでダウンロードしたい」「アップロードされたExcelを取り込みたい」という要件、必ずと言っていいほど出てきますよね。Javaでこれをやるなら定番は Apache POI です。
この記事では、Apache POIでxlsxを生成してSpring BootのREST APIからダウンロードさせるところから、大量行でメモリが溢れないようにする SXSSFWorkbook、そしてアップロードされたxlsxをパースしてDTOに詰め替えるところまで、コード中心に一気に見ていきます。
ポイントを先に言っておくと、出力は 通常は XSSFWorkbook、大量データなら SXSSFWorkbook の使い分けが軸になります。ここを押さえておくと全体の見通しがよくなります。
Apache POIの依存追加とセットアップ
まずは依存追加です。xlsx(新形式)を扱うので poi と poi-ooxml の両方を入れます。
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version>
</dependency>
Gradleなら次の通りです。
implementation 'org.apache.poi:poi-ooxml:5.3.0'
poi-ooxml を入れると poi 本体も推移的に入ります。なお .xls(旧形式)は HSSFWorkbook、.xlsx(新形式)は XSSFWorkbook ですが、いまから作るものは基本的にxlsxでよいので、本記事はxlsx前提で進めます。
Apache POIのデータ構造は Workbook(ブック)→ Sheet(シート)→ Row(行)→ Cell(セル) という素直な階層です。この入れ子をコードで組み立てていくイメージを持っておけば迷いません。
XSSFWorkbookでExcelを生成する基本
ヘッダー付きの一覧を作ってみます。ヘッダーだけ太字にして、列幅も自動調整します。
public Workbook createUserSheet(List<User> users) {
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("ユーザー一覧");
// ヘッダー用スタイル
CellStyle headerStyle = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
headerStyle.setFont(font);
Row header = sheet.createRow(0);
String[] titles = {"ID", "名前", "登録日"};
for (int i = 0; i < titles.length; i++) {
Cell cell = header.createCell(i);
cell.setCellValue(titles[i]);
cell.setCellStyle(headerStyle);
}
// 日付セル用スタイル
CellStyle dateStyle = workbook.createCellStyle();
CreationHelper helper = workbook.getCreationHelper();
dateStyle.setDataFormat(helper.createDataFormat().getFormat("yyyy/mm/dd"));
int rowNum = 1;
for (User u : users) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(u.getId()); // 数値
row.createCell(1).setCellValue(u.getName()); // 文字列
Cell dateCell = row.createCell(2);
dateCell.setCellValue(u.getCreatedAt()); // LocalDateTime
dateCell.setCellStyle(dateStyle);
}
for (int i = 0; i < titles.length; i++) {
sheet.autoSizeColumn(i);
}
return workbook;
}
setCellValue は引数の型で挙動が変わります。String ならそのまま文字列、double や int なら数値、LocalDateTime / Date なら日付シリアル値として書き込まれます。日付は 値だけでなく表示書式(DataFormat)も指定 しないとただの数値に見えてしまうので注意しましょう。
autoSizeColumn は便利ですが、全行を走査するので大量データでは重くなります。後述のSXSSFでは原則使えないので、その場合は setColumnWidth で固定するのが現実的です。
REST APIでExcelをダウンロードさせる
作ったWorkbookをHTTPレスポンスとして返します。ByteArrayOutputStream に書き出して ResponseEntity<byte[]> で返すのが扱いやすいです。
@GetMapping("/users/excel")
public ResponseEntity<byte[]> download() throws IOException {
byte[] bytes;
try (Workbook workbook = createUserSheet(userService.findAll());
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
workbook.write(out);
bytes = out.toByteArray();
}
String filename = URLEncoder.encode("ユーザー一覧.xlsx", StandardCharsets.UTF_8)
.replace("+", "%20");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
headers.set(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''" + filename);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
}
ポイントは3つです。Content-Typeはxlsx専用の長いMIMEタイプを指定します。日本語ファイル名は文字化けしやすいので filename*=UTF-8'' 形式でURLエンコードした名前を渡します。そして try-with-resources で Workbookを必ずclose します。閉じ忘れはメモリリークや一時ファイルの残留につながります。
ファイルダウンロードの一般的な作法そのものは Spring BootでのファイルアップロードとダウンロードをMultipartFileで実装する でも触れているので、合わせて読むと理解が深まります。
大量データ対策:SXSSFWorkbookでメモリ枯渇を防ぐ
XSSFWorkbook は全行をメモリ上に保持します。数百行なら問題ありませんが、数万〜数十万行になると OutOfMemoryError のリスクが一気に高まります。
そこで使うのが SXSSFWorkbook です。指定した行数ぶんだけメモリに保持し、ウィンドウから外れた古い行は一時ファイルへ書き出していく ストリーミング方式 で動きます。
public void writeLargeExcel(OutputStream os, List<Order> orders) throws IOException {
// 直近100行だけメモリに保持
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
Sheet sheet = workbook.createSheet("注文");
int rowNum = 0;
for (Order o : orders) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(o.getId());
row.createCell(1).setCellValue(o.getAmount());
}
workbook.write(os);
workbook.dispose(); // 一時ファイルを削除
}
}
コンストラクタの引数 rowAccessWindowSize がメモリに残す行数です。ウィンドウより前の行はフラッシュ済みなので、後から getRow で書き換えできない点だけ気をつけてください。
もう一つ大事なのが dispose() です。SXSSFはディスク上に一時ファイルを作るので、これを呼ばないと一時ファイルが残り続けます。close() とあわせて確実に呼びましょう。
使い分けはシンプルです。小規模で書式を凝るなら XSSFWorkbook、行数が多くてとにかく軽く出したいなら SXSSFWorkbook と考えておけば大きく外しません。出力に時間がかかるなら Spring Bootで非同期処理を実装する を参考に別スレッドへ逃がすのも手です。
アップロードされたExcelを読み込んでDTOへマッピング
今度は逆方向、アップロードされたxlsxを読み込みます。MultipartFile から InputStream を取り、WorkbookFactory.create() に渡せば形式(xls/xlsx)を自動判別してくれます。
public List<User> parse(MultipartFile file) throws IOException {
List<User> result = new ArrayList<>();
DataFormatter formatter = new DataFormatter();
try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
if (row.getRowNum() == 0) continue; // ヘッダー行をスキップ
if (isEmptyRow(row)) continue; // 空行をスキップ
User user = new User();
user.setId((long) row.getCell(0).getNumericCellValue());
user.setName(formatter.formatCellValue(row.getCell(1)));
result.add(user);
}
}
return result;
}
文字列として素直に取りたいなら DataFormatter が便利です。これは Excelの表示上の値そのまま を文字列で返してくれるので、書式付き数値や日付をそのまま取り込みたいケースで重宝します。1行ぶんのセルを読んでDTOへ詰め、List にまとめて返す、という流れですね。
CRUDの土台づくりは Spring BootでREST APIのCRUDを実装する も参考にしてください。
セルの型判定とよくある例外への対処
Excelパースでハマりやすいのが型です。数値セルに対して getStringCellValue() を呼ぶと IllegalStateException: Cannot get a STRING value from a NUMERIC cell が飛びます。セルの型は getCellType() で判定しましょう。
String readAsString(Cell cell) {
if (cell == null) return "";
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue();
case NUMERIC -> DateUtil.isCellDateFormatted(cell)
? cell.getLocalDateTimeCellValue().toString()
: String.valueOf(cell.getNumericCellValue());
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
case BLANK -> "";
default -> "";
};
}
日付はExcel内部では数値なので、CellType.NUMERIC のうち DateUtil.isCellDateFormatted() が真のものが日付、というのがコツです。
もう一つの落とし穴が欠損セルです。値が空のセルを getCell すると null が返ることがあり、そのまま触ると NullPointerException になります。getCell に MissingCellPolicy を渡すと挙動を制御できます。
// 欠損セルを null ではなく空セルとして返す
Cell cell = row.getCell(2, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
取り込みでは想定外フォーマットがつきものなので、行番号を添えて「3行目の金額が数値ではありません」のようなメッセージで返すと、利用者が修正しやすくなります。例外を握りつぶさず、どの行のどの列で失敗したかを伝えるのが実務では効きます。
まとめ
Apache POIでのExcel入出力を一通り見てきました。要点を整理しておきます。
- 出力は
XSSFWorkbookが基本、大量行はSXSSFWorkbookでメモリ対策。後者はdispose()を忘れずに - ダウンロードはContent-Typeにxlsx用MIMEタイプ、日本語ファイル名は
filename*=UTF-8''でエンコード - 読み込みは
WorkbookFactoryで開き、getCellType()の型判定とDataFormatterが肝。欠損セルはMissingCellPolicyで安全に
Excelは型判定と例外処理さえ丁寧にやれば怖くありません。同じ帳票でもPDFが必要なら Spring BootでPDFを生成する(OpenPDF / iText) もあるので、要件にあわせて使い分けてみてください。