夜雨聆风学习资料网

ARTICLE · 1150580

EasyExcel 悄悄变身 Apache Fesod?百万级 Excel 防 OOM 终极导入方案,带你一行代码搞定!

EasyExcel 悄悄变身 Apache Fesod?百万级 Excel 防 OOM 终极导入方案,带你一行代码搞定!
摘要:还在用doReadSync() 一次性加载 10 万行 Excel?小心你的 JVM 内存被打爆!今天不仅带你吃一口 EasyExcel 社区的最新大瓜,还要教你如何利用“函数式编程”写出极简、优雅、绝对防 OOM 的百万级 Excel 导入组件!

💣 生产环境的“定时炸弹”:你的 Excel 导入写对了吗?

很多后端同学在做 Excel 导入时,为了图省事,经常会写出这样的代码:
// 危险操作:同步读取全部数据List<UserDTO> list = EasyExcel.read(in, UserDTO.class).doReadSync();userService.saveBatch(list);
⚠️ 警报拉响!doReadSync() 会将 Excel 中的所有数据一次性死死拽进内存中。如果运营小姐姐手抖,传了一个包含 50 万行数据的多 Sheet 巨无霸 Excel……恭喜你,你的服务器下一秒就会抛出惨烈的 java.lang.OutOfMemoryError,直接拉跨整个微服务!
老司机都知道,防 OOM 的标准姿势是使用SAX 流式读取(分批处理)。但是,官方推荐的写法要求我们为每一个具体的业务实体类写一个ReadListener。
用户表写一个 UserListener,商品表写一个 ProductListener……久而久之,项目里就会出现严重的“类爆炸”,满屏都是长得差不多的样板代码。
难道就没有一种既能防 OOM,又能极简调用的完美方案吗?
有的。但在祭出大招之前,我们先来吃个瓜 🍉。

🚀 行业速递:再见了 EasyExcel,你好 Apache Fesod!

如果你还在项目中疯狂搜寻 com.alibaba.excel,那你可能已经 out 了!
就在 2025 年末,作为 EasyExcel 的正统接班人,FastExcel 社区迎来了历史性的一刻——成功进入 Apache 软件基金会孵化器!
为了符合 Apache 基金会的开源合规要求,并规避潜在的商标冲突,项目正式更名为Apache Fesod (Incubating)。

💡 小科普:Fesod 是什么意思?它的全称是:Fast Easy Spreadsheet and Other Documents(快速简单的电子表格及其他文档)。 音标读作 /ˈfɛsɒd/。虽然名字变了,包名换成了 org.apache.fesod,但那套极其好用的核心 API 原汁原味地保留了下来!

💡 终极方案:函数式编程 + 通用 Listener

为了解决“类爆炸”和“内存溢出”的双重痛点,我们可以利用 Java 8 的函数式接口Consumer>,把【底层的分批控制】和【上层的业务入库】彻底解耦!
第一步:编写全局唯一的“通用监听器”
全局只需要这一个 Listener!它利用泛型接收任何实体类,每攒够 2000 条数据,就自动丢给业务层去处理,然后清空内存。

⚠️ 避坑指南: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;    }    @Override    public void invoke(T data, AnalysisContext context) {        cachedDataList.add(data);        if (cachedDataList.size() >= BATCH_COUNT) {            // 满载!交给业务层批量入库            batchConsumer.accept(cachedDataList);            // 重新初始化,释放旧内存            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);        }    }    @Override    public void doAfterAllAnalysed(AnalysisContext context) {        if (!CollectionUtils.isEmpty(cachedDataList)) {            // 处理最后一批不足 2000 条的尾部数据            batchConsumer.accept(cachedDataList);        }    }}
第二步:封装极简工具类
把流的关闭和底层的 Fesod.read() 封装起来,屏蔽复杂细节。
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);        }    }}
第三步:见证奇迹的时刻(业务层调用)
现在,你的 Controller 或 Service 代码将变得极其清爽!借助于 Lambda 表达式,一行代码就能搞定带分批逻辑的百万级导入:
场景 1:闭着眼睛直接入库
// 每满 2000 条,自动执行一次 saveBatch,稳!ExcelUtils.readExcelWithListener(file, UserExcelDTO.class, new GenericBatchReadListener<>(dataList -> {    userService.saveBatch(dataList);}));
场景 2:带过滤校验的复杂入库
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);    }}));
代码是不是瞬间高大上起来了?😎

🕵️‍♂️ 性能极客专属:newArrayListWithExpectedSize 的秘密

眼尖的同学可能注意到了,我们在初始化 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 时踩过什么坑?👇

相关学习资料