昨天,產品提了一個緊急需求,讓把十一月份已發貨的商品數據導出來,寫好SQL發給DBA執行之後,得到了三十多個100W數據的Excel文件。有一個屬性是以JSON格式存在表中一個字段裏面的,需要加工Excel文件將其單獨取出來(如圖的第四列)。
處理程序也在數據導出的過程中寫好了,大概思路就是讀入Excel構建Workbook對象,然後對指定列的值進行轉換,最後寫回原文件。想法很奈斯,結果很悲哀,OOM了。即使把Xmx和Xms調到最大,還是無濟於事。
系統平時的導入導出不會遇到這種問題,導入的話限制了上傳文件大小,導出的話應用裏面設置了最大導出數量校驗。直接從數據庫的導出是寫好SQL交給DBA處理的,他們有自己的工具。
本着遇到困難就解決的優秀品質,所以總結了一下POI中專門處理大數據量的SXSSF。
一、HSSF、XSSF與SXSSF
首先是HSSF,支持2003及以下的版本,即.xls結尾的Excel文件,最多隻允許存儲65536條數據。
然後是XSSF,支持2007及以上的版本,即.xlsx結尾的Excel文件,雖然單個Sheet就支持1048576條數據,但是性能不好,而且那麼多的數據存在內存中也容易OOM。
最後是SXSSF,它使用了不一樣的存儲方式,具體就不說了,只要知道大數量用它就OK了。呃,它只支持.xlsx文件,看它名字就知道了:"S" + "XSSF",S表示SAX事件驅動模式。
二、SXSSF導出數據
這個比較簡單,SXSSF也是實現了Workbook接口,所以就跟HSSF和XSSF用起來差不多,只是構建實例的方式不一樣而已。
它的基本思路是:當內存中的數據夠了一定行數(構造函數可以設置)之後就先刷到硬盤中。但是,你也別就真把100W數據一次性讀到內存中,應該根據總數分批加載到內存(比如從數據庫讀需要分頁一樣)。
直接上代碼吧
/**
* POI 導出
*
* @author Zhou Huanghua
* @date 2020/1/11 14:03
*/
public class PoiExport {
private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
public static void main(String[] args) throws Exception {
long begin = System.currentTimeMillis();
// keep 100 rows in memory, exceeding rows will be flushed to disk
try (SXSSFWorkbook wb = new SXSSFWorkbook(100);
OutputStream os = new FileOutputStream("C:/Users/dell/Desktop/tmp/demo.xlsx")) {
Sheet sh = wb.createSheet();
String val = "第%s行第%s列";
for (int rowNum = 0; rowNum < 100_0000; rowNum++) {
Row row = sh.createRow(rowNum);
int realRowNum = rowNum + 1;
Cell cell1 = row.createCell(0);
cell1.setCellValue(format(val, realRowNum, 1));
Cell cell2 = row.createCell(1);
cell2.setCellValue(format(val, realRowNum, 2));
Cell cell3 = row.createCell(2);
cell3.setCellValue(format(val, realRowNum, 3));
Cell cell4 = row.createCell(3);
cell4.setCellValue(format(val, realRowNum, 4));
}
wb.write(os);
}
LOGGER.info("導出100W行數據耗時(秒):" + (System.currentTimeMillis() - begin)/1000);
}
}
測試得到一個17M的文件,耗時17秒。
三、SXSSF導入數據
這個比較複雜一些,不過使用步驟也還統一,區別就是需要實現自己的SheetContentsHandler,我管它叫內容處理器。
直接上代碼
/**
* POI 導入
*
* @author Zhou Huanghua
* @date 2020/1/11 14:02
*/
public class PoiImport {
private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
public static void main(String[] args) throws Exception {
String filePath = "C:/Users/dell/Desktop/tmp/demo.xlsx";
// OPCPackage.open(...)有多個重載方法,比如String path、File file、InputStream in等
try (OPCPackage opcPackage = OPCPackage.open(filePath);) {
// 創建XSSFReader讀取StylesTable和ReadOnlySharedStringsTable
XSSFReader xssfReader = new XSSFReader(opcPackage);
StylesTable stylesTable = xssfReader.getStylesTable();
ReadOnlySharedStringsTable sharedStringsTable = new ReadOnlySharedStringsTable(opcPackage);
// 創建XMLReader,設置ContentHandler
XMLReader xmlReader = SAXHelper.newXMLReader();
xmlReader.setContentHandler(new XSSFSheetXMLHandler(stylesTable, sharedStringsTable, new SimpleSheetContentsHandler(), false));
// 解析每個Sheet數據
Iterator<InputStream> sheetsData = xssfReader.getSheetsData();
while (sheetsData.hasNext()) {
try (InputStream inputStream = sheetsData.next();) {
xmlReader.parse(new InputSource(inputStream));
}
}
}
}
/**
* 內容處理器
*/
public static class SimpleSheetContentsHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
protected List<String> row;
/**
* A row with the (zero based) row number has started
*
* @param rowNum
*/
@Override
public void startRow(int rowNum) {
row = new ArrayList<>();
}
/**
* A row with the (zero based) row number has ended
*
* @param rowNum
*/
@Override
public void endRow(int rowNum) {
if (row.isEmpty()) {
return;
}
// 處理數據
LOGGER.info(row.stream().collect(Collectors.joining(" ")));
}
/**
* A cell, with the given formatted value (may be null),
* and possibly a comment (may be null), was encountered
*
* @param cellReference
* @param formattedValue
* @param comment
*/
@Override
public void cell(String cellReference, String formattedValue, XSSFComment comment) {
row.add(formattedValue);
}
/**
* A header or footer has been encountered
*
* @param text
* @param isHeader
* @param tagName
*/
@Override
public void headerFooter(String text, boolean isHeader, String tagName) {
}
}
}
目前是解析一行數據就處理,這個看你使用場景是否能夠接受,異步的話還OK。
如果你覺得數據量和你JVM的堆內存還OK的話,可以在SimpleSheetContentsHandler的構造函數傳一個集合進來收集每行數據,等把數據全部解析完再統一處理。BUT,既然你用了SXSSF,那麼這麼做有OOM風險。
比較好的做法,還是先將每行數據逐一收集,不過需要達到一定行數之後批量處理,然後清空集合重新收集,這個還沒想好怎麼把代碼寫得優雅,就先不獻醜了。
對了,我使用的maven依賴是
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
demo代碼地址:https://github.com/zhouhuanghua/poi-demo。