最近在寫 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 |
防止內存溢出 |
|
確認實際寫入數據 |
不要生成空文件 |