4. Apache POI 实战:HSSF 与 XSSF 区别、SXSSF 流式写入原理、Workbook/Sheet/Row/Cell 操作、样式与公式注入
Apache POI 是 Java 生态中操作 Office 文档的事实标准。在报表导出场景中,POI 的三大子模块——HSSF、XSSF、SXSSF——分别对应不同的内存模型与写入策略。选错模型可能导致百万行数据导出时 OOM,或 10 万行数据写入耗时数分钟。本章从架构视角拆解三者的差异、流式写入原理,并给出 Workbook/Sheet/Row/Cell 的实操模板,最后覆盖样式与公式注入的常见陷阱。
4.1 HSSF vs XSSF vs SXSSF:三大模型对比
POI 的三个子模块并非简单的版本升级,而是针对不同数据规模与文件格式的独立实现。HSSF 操作 Excel 97-2003 (.xls),XSSF 操作 Excel 2007+ (.xlsx),SXSSF 是 XSSF 的流式变体。下表从核心维度进行对比:
| 维度 | HSSF | XSSF | SXSSF |
|---|---|---|---|
| 文件格式 | .xls (BIFF8) | .xlsx (OOXML) | .xlsx (OOXML) |
| 内存模型 | 全部加载到内存 | 全部加载到内存 | 滑动窗口 + 临时文件 |
| 最大行数 | 65,536 | 1,048,576 | 1,048,576 |
| 写入速度(10万行) | 慢(约 30s) | 中等(约 15s) | 快(约 5s) |
| 内存占用(10万行) | 高(约 500MB) | 高(约 600MB) | 低(约 50MB) |
| 随机读支持 | 是 | 是 | 否(仅写模式) |
| 适用场景 | 遗留系统、小数据量 | 中等数据量、需要模板编辑 | 大数据量导出、流式写入 |
4.2 SXSSF 流式写入原理
SXSSF 的核心思想是“滑动窗口 + 临时文件”。它并不将整个 Workbook 保存在内存中,而是维护一个可配置的行窗口(默认 100 行),窗口之外的行被序列化到临时文件。当调用 flushRows() 或窗口滑动时,旧行被写入磁盘,内存只保留当前窗口内的行对象。
关键参数与行为:
- 窗口大小(windowSize): 通过
new SXSSFWorkbook(100)设置,表示内存中最多保留 100 行。超过时最早的行被刷入临时文件。 - 临时文件清理: 调用
workbook.dispose()后,临时文件被删除。若 JVM 异常退出,临时文件可能残留,建议在 finally 块中确保清理。 - 限制: SXSSF 不支持
getRow()随机读取已刷出的行,因此无法在写入后修改已刷出的数据。如果需要模板读取 + 大数据写入,应先用 XSSF 读取模板,再转为 SXSSF 写入。
getSheetAt(0).getRow(0) 来读取已写入的数据——如果该行已被刷出,会返回 null。SXSSF 是“只写”模型,所有读取操作应在刷出前完成。
4.3 Workbook / Sheet / Row / Cell 操作实战
无论使用 HSSF、XSSF 还是 SXSSF,Workbook、Sheet、Row、Cell 的 API 接口是统一的(均继承自 org.apache.poi.ss.usermodel 包)。以下代码展示一个完整的报表导出模板:
// 1. 创建工作簿(根据数据量选择实现类)
SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 窗口大小 100
// XSSFWorkbook workbook = new XSSFWorkbook(); // 小数据量
// HSSFWorkbook workbook = new HSSFWorkbook(); // 仅 .xls
// 2. 创建 Sheet
Sheet sheet = workbook.createSheet("报表数据");
// 3. 创建标题行(Row 0)
Row headerRow = sheet.createRow(0);
String[] headers = {"订单ID", "客户名称", "金额", "状态"};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
// 样式注入(见 4.4 节)
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerFont.setFontHeightInPoints((short) 12);
headerStyle.setFont(headerFont);
headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
cell.setCellStyle(headerStyle);
}
// 4. 写入数据行(从 Row 1 开始)
List<Order> orders = fetchOrders(); // 假设有 10 万条数据
int rowNum = 1;
for (Order order : orders) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(order.getId());
row.createCell(1).setCellValue(order.getCustomerName());
row.createCell(2).setCellValue(order.getAmount());
row.createCell(3).setCellValue(order.getStatus());
// 每 1000 行触发一次 flush,防止内存堆积
if (rowNum % 1000 == 0) {
((SXSSFSheet) sheet).flushRows(100); // 保留最近 100 行
}
}
// 5. 输出到文件
try (FileOutputStream fos = new FileOutputStream("report.xlsx")) {
workbook.write(fos);
} finally {
workbook.dispose(); // 清理临时文件
}
flushRows(),避免单次刷出行数过多导致 I/O 抖动。同时,createRow() 和 createCell() 应尽量复用对象引用,减少 GC 压力。
4.4 样式与公式注入
样式和公式是报表导出中最容易出错的环节。POI 的样式对象(CellStyle)是 Workbook 级别的共享资源,每个 Workbook 最多支持约 64,000 种样式(XSSF/SXSSF 限制更宽松,但仍建议复用)。公式注入则需注意公式字符串的语法与 Excel 兼容性。
4.4.1 样式复用策略
错误的做法是为每个 Cell 创建独立的 CellStyle,这会导致样式表爆炸,文件体积剧增。正确的做法是预定义样式池:
// 样式池:在 Workbook 创建后立即初始化
Map<String, CellStyle> stylePool = new HashMap<>();
// 创建并缓存样式
private CellStyle getStyle(String key, SXSSFWorkbook wb) {
return stylePool.computeIfAbsent(key, k -> {
CellStyle style = wb.createCellStyle();
Font font = wb.createFont();
switch (k) {
case "header":
font.setBold(true);
font.setColor(IndexedColors.WHITE.getIndex());
style.setFont(font);
style.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
break;
case "money":
style.setDataFormat(wb.createDataFormat().getFormat("#,##0.00"));
break;
case "date":
style.setDataFormat(wb.createDataFormat().getFormat("yyyy-MM-dd"));
break;
}
return style;
});
}
// 使用时直接获取
Cell cell = row.createCell(2);
cell.setCellValue(12345.67);
cell.setCellStyle(getStyle("money", workbook));
4.4.2 公式注入
POI 支持通过 setCellFormula() 注入公式,公式字符串必须使用英文语法,且引用单元格时使用 Excel 的 A1 引用风格。常见场景包括汇总行和条件计算:
// 在数据行写入完成后,在最后一行添加汇总公式
int lastRowNum = sheet.getLastRowNum();
Row summaryRow = sheet.createRow(lastRowNum + 1);
// SUM 公式:对 C2 到 C{lastRowNum} 求和
Cell sumCell = summaryRow.createCell(2);
sumCell.setCellFormula("SUM(C2:C" + lastRowNum + ")");
// 设置公式单元格样式(可选)
CellStyle sumStyle = workbook.createCellStyle();
sumStyle.setDataFormat(workbook.createDataFormat().getFormat("#,##0.00"));
Font sumFont = workbook.createFont();
sumFont.setBold(true);
sumStyle.setFont(sumFont);
sumCell.setCellStyle(sumStyle);
// 公式不会自动计算,需要手动触发或依赖 Excel 打开时计算
// 如果需要预计算,使用 FormulaEvaluator
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
evaluator.evaluateFormulaCell(sumCell);
- SXSSF 不支持公式预计算(
evaluateFormulaCell会抛出异常),因为已刷出的行无法被读取。解决方案:在写入数据时同时维护一个内存中的汇总值,最后直接写入计算结果而非公式。 - 公式中的行号引用必须使用绝对行号(如
C2:C100000),不要使用C:C整列引用,否则 Excel 打开时可能因引用范围过大而卡死。 - 跨 Sheet 引用时,Sheet 名必须用单引号包裹(如
'Sheet1'!A1),且 Sheet 名中不能包含特殊字符。
4.4.3 样式与公式的架构建议
在报表导出系统中,样式和公式应作为“模板配置”的一部分,而非硬编码在代码中。推荐的做法是:
- 样式模板化: 将颜色、字体、边框、数据格式等定义在配置文件或数据库中,运行时动态构建样式池。
- 公式模板化: 使用占位符(如
${sumRange})替换公式中的动态部分,避免字符串拼接错误。 - 性能监控: 在导出日志中记录样式创建数量(
workbook.getNumCellStyles()),超过 1000 种样式时发出告警,提示优化样式复用。