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 | 文字 |
Number(Integer/Long/Double/BigDecimal…) | 數值 |
Date | 日期(未設格式時自動套用 yyyy-mm-dd hh:mm:ss) |
Boolean | 布林 |
null | 空白儲存格 |
| 其他 | 轉為字串 |
讀取(Cell#getValue() 依儲存格型別還原)
| 儲存格型別 | 還原為 |
|---|---|
| 文字 | String(空字串與字面 NULL 視為 null) |
| 數值(整數值) | Long |
| 數值(非整數) | Double |
| 數值(日期格式) | Date |
| 布林 | Boolean |
| 公式 | 先求值,再依結果型別還原 |
| 空白 | null |
讀出後可用 WorkbookRecord 的型別安全方法進一步轉換:getAsString、getAsInteger、getAsLong、getAsBigDecimal、getAsDate、getAsLocalDateTime、getAsBoolean 等。
常見場景
場景 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 | @ | 純文字 |
DATE | yyyy-mm-dd | 日期 |
DATETIME | yyyy-mm-dd hh:mm:ss | 日期時間(Date 預設) |
TIME | hh:mm:ss | 時間 |
INTEGER | #,##0 | 整數(千分位) |
FLOAT | #,##0.00 | 小數(千分位) |
MONEY | $#,##0.00 | 貨幣 |
PERCENT | 0.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。