ARTICLE · 1150580
EasyExcel 悄悄变身 Apache Fesod?百万级 Excel 防 OOM 终极导入方案,带你一行代码搞定!
EasyExcel 悄悄变身 Apache Fesod?百万级 Excel 防 OOM 终极导入方案,带你一行代码搞定!摘要:还在用doReadSync() 一次性加载 10 万行 Excel?小心你的 JVM 内存被打爆!今天不仅带你吃一口 EasyExcel 社区的最新大瓜,还要教你如何利用“函数式编程”写出极简、优雅、绝对防 OOM 的百万级 Excel 导入组件! 很多后端同学在做 Excel 导入时,为了图省事,经常会写出这样的代码: ⚠️ 警报拉响!doReadSync() 会将 Excel 中的所有数据一次性死死拽进内存中。如果运营小姐姐手抖,传了一个包含 50 万行数据的多 Sheet 巨无霸 Excel……恭喜你,你的服务器下一秒就会抛出惨烈的 java.lang.OutOfMemoryError,直接拉跨整个微服务! 老司机都知道,防 OOM 的标准姿势是使用SAX 流式读取(分批处理)。但是,官方推荐的写法要求我们为每一个具体的业务实体类写一个ReadListener。 用户表写一个 UserListener,商品表写一个 ProductListener……久而久之,项目里就会出现严重的“类爆炸”,满屏都是长得差不多的样板代码。 难道就没有一种既能防 OOM,又能极简调用的完美方案吗? 有的。但在祭出大招之前,我们先来吃个瓜 🍉。 如果你还在项目中疯狂搜寻 com.alibaba.excel,那你可能已经 out 了! 就在 2025 年末,作为 EasyExcel 的正统接班人,FastExcel 社区迎来了历史性的一刻——成功进入 Apache 软件基金会孵化器! 为了符合 Apache 基金会的开源合规要求,并规避潜在的商标冲突,项目正式更名为Apache Fesod (Incubating)。 为了解决“类爆炸”和“内存溢出”的双重痛点,我们可以利用 Java 8 的函数式接口Consumer>,把【底层的分批控制】和【上层的业务入库】彻底解耦! 第一步:编写全局唯一的“通用监听器” 全局只需要这一个 Listener!它利用泛型接收任何实体类,每攒够 2000 条数据,就自动丢给业务层去处理,然后清空内存。 第二步:封装极简工具类 把流的关闭和底层的 Fesod.read() 封装起来,屏蔽复杂细节。 第三步:见证奇迹的时刻(业务层调用) 现在,你的 Controller 或 Service 代码将变得极其清爽!借助于 Lambda 表达式,一行代码就能搞定带分批逻辑的百万级导入: 场景 1:闭着眼睛直接入库 场景 2:带过滤校验的复杂入库 代码是不是瞬间高大上起来了?😎 眼尖的同学可能注意到了,我们在初始化 List 时,并没有使用常见的 new ArrayList<>(),而是使用了 ListUtils.newArrayListWithExpectedSize(BATCH_COUNT)。 这可不是在装 X,而是实打实的性能优化! 如果你直接 new ArrayList<>(),JDK 会给你分配一个初始容量为 10 的小数组。当我们塞入 2000 条数据时,底层数组会被迫扩容十几次(10 -> 15 -> 22 -> 33...)。每次扩容,都要在内存中开辟新空间,并使用 Arrays.copyOf() 把老数据搬家,极大地浪费 CPU 并产生内存碎片。 而 newArrayListWithExpectedSize(2000) 会在底层精准计算,一次性为你分配好能容纳 2000 个元素的连续内存空间。0 扩容,0 数据搬运,极致顺滑!这也是 Google Guava 核心库一直推崇的高级 Java 编码规范。 面对企业级的 Excel 导入: 拥抱 Apache Fesod,它依然是你处理复杂表格的首选方案。 拒绝doReadSync(),用事件驱动和分批处理保卫你的服务器内存。 巧用Consumer函数式接口,把啰嗦的 Listener 抽象成优雅的通用组件。 关注底层数据结构,指定集合初始容量,榨干最后一滴性能。 赶快把这套代码集成到你的项目工具类中,惊艳你的架构师吧! 如果这篇文章对你有启发,欢迎点赞、在看、转发给身边的 Java 开发小伙伴!也欢迎在评论区交流你在处理 Excel 时踩过什么坑?👇
💣 生产环境的“定时炸弹”:你的 Excel 导入写对了吗?
// 危险操作:同步读取全部数据List<UserDTO> list = EasyExcel.read(in, UserDTO.class).doReadSync();userService.saveBatch(list);
🚀 行业速递:再见了 EasyExcel,你好 Apache Fesod!
💡 小科普:Fesod 是什么意思?它的全称是:Fast Easy Spreadsheet and Other Documents(快速简单的电子表格及其他文档)。 音标读作
/ˈfɛsɒd/。虽然名字变了,包名换成了org.apache.fesod,但那套极其好用的核心 API 原汁原味地保留了下来!
💡 终极方案:函数式编程 + 通用 Listener
⚠️ 避坑指南:Listener 是有状态的(内部存了 List),绝对不能加 @Component 注解,每次读取必须 new 一个新对象!
import org.apache.fesod.context.AnalysisContext;import org.apache.fesod.read.listener.ReadListener;import org.apache.fesod.util.ListUtils;import org.springframework.util.CollectionUtils;import java.util.List;import java.util.function.Consumer;public class GenericBatchReadListener<T> implements ReadListener<T> {// 内存护城河:每 2000 条清理一次private static final int BATCH_COUNT = 2000;// 初始化集合private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);// 外部传入的具体业务消费逻辑private final Consumer<List<T>> batchConsumer;public GenericBatchReadListener(Consumer<List<T>> batchConsumer) {this.batchConsumer = batchConsumer;}@Overridepublic void invoke(T data, AnalysisContext context) {cachedDataList.add(data);if (cachedDataList.size() >= BATCH_COUNT) {// 满载!交给业务层批量入库batchConsumer.accept(cachedDataList);// 重新初始化,释放旧内存cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {if (!CollectionUtils.isEmpty(cachedDataList)) {// 处理最后一批不足 2000 条的尾部数据batchConsumer.accept(cachedDataList);}}}
import org.apache.fesod.Fesod;import org.apache.fesod.read.listener.ReadListener;import lombok.extern.slf4j.Slf4j;import org.springframework.web.multipart.MultipartFile;import java.io.InputStream;@Slf4jpublic class ExcelUtils {public static <T> void readExcelWithListener(MultipartFile file, Class<T> clazz, ReadListener<T> listener) {// try-with-resources 自动关流,双重安全try (InputStream inputStream = file.getInputStream()) {Fesod.read(inputStream, clazz, listener).sheet(0) // 明确指定只读第一个 Sheet(索引从 0 开始).headRowNumber(1).doRead(); // 注意:这里是 doRead() 触发 SAX 解析} catch (Exception e) {log.error("Excel 数据解析失败", e);throw new RuntimeException("Excel 数据解析失败: " + e.getMessage(), e);}}}
// 每满 2000 条,自动执行一次 saveBatch,稳!ExcelUtils.readExcelWithListener(file, UserExcelDTO.class, new GenericBatchReadListener<>(dataList -> {userService.saveBatch(dataList);}));
ExcelUtils.readExcelWithListener(file, ProductExcelDTO.class, new GenericBatchReadListener<>(dataList -> {// 先在内存里把价格不合法的数据踢掉List<ProductExcelDTO> validData = dataList.stream().filter(p -> p.getPrice() != null).collect(Collectors.toList());if (!validData.isEmpty()) {productService.saveBatch(validData);}}));