导读:本期聚焦于小伙伴创作的《怎么通过 Apache POI 的 SXSSF 模式实现在低内存消耗下导出百万级超大规模的 Excel 报表数据》,敬请观看详情。导出百万行Excel时,传统XSSF会把整张表读进内存,几万行就撑爆堆空间。SXSSF基于滑动窗口机制,只保留少量行在内存,其余刷到临时文件。本文说明如何创建SXSSFWorkbook、设置行访问窗口、流式写数据并清理临时文件,同时给出分批查询数据库与自定义样式时的注意点,帮你在普通服务器上稳定生成超大报表。

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

怎么通过 Apache POI 的 SXSSF 模式实现在低内存消耗下导出百万级超大规模的 Excel 报表数据

一、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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。