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

最近在写 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 防止内存溢出
确认实际写入数据 不要生成空文件
相关推荐
Lee川24 分钟前
从回调地狱到同步之美:JavaScript异步编程的演进之路
javascript·面试
序安InToo26 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12326 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记29 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0529 分钟前
VS Code 配置 Markdown 环境
后端
navms32 分钟前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang0532 分钟前
离线数仓的优化及重构
后端
Nyarlathotep011333 分钟前
gin01:初探gin的启动
后端·go
JxWang0534 分钟前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang0535 分钟前
Windows Terminal 配置 oh-my-posh
后端