Loading...

文章背景图

EasyExcel:阿里低内存 Excel 读写工具引入与使用指南

2024-10-29
0
- 字
- 分钟
|

官方信息

项目 内容
项目名称 EasyExcel
开源方 阿里巴巴(alibaba)
代码仓库 alibaba/easyexcel
备用仓库 gitee.com/easyexcel/easyexcel
官方网站 easyexcel.opensource.alibaba.com
开发语言 Java
最新版本 4.0.3(Maven 中央仓库)
开源协议 Apache License 2.0
维护状态 已进入维护模式(持续修复 Bug,不再新增功能)

EasyExcel 官方图

项目背景:传统 Excel 解析为什么这么吃内存

在 Java 生态里,解析和生成 Excel 文件比较有名的框架主要是 Apache POI 与 jxl。但它们都存在一个非常严重的问题:极其耗费内存。以 Apache POI 为例,它默认的 DOM 模式会把整个工作簿连同每一行、每一个单元格都构建成内存对象,一个稍微大一点的报表就可能把虚拟机堆撑爆。POI 后来虽然提供了一套 SAX 模式的流式 API,可以在一定程度上缓解内存溢出的问题,但它依然存在明显缺陷——尤其是处理 07 版 Excel(也就是 xlsx 格式)时,xlsx 本质上是一个 zip 压缩包,POI 需要先把整个压缩包解压、再把解压后的 XML 内容读入内存进行解析,解压缩和解析过程中产生的大量临时对象全部驻留在堆里,内存消耗依然居高不下。这就是为什么很多业务系统在做大批量 Excel 导出时,动辄就要给 JVM 分配好几个 G 的内存,还时不时抛出 OutOfMemoryError。

EasyExcel 正是为了解决这一痛点而生。它重写了 POI 对 07 版 Excel 的解析逻辑:一个 3M 大小的 Excel 文件,如果用 POI 的 SAX 方式解析,仍然需要占用大约 100M 左右的内存;而改用 EasyExcel 之后,内存占用可以降低到几 M,并且无论 Excel 文件多大都不会再出现内存溢出。其核心思路是把「一次性把全部数据加载进内存」改为「边读边处理、读完一行就释放一行」,用一种逐行回调的方式把单元格数据直接映射到业务对象上,而不是在内存里保留一棵巨大的文档对象树。对于 03 版 Excel(xls),EasyExcel 仍然依赖 POI 的 SAX 模式,因为旧的二进制格式本身已经是流式可解析的;但 EasyExcel 在其上层做了模型转换的封装,把繁琐的 SAX 事件处理屏蔽掉,让使用者只需要关心业务对象和监听器,调用起来更加简单方便。

在性能表现上,EasyExcel(3.2.1 及以上版本)可以做到用 16M 内存、23 秒读取一个 75M 大小、包含 46 万行 25 列的 Excel 文件。对于需要批量导入几十万行业务数据的后台系统而言,这个内存占用几乎可以忽略不计。此外,官方还提供了「极速模式」,开启之后读取速度会更快,代价是内存占用会上升到 100M 多一点,适合对吞吐量要求极高、且机器内存充足的场景;普通业务报表导入则使用默认的低内存模式即可。

与传统方案相比,EasyExcel 的另一个好处是对业务代码友好。开发者不再需要理解 POI 里 Workbook、Sheet、Row、Cell 这一整套对象模型,也不需要自己处理 SAX 事件里那些晦涩的单元格引用,只需要定义一个带注解的普通 Java 实体,再写一个处理每一行数据的监听器,就完成了大部分工作。这种「面向业务对象编程」的方式,把原本几十行、容易出错的解析代码压缩到寥寥几行,也降低了新同事接手时的理解成本。对于 jxl 这个老牌工具而言,它已经多年不维护,对 xlsx 格式的支持也非常有限,EasyExcel 基本上可以看作它在现代 JDK 环境下的替代者。

维护公告:现在该如何看待这个项目

阿里巴巴 EasyExcel 团队近期发布了维护公告:EasyExcel 将逐步进入维护模式,并给予用户充足的时间评估并迁移到其他产品。需要特别说明的是,进入维护模式并不意味着项目停止维护——官方仍然会确保 EasyExcel 的基本功能稳定运行,并持续进行 Bug 修复,只是不再主动新增功能。团队同时表示,欢迎社区内外分享关于同类优秀产品的使用心得和迁移经验,共同促进技术生态的健康发展。因此,存量基于 EasyExcel 的系统可以放心继续运行;但如果是全新立项的项目,在选型时应当把这一维护状态纳入考量,并提前评估迁移成本。

环境要求与安装

EasyExcel 是一个纯 Java 类库,运行只需要 JDK 环境,不需要安装任何原生组件,也不提供 Docker 镜像——它是一个以 jar 形式嵌入业务工程的库,而不是独立部署的服务。它对 03 版 xls 的处理依赖 POI 的 SAX 能力,对 07 版 xlsx 则使用自研的低内存解析。目前最新发布版本为 4.0.3,通过 Maven 引入即可:

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

如果使用 Gradle 等其他构建工具,只需将对应的 com.alibaba:easyexcel:4.0.3 坐标加入依赖管理即可。引入之后,无需任何额外配置,就可以在业务代码中直接调用 EasyExcel 入口类进行读或写。它典型的适用场景包括:后台管理系统里大批量订单或报表的导入导出、定时任务里把数据库查询结果写成 Excel 邮件附件、Web 端用户上传 Excel 后逐行校验并入库、以及对内存敏感的容器化微服务里处理大表格。凡是过去因为 POI 内存过大而不得不把 JVM 堆调到几个 G 的场景,都可以优先评估 EasyExcel。

使用方法

读取 Excel:监听器模式是关键

最简单的读取分为三步:第一步,创建 Excel 对应的实体对象(例如 DemoData),用注解把 Java 字段和 Excel 的列一一对应起来;第二步,由于默认是一行一行地读取 Excel,所以需要创建一个逐行回调的监听器(例如 DemoDataListener),在监听器里决定每行数据是直接入库还是攒成一批再处理;第三步,直接调用读取 API。EasyExcel 会自动读取第一个 sheet,并且文件流会在读完后自动关闭,业务代码不需要手动流管理。

/**
 * 最简单的读
 * 1. 创建excel对应的实体对象 参照 DemoData
 * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照 DemoDataListener
 * 3. 直接读即可
 */
@Test
public void simpleRead() {
    String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
    // 这里需要指定读用哪个class去读,然后读取第一个sheet,文件流会自动关闭
    EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
}

监听器是 EasyExcel 低内存能力的核心:因为数据是逐行推送过来的,监听器可以每读到 N 行就批量写入数据库,然后清空内存中的缓存,保证整个读取过程内存曲线始终保持平稳,不会随着行数增加而膨胀。完整的读取示例代码见官方示例仓库的 ReadTest.java,详细文档见官方快速开始·读 Excel。

写出 Excel

写出同样只需两步:先创建 Excel 对应的实体对象,然后直接调用写出 API。下面的代码会写到第一个 sheet,sheet 名为「模板」,文件流写完后自动关闭;如果想使用 03 版 xls,只需传入 excelType 参数即可。写出时业务侧只需要准备好一个集合,EasyExcel 会负责把集合里的对象逐行渲染成单元格,而不需要业务代码操作单元格对象。

/**
 * 最简单的写
 * 1. 创建excel对应的实体对象
 * 2. 直接写即可
 */
@Test
public void simpleWrite() {
    String fileName = TestFileUtil.getPath() + "write" + System.currentTimeMillis() + ".xlsx";
    // 这里需要指定写用哪个class,然后写到第一个sheet,名字为模板,文件流会自动关闭
    // 如果这里想使用03,则传入excelType参数即可
    EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
}

完整写出示例见 WriteTest.java,详细文档见官方快速开始·写 Excel。

Web 上传与下载

在 Web 场景下,EasyExcel 可以直接把内容写到 HttpServletResponse 的输出流,实现文件下载;也可以直接读取前端上传的 MultipartFile 输入流,把上传的 Excel 解析成业务对象后交给 Service 层处理。下载时需要注意设置正确的 Content-Type 和文件名编码,URLEncoder.encode 用于防止中文文件名乱码;另外 finish 时会自动关闭 OutputStream,外面再关一次流也没有问题。有用户反馈使用 Swagger 测试下载会导致各种问题,建议直接用浏览器或 Postman 测试。

/**
 * 文件下载(失败了会返回一个有部分数据的Excel)
 */
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setCharacterEncoding("utf-8");
    // URLEncoder.encode 可以防止中文乱码
    String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
    response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
    EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
}

/**
 * 文件上传
 */
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
    EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead();
    return "success";
}

常见问题(FAQ)

问:EasyExcel 现在还适合用于新项目吗? 答:官方已宣布进入维护模式,会持续修复 Bug 但不再新增功能。存量项目可以继续使用,基本功能稳定;新项目建议评估这一维护状态,并提前考察同类替代方案的迁移成本。

问:为什么读取大文件时内存占用仍然偏高? 答:EasyExcel 默认模式已经很低内存。如果开启了「极速模式」,内存占用会上升到 100M 多一点以换取速度;普通模式下大文件仍建议结合监听器分批处理,每读到一批就入库并清空缓存,不要一次性把所有行加载到 List 里,否则内存优势会被业务侧抵消。

问:03 版 xls 和 07 版 xlsx 有什么区别? 答:03 版 xls 依赖 POI 的 SAX 模式,EasyExcel 在上层做了模型封装;07 版 xlsx 是 EasyExcel 自研重写的低内存解析,内存优化效果最明显。需要写 xls 时传入 excelType 参数即可。

问:为什么要用监听器,不能直接拿到一个 List 吗? 答:可以一次性拿到 List,但那样会把所有行都驻留在内存里,行数一大就会失去 EasyExcel 的低内存优势。监听器模式让你逐行处理、批量落库,这正是它能在 16M 内存里读 75M 文件的关键。

问:如何获取帮助? 答:优先查阅官方文档——包括快速开始、常见问题和 API 文档;其次可以去 GitHub Issues 或 Gitee Issues 搜索或新建 issue,把问题写出来也方便后来遇到同样问题的同学查阅;紧急问题提完 issue 后可在官方钉钉群艾特群主并发送 issue 地址。

问:谁在维护这个项目? 答:核心维护者为姬朋飞(花名玉霄)与庄家钜。

问:EasyExcel 会不会把表头也当成数据读进来? 答:不会。默认情况下 EasyExcel 会跳过表头行,从第一行真实数据开始逐行推送给监听器;实体类字段上的注解负责把列与字段对应起来,复杂表头、合并单元格等场景在官方文档里都有专门章节说明。

问:导出失败时,前端会拿到什么? 答:官方文档明确说明,文件下载即使中途失败,也会返回一个已经写入部分数据的 Excel,而不是直接报错;业务侧在 doWrite 时只需要传入数据集,EasyExcel 会边生成边刷新到输出流。

问:除了读写,还有哪些常见用法? 答:README 与官方文档覆盖了读 Excel、写 Excel、Web 上传下载三类最常用场景;更复杂的组合表头、模板填充、重复标题行等进阶用法,都可以在官方文档的快速开始与常见问题章节中找到对应示例,建议按官方示例仓库里的 easyexcel-test 模块对照学习。

问:EasyExcel 是怎么解决内存问题的? 答:它没有沿用 POI 把整个工作簿读进内存再遍历的做法,而是在解析 07 版 xlsx 时边解压、边以事件流的方式逐行吐出单元格数据,业务侧通过监听器一行一行地接收并处理,处理完即可丢弃,内存占用不再随行数线性增长。这也是它能在 16M 内存里读出 75M 文件的根本原因。

跳转链接

评论交流

文章目录