Alibaba工具型技术系列「EasyExcel技术专题」摒除OOM!让你的Excel操作变

家资是何物,积帙列梁梠。这篇文章主要讲述Alibaba工具型技术系列「EasyExcel技术专题」摒除OOM!让你的Excel操作变相关的知识,希望能为你提供帮助。
前提概要
针对于后端开发者而言的,作为报表的导入和导出是一个很基础且有很棘手的问题!之前常用的工具和方案大概有这么几种:

  1. JXL(java Excel API 工具服务),此种只支持xls的文件格式,而且对于内存的管理特别的差,现在基本不用了!
  2. 目前大多数会操作Excel工具服务或者解析都是利用Apache POI进行操作。
  3. 其他第三方的工具很多也是基于POI作为实现基础!
存在隐患问题解决方案
  • 首先,在百万级大数据量Excel文件的导出我们可以采用分批查询数据来避免内存溢出的剧增!
  • 【Alibaba工具型技术系列「EasyExcel技术专题」摒除OOM!让你的Excel操作变】此外,POI给出的方案是:使用SXSSFWorkbook方式缓存数据到文件上以解决下载大文件EXCEL卡死页面的问题。
    • SXSSFWorkbook数据模型:主要可以解决在下载传输到浏览器的时候大Excel文件转换的输出流内存溢出
    • 此外其还可以通过其构造函数执指定在内存中缓存的行数,剩余的会自动缓存在硬盘的临时目录上,同时,并不会存在页面卡顿的情况。
    1. SXSSF机制而言还是需要手动进行封装及定制化开发增加了一定的工作量!
    2. POI的操作方式仍然还是存在内存占用过大的问题,仍会存在内存溢出的隐患。
    3. 存在空循环和整除的时候数据有缺陷的问题。
更优秀的选择EasyExcel的介绍说明
  • 源码库(github地址):https://github.com/alibaba/easyexcel
  • 官方文档:https://alibaba-easyexcel.github.io/index.html
技术原理对比 POIEasyExcel技术原理图节省内存的开销
Alibaba工具型技术系列「EasyExcel技术专题」摒除OOM!让你的Excel操作变

文章图片

Maven仓库依赖
< dependency> < groupId> com.alibaba< /groupId> < artifactId> easyexcel< /artifactId> < version> 2.2.6< /version> < /dependency>

注: 如果是springboot2.0,则不需要poi依赖,如果是1.0,则需要poi依赖,并且poi和poi-ooxml的版本要保持一致。
< !-- poi --> < 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>

基础API介绍(参考官方文档)
  • EasyExcel 入口类,用于构建开始各种操作,属于典型的门面+工厂模式
public void testReadEntity() { // 被读取的文件绝对路径 String fileName = "path/testDemo.xlsx"; // 接收解析出的目标对象(Entity)指的是你的实体类 List< Entity> entityList = new ArrayList< > (); // 这里需要指定读用哪个class去读,然后读取第一个sheet文件流会自动关闭 // excel中表的列要与对象的字段相对应 EasyExcel.read(fileName, Entity.class, new AnalysisEventListener< Student> () { // 每解析一条数据都会调用该方法 @Override public void invoke(Entity entity, AnalysisContext analysisContext) { System.out.println("解析一条Row行对象:" + JSON.toJSONString(entity)); entityList.add(student); } // 解析完毕的回调方法 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("excel文件读取完毕!"); } }).sheet().doRead(); }// 通过Map作为整体的数据结构模型 public void readWithoutObj() { // 被读取的文件绝对路径 String fileName = "path/testDemo.xlsx"; // 接收结果集,为一个List列表,每个元素为一个map对象,key-value对为excel中每个列对应的值 List< Map< Integer,String> > resultList = new ArrayList< > (); EasyExcel.read(fileName, new AnalysisEventListener< Map< Integer,String> > () { @Override public void invoke(Map< Integer, String> map, AnalysisContext analysisContext) { System.out.println("解析到一条数据:" + JSON.toJSONString(map)); resultList.add(map); } @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("excel文件解析完毕!" + JSON.toJSONString(resultList)); } }).sheet().doRead(); }

  • invoke方法代表每解析一行就会调用一次,data数据表示解析出来一行的数据。
  • doAfterAllAnalysed 该方法表示将所有数据解析完毕以后才会去调用该方法。
  • 内部实现机制:(可以理解成一个excel对象,一个excel只要构建一个)
    • ExcelReaderBuilder 构建出一个 ReadWorkbook
      • excelType 当前excel的类型 默认会自动判断
      • inputStream 与file二选一。读取文件的流,如果接收到的是流就只用,不用流建议使用file参数。因为使用了inputStream easyexcel会帮忙创建临时文件,最终还是file
      • file 与inputStream二选一。读取文件的文件。
      • autoCloseStream 自动关闭流。
      • readCache 默认小于5M用 内存,超过5M会使用 EhCache,这里不建议使用这个参数。
      • useDefaultListener @since 2.1.4 默认会加入ModelBuildEventListener 来帮忙转换成传入class的对象,设置成false后将不会协助转换对象,自定义的监听器会接收到Map< Integer,CellData> 对象,如果还想继续接听到class对象,请调用readListener方法,加入自定义的beforeListener、 ModelBuildEventListener、 自定义的afterListener即可。
      • 方法体现:
        • EasyExcel.read 该方法是用来创建ExcelReaderBuilder对象,该对象就是用来解析Excel文档
        • read方法需要传入三个参数:
          • 第一个参数:需要解析文件的路径,当然除了传入一个文件路径以外,还可以传入InputStream
          • 第二参数:数据类型的Class类型对象,可以不传
          • 第三个参数:事件监听器,在之前介绍这款框架时说过,该框架是基于SAX的一种解析,加载一行数据到内存就会去解析一行,主要是为了节约内存。
    • ExcelWriterBuilder 构建出一个 WriteWorkbook
      • excelType 当前excel的类型 默认xlsx
      • outputStream 与file二选一。写入文件的流
      • file 与outputStream二选一。写入的文件
      • templateInputStream 模板的文件流
      • templateFile 模板文件
      • autoCloseStream 自动关闭流。
      • password 写的时候是否需要使用密码
      • useDefaultStyle 写的时候是否是使用默认头
    • WriteTable(就把excel的一个Sheet,一块区域看一个table)参数
      • tableNo 需要写入的编码。默认0
  • 内部实现机制:(可以理解成excel里面的一页,每一页都要构建一个)
    • ExcelReaderSheetBuilder:构建出一个 ReadSheet对象。
      • sheetNo 需要读取Sheet的编码,建议使用这个来指定读取哪个Sheet。
      • sheetName 根据名字去匹配Sheet,excel 2003不支持根据名字去匹配。
    • ExcelWriterSheetBuilder构建出一个 WriteSheet对象。
      • 需要写入的编码。默认0
      • sheetName 需要些的Sheet名称,默认同sheetNo
  • 内部实现机制:(可以理解成excel里面的一页,每一页都要构建一个)
    • ReadListener:每一行读取完毕后都会调用ReadListener来处理数据
    • WriteHandler :每一个操作包括创建单元格、创建表格等都会调用WriteHandler来处理数据
  • 相关使用注解
    • ExcelProperty index 指定写到第几列,默认根据成员变量排序。value指定写入的名称,默认成员变量的名字,多个value可以参照快速开始中的复杂头。
    • ExcelIgnore 默认所有字段都会写入excel,这个注解会忽略这个字段
    • DateTimeFormat 日期转换,将Date写到excel会调用这个注解。里面的value参照java.text.SimpleDateFormat
    • NumberFormat 数字转换,用Number写excel会调用这个注解。里面的value参照java.text.DecimalFormat
    • ExcelIgnoreUnannotated 默认不加ExcelProperty的注解的都会参与读写,加了不会参与。
    • 使用案例:
      @Data public class TestEntity { /** * 从0开始,2代表强制读取第三个, * 不建议 index 和 name 同时用 */ @ExcelProperty(index = 2) private Double doubleData; /** * 用名字去匹配,这里需要注意, * 名字重复,会导致只有一个字段读取到数据 */ @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") private Date date; }

      @Data public class MultiHeaderEntity implements Serializable {@ExcelProperty(value = https://www.songbingjia.com/android/{"一层信息","二层1"}) private Integer id; @ExcelProperty(value = https://www.songbingjia.com/android/{"一层信息","二层2"}) private String name; @ExcelProperty(value = https://www.songbingjia.com/android/{"一层信息","二层4"}) private String description; @ExcelProperty(value = https://www.songbingjia.com/android/{"一层信息","二层3"}) private Date birthday; }

      @Data public class ConverterData { /** * 我自定义 转换器,不管数据库传过来什么 。我给他加上“自定义:” */ @ExcelProperty(converter = CustomStringStringConverter.class) private String string; /** * 这里用string 去接日期才能格式化。我想接收年月日格式 */ @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒") private String date; /** * 我想接收百分比的数字 */ @NumberFormat("#.##%") private String doubleData; }

实战案例 读取Excel实现Demo 数据模型DemoModel
@Data public class DemoModel { private String attribute1; private Date attribute2; private Double attribute3; }

读取回调监听器
@Slf4j public class DemoModelAnalysisListener extends AnalysisEventListener< DemoData> { /** * 每隔500条存储数据库,然后清理list ,方便内存回收 */ static final int BATCH_COUNT = 500; List< DemoModel> list = new ArrayList< > (); /** * 这个也可以是一个service。当然如果不用存储这个对象没用。 */ private DemoService demoService; public DemoModelAnalysisListener(DemoService demoService) { this.demoService = demoService; } /** * 这个每一条数据解析都会来调用 * @param data *one row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context */ @Override public void invoke(DemoData data, AnalysisContext context) { log.info("解析到一条数据:{}", JSON.toJSONString(data)); list.add(data); // 达到BATCH_COUNT了,需要去存储一次数据库, // 防止数据几万条数据在内存,容易OOM if (list.size() > = BATCH_COUNT) { saveData(); } }/** * 所有数据解析完成了 都会来调用 * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 saveData(); log.info("所有数据解析完成!"); }/** * 加上存储数据库 */ private void saveData() { log.info("{}条数据,开始存储数据库!", list.size()); demoService.insert(list); // 存储完成清理 list list.clear(); //解析结束销毁不用的资源 log.info("存储数据库成功!"); } }

读取操作测试类
@Test public void readExcelTest() { // 写法1: DemoService demoService = getService(); //获取service业务实现类 String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx"; // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭 EasyExcel.read(fileName, DemoData.class, new DemoModelAnalysisListener(demoService)).sheet().doRead(); // 写法2: fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx"; ExcelReader excelReader = null; try { excelReader = EasyExcel.read(fileName, DemoData.class, new DemoModelAnalysisListener(demoService)).build(); ReadSheet readSheet = EasyExcel.readSheet(0).build(); excelReader.read(readSheet); } finally { if (excelReader != null) { // 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的 excelReader.finish(); } } }

写入Excel实现Demo 写入数据模型
@Data public class DemoData { @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") private Date date; @ExcelProperty("数字标题") private Double doubleData; /** * 忽略这个字段 */ @ExcelIgnore private String ignore; }

写入操作代码
@Test public void demoWriteTest() { // 写法1 String fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 // 如果这里想使用03 则 传入excelType参数即可 EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data()); // 写法2 fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; // 这里 需要指定写用哪个class去写 ExcelWriter excelWriter = null; try { excelWriter = EasyExcel.write(fileName, DemoData.class).build(); WriteSheet writeSheet = EasyExcel.writerSheet("模板").build(); excelWriter.write(data(), writeSheet); } finally { // 千万别忘记finish 会帮忙关闭流 if (excelWriter != null) { excelWriter.finish(); } } }

复杂头写入
@Data public class MultiHeaderEntity implements Serializable {@ExcelProperty(value = https://www.songbingjia.com/android/{"一层信息","二层1"}) private Integer id; @ExcelProperty(value = https://www.songbingjia.com/android/{"一层信息","二层2"}) private String name; @ExcelProperty(value = https://www.songbingjia.com/android/{"一层信息","二层4"}) private String description; @ExcelProperty(value = https://www.songbingjia.com/android/{"一层信息","二层3"}) private Date birthday; }

复杂头的写入操作
@Test public void complexHeadWrite() { String fileName = TestFileUtil.getPath() + "complexHeadWrite" + System.currentTimeMillis() + ".xlsx"; // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 EasyExcel.write(fileName, MultiHeaderEntity.class).sheet("复杂").doWrite(datalist()); }


    推荐阅读