最近在写 Java 代码处理 Excel 文件的时候,遇到了一个挺头疼的问题:使用 Apache POI 的 XSSFWorkbook.write(FileOutputStream) 方法写文件,代码执行得好好的,也没有抛出异常,但生成的 Excel 文件却打不开,甚至有时候文件大小还是 0 字节,一点数据都没有。
本来以为是 POI 的问题,结果查了一圈文档才发现------锅还真不在 POI,而是我自己对文件输出流的使用方式不太对,尤其是涉及到 FileOutputStream 的时候,有些隐藏的"坑"没注意到。
这篇文章就把我踩坑的过程整理一下,顺便聊聊 Java 中如何正确地使用输出流写 Excel 文件,避免"写了但没落盘"的问题。
1. FileOutputStream 本身是没有缓冲的
我们先来看看一个最常见的代码片段:
ini
Workbook workbook = new XSSFWorkbook(inputStream);
workbook.write(new FileOutputStream("output.xlsx"));
这样写看起来挺顺,但你知道吗?这里的 FileOutputStream
是直接把数据写到操作系统的,没有中间的缓冲区。如果你的数据量很大,比如几百 KB,甚至几 MB,虽然代码没报错,但你可能会发现文件根本没写完整,或者干脆就是个空壳文件。
为什么?
因为操作系统本身还会有一个写入缓冲区(Page Cache),你并不能保证调用了 write() 之后,数据就马上稳稳当当地落到了磁盘上。如果你没有关闭输出流或者手动调用 flush(),这些数据可能就一直在内存里排队,根本没真正写进文件。
2. 没有 flush() 或 close(),数据可能永远不会写进硬盘
这是很多人常犯的一个错误。看上去代码没问题,但一旦你漏掉了 flush() 或者 close(),就会导致写入的数据停留在缓冲区里,始终不落盘。
比如下面这段代码就是"反面教材":
ini
FileOutputStream fos = new FileOutputStream("output.xlsx");
workbook.write(fos);
// 没有 fos.flush()
// 没有 fos.close()
你以为 write() 就完事了,其实根本没有。解决方案很简单,要么在写完之后手动调用:
ini
fos.flush();
fos.close();
要么------更推荐的方式是使用 try-with-resources 来自动帮你处理这些关闭操作。
3. 用 BufferedOutputStream 包装一下,写得更稳也更快
前面说了,FileOutputStream 是没有缓冲的,这意味着它每调用一次 write() 就是一次底层系统调用,效率其实挺低的,尤其是在 Apache POI 这种写 Excel 文件会反复调用 write() 的场景下。
所以非常推荐你用 BufferedOutputStream 包一下:
ini
OutputStream bos = new BufferedOutputStream(new FileOutputStream("output.xlsx"));
workbook.write(bos);
bos.flush();
bos.close();
多一层缓冲不仅能提升写入速度,更重要的是减少系统调用的频率,能让写入过程更加稳定可靠。
4. 推荐用法:try-with-resources,优雅又安全
说了这么多,其实最靠谱、最简单、最不容易出错的写法,还是 Java 7 引入的 try-with-resources。
你只要这么写:
ini
try (
InputStream inputStream = new FileInputStream("template.xlsx");
Workbook workbook = new XSSFWorkbook(inputStream);
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream("output.xlsx"))
) {
workbook.write(outputStream);
}
Java 会自动帮你在块结束后关闭 inputStream、workbook
和 outputStream,再也不用担心忘了 flush()
或 close()
了,简直不要太爽。
5. 如果你就是不想用 try-with-resources,也请手动关闭资源
当然,也不是所有项目都能用上 Java 7 及以上版本的语法,博主前些时间就接到了一个Java 6的项目咨询,还真不是,你发任你发,我用Java 8。哈哈,有些老项目没法用 try-with-resources。那也不是不能写,你只要自己负责把所有资源都在 finally 中手动关闭,也一样可以稳稳落盘。
注意关闭的顺序要搞对,先关 workbook,再关输出流。
示例如下:
ini
Workbook workbook = null;
BufferedOutputStream bos = null;
try {
workbook = new XSSFWorkbook();
bos = new BufferedOutputStream(new FileOutputStream("output.xlsx"));
workbook.write(bos);
bos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (workbook != null) workbook.close();
if (bos != null) bos.close(); // close 会自动 flush
} catch (IOException e) {
e.printStackTrace();
}
}
记住:close() 会自动调用 flush(),但你也可以显式加一遍 flush(),确保保险。
大 Excel 文件时内存溢出风险
- XSSFWorkbook 加载整个 .xlsx 到内存;
- 写入也可能消耗大量内存;
- 超过几十万行时可能抛出 OOM。
大数据量推荐使用 SXSSFWorkbook:
ini
SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook((XSSFWorkbook) workbook);
sxssfWorkbook.write(outputStream);
sxssfWorkbook.dispose(); // 清理临时文件
6. 工作簿数据本身也别忘了检查
最后还有一个冷门但真实的情况是------你其实根本就没有往 Workbook
里写任何东西。这样写出来的 Excel 文件虽然也是合法的 .xlsx
,但打开后是空白页,或者打开报错,看上去像是"没写进去",其实是你没写进去数据......
你可以加个调试代码确认:
c
log.info("sheet count: {}", workbook.getNumberOfSheets());
7. 防止"写了但没落盘"的几点 checklist
检查项 | 建议 |
---|---|
使用缓冲流 | BufferedOutputStream 性能更稳 |
手动或自动关闭 | flush() + close 必不可少 |
优先使用 try-with-resources | 推荐写法,防忘关 |
大文件用 SXSSFWorkbook | 防止内存溢出 |
确认实际写入数据 | 不要生成空文件 |