You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Apache POI流式写入XLSX至IO遇流关闭异常,求可行方案

问题描述

我开发了一个Web应用,需要返回包含大量记录的XLSX文件,同时避免OOM(内存溢出)。我希望在循环中逐页写入OutputStream并执行刷新操作,但运行时出现了如下异常:

java.io.IOException: Stream closed
    at java.base/java.io.BufferedWriter.ensureOpen(BufferedWriter.java:132) ~[na:na]
    at java.base/java.io.BufferedWriter.implWrite(BufferedWriter.java:325) ~[na:na]
    at java.base/java.io.BufferedWriter.write(BufferedWriter.java:313) ~[na:na]
    at java.base/java.io.Writer.write(Writer.java:278) ~[na:na]

请问是否支持逐页向输出流写入数据,还是必须将所有数据加载到内存中才能实现?


相关代码

导出服务代码

public void export(final OutputStream os)  {
        
    try (SXSSFWorkbook workbook = new SXSSFWorkbook(-1)) {

        final SXSSFSheet sheet = workbook.createSheet();

        for (int i = 0; i < 100; i++) {
            final SXSSFRow row = sheet.createRow(i);
            final SXSSFCell cell = row.createCell(0, CellType.STRING);
            cell.setCellValue("Test");

            try {
                workbook.writeAvoidingTempFiles(os);
                os.flush();
            } catch (IOException e) {
                log.info("Error", e);
            }

        }
        workbook.dispose();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

}

public void export(final HttpServletResponse httpServletResponse) throws IOException {
        
    try (TestBook workbook = new TestBook()) {

        final SXSSFSheet sheet = workbook.createSheet();

        for (int i = 0; i < 100; i++) {
            final SXSSFRow row = sheet.createRow(i);
            final SXSSFCell cell = row.createCell(0, CellType.STRING);
            cell.setCellValue("Test");
            workbook.write(httpServletResponse.getOutputStream());
            httpServletResponse.flushBuffer();
        }
    }
}

Controller代码

@GetMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, path = "/1")
public ResponseEntity<StreamingResponseBody> download1() {
    return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .header(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=\"" +
                    "test" +
                    ".xlsx\"")
            .body(exportService::export);
}

@GetMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, path = "/2")
public void download2(final HttpServletResponse httpServletResponse) throws IOException {

    httpServletResponse.setHeader("Content-Type", MediaType.APPLICATION_OCTET_STREAM_VALUE);
    httpServletResponse.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=\"" +
            "test2" +
            ".xlsx\"");

    exportService.export(httpServletResponse);
}

自定义Workbook类

static class TestBook extends SXSSFWorkbook {
    public TestBook() {
        super(-1);
    }
}

SXSSFWorkbook原始write方法

public void write(OutputStream stream) throws IOException {
    flushSheets();

    //Save the template
    File tmplFile = TempFile.createTempFile("poi-sxssf-template", ".xlsx");
    boolean deleted;
    try {
        try (FileOutputStream os = new FileOutputStream(tmplFile)) {
            _wb.write(os);
        }

        //Substitute the template entries with the generated sheet data files
        try (
                ZipSecureFile zf = new ZipSecureFile(tmplFile);
                ZipFileZipEntrySource source = new ZipFileZipEntrySource(zf)
        ) {
            injectData(source, stream);
        }
    } finally {
        deleted = tmplFile.delete();
    }
    if (!deleted) {
        throw new IOException("Could not delete temporary file after processing: " + tmplFile);
    }
}

解决方案

首先,SXSSF本身就是为避免OOM设计的,但它不支持循环多次调用write或writeAvoidingTempFiles方法向同一个输出流写入数据。你遇到的Stream closed异常,本质是第一次调用write方法时,POI会完成整个XLSX文件的写入流程,包括关闭相关流资源,后续再调用就会报错。

问题根源

从你贴出的原始write方法可以看到:

  1. 方法内部会将工作簿模板写入临时文件,再将生成的工作表数据注入到输出流
  2. 流程中使用了try-with-resources,会自动关闭相关流资源
  3. 循环重复调用write时,第二次调用的输出流已经被关闭,因此抛出异常

正确实现方式

不需要循环写入输出流,SXSSF已经帮你做了内存优化:

  • 设置SXSSFWorkbook(-1)时,所有行都会被写入临时文件,内存中不会保存过多数据
  • 只需要在所有数据写入完成后,调用一次write方法即可

修改后的导出方法示例:

public void export(final OutputStream os) {
    try (SXSSFWorkbook workbook = new SXSSFWorkbook(-1)) {
        SXSSFSheet sheet = workbook.createSheet();
        
        // 循环写入所有数据,SXSSF会自动将溢出的行写到临时文件
        for (int i = 0; i < 100000; i++) {
            SXSSFRow row = sheet.createRow(i);
            SXSSFCell cell = row.createCell(0, CellType.STRING);
            cell.setCellValue("Test " + i);
        }
        
        // 所有数据写完后,一次性写入输出流
        workbook.write(os);
        os.flush();
        workbook.dispose();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

额外说明

  • 如果你需要更极致的流式写入,可以考虑使用Apache POI的Streaming API(XSSFStreamingSheet),但SXSSF已经能满足绝大多数海量数据导出场景
  • 不要在循环中调用write方法,这不符合XLSX文件的结构逻辑——XLSX是压缩包格式,必须整体生成后才能完整输出,无法像CSV那样逐行流式写入

内容的提问来源于stack exchange,提问作者Matrix12

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 01:32:21