【Alibaba工具型技术系列】「EasyExcel技术专题」摒除OOM!让你的Excel操作变得更加优雅和安全

摒除OOM!让你的Excel操作变得更加优雅和安全

      • 前提概要
        • 存在隐患问题
        • 解决方案
        • 更优秀的选择
      • EasyExcel的介绍说明
        • 技术原理对比
          • POI
          • EasyExcel
            • 技术原理图
            • 节省内存的开销
        • Maven仓库依赖
        • 基础API介绍(参考官方文档)
        • 实战案例
          • 读取Excel实现Demo
            • 数据模型DemoModel
            • 读取回调监听器
            • 读取操作测试类
          • 写入Excel实现Demo
            • 写入数据模型
            • 写入操作代码
          • 复杂头写入
          • 复杂头的写入操作

前提概要

针对于后端开发者而言的,作为报表的导入和导出是一个很基础且有很棘手的问题!之前常用的工具和方案大概有这么几种:

  1. JXL(Java Excel API 工具服务),此种只支持xls的文件格式,而且对于内存的管理特别的差,现在基本不用了!
  2. 目前大多数会操作Excel工具服务或者解析都是利用Apache POI进行操作。
  3. 其他第三方的工具很多也是基于POI作为实现基础!
存在隐患问题

因为当数据量特别大的时候,比如:说Excel导出,如果数据量在百万级,很有可能会出现俩点内存溢出的问题以及页面极具卡顿。

解决方案
  • 首先,在百万级大数据量Excel文件的导出我们可以采用分批查询数据来避免内存溢出的剧增!

  • 此外,POI给出的方案是:使用SXSSFWorkbook方式缓存数据到文件上以解决下载大文件EXCEL卡死页面的问题。

    • SXSSFWorkbook数据模型:主要可以解决在下载传输到浏览器的时候大Excel文件转换的输出流内存溢出
    • 此外其还可以通过其构造函数执指定在内存中缓存的行数剩余的会自动缓存在硬盘的临时目录上,同时,并不会存在页面卡顿的情况
    1. SXSSF机制而言还是需要手动进行封装及定制化开发增加了一定的工作量!
    2. POI的操作方式仍然还是存在内存占用过大的问题,仍会存在内存溢出的隐患
    3. 存在空循环和整除的时候数据有缺陷的问题
更优秀的选择

对此阿里巴巴发明咯一个“万金油”!EasyExcel(简单Excel操作工具,让Excel变得更简单!),除此之外它可以将解析的Excel的内存占用控制在KB级别,并且绝对不会内存溢出,还有就是速度极快, 不用想了就它了!


EasyExcel的介绍说明

EasyExcel是一个基于Java实现的、以节省内存为主要目标的的读写Excel文件的开源项目。经过官方统计,在尽可能节约内存的情况下支持读写百M的Excel文件的读写操作能力!

  • 源码库(github地址):https://github.com/alibaba/easyexcel
  • 官方文档:https://alibaba-easyexcel.github.io/index.html
技术原理对比
POI

当利用POI去读取Excel时,首先会将数据全部加载到内存中,然后返回给调用者,当数据量比较大时,及其容易发生OOM。以下是执行流程图:

EasyExcel

与POI 不用的是,EasyExcel主要是采用sax模式一行一行解析,并将一行的解析结果以观察者的模式通知处理,即使数据量较大时也不会发生OOM,以下是其执行流程图

技术原理图

借用官方图

节省内存的开销

借用官方图片:https://alibaba-easyexcel.github.io/images/large.png

64M内存1分钟内读取75M(46W行25列)的ExcelExcel 内存开销图。当然还有急速模式能更快,但是内存占用会在100M多一点。其实就是拿空间换时间!

Maven仓库依赖

我用的版本是2.2.6

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.6</version>
</dependency>

注: 系统内部如果有POI的包一定让poi和poi-ooxml的版本要保持一致。

注: 如果是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>() {// 每解析一条数据都会调用该方法@Overridepublic void invoke(Entity entity, AnalysisContext analysisContext) {System.out.println("解析一条Row行对象:" + JSON.toJSONString(entity));entityList.add(student);}// 解析完毕的回调方法@Overridepublic 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>>() {@Overridepublic void invoke(Map<Integer, String> map, AnalysisContext analysisContext) {System.out.println("解析到一条数据:" + JSON.toJSONString(map));resultList.add(map);}@Overridepublic 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来处理数据

所有配置都是继承的,Workbook的配置会被Sheet继承,所以在用EasyExcel设置参数的时候,在EasyExcel…sheet()方法之前作用域是整个sheet,之后针对单个sheet。

  • 相关使用注解
    • ExcelProperty index 指定写到第几列,默认根据成员变量排序。value指定写入的名称,默认成员变量的名字,多个value可以参照快速开始中的复杂头。
    • ExcelIgnore 默认所有字段都会写入excel,这个注解会忽略这个字段
    • DateTimeFormat 日期转换,将Date写到excel会调用这个注解。里面的value参照java.text.SimpleDateFormat
    • NumberFormat 数字转换,用Number写excel会调用这个注解。里面的value参照java.text.DecimalFormat
    • ExcelIgnoreUnannotated 默认不加ExcelProperty的注解的都会参与读写,加了不会参与。
    • 使用案例:
    	@Datapublic class TestEntity {/*** 从0开始,2代表强制读取第三个,* 不建议 index 和 name 同时用*/@ExcelProperty(index = 2)private Double doubleData;/*** 用名字去匹配,这里需要注意,* 名字重复,会导致只有一个字段读取到数据*/@ExcelProperty("字符串标题")private String string;@ExcelProperty("日期标题")private Date date;}
    
    	@Datapublic class MultiHeaderEntity implements Serializable {@ExcelProperty(value = {"一层信息","二层1"})private Integer id;@ExcelProperty(value = {"一层信息","二层2"})private String name;@ExcelProperty(value = {"一层信息","二层4"})private String description;@ExcelProperty(value = {"一层信息","二层3"})private Date birthday;}
    
    	@Datapublic 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中的数据,那么读取时就只能读取以下样式的Excel数据模型,否则数据部分丢失或者全部丢失

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

注意:DemoModelAnalysisListener 不要是单例模式或者全局共享,要每次读取excel都要new,否则会出现复用之前读取的过程数据!

@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*/@Overridepublic void invoke(DemoData data, AnalysisContext context) {log.info("解析到一条数据:{}", JSON.toJSONString(data));list.add(data);// 达到BATCH_COUNT了,需要去存储一次数据库,// 防止数据几万条数据在内存,容易OOMif (list.size() >= BATCH_COUNT) {saveData();}}/*** 所有数据解析完成了 都会来调用* @param context*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存数据,确保最后遗留的数据也存储到数据库saveData();log.info("所有数据解析完成!");}/*** 加上存储数据库*/private void saveData() {log.info("{}条数据,开始存储数据库!", list.size());demoService.insert(list);// 存储完成清理 listlist.clear();//解析结束销毁不用的资源log.info("存储数据库成功!");}
}
读取操作测试类
    @Testpublic 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;/*** 忽略这个字段*/@ExcelIgnoreprivate String ignore;
}
写入操作代码
    @Testpublic void demoWriteTest() {// 写法1String fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭// 如果这里想使用03 则 传入excelType参数即可EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());// 写法2fileName = 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 = {"一层信息","二层1"})private Integer id;@ExcelProperty(value = {"一层信息","二层2"})private String name;@ExcelProperty(value = {"一层信息","二层4"})private String description;@ExcelProperty(value = {"一层信息","二层3"})private Date birthday;}
复杂头的写入操作
    @Testpublic void complexHeadWrite() {String fileName = TestFileUtil.getPath() + "complexHeadWrite" + System.currentTimeMillis() + ".xlsx";// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭EasyExcel.write(fileName, MultiHeaderEntity.class).sheet("复杂").doWrite(datalist());}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/414272.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

跨域原理和解决方案

前置知识 什么是跨域 主要是由于浏览器的同源策略引起的&#xff0c;同源策略是浏览器的安全机制&#xff0c;当 协议&#xff0c;域名&#xff0c;端口 三者有一个不同&#xff0c;浏览器就禁止访问资源。 比如&#xff1a;http://www.company.com:80 http://www.company.…

绝地求生【违规处罚工作公示】1月8日-1月14日

1月8日至1月14日期间&#xff0c;共计对174,636个违规账号进行了封禁&#xff0c;其中164,757个账号因使用外挂被永久封禁。 若您游戏中遇到违规行为&#xff0c;建议您优先在游戏内进行举报&#xff1b; 另外您也可以在官方微信公众号【PUBG国际版】中点击“ 服务中心 - 举报…

【启扬方案】基于RK3568核心板的工业示教器解决方案

机器人作为现代制造业中的重要一环&#xff0c;在生产效率、产品质量和劳动安全等方面带来了巨大的提升&#xff0c;给企业带来了巨大的竞争优势。但机器人的应用存在开发难、安全性差等问题。“示教器”可用丰富的组件解决机器人开发难的问题&#xff0c;且自带的监控功能可提…

[HTML]Web前端开发技术14(HTML5、CSS3、JavaScript )鼠标经过图片显示大图 网页标题:表格标签的综合应用——喵喵画网页

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;佬佬会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…

压缩编码之不同缩放参数对重建图像质量的影响的python实现——JPEG变换编码不同压缩率的模拟

原理 JPEG&#xff08;Joint Photographic Experts Group&#xff09;是一种常用的图像压缩标准&#xff0c;它通过采用离散余弦变换&#xff08;DCT&#xff09;和量化来实现图像的压缩。 离散余弦变换&#xff08;DCT&#xff09;&#xff1a; JPEG首先将图像分割成8x8的块…

阿里云国际虚拟数字人服务如何助力企业出海?

随着越来越多的中国企业开始走向全球&#xff0c;越来越多的电商&#xff0c;选择开始走向全球化&#xff0c;对于电商来说&#xff0c;走向全球化的电商直播&#xff0c;所面临语言的问题&#xff0c;随而出现。小语种主播相较于英语主播更不好招聘&#xff0c;在一线城市招聘…

使用 Python 第三方库 xlwt 写入数据到 Excel 工作表

1. 安装 xlwt 库 Python 写入数据到 Excel 工作簿中可以使用第三方库 xlwt. xlwt 拆分下来看就是 excel 和 write 的简化拼接&#xff0c;意思就是写数据到 Excel. 这个第三方库的 pip 安装命令如下所示&#xff1a; pip install xlwt -i https://mirrors.aliyun.com/pypi/si…

PyTorch 中的距离函数深度解析:掌握向量间的距离和相似度计算

目录 Pytorch中Distance functions详解 pairwise_distance 用途 用法 参数 数学理论公式 示例代码 cosine_similarity 用途 用法 参数 数学理论 示例代码 输出结果 pdist 用途 用法 参数 数学理论 示例代码 总结 Pytorch中Distance functions详解 pair…

Filter过滤器、使用场景、使用办法、创建和配置等

这里写目录标题 过滤器应用场景自动登录统一设置编码格式访问权限控制敏感字符过滤 Filter使用Filter的创建和配置 过滤器 过滤器实际上就是对 web资源进行拦截&#xff0c;做一些处理后再交给下一个过滤器或 servlet处理通常都是用来拦截request进行处理的&#xff0c;也可以…

JVM:性能监控工具分析和线上问题排查实践

前言 在日常开发过程中&#xff0c;多少都会碰到一些jvm相关的问题&#xff0c;比如&#xff1a;内存溢出、内存泄漏、cpu利用率飙升到100%、线程死锁、应用异常宕机等。 在这个日益内卷的环境&#xff0c;如何运用好工具分析jvm问题&#xff0c;成为每个java攻城狮必备的技能…

Docker(二)安装指南:主要介绍 Docker 在 Linux 、Windows 10 和 macOS 上的安装

作者主页&#xff1a; 正函数的个人主页 文章收录专栏&#xff1a; Docker 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01; 安装 Docker Docker 分为 stable test 和 nightly 三个更新频道。 官方网站上有各种环境下的 安装指南&#xff0c;这里主要介绍 Docker 在…

物联网网关与plc怎么连接?

物联网网关与plc怎么连接&#xff1f; 物联网是当今社会中最热门的技术之一&#xff0c;而物联网网关则是连接物联网设备与云平台的核心设备之一。物联网网关在连接各种传感器和设备时起着至关重要的作用。而另一种广泛应用于工业控制和自动化领域的设备是可编程逻辑控制器&…