Java 使用 FileOutputStream 写 Excel 文件不落盘?

最近在写 Java 代码处理 Excel 文件的时候,遇到了一个挺头疼的问题:使用 Apache POI 的 XSSFWorkbook.write(FileOutputStream) 方法写文件,代码执行得好好的,也没有抛出异常,但生成的 Excel 文件却打不开,甚至有时候文件大小还是 0 字节,一点数据都没有。

本来以为是 POI 的问题,结果查了一圈文档才发现------锅还真不在 POI,而是我自己对文件输出流的使用方式不太对,尤其是涉及到 FileOutputStream 的时候,有些隐藏的"坑"没注意到。

这篇文章就把我踩坑的过程整理一下,顺便聊聊 Java 中如何正确地使用输出流写 Excel 文件,避免"写了但没落盘"的问题。

1. FileOutputStream 本身是没有缓冲

我们先来看看一个最常见的代码片段:

复制代码
Workbook workbook = new XSSFWorkbook(inputStream);
workbook.write(new FileOutputStream("output.xlsx"));

这样写看起来挺顺,但你知道吗?这里的 FileOutputStream 直接把数据写到操作系统的,没有中间的缓冲区。如果你的数据量很大,比如几百 KB,甚至几 MB,虽然代码没报错,但你可能会发现文件根本没写完整,或者干脆就是个空壳文件。

为什么?

因为操作系统本身还会有一个写入缓冲区(Page Cache),你并不能保证调用了 write() 之后,数据就马上稳稳当当地落到了磁盘上。如果你没有关闭输出流或者手动调用flush(),这些数据可能就一直在内存里排队,根本没真正写进文件。

2. 没有 flush() 或 close(),数据可能永远不会写进硬盘

这是很多人常犯的一个错误。看上去代码没问题,但一旦你漏掉了 flush() 或者 close(),就会导致写入的数据停留在缓冲区里,始终不落盘。

比如下面这段代码就是"反面教材":

复制代码
FileOutputStream fos = new FileOutputStream("output.xlsx");
workbook.write(fos);
// 没有 fos.flush()
// 没有 fos.close()

你以为 write() 就完事了,其实根本没有。解决方案很简单,要么在写完之后手动调用:

复制代码
fos.flush();
fos.close();

要么------更推荐的方式是使用 try-with-resources 来自动帮你处理这些关闭操作。

3. 用 BufferedOutputStream 包装一下,写得更稳也更快

前面说了,FileOutputStream 是没有缓冲的,这意味着它每调用一次 write() 就是一次底层系统调用,效率其实挺低的,尤其是在 Apache POI 这种写 Excel 文件会反复调用 write() 的场景下。

所以非常推荐你用 BufferedOutputStream 包一下:

复制代码
OutputStream bos = new BufferedOutputStream(new FileOutputStream("output.xlsx"));
workbook.write(bos);
bos.flush();
bos.close();

多一层缓冲不仅能提升写入速度,更重要的是减少系统调用的频率,能让写入过程更加稳定可靠。

4. 推荐用法:try-with-resources,优雅又安全

说了这么多,其实最靠谱、最简单、最不容易出错的写法,还是 Java 7 引入的 try-with-resources。

你只要这么写:

复制代码
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,再关输出流。

示例如下:

复制代码
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:

复制代码
SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook((XSSFWorkbook) workbook); 
sxssfWorkbook.write(outputStream);
sxssfWorkbook.dispose(); // 清理临时文件

6. 工作簿数据本身也别忘了检查

最后还有一个冷门但真实的情况是------你其实根本就没有往 Workbook 里写任何东西。这样写出来的 Excel 文件虽然也是合法的 .xlsx,但打开后是空白页,或者打开报错,看上去像是"没写进去",其实是你没写进去数据......

你可以加个调试代码确认:

复制代码
log.info("sheet count: {}", workbook.getNumberOfSheets());

7. 防止"写了但没落盘"的几点 checklist

检查项 建议
使用缓冲流 BufferedOutputStream 性能更稳
手动或自动关闭 flush() + close 必不可少
优先使用 try-with-resources 推荐写法,防忘关
大文件用 SXSSFWorkbook 防止内存溢出
确认实际写入数据 不要生成空文件
相关推荐
hqwest16 小时前
码上通QT实战08--导航按钮切换界面
开发语言·qt·slot·信号与槽·connect·signals·emit
AC赳赳老秦17 小时前
DeepSeek 私有化部署避坑指南:敏感数据本地化处理与合规性检测详解
大数据·开发语言·数据库·人工智能·自动化·php·deepseek
默默前行的虫虫17 小时前
nicegui文件上传归纳
python
不知道累,只知道类17 小时前
深入理解 Java 虚拟线程 (Project Loom)
java·开发语言
一个没有本领的人18 小时前
UIU-Net运行记录
python
国强_dev18 小时前
Python 的“非直接原因”报错
开发语言·python
YMatrix 官方技术社区18 小时前
YMatrix 存储引擎解密:MARS3 存储引擎如何超越传统行存、列存实现“时序+分析“场景性能大幅提升?
开发语言·数据库·时序数据库·数据库架构·智慧工厂·存储引擎·ymatrix
玖疯子18 小时前
技术文章大纲:Bug悬案侦破大会
开发语言·ar
副露のmagic18 小时前
更弱智的算法学习 day24
python·学习·算法
廖圣平18 小时前
从零开始,福袋直播间脚本研究【三】《多进程执行selenium》
python·selenium·测试工具