跳至主要内容

Workbook (Excel) 模組使用指南

Package: io.leandev.appfuse.workbook.* 狀態: 穩定(v1) 格式: Office Open XML(.xlsx


簡介

AppFuse Workbook 提供穩定的 Excel(.xlsx)讀寫 API,封裝底層 Apache POI,讓應用層不直接接觸 POI 型別。

核心特色

特色說明價值
型別感知讀寫setValue(Object) 自動對映、getValue() 還原 Java 型別不必手動判斷 cell type
欄位名稱存取record.getAsBigDecimal("price")沿用框架 Record 的型別安全轉換
物件序列化writer.write(product) 依 Header 自動提取屬性無需手動映射
依賴隔離層底層 POI 不洩漏到公開 API升級 / 替換底層不影響應用層

範圍與限制(v1)

項目支援
.xlsx(XSSF)
.xls(HSSF,舊版二進位)❌ 規劃中
Rich Text(單格內混排字型)❌ 規劃中
串流寫出(SXSSF,超大檔)❌ 規劃中;目前為記憶體模式

記憶體模式:整份活頁簿常駐記憶體。匯出數十萬列以上時請留意記憶體用量。


快速開始

寫入 Excel

import io.leandev.appfuse.workbook.*;

try (Workbook workbook = Workbook.create();
OutputStream os = new FileOutputStream("products.xlsx")) {

WorkbookWriter writer = new WorkbookWriter(workbook.createSheet("Products"));
writer.writeHeaders("id", "name", "price", "stock");

for (Product product : productService.findAll()) {
writer.write(product); // 依 Header 順序自動提取物件屬性
}

workbook.autoSizeColumns();
workbook.write(os);
}

讀取 Excel

try (InputStream is = new FileInputStream("products.xlsx");
Workbook workbook = Workbook.open(is)) {

WorkbookReader reader = new WorkbookReader(workbook.getSheetAt(0));
reader.readHeaders();

for (WorkbookRecord record = reader.read(); record != null; record = reader.read()) {
String name = record.getAsString("name");
BigDecimal price = record.getAsBigDecimal("price");
Integer stock = record.getAsInteger("stock");
// 處理資料...
}
}

使用 Stream API

try (Workbook workbook = Workbook.open(inputStream)) {
WorkbookReader reader = new WorkbookReader(workbook.getSheetAt(0));
reader.readHeaders();

List<Product> lowStock = reader.stream()
.filter(record -> record.getAsInteger("stock") < 10)
.map(record -> new Product(
record.getAsString("name"),
record.getAsBigDecimal("price"),
record.getAsInteger("stock")))
.toList();
}

read() / stream() 會自動跳過空白列。


支援的資料型別

寫入(Cell#setValue(Object) 依執行期型別對映)

Java 型別寫入結果
String文字
NumberInteger/Long/Double/BigDecimal…)數值
Date日期(未設格式時自動套用 yyyy-mm-dd hh:mm:ss
Boolean布林
null空白儲存格
其他轉為字串

讀取(Cell#getValue() 依儲存格型別還原)

儲存格型別還原為
文字String(空字串與字面 NULL 視為 null
數值(整數值)Long
數值(非整數)Double
數值(日期格式)Date
布林Boolean
公式先求值,再依結果型別還原
空白null

讀出後可用 WorkbookRecord 的型別安全方法進一步轉換:getAsStringgetAsIntegergetAsLonggetAsBigDecimalgetAsDategetAsLocalDateTimegetAsBoolean 等。


常見場景

場景 1: 從資料庫匯出報表

List<Order> orders = orderRepository.findAll();

try (Workbook workbook = Workbook.create();
OutputStream os = response.getOutputStream()) {

WorkbookWriter writer = new WorkbookWriter(workbook.createSheet("Orders"));
writer.writeHeaders("orderNo", "customer", "amount", "createdOn");
orders.forEach(writer::write);

workbook.autoSizeColumns();
workbook.write(os);
}

場景 2: 匯入到資料庫

try (Workbook workbook = Workbook.open(uploadedFile.getInputStream())) {
WorkbookReader reader = new WorkbookReader(workbook.getSheetAt(0));
reader.readHeaders();

List<Product> products = reader.stream()
.map(record -> new Product(
record.getAsString("name"),
record.getAsBigDecimal("price"),
record.getAsInteger("stock")))
.toList();

productRepository.saveAll(products);
}

場景 3: 逐格控制(標題列樣式 + 合併儲存格)

try (Workbook workbook = Workbook.create()) {
Worksheet sheet = workbook.createSheet("Report");

// 標題樣式(共用)
CellStyle titleStyle = workbook.createCellStyle()
.setFontSize((short) 16)
.setBold(true)
.setAlign(Align.CENTER);

// 合併首列三欄作為標題
Row titleRow = sheet.createRow();
Cell title = titleRow.createCell();
title.setValue("2026 Q1 銷售報表");
title.setStyle(titleStyle);
sheet.addMergedRegion(0, 0, 0, 2);

Row header = sheet.createRow();
header.createCell().setValue("產品");
header.createCell().setValue("數量");
header.createCell().setValue("金額");

Row data = sheet.createRow();
data.createCell().setValue("玫瑰花束");
data.createCell().setValue(12);
Cell amount = data.createCell();
amount.setValue(new BigDecimal("15360"));
amount.setDataFormat(DataFormat.MONEY);

workbook.write(outputStream);
}

樣式

逐格設定

Cell cell = row.createCell();
cell.setValue("重要");
cell.setColor(Color.RED); // java.awt.Color
cell.setBackground(Color.YELLOW);
cell.setAlign(Align.CENTER);
cell.setBorderStyle(BorderStyle.THIN);
cell.setDataFormat(DataFormat.MONEY); // 或 setDataFormat("#,##0.00")
cell.setWrapText(true);

共用樣式(建議:大量套用時)

Excel 對單一活頁簿的樣式數量有上限(約 64,000)。大量套用相同外觀時,建立一次共用樣式再套用到多個儲存格,避免逐格產生新樣式:

CellStyle money = workbook.createCellStyle()
.setDataFormat(DataFormat.MONEY.pattern())
.setColor(Color.BLACK)
.setBold(true)
.setAlign(Align.RIGHT);

for (Order order : orders) {
Cell cell = sheet.createRow().createCell();
cell.setValue(order.getAmount());
cell.setStyle(money); // 重用同一個樣式物件
}

內建資料格式(DataFormat

列舉Excel 格式用途
TEXT@純文字
DATEyyyy-mm-dd日期
DATETIMEyyyy-mm-dd hh:mm:ss日期時間(Date 預設)
TIMEhh:mm:ss時間
INTEGER#,##0整數(千分位)
FLOAT#,##0.00小數(千分位)
MONEY$#,##0.00貨幣
PERCENT0.00%百分比

需要自訂格式時改用 cell.setDataFormat("自訂 Excel 格式字串")


公式

// 寫入公式(不含前導 =)
Cell total = row.createCell();
total.setFormula("SUM(B2:B10)");

// 讀取時自動求值
Object value = workbook.getSheetAt(0).getRow(0).getCell(2).getValue();

常見問題

Q: 為什麼不直接用 Apache POI?

A: AppFuse Workbook 提供穩定的 API 契約並隔離底層 POI 型別,與 CSV 模組同策略。底層升級或替換時,應用層代碼不受影響;同時統一了型別讀寫與 Record 存取慣例。

Q: 支援 .xls(舊版二進位)嗎?

A: v1 僅支援 .xlsx(XSSF)。.xls(HSSF)在未來規劃中。

Q: 要匯出非常大的檔案(數十萬列)會 OOM 嗎?

A: v1 為記憶體模式,整份活頁簿常駐記憶體。超大檔的串流寫出(SXSSF)在規劃中;現階段建議改用 CSV 模組 串流匯出。

Q: 如何處理欄位不存在的情況?

if (record.indexOf("optional_column") >= 0) {
String value = record.getAsString("optional_column");
}

API 參考

詳細的類別設計和方法簽名,請參閱 Javadoc