轉https://blog.csdn.net/alex_xfboy/article/details/84844667
說實話之前之前沒怎么接觸過POI組件,只知道有這么一個東西可以解決excel讀寫問題,但不用不知道,使用起來真心無語,到處都是坑。接下來我講分享一些在項目中遇到的坑及解決方法,其實社區也有不少類似文章,但講的都比較零散。
1. .xls與.xlsx
首先,科普一些基礎常識:
.xls 是用03版Office Excel ,新建Excel默認保存的Excel文件格式的后綴是.xls,不可以打開編輯07版的xlsx文件,否則出現亂碼或者卡死。行列的上限為 65536行,256列。
.xlsx 是用07版Office Excel ,新建Excel默認保存的的Excel文件格式后綴是.xlsx,也能打開編輯03版的xls文件。行列的上限為 1048575行,16384列。
然后,在存儲格式上:
.xls,文件存儲格式實現原理是基於微軟的ole db是微軟com組件的一種實現,本質上也是一個微型數據庫,由於微軟的東西很多不開源,基本上也已經被淘汰,了解它的細節意義不大。
.xlsx ,文件存儲格式實現是基於openXml和zip技術((用winrar可以打開看)。它的優點是簡單存儲、安全傳輸方便、處理數據簡單。
.csv,純文本文件(以","為分割符),可以被excel打開。他的格式非常簡單,解析起來和解析文本文件一樣。
2. poi讀寫大文件的坑
為什么一定要用POI呢?雖然POI是目前使用最多的用來做excel解析的框架,但這個框架並不那么完美。大部分使用POI都是使用他的userModel模式,它上手容易使用簡單,隨便拷貝個API代碼並填寫業務代碼就可以完成讀寫操作。但它的問題也會比較明顯:
我的excel只有5行數據,為啥它就消耗了200MB內存?
在處理比較大的 excel 的時候(2w行),有時候會會出現內存溢出(2G)
它帶來的后遺症就是,稍微幾個並發(估計都不用並發),可怕的問題就來了出現full gc。
再分享一個最“熱門”的坑,當它在大並發情況下就拋的一個異常
Caused by: java.io.IOException: Could not create temporary directory '/home/admin/dio2o/.default/temp/poifiles'
at org.apache.poi.util.DefaultTempFileCreationStrategy.createTempDirectory(DefaultTempFileCreationStrategy.java:93) ~[poi-3.15.jar:3.15]
at org.apache.poi.util.DefaultTempFileCreationStrategy.createPOIFilesDirectory(DefaultTempFileCreationStrategy.java:82) ~[poi-3.15.jar:3.15]
//引自org.apache.poi.util.DefaultTempFileCreationStrategy
private void createTempDirectory(File directory) throws IOException {
if (!(directory.exists() || directory.mkdirs()) || !directory.isDirectory()) {
throw new IOException("Could not create temporary directory '" + directory + "'");
}
}
/**
*
如果2個線程同時判斷directory.exists()都為false,但執行directory.mkdirs()如果一些線程優先執行完,另外一個線程就會返回false。
最終 throw new IOException(“Could not create temporary directory ‘” + directory + “’”)。
針對這個問題easyexcel在寫文件時候首先創建了該臨時目錄,避免poi在並發創建時候引起不該有的報錯。
**/
不過還好官方於2018-8月在4.0.0版本得以解決。
3. 重新認識poi及流式支持
先貼一張來自官網非常經典的圖
usermodel,它是基於DOM的文檔驅動,讀寫都支持,基於內存的,總之就是很垃圾
SAX,它是Simple API for XML的縮寫,主要用在讀文件,它並不是由W3C官方所提出的標准,但使用SAX的還是不少,幾乎所有的XML解析器都會支持它。 SAX在概念上與DOM完全不同。它不同於DOM的文檔驅動,它是事件驅動的,它並不需要讀入整個文檔,而文檔的讀入過程一行一行解析。所謂eventmodel(事件驅動),將一行的解析結果以觀察者的模式通知處理,通知的方式基於回調(callback)機制的程序運行方法。how use?
sxssf,它你可理解為緩存流式支持,在寫文件很重要。
到這里基本上對上圖的理解已經到位了,可以下手進一步了解API了。
1. HSSFWorkbook(excel 2003)
它是基於usermodel,HSSFWorkbook 針對是 EXCEL2003 版本,擴展名為 .xls;所以 此種的局限就是 導出的行數 至多為 65535 行,此種因為行數不夠多所以一般不會發生OOM。
2. XSSFWorkbook (excel 2007)
它是基於usermodel,這種形式的出現是由於第一種HSSFWorkbook 的局限性而產生的,因為其所導出的行數比較少,所以XSSFWookbook應運而生 其 對應的是EXCEL2007+(1048576行,16384列)擴展名 .xlsx,最多可以 導出 104 萬行,不過 這樣 就伴隨着一個問題---OOM 內存溢出,原因是 你所 創建的 book sheet row cell 等 此時是存在內存的並沒有持久化,那么隨着數據量增大內存的需求量也就增大,那么很大可能就是要 OOM了。
3. SXSSFWorkbook(excel 2007后,poi使用3.8+版本)
它是基於sxssf,因為數據量過大導致內存吃不消無法寫文件,有讀一批寫一批的做法嗎? 答案是肯定的。怎么做?此種的情況就是設置最大內存條數。比如:設置最大內存量為5000 rows(new SXSSFWookbook(5000))或者手動flush(),此時當行數達到 5000 時,把內存中的數據寫到文件中,以此逐步寫入避免OOM,那么這樣 就完美解決了大數據下導出的問題。
性能參數:SXSSFWorkbook.setCompressTempFiles(true),SXSSF將sheet data刷新到臨時文件(每張sheet一個臨時文件)中,並且這些臨時文件的大小可以增長到非常大的值。例如,對於一個20MB的CSV數據,臨時XML的大小變得大於千兆字節。如果臨時文件的大小是一個問題,你可以開啟使用GZIP壓縮。
4. WorkbookFactory.create(InputStream inputStm)
// 它會基於xls或xlsx判斷創建HSSFWorkbook或XSSFWorkbook
public static Workbook create(InputStream inp, String password) throws IOException, InvalidFormatException, EncryptedDocumentException {
// If clearly doesn't do mark/reset, wrap up
if (! inp.markSupported()) {
inp = new PushbackInputStream(inp, 8);
}
// Ensure that there is at least some data there
byte[] header8 = IOUtils.peekFirst8Bytes(inp);
// Try to create
if (NPOIFSFileSystem.hasPOIFSHeader(header8)) {
NPOIFSFileSystem fs = new NPOIFSFileSystem(inp);
return create(fs, password);
}
if (DocumentFactoryHelper.hasOOXMLHeader(inp)) {
return new XSSFWorkbook(OPCPackage.open(inp));
}
throw new InvalidFormatException("Your InputStream was neither an OLE2 stream, nor an OOXML stream");
}
5. 拋棄不重要的數據
Excel解析時候會包含樣式,字體,寬度等數據,但這些數據占了相當大的空間,卻是我們不關注的,如果將這部分數據拋棄可以大大降低內存使用。
4. 其他解決方案
1. easyexcel
alibaba開源的,基於注解,api可讀性好,更多
2. Hutool-poi
api可讀性好,本質是對POI封裝,更多
5. 總結
如果對有一定並發的項目,大文件讀最好是使用SAX模式,但它有一定的編碼量,大文件的寫最好基於sxssf。當然結合項目的實際情況,我們項目中是有定時做System.gc(),如果你gc不是cms模式要在啟動項中要添加配置(-XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled)。