• 当使用POI打开Excel文件遇到out of memory时该如何处理?


    摘要:本文由葡萄城技术团队于博客园原创并首发。转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。

    当我们开发处理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.\(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\ {
    
    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单维度交叉表

  • 相关阅读:
    编程软件大全(下载+安装+破解)
    整合JVM-SANDBOX与VMTOOL,实现支持OGNL的增强自定义MOCK
    【深度学习】 Python 和 NumPy 系列教程(四):Python容器:2、元组tuple详解(初始化、索引和切片、元组特性、常用操作、拆包、遍历)
    Bazzite:专为 Steam Deck 和 PC 上的 Linux 游戏打造的发行版
    大数据要怎么样学才可以到企业级实战
    二叉堆及堆排序详解保姆级教程略显罗嗦但保证能看懂
    Whale 帷幄:用户旅程的「情绪算法」
    3分钟教你用Python+Appium实现自动化测试
    python实现 线性卷积用Toeplitz 矩阵运算
    准备用HashMap存1W条数据,构造时传10000还会触发扩容吗?存1000呢?
  • 原文地址:https://www.cnblogs.com/powertoolsteam/p/17509536.html