写点什么

为什么实际业务中不建议直接使用 POI 操作 Excel?

作者:IT学习日记
  • 2022 年 8 月 13 日
    广东
  • 本文字数:6436 字

    阅读完需:约 21 分钟

为什么实际业务中不建议直接使用POI操作Excel?

一: 使用场景

在日常的系统开发中,系统支持批量数据的操作是一个很常见的功能,其中,最常用的方式是使用 excel 表格对数据进行批量添加、删除,如:批量新建订单、批量添加商品等。

二: 技术选型

  现在市面上有很多技术实现来支持 excel 数据解析如:POI、JXL 等,但是,这些技术或多或少都存在着一些问题,下面进行具体分析:

(一)、POI

  POI 是目前使用最多的用来做 excel 解析的框架,但这个框架还存在在这个许多问题。现在使用 POI 技术来解析 excel 文件的,大多数都是使用到它的 userMode 模式,好处是上手比较简单,而且网上比较多封装好的代码,虽然复制一下就可以运行,这个对于数据量不大的文件的时候是可以使用,但是当数据量大的时候会存在巨大隐患。


  • 1、userMode 模式存在着一个巨大的问题就是内存消耗很大,一个几兆的文件解析需要上百兆的内存,当并发量大的时候就会容易出现 OOM(内存溢出)或者频繁进行 full GC 回收),导致程序执行缓慢甚至崩溃

  • 2、如果有深入了解过 POI 的会发现,其他它针对这个情况提供了一种叫 SAX 的模式,但是,这种模式相对复杂,且对 excel 03 版本和 07 版本不兼容,两个版本的数据存储方式不一样,所以解析也不一样,这样需要同个功能需要进行两套代码开发,时间周期长,且不易于维护。

  • 3、在大并发情况下,POI 还存在着一些未知的错误,如果需要 POI 团队修复,周期不确定。

(二)、JXL

  它是纯 javaAPI,在跨平台上表现的非常完美,代码可以再 windows 或者 Linux 上运行而无需重新编写,但是它也存在着许多缺点。


  • 1、效率低,格式支持比 POI 还少。

  • 2、支持 Excel 95-2000 的所有版本,但是 excel2007 以后的版本暂时不支持。

(三)、EasyExcel(推荐使用)

  阿里巴巴出的产品,相信看到这里很多人应该更有信心(毕竟阿里出的东西很是很有质量保障滴)。它是一个基于 Java 的简单、省内存的读写 Excel 的开源项目。在尽可能节约内存的情况下支持读写百 M 的 Excel,选择使用它有以下原因:


  • 1、开源,代码放在 github 上,有问题随时 issue


  • 2、解决了 POI 解析 excel 非常耗费内存的问题,它是通过磁盘存储,一行一返回,最大程度解决了内存占用大的问题。


  • 3、社区活跃度量大,网上的相关文档也比较多。

(四)、POI 解析模式和 EasyExcel 解析模型图


三: 常用 API 介绍

  (一)监视器(不能被 Spring 容器管理,每次读取 Excel 都需要新 new 一个,如果需要使用 Spring 容器对象,则通过构造函数传入):


   由于默认一行行的读取 excel,所以需要创建 excel 一行一行的回调监听器(这个是必须实现的,所以我们要兼容所有的对象,监听器的泛型使用 Object 类型)



  (二) 读 Excel:


  1、EasyExcel.read(...) </font>---》它有三个重载的方法



  2、sheet()</font> --》指定读取的 sheet,doRead --》执行读取数据操作



  3、ExcelReader.readAll()</font> --》 执行读取 Excel 文件中的所有 sheet



  4、ExcelReader 实例.finish() </font>--》完成读取操作,并关闭流(一定要注意关闭流,因为 easyExcel 是使用磁盘的方式进行数据解析,所以解析过程中会创建临时文件,如果不关闭,最后可能会导致磁盘崩溃)


  (三) 写 Excel:


  1、EasyExcel.write(...) </font>---》它有六个重载的方法



  2、writeSheet() </font>---》向 excel 文件中的 sheet 写入数据



  3、ExcelWriter.write(...) </font>---》插入 sheet 到 excel 文件中,这样就完成了数据写入,实际上就是嵌套一样,现将数据写入到 sheet,再将 sheet 插入到 excel 中


  4、ExcelWriter 实例.finish() --》完成写入操作,并关闭流(一定要注意关闭流,因为 easyExcel 是使用磁盘的方式进行数据解析,所以解析过程中会创建临时文件,如果不关闭,最后可能会导致磁盘崩溃)


  (四) 常用注解


  1、@ExcelProperty:</font> 作用在 excel 表数据对应的 JAVA 实体上,有以下属性:


  • (1) : value -- 指定导出时该字段对应的标题名称,或者是读取时匹配 excel 表格中表头的名称,符合则将表头中对应的数据填充到此处,如果这个名称存在多个,只能读取到一个。


  • (2) : index -- 指定该字段和 excel 文件的哪一列对应,默认是 0,不推荐和 value 属性同时指定,如果需要指定,那么 value 的值最好指定为导出数据对应表头的标题名,index 的值则指定为读取 excel 文件时该字段属性对应的列的位置。


  • (3) : converter 属性则是指定对应的转换器,可以自己书写一个转换器,在读取数据的时候进行对数据的格式化,如:给每一列数据都加上自己自定义的东西


  2、@ExcelIgnoreUnannotated:默认情况下 Java 类中的所有属性都添加读写,在类上面加入 @ExcelIgnoreUnannotated 注解,加入这个注解后只有加了 @ExcelProperty 才会参与读写。


  3、@ExcelIgnore: 被标注的属性不参加 Excel 的读写,相当于直接省略。

四: 实战

   (一):添加依赖


  // easyExcel坐标        <dependency>            <groupId>com.alibaba</groupId>            <artifactId>easyexcel</artifactId>            <version>2.1.7</version>        </dependency>
复制代码


  (二): JAVA 映射实体


package com.elvis.easyexcel.model;
import com.alibaba.excel.annotation.ExcelIgnore;import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;import com.alibaba.excel.annotation.ExcelProperty;import com.alibaba.excel.annotation.format.DateTimeFormat;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;
import java.io.Serializable;import java.util.Date;
@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class Demo implements Serializable { private static final long serialVersionUID = -920481620956257604L; @ExcelIgnore @ExcelProperty(value = "姓名", index = 0) private String stringType; @ExcelProperty(value = "姓名2", index = 1) private Integer integerType; // 这里使用String类型接收才能格式化,如果使用Date类型则无法格式化 @ExcelProperty(value = "姓名3", index = 2) @DateTimeFormat("yyyy-MM-dd HH:mm:ss") private String dateType; @ExcelProperty(value = "姓名4", index = 3) private Double doubleType; @ExcelProperty(value = "姓名5", index = 4) private Long longType; @ExcelProperty(value = "姓名6", index = 5) private Float floatType; @ExcelProperty(value = "姓名7", index = 6) private Boolean booleanType; @ExcelProperty(value = "姓名8", index = 7) private Short shortType;}
复制代码


  (三): 添加监听器


package com.elvis.easyexcel.listener;
import com.alibaba.excel.context.AnalysisContext;import com.alibaba.excel.event.AnalysisEventListener;import com.alibaba.excel.exception.ExcelDataConvertException;import lombok.extern.slf4j.Slf4j;import org.json.JSONObject;
import java.util.ArrayList;import java.util.List;import java.util.Objects;
@Slf4jpublic class ObjectListener extends AnalysisEventListener<Object> { // 读取到的数据 private List<Object> readData = new ArrayList<>();
/** * 解析数据进入的方法 * @param o 本次读到的数据 * @param analysisContext */ @Override public void invoke(Object o, AnalysisContext analysisContext) { JSONObject jsonObject = new JSONObject(o); log.info("读取到的数据:{}", jsonObject.toString()); if(Objects.nonNull(o)){ readData.add(o); } } /** * 所有数据解析完成了 都会来调用 * @param analysisContext */ @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { log.info("所有数据解析完成了 都会来调用"); }
/** * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。 * 如果你的程序在读取解析时即使有异常也不想后面的解析失败的,在此处打出解析错误日志即可 * 如果你的程序只有解析过程出错就解析解析的话,这在此处手动抛出异常即可 * @param exception * @param context * @throws Exception */ @Override public void onException(Exception exception, AnalysisContext context) { log.error("解析失败,但是继续解析下一行:{}", exception.getMessage()); // 如果是某一个单元格的转换异常 能获取到具体行号 // 如果要获取头的信息 配合invokeHeadMap使用 if (exception instanceof ExcelDataConvertException) { ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception; log.error("第{}行,第{}列解析异常", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex()); } } // 反馈解析完成的数据 public List<Object> getReadData(){ return readData; }}
复制代码


  (四): 书写工具类(这个工具类可以直接使用,如果有需要的,直接复制就可以)


package com.elvis.easyexcel.utils;
import com.alibaba.excel.EasyExcel;import com.alibaba.excel.ExcelReader;import com.alibaba.excel.ExcelWriter;import com.alibaba.excel.write.builder.ExcelWriterBuilder;import com.alibaba.excel.write.metadata.WriteSheet;import com.elvis.easyexcel.listener.ObjectListener;import com.elvis.easyexcel.model.Demo;import lombok.extern.slf4j.Slf4j;import org.apache.commons.collections4.CollectionUtils;
import java.io.File;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import java.util.Collections;import java.util.List;
@Slf4jpublic class EasyExcelUtils { /** * @param in 读取的文件流 * @param model 与excel文件数据对应的实体 * @param type 通用的数据读取解析监听器 * @return */ public static List<Object> readExcelFile(InputStream in, Object model, ObjectListener type) { ExcelReader reader = null; try { reader = EasyExcel.read(in, model.getClass(), type).build(); reader.readAll(); } catch (Exception e) { log.error("读取excel文件错误:" + e.getMessage()); return null; } finally { // 关闭流,读的时候会创建临时文件,不关闭到时磁盘会崩的 if (reader != null) { reader.finish(); } } return type.getReadData(); }

/** * 保存数据到excel文件 * * @param data 数据(支持多个sheet写入,根据数据的个数写入对应个sheet,默认多个sheet写入的数据是同一个实体的) * @param savePath 保存的路径 * @return 是否保存成功 */ public static Boolean writeExcelFileWithCommonEntity(List<List<Object>> data, String savePath) { if (CollectionUtils.isNotEmpty(data)) { ExcelWriter excelWriter = null; // 输出流放到try的小括号中,方法结束时会自动关闭流,这个是jdk1.8的新特性,对于经常忘记关流的小伙伴很友好哦 try { // 获取到操作写入excel的操作对象,第二个参数是导出的excel文件的标题名对应的实体 // 获取写入数据中的第一个元素的类类型 excelWriter = EasyExcel.write(savePath).build(); // 设置每个sheet的名称 for (List<Object> objectList : data) { Object item = objectList.get(0); WriteSheet writeSheet = EasyExcel.writerSheet(1, "模板").head(item.getClass()).build(); excelWriter.write(objectList, writeSheet); } } catch (Exception e) { log.error("保存数据到excel错误:{}", e.getMessage()); return false; } finally { if (excelWriter != null) { excelWriter.finish(); } } } else { return false; } return true; }

}
复制代码


  (五): 测试案例


package com.elvis;
import com.alibaba.excel.EasyExcel;import com.alibaba.excel.read.builder.ExcelReaderBuilder;import com.alibaba.excel.read.listener.ReadListener;import com.alibaba.excel.write.builder.ExcelWriterBuilder;import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;import com.elvis.easyexcel.listener.ObjectListener;import com.elvis.easyexcel.model.Demo;import com.elvis.easyexcel.utils.EasyExcelUtils;import org.json.JSONArray;import org.junit.Test;
import javax.jws.Oneway;import java.io.File;import java.io.FileInputStream;import java.io.InputStream;import java.io.OutputStream;import java.util.ArrayList;import java.util.List;
public class demo { @Test public void demo1() throws Exception{ // 读取文件 System.out.println("开始读取文件------------------------------------"); String fileName = "E:\\newpath\\excelutils\\build\\classes\\小明.xlsx"; InputStream in = new FileInputStream(new File(fileName)); List<Object> objects = EasyExcelUtils.readExcelFile(in, new Demo(), new ObjectListener()); JSONArray array = new JSONArray(objects); System.out.println(array); System.out.println("--------------------------------------------------------------"); System.out.println("开始保存文件------------------------------------"); String savePath = "E:\\newpath\\excelutils\\build\\classes\\保存文件测试.xlsx"; List<List<Object>> data = new ArrayList<>(); List<Object> item = new ArrayList<>(); Demo abc = new Demo("abc",12,"2020-12-12 19:10:10",12.2,12l,12f,false,Short.parseShort("12")); item.add(abc); data.add(item); EasyExcelUtils.writeExcelFileWithCommonEntity(data,savePath); }
}
复制代码


  (六): 效果展示



五: 总结

  通过亲自测试发现 EasyExcel 的 API 很简洁,使用也很容易上手,而且速度也很快,所以,快动手学起来吧,这个框架现在越来越流行了,面试只要涉及到导出的,基本不是 POI 的就是 EasyExcel。


  由于篇幅限制,所以本文讲解的也只是 EasyExcel 常使用到的知识,其实它还有许多东西值的我们去研究,更加详细的请到EasyExcel官方手册进行查看


  非常感谢你阅读本文,如果有什么疑问或者建议,欢迎在文章下方留言或者私信我,如果你觉的文字对你有帮助,请给我点赞和关注,后面还会书写更多的文章跟大家分享其他的知识。


  我已经将代码上传到个人开源的轮子之王项目中,如果你觉的需要看更详细的代码,请点击下面链接访问:


  Gitee 地址: https://gitee.com/it-learning-diary/it-wheels-king


  Github 地址: https://github.com/it-learning-diary/it-wheels-king

发布于: 2022 年 08 月 13 日阅读数: 169
用户头像

公众号:IT学习日记 2019.11.25 加入

平台:InfoQ&阿里云签约作者,CSDN、头条、知乎、掘金优质创作者,全网粉丝5w+。 领域:专注输出JAVA、数据库、算法等领域干货知识。 微信:yyuuyy1235

评论 (1 条评论)

发布
用户头像
有帮助的朋友可以留下点赞和关注,让我看见你的脚印哦!
2022 年 08 月 13 日 17:20 · 广东
回复
没有更多了
为什么实际业务中不建议直接使用POI操作Excel?_POI_IT学习日记_InfoQ写作社区