當使用POI開啟Excel檔案遇到out of memory時該如何處理?

2023-07-04 09:00:17

摘要:本文由葡萄城技術團隊於部落格園原創並首發。轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

當我們開發處理Excel檔案時,Apache POI 是許多人首選的工具。但是,隨著需求的增加、工程複雜,在開啟複雜的Excel檔案的時候可能會出現一些異常情況。

根據測試,當開啟50萬個單元格資料的時候,就會遇到OOM(OutOfMemory)的問題;或者當開啟包含有20萬個合併單元格(包含border或者背景色)的時候,也會遇到OOM(OutOfMemory)的問題。

使用的是WorkbookFactory,直接開啟Excel檔案,程式碼如下:

File file = new File("testFile.xlsx");

Workbook workbook = WorkbookFactory.create(file);

//開啟檔案後進行其他處理

以上程式碼在處理大型Excel檔案時會導致OOM問題的發生。

在網上查了一下,有兩個方法:

  1. 可以把檔案轉化為CSV然後匯入。
  2. 把Excel檔案風格為小的Excel檔案,分別構建workbook,然後進行處理。

第一個辦法,對於僅匯入資料時很有效。但當Excel是有樣式的情況時,把Excel轉成CSV就會導致樣式丟失,所以pass了這個方法。

似乎可以考慮一下第二個辦法,把檔案分割成多個小檔案,分別構建workbook,然後去處理。

於是手動把Excel檔案拆分開,把程式碼簡單改了一下,進行測試。

File file = new File("test.xlsx");

File file1 = new File("test1.xlsx");

File file2 = new File("test2.xlsx");

File file3 = new File("test3.xlsx");

File file4 = new File("test4.xlsx");

File file5 = new File("test5.xlsx");

File file6 = new File("test6.xlsx");

Workbook workbook = WorkbookFactory.create(file);

Workbook workbook1 = WorkbookFactory.create(file1);

Workbook workbook2 = WorkbookFactory.create(file2);

Workbook workbook3 = WorkbookFactory.create(file3);

Workbook workbook4 = WorkbookFactory.create(file4);

Workbook workbook5 = WorkbookFactory.create(file5);

Workbook workbook6 = WorkbookFactory.create(file6);

但還是遇到了問題,還是出現了oom的問題,使用的是unit test做的測試,報錯內容如下:

...

at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)

at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)

at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)

at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)

at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded

at java.util.Arrays.copyOfRange(Arrays.java:3664)

at java.lang.String.\<init\>(String.java:207)

at com.sun.org.apache.xerces.internal.xni.XMLString.toString(XMLString.java:190)

at com.sun.org.apache.xerces.internal.util.XMLAttributesImpl.getValue(XMLAttributesImpl.java:523)

at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser\$AttributesProxy.getValue(AbstractSAXParser.java:2321)

...

經過一些嘗試,發現是同一時間構建的workbook太多了,當減少到4個時,單元測試就可以正常跑完。

這樣來看,POI的問題還真是讓人挺頭疼。測試的時候,檔案是可以知道被分為幾個的,但是實際應用時,就沒法預測檔案的數量。此外根據測試來看,workbook的數量,可能是跟Excel檔案的大小相關,這會導致後續開發時可能會遇到更多的問題。

繼續網上衝浪,看到除了POI的優化方法,還看到有EasyExcel和GcExcel等其他產品。

簡單check了一下,EasyExcel是開源的,主要是對高並行的讀寫場景做得很好。GcExcel是商業軟體,API很全。

那可以分別使用這兩個元件驗證一下,我們主要想解決的問題有兩個:

  1. 大量資料和樣式的Excel檔案能一次性開啟
  2. 可以有辦法保留樣式或者操複製樣式

對於問題1,EasyExcel和GcExcel都可以做的很好,沒有出現OOM的問題了。程式碼上兩個元件風格不太一樣,GcExcel和POI比較相似,是直接構建workbook。POI給的例子是通過註解,更像是反序列化的體驗,同時每次讀取要寫一個監聽器,通過監聽器處理特殊邏輯。

對於問題2,寫了一下UT,程式碼分別如下:

先看看EasyExcel,

首先EasyExcel需要定義一個Data類,來讀取資料。

@Getter

@Setter

@EqualsAndHashCode

public class DemoData {

private String cell1;

private String cell2;

}

定義一個listener類,處理style的邏輯需要在invoke裡進行處理,沒找到EasyExcel相關的API,還是使用到了POI本身的API來處理樣式相關的內容。

@Slf4j

public class DemoListener implements ReadListener\<DemoData\> {

private int rowNum = 0;

private Sheet sheet;

@Override

public void invoke(DemoData data, AnalysisContext context) {

if (sheet == null) {

sheet = (Sheet) context.readSheetHolder().getReadSheet();

}

Row row = sheet.getRow(rowNum);

// 獲取第一列

Cell cell0 = row.getCell(0);

CellStyle style0 = cell0.getCellStyle();

// 建立樣式物件

Workbook workbook = sheet.getWorkbook();

CellStyle newStyle = workbook.createCellStyle();

// 複製原有樣式到新建立的樣式物件中

newStyle.cloneStyleFrom(style0);

// TODO: 其他操作

rowNum++;

}

@Override

public void doAfterAllAnalysed(AnalysisContext context) {

}

}

從官網看到,在EasyExcel 2.0.0-beta1以後,可以使用extra方法獲取批註,超連結,合併單元格資訊。但是如果有border或者其他的樣式,似乎好像不能用這個方法。

經過簡單的測試,問題可以解決,但是樣式處理起來還是比較複雜。

對於GcExcel,根據官方檔案程式碼書上很簡單。直接基於Range的概念就可以通過set/get方法獲取各種樣式。
https://www.grapecity.com.cn/developer/grapecitydocuments/excel-java/docs/Features/ApplyStyle

做一下簡單的測試吧,用起來很簡單,只要理解Excel相關的概念就可以輕鬆獲取到style。

@Test

public void testRepeatCreateObject() throws IOException {

String fileName = "test.xlsx";

Workbook workbook = new Workbook();

workbook.open(fileName);

IWorksheet sheet = workbook.getWorksheets().get(0);

IStyle style = sheet.getRange(0,0).getStyle();

System.out.println("font "+style.getFont().getName());

System.out.println("border "+style.getBorders().getLineStyle().name());

}

至此,整體上看,喜歡使用開源的話,可以選擇EasyExcel。EasyExcel提供了反序列化一樣的註解方式,讀取資料。在資料讀取方面很簡單。但是在樣式處理上,得依賴事件機制去處理,這個還是有一點麻煩的。

如果是做商業專案開發,可以考慮GcExcel。GcExcel在API上十分簡單易用,另外在測試中發現,開啟檔案的速度也快很多,可以降低開發成本。

擴充套件連結:

在伺服器端匯入匯出Excel

如何用C1實現應用程式與微軟Excel的互動

中國式複雜報表開發教學(1)—類Excel單維度交叉表