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 防止内存溢出
确认实际写入数据 不要生成空文件
相关推荐
数据智能老司机9 分钟前
DevOps 安全与自动化——理解 DevOps 文化与原则
架构·自动化运维·devops
LaoZhangAI9 分钟前
GPT-5推理能力全解析:o3架构、链式思考与2025年8月发布
前端·后端
海风极客11 分钟前
Go内存逃逸分析,真的很神奇吗?
后端·面试
数据智能老司机12 分钟前
DevOps 安全与自动化——开发环境搭建
架构·自动化运维·devops
寻月隐君26 分钟前
Rust 泛型 Trait:关联类型与泛型参数的核心区别
后端·rust·github
泥泞开出花朵28 分钟前
LRU缓存淘汰算法的详细介绍与具体实现
java·数据结构·后端·算法·缓存
子洋34 分钟前
快速目录跳转工具 zoxide 使用指南
前端·后端·shell
天下无贼!35 分钟前
【自制组件库】从零到一实现属于自己的 Vue3 组件库!!!
前端·javascript·vue.js·ui·架构·scss
Doris_LMS1 小时前
Linux的访问权限(保姆级别)
linux·运维·服务器·面试