Apache Fesod 入门教程:高性能 Excel 处理从 0 到 1(告别 OOM,百万行数据轻松拿捏)

在 Java 企业级开发中,Excel 导入导出 是一个绕不开的”标配需求”——从批量导入用户数据、导出业务报表,到对账文件解析、数据迁移,几乎每个项目都得跟 Excel 打交道。

但传统方案的痛点同样令人头疼:

  • Apache POI:基于 DOM 模型,读 10 万行就得吃 2GB+ 内存,动不动就 OutOfMemoryError;
  • EasyExcel:虽然解决了流式读取的问题,但格式支持有限、社区迭代放缓;
  • 手写 SXSSF:API 繁琐,维护成本高,新人上手劝退。

今天给大家介绍的 Apache Fesod(Incubating),正是为解决这些痛点而生的新一代 Excel 处理库。它继承了 EasyExcel 的流式处理基因,底层架构全面重构,性能更猛、API 更优雅,已于 2025 年 9 月正式进入 Apache 孵化器,前途不可限量。

这篇教程带你从 0 基础 → 独立落地,覆盖安装、读、写、高级技巧全套实战,每个示例都可直接复制运行。


0)Apache Fesod 是什么 & 为什么选它

0.1 项目背景 & 定位

Apache Fesod(发音 /ˈfɛsɒd/)是一个高性能、低内存的 Java 电子表格处理库,名字是 “Fast Easy Spreadsheet and Other Documents” 的缩写。

项目前身为 2024 年 10 月开源的 FastExcel,由原 EasyExcel 核心维护团队发起并彻底重构底层。2025 年 9 月 17 日,经 Apache 基金会孵化器投票通过(9 票赞成 / 0 票反对),正式更名为 Fesod 进入 Apache 孵化。

截至目前,GitHub Star 已突破 5K+,累计发布 5 个社区版本,拥有 20+ 贡献者,是近年来 Java Excel 生态中最受关注的新星项目。

0.2 三大核心特性

特性说明
🚀 高性能读写底层基于 SAX 事件驱动 + 零拷贝流式处理,百万行数据内存占用稳定在 50MB 以内,比 POI 快 3~5 倍
🎯 极简 API链式调用 + 注解驱动,3 行代码完成读取,5 行代码完成写入,学习成本极低
📦 格式全面支持 .xlsx(Excel 2007+)、.xls(Excel 97-2003)、CSV,未来还将扩展 PDF/HTML 导出

0.3 常见方案横向对比

方案内存模型百万行内存API 易用性格式支持社区活跃
Apache POI(UserModel)DOM 全量加载2GB+⭐⭐ 繁琐xls/xlsx✅ 老牌稳定
Apache POI(SXSSF)流式写入~500MB⭐ 难用xlsx✅
EasyExcelSAX 流式~100MB⭐⭐⭐⭐xls/xlsx⚠️ 放缓
Apache FesodSAX + 零拷贝~50MB⭐⭐⭐⭐⭐xls/xlsx/CSV🔥 Apache 孵化中

一句话结论:新项目直接上 Fesod,大文件、高并发场景下它就是目前的最优解。


1)快速开始:5 分钟跑通第一个 Demo

1.1 环境要求 & 版本兼容

Fesod 对 JDK 版本的支持非常友好:

Fesod 版本JDK 支持范围备注
1.3.x(最新)JDK 8 ~ JDK 25✅ 推荐使用
1.2.xJDK 8 ~ JDK 21
1.1.xJDK 8 ~ JDK 21
1.0.xJDK 8 ~ JDK 21

⚠️ 注意:Fesod 底层仍依赖 POI 解析二进制格式。如果你的项目中已经引入了 poi、poi-ooxml 等依赖,需要手动排除冲突版本,建议统一使用 Fesod 传递依赖的 POI 版本。

1.2 Maven / Gradle 引入

Maven(pom.xml) — 最常用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependencies>
<!-- Apache Fesod 核心依赖(目前仍使用原 groupId,孵化毕业前会切换到 org.apache.fesod) -->
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>1.3.0</version>
</dependency>

<!-- 可选:Lombok,减少样板代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>

<!-- 可选:SLF4J 日志实现,用你项目里已有的即可 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.13</version>
</dependency>
</dependencies>

Gradle(build.gradle):

1
2
3
4
5
dependencies {
implementation 'cn.idev.excel:fastexcel:1.3.0'
compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
}

引入成功后,先编译一下确认没有版本冲突,就可以进入实战环节了。


2)核心概念扫盲:POJO、监听器、流式 vs 同步

在正式写代码前,先理解 Fesod 的三个核心设计,后续读和写都会围绕它们展开:

2.1 POJO 模型 + 注解映射

Fesod 用 Java Bean + 注解 来描述 Excel 的行列结构,常见注解:

注解作用常用属性
@ExcelProperty("表头名")标记字段对应哪一列value 表头名;index 列号(从 0 开始)
@ExcelIgnore标记该字段不参与读写-
@DateTimeFormat("yyyy-MM-dd")日期格式转换value 格式字符串
@NumberFormat("#.##%")数字格式转换(百分比等)value 格式字符串

2.2 监听器模式(ReadListener)

读取大文件必须用监听器,这是 Fesod 内存友好的核心:

  • 数据不会一次性加载到内存,而是每解析一行就回调一次 invoke() 方法;
  • 你在 invoke() 里做业务处理(校验、入库、转发等),处理完对象就可以被 GC;
  • doAfterAllAnalysed() 会在全部解析完毕后回调,适合做收尾动作(如关闭资源、统计总数)。

2.3 流式 vs 同步读取

方式API适用场景内存占用
流式(推荐).doRead() + ReadListener数据量大(>1万行)、生产环境恒定低内存
同步.doReadSync() 返回 List数据量小(<1万行)、单元测试全部加载进内存

💡 生产环境永远优先用流式 + 监听器。同步读虽然写起来简单,但大数据量下直接 OOM。


3)实战一:读取 Excel(4 种常用姿势全覆盖)

3.1 准备数据模型 DemoData

先定义一个最简单的用户数据模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;
import java.util.Date;

@Data
public class DemoData {

@ExcelProperty("字符串标题")
private String string;

@ExcelProperty("日期标题")
private Date date;

@ExcelProperty("数字标题")
private Double doubleData;

@ExcelIgnore
private String ignoreThisField; // 不会被读写
}

3.2 方式 1:自定义监听器 + 流式读取(最常用 ✅)

先写监听器类,每读到一行就会进 invoke():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.read.listener.ReadListener;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {

@Override
public void invoke(DemoData data, AnalysisContext context) {
// 这里每解析一行就会被调用一次
log.info("解析到一条数据: {}", JSON.toJSONString(data));
// 实际业务中:在这里做数据校验 → 分批入库 / 发 MQ / 写缓存等
}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("所有数据解析完成!");
// 收尾动作:刷新缓存、提交事务、发送通知等
}
}

主程序调用:

1
2
3
4
5
6
7
8
9
10
public class ReadDemo {
public static void main(String[] args) {
String fileName = "demo.xlsx";

// 一行代码搞定读取:文件路径 + 数据模型 + 监听器
FesodSheet.read(fileName, DemoData.class, new DemoDataListener())
.sheet() // 读取第 1 个 sheet,也可以 .sheet(1) 或 .sheet("Sheet名")
.doRead(); // 启动流式读取
}
}

3.3 方式 2:Lambda 匿名监听器(代码更少)

如果逻辑简单,不想单独写监听器类,直接用 PageReadListener 或匿名内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import cn.idev.excel.read.listener.PageReadListener;

public class ReadLambdaDemo {
public static void main(String[] args) {
String fileName = "demo.xlsx";

// PageReadListener 会默认按 100 行一批回调,适合批量入库场景
FesodSheet.read(fileName, DemoData.class, new PageReadListener<DemoData>(dataList -> {
// 每次拿到 100 行的一批数据
log.info("读取到 {} 条数据,准备批量入库", dataList.size());
// userService.batchSave(dataList);
})).sheet().doRead();
}
}

3.4 方式 3:同步读取(小数据量用)

测试场景或者确定行数很少时,直接拿 List:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ReadSyncDemo {
public static void main(String[] args) {
String fileName = "demo.xlsx";

// 直接返回 List<DemoData>,全部在内存里
List<DemoData> list = FesodSheet.read(fileName)
.head(DemoData.class)
.sheet()
.doReadSync();

list.forEach(data -> log.info("数据: {}", JSON.toJSONString(data)));
}
}

3.5 方式 4:不定义 POJO,直接用 Map 读取

如果 Excel 结构不固定、或者临时写脚本,用 Map<Integer, String> 按列索引读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ReadMapDemo {
public static void main(String[] args) {
String fileName = "demo.xlsx";

// 同步读成 List<Map>,key 是列号(从 0 开始)
List<Map<Integer, String>> list = FesodSheet.read(fileName)
.sheet()
.doReadSync();

for (Map<Integer, String> row : list) {
log.info("第 1 列={}, 第 2 列={}, 第 3 列={}",
row.get(0), row.get(1), row.get(2));
}
}
}

3.6 进阶:只读取指定列(列过滤)

文件有 20 列但你只关心第 1 列和第 3 列?用 includeColumnIndexes 节省内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ReadColumnFilterDemo {
public static void main(String[] args) {
String fileName = "demo.xlsx";

List<Integer> targetColumns = Arrays.asList(0, 2); // 只要第 1 列(A)和第 3 列(C)

List<Map<Integer, String>> list = FesodSheet.read(fileName)
.sheet()
.includeColumnIndexes(targetColumns)
.doReadSync();

// 返回的 Map 的 key 会重新按 0、1 顺序编号,对应你指定的列
String colA = list.get(0).get(0); // 原来的第 0 列
String colC = list.get(0).get(1); // 原来的第 2 列,映射到新 key 1
}
}

4)实战二:写入 Excel(3 种方式 + 注解详解)

写入比读取更简单,Fesod 提供了 链式 API,一路 .write().sheet().doWrite() 就行。

4.1 最简单的写入(列表直接写)

还是用刚才的 DemoData 类(注意 @ExcelProperty 的表头名):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class WriteDemo {

private static List<DemoData> data() {
List<DemoData> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
DemoData data = new DemoData();
data.setString("字符串" + i);
data.setDate(new Date());
data.setDoubleData(0.56 + i);
list.add(data);
}
return list;
}

public static void main(String[] args) {
String fileName = "simpleWrite_" + System.currentTimeMillis() + ".xlsx";

// 一行写入:文件路径 + 模型类 → 指定 sheet 名 → 写数据
FesodSheet.write(fileName, DemoData.class)
.sheet("模板") // 指定第一个 sheet 的名称
.doWrite(data()); // 直接传 List
}
}

生成的 Excel 长这样:

A(字符串标题)B(日期标题)C(数字标题)
1字符串标题日期标题数字标题
2字符串02026-09-29 10:000.56
3字符串12026-09-29 10:001.56
…………

4.2 Lambda 供给式写入(分批写,内存友好)

如果数据量特别大(比如几十万行),不要一次性构造 ArrayList,用 Lambda 按需供给:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class WriteLambdaDemo {
public static void main(String[] args) {
String fileName = "bigDataWrite.xlsx";

FesodSheet.write(fileName, DemoData.class)
.sheet("大数据导出")
.doWrite(() -> {
// 在这里按需生成 / 查询一批数据,比如分页从 DB 拉取
// Fesod 会边写边刷盘,不会把全部数据堆在内存
return fetchDataFromDB(0, 10000);
});
}

private static List<DemoData> fetchDataFromDB(int offset, int limit) {
// 实际业务:userMapper.selectPage(new Page<>(offset/limit+1, limit))
List<DemoData> list = new ArrayList<>();
for (int i = offset; i < offset + limit; i++) {
DemoData d = new DemoData();
d.setString("行_" + i);
d.setDate(new Date());
d.setDoubleData(Math.random() * 100);
list.add(d);
}
return list;
}
}

4.3 多 Sheet / 多次写入(ExcelWriter 手动控制)

需要写多个 Sheet 或者追加数据时,用 try-with-resources 管理 ExcelWriter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WriteMultiSheetDemo {
public static void main(String[] args) {
String fileName = "multiSheet.xlsx";

try (ExcelWriter excelWriter = FesodSheet.write(fileName, DemoData.class).build()) {

// 第 1 个 Sheet:用户表
WriteSheet sheet1 = FesodSheet.writerSheet("用户数据").build();
excelWriter.write(userData(), sheet1);

// 第 2 个 Sheet:订单表(也可以换另一个 POJO 类)
WriteSheet sheet2 = FesodSheet.writerSheet(1, "订单数据").build();
excelWriter.write(orderData(), sheet2);

} // 自动关流、刷盘
}
}

4.4 写入注解详解

注解写入时作用示例
@ExcelProperty("列名")指定表头名称和顺序@ExcelProperty("用户名")
@ExcelProperty(index = 2)按列号固定写入位置@ExcelProperty(index = 0) 写 A 列
@ExcelIgnore该字段不写入 Excel
@ColumnWidth(20)设置列宽(字符数)@ColumnWidth(30)
@HeadRowHeight(30)设置表头行高
@ContentRowHeight(20)设置数据行高

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
public class UserExportVO {

@ExcelProperty("ID")
@ColumnWidth(8)
private Long id;

@ExcelProperty("用户名")
@ColumnWidth(15)
private String username;

@ExcelProperty("注册时间")
@ColumnWidth(22)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date registerTime;

@ExcelProperty("账户余额")
@NumberFormat("¥#,##0.00")
@ColumnWidth(15)
private BigDecimal balance;

@ExcelIgnore
private String password; // 敏感字段绝不导出
}

5)生产环境最佳实践 & 避坑指南

5.1 大文件读取:分批入库的标准写法

监听器不要持有大量数据,每读一批就入库一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Slf4j
public class BatchSaveListener implements ReadListener<UserImportVO> {

private static final int BATCH_SIZE = 500;
private final List<UserImportVO> batch = new ArrayList<>(BATCH_SIZE);
private final UserService userService;

// 监听器不能被 Spring 单例管理!每次读取都要 new,手动把 Bean 传进来
public BatchSaveListener(UserService userService) {
this.userService = userService;
}

@Override
public void invoke(UserImportVO data, AnalysisContext context) {
// 1. 单条校验(JSR-380 Validator 之类)
ValidatorUtil.validate(data);
batch.add(data);

// 2. 达到批次大小就入库
if (batch.size() >= BATCH_SIZE) {
userService.batchSave(batch);
batch.clear();
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 3. 收尾:把不足 500 条的最后一批入库
if (!batch.isEmpty()) {
userService.batchSave(batch);
batch.clear();
}
log.info("✅ 导入全部完成");
}
}

// 使用方:
UserService userService = SpringContext.getBean(UserService.class);
FesodSheet.read(file, UserImportVO.class, new BatchSaveListener(userService))
.sheet().doRead();

5.2 避坑清单 ⚠️

坑点说明正确做法
监听器被 Spring 单例管理多线程下同一个 List 会被多个请求同时写,数据串了每次读取都 new 一个 Listener,不要加 @Component
invoke 里一条一条入库10 万行数据就发 10 万条 SQL,数据库直接挂攒够 BATCH_SIZE(300~1000)批量写
doReadSync 读大文件全量加载进内存,OOM 分分钟数据 >1 万行一律用 doRead() + ReadListener
POI 版本冲突项目里老版本 POI 跟 Fesod 依赖的版本不一致exclusions 排除旧 POI,统一用 Fesod 传递的版本
日期/数字格式化不生效注解放错位置或者类型不匹配Date 用 @DateTimeFormat、BigDecimal/Number 用 @NumberFormat

5.3 异常处理:某一行失败不中断整体

监听器里加 try-catch + 错误记录,不要让单条脏数据导致整个任务失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void invoke(UserImportVO data, AnalysisContext context) {
try {
ValidatorUtil.validate(data);
batch.add(data);
if (batch.size() >= BATCH_SIZE) { flush(); }
} catch (Exception e) {
// 记录错误行号(context 能拿到当前行号)+ 错误原因
Integer rowNum = context.readRowHolder().getRowIndex() + 1;
log.error("第 {} 行解析失败:{},数据:{}", rowNum, e.getMessage(), JSON.toJSONString(data));
// errorList.add(new ErrorRow(rowNum, e.getMessage())); 最后汇总导出错误报告
}
}

6)从 EasyExcel / FastExcel 迁移到 Fesod

如果你之前用的是 EasyExcel 或旧版 FastExcel,迁移到 Fesod 几乎是 无缝的——API 设计基本保持一致,主要改动点:

旧写法(EasyExcel / FastExcel)新写法(Apache Fesod)
EasyExcel.read(...) / FastExcel.read(...)FesodSheet.read(...)
EasyExcel.write(...) / FastExcel.write(...)FesodSheet.write(...)
EasyExcel.writerSheet(...)FesodSheet.writerSheet(...)
包名 com.alibaba.excel.* 或 cn.idev.excel.*目前仍然是 cn.idev.excel.*,孵化毕业后会逐步迁移到 org.apache.fesod.*,届时官方会出迁移指南

核心注解保持 100% 兼容:@ExcelProperty、@ExcelIgnore、@DateTimeFormat、@NumberFormat 都不需要改。

所以如果你手上有 EasyExcel 的老项目,试一下把入口类名替换成 FesodSheet,大概率直接跑起来,还能免费获得一波性能提升 👍。


7)总结 & 学习资源

7.1 一句话复盘

  • 读 Excel:大文件用 FesodSheet.read().sheet().doRead() + 自定义 ReadListener,小文件用 .doReadSync();
  • 写 Excel:简单场景 FesodSheet.write().sheet().doWrite(list),多 Sheet / 大数据量用 ExcelWriter + try-with-resources;
  • 生产环境:监听器别加 @Component、数据攒批入库、异常单独捕获记录行号。

7.2 官方学习资源

资源链接
项目官网https://fesod.apache.org/
官方文档https://fesod.apache.org/docs/
GitHub 源码(含大量 Examples)https://github.com/apache/fesod
Maven 中央仓库https://mvnrepository.com/artifact/cn.idev.excel/fastexcel
开发者邮件列表dev@fesod.apache.org

7.3 后续进阶方向

掌握本篇基础后,可以进一步研究 Fesod 的高级能力:

  1. 模板填充(Fill API):用 .xlsx 模板 + 数据变量,生成复杂格式的合同、报价单、财务报表;
  2. 自定义 Converter:处理特殊字段类型(枚举、字典翻译、加密字段等);
  3. 单元格样式 / 合并单元格:动态生成带颜色、边框、公式的报表;
  4. Web 场景下的导入导出:配合 Spring Boot MultipartFile 和 HttpServletResponse 实现接口级 Excel 能力。

下一篇我会基于 Spring Boot 写一个 “生产级 Excel 导入导出通用组件”,从文件上传、参数校验、异步任务、错误报告下载全链路打通,敬请期待 🚀。



Apache Fesod 入门教程:高性能 Excel 处理从 0 到 1(告别 OOM,百万行数据轻松拿捏)
https://www.pcboy.com.cn/2026/09/28/Apache-Fesod-入门教程:高性能Excel处理从0到1/
作者
chituer
发布于
2026年9月28日
许可协议