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方法可以看到:
- 方法内部会将工作簿模板写入临时文件,再将生成的工作表数据注入到输出流
- 流程中使用了try-with-resources,会自动关闭相关流资源
- 循环重复调用
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
相关产品推荐
相关产品推荐

