摘要:本文由葡萄城技術團隊於部落格園原創並首發。轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。
當我們開發處理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問題的發生。
在網上查了一下,有兩個方法:
第一個辦法,對於僅匯入資料時很有效。但當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,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上十分簡單易用,另外在測試中發現,開啟檔案的速度也快很多,可以降低開發成本。
擴充套件連結: