最近在寫 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

防止內存溢出

確認實際寫入數據

不要生成空文件