在处理业务系统后台的导出需求时,经常会遇到一次性生成上百万行记录的 Excel 文件。如果使用 Apache POI 默认的 XSSF 模式,所有单元格对象都会驻留于 JVM 堆内存,极易触发 OutOfMemoryError。SXSSF(Streaming Usermodel API)是 POI 提供的低内存写入方案,通过在内存中只保留可配置数量的行,其余行自动刷入磁盘临时文件,从而让普通配置的服务也能完成海量数据导出。

一、SXSSF 的核心原理与基础用法
SXSSF 本质上是对 XSSF 的包装,它在内部维护一个固定大小的“行访问窗口”。当写入的行数超过窗口大小,最旧的行会被写入临时 XML 文件并从内存移除。最终生成文件时,POI 将这些临时片段合并为合法的 XLSX 压缩包。默认窗口大小为 100,即内存中最多保留 100 行,开发者也可自行设定。
下面是一段最基础的 SXSSF 导出示例,演示如何创建 Workbook、Sheet 并逐行写入十万条数据。注意 SXSSFWorkbook 构造时可传入窗口大小参数,且使用完后必须调用 dispose 清理临时文件。
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import java.io.FileOutputStream;
public class SimpleExport {
public static void main(String[] args) throws Exception {
// 设置行窗口为500,内存中最多保留500行
SXSSFWorkbook wb = new SXSSFWorkbook(500);
Sheet sheet = wb.createSheet("大报表");
for (int i = 0; i < 100000; i++) {
Row row = sheet.createRow(i);
Cell c0 = row.createCell(0);
c0.setCellValue("编号" + i);
Cell c1 = row.createCell(1);
c1.setCellValue(i * 1.5);
}
try (FileOutputStream out = new FileOutputStream("big.xlsx")) {
wb.write(out);
}
// 必须释放临时文件
wb.dispose();
}
}
上述代码在循环过程中,POI 会自动将超出窗口的旧行刷到磁盘。即便循环一百万次,堆内存占用也维持在较低水平。不过要注意,如果单行单元格数量极多或使用了复杂样式,单行的内存开销仍不可忽视,此时应适当调小窗口值。
与之相比,若使用 XSSFWorkbook 直接循环写入百万行,所有 Row 和 Cell 对象都不会被回收,通常堆内存达到几百 MB 到数 GB 就会崩溃。SXSSF 用时间换空间,牺牲了少量 IO 性能,换取了稳定的内存表现,是服务端批量导出的首选。
二、结合数据库分批查询避免内存叠加
即便 SXSSF 控制了 Excel 对象的内存,若一次性从数据库查出百万条实体到 List 中,同样会造成内存溢出。正确做法是利用数据库游标或分页,每次只取一小批(如 5000 条),写入 Excel 后立即释放集合引用,让 GC 回收。
以下示例展示使用 MyBatis 的流式查询,配合 SXSSF 边读边写。通过 fetchSize 提示驱动使用服务端游标,避免客户端缓存全部结果。每写入一批后,将局部列表置为 null 有助于及时回收。
import org.apache.ibatis.cursor.Cursor;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import java.io.OutputStream;
public void exportStream(Cursor<DataEntity> cursor, OutputStream out) throws Exception {
SXSSFWorkbook wb = new SXSSFWorkbook(1000);
Sheet sheet = wb.createSheet("数据");
int rowNum = 0;
for (DataEntity entity : cursor) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(entity.getId());
row.createCell(1).setCellValue(entity.getName());
// 每1万行手动刷盘一次,减轻临时文件堆积
if (rowNum % 10000 == 0) {
((SXSSFSheet) sheet).flushRows(10000);
}
}
wb.write(out);
wb.dispose();
}
flushRows 方法可强制将指定数量的行写入磁盘并移出内存,虽然 SXSSF 会自动管理,但在极大批量场景下主动调用能让内存曲线更平稳。同时,数据库侧也应创建合适的索引,保证流式读取不会因全表扫描而阻塞。
另外,导出任务通常耗时较长,建议将其放入异步线程或通过消息队列触发,并向前端返回任务编号供后续下载。这样既能避免 HTTP 超时,也能让系统资源调度更可控。
三、样式与合并单元格的注意事项
在 SXSSF 中,CellStyle 和 Font 等样式对象仍由底层 XSSF 统一管理,并不会随行刷盘而释放。如果为每一行都 createCellStyle,会导致样式数量暴涨,最终在写入临时文件时占用大量内存并拖慢速度。应当复用少量样式对象。
下面的代码演示如何只创建一次表头样式与数据样式,供所有行共用。同时展示在 SXSSF 中合并单元格的写法,合并信息会在内存中记录,不宜过多。
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
public class StyleExport {
public static void main(String[] args) throws Exception {
SXSSFWorkbook wb = new SXSSFWorkbook(200);
Sheet sheet = wb.createSheet("报表");
// 复用表头样式
CellStyle headStyle = wb.createCellStyle();
headStyle.setBold(true);
Font font = wb.createFont();
font.setBold(true);
headStyle.setFont(font);
Row head = sheet.createRow(0);
Cell hc = head.createCell(0);
hc.setCellValue("列名");
hc.setCellStyle(headStyle);
// 合并第一行前两列
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 1));
for (int i = 1; i < 500000; i++) {
Row row = sheet.createRow(i);
Cell c = row.createCell(0);
c.setCellValue("数据" + i);
// 不新建样式,使用默认
}
wb.write(new java.io.FileOutputStream("style.xlsx"));
wb.dispose();
}
}
需要特别留意,SXSSF 不支持修改已经刷盘的行,也就是说一旦某行被写入临时文件,就无法再对其设置值或样式。因此所有数据填充必须在行创建后立即完成,不能依赖后续回写逻辑。
若业务必须生成复杂格式(如多级表头、大量公式),可考虑在内存允许范围内适当增大窗口,或拆分成多个 Sheet 分别导出再打包,以平衡实现成本与内存安全。
四、临时文件与部署环境调优
SXSSF 默认将临时文件存放在 JVM 的 java.io.tmpdir 目录下,文件名以 poi-sxssf 开头。在容器化环境中,若该目录空间较小或禁止写入,导出会失败。可通过 SXSSFWorkbook 的构造参数注入自定义的 TemporaryFileCreationStrategy 来指定挂载卷路径。
以下片段展示如何设置临时目录到指定挂载路径,避免容器内部存储受限问题。同时在高并发导出时,应控制同时执行的任务数,防止磁盘 IO 竞争。
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.streaming.DefaultTemporaryFileCreationStrategy;
import java.io.File;
public SXSSFWorkbook buildWorkbook() {
File tmpDir = new File("/mnt/export_tmp");
if (!tmpDir.exists()) {
tmpDir.mkdirs();
}
DefaultTemporaryFileCreationStrategy strategy =
new DefaultTemporaryFileCreationStrategy(tmpDir, "export", 0);
return new SXSSFWorkbook(null, 500, false, strategy);
}
此外,在 Linux 服务器上可通过 free 与 iostat 观察导出时的内存与磁盘负载。若发现 IO 等待过高,可改用更快的 SSD 或调整窗口大小减少刷盘频率。最后务必在 finally 块或 try-with-resources 中调用 dispose,否则临时文件会残留并慢慢占满磁盘。
综合来看,SXSSF 模式通过滑动窗口与磁盘溢写,把原本不可能完成的单机百万级 Excel 导出变得可行。只要配合分批查数、样式复用与临时目录规划,就能在普通业务服务器上稳定输出超大规模报表文件。
Apache_POISXSSFExcel导出修改时间:2026-08-06 13:45:42