FastExcel 入门到实战:Java 后端如何优雅实现 Excel 导入导出
在后台管理系统开发中,我们经常会遇到这样的需求:
- 用户批量导入 Excel 数据
- 后台导出用户列表
- 导出订单、账单、统计报表
- 一次处理几万甚至几十万条数据
例如:
Excel
↓
Spring Boot
↓
读取 Excel
↓
转换 Java 对象
↓
数据校验
↓
批量写入 MySQL
或者反过来:
MySQL
↓
查询业务数据
↓
Spring Boot
↓
生成 Excel
↓
浏览器下载
Java 生态里最经典的 Excel 操作库是 Apache POI。
但是,如果只是为了完成日常业务里的 Excel 导入导出,直接使用 POI 往往需要编写不少样板代码。
这时候,就可以使用 FastExcel。
本文从零开始搞清楚:
FastExcel 是什么?为什么要用 FastExcel?如何在 Spring Boot 中实现 Excel 导入导出?大数据量 Excel 又应该如何处理?
一、FastExcel 是什么?
FastExcel 可以简单理解为:
Java 中用于高效读取、写入 Excel 文件的工具库。
它特别适合后台管理系统中常见的:
Excel 导入
Excel 导出
批量数据处理
例如,现在有一个 Excel:
| 姓名 | 年龄 | 手机号 |
|---|---|---|
| 张三 | 20 | 13800000001 |
| 李四 | 21 | 13800000002 |
| 王五 | 22 | 13800000003 |
我们希望把它导入数据库。
整个流程大概就是:
用户上传 users.xlsx
↓
Spring Boot Controller
↓
FastExcel
↓
逐行解析
↓
UserExcelDTO
↓
参数校验
↓
批量 INSERT
↓
MySQL
导出则完全相反:
用户点击“导出”
↓
Spring Boot
↓
查询 MySQL
↓
List<UserExcelDTO>
↓
FastExcel
↓
生成 users.xlsx
↓
浏览器下载
所以 FastExcel 并不像 MySQL、Redis、RabbitMQ、Elasticsearch 那样属于基础设施。
它更像一个:
业务开发工具库。
二、FastExcel、EasyExcel、Apache POI 到底是什么关系?
很多刚接触 FastExcel 的同学很容易懵,因为网上还会同时看到:
Apache POI
EasyExcel
FastExcel
首先看 Apache POI。
Apache POI 是 Java 生态里非常经典的 Office 文档处理框架,可以操作:
Excel
Word
PowerPoint
功能非常强大。
但如果业务需求只是:
Excel 导入
Excel 导出
直接使用 POI 往往会比较繁琐。
例如:
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("用户列表");
Row row = sheet.createRow(0);
Cell cell = row.createCell(0);
cell.setCellValue("姓名");
你需要自己:
创建 Workbook
创建 Sheet
创建 Row
创建 Cell
设置样式
处理数据类型
处理资源关闭
对于后台业务开发来说,代码量比较大。
EasyExcel
后来阿里开源了 EasyExcel。
EasyExcel 的核心思想之一,就是:
降低 Java 操作 Excel 的复杂度,同时更加关注大数据量 Excel 读写场景。
例如导出数据时,可以直接把:
Java 对象
映射成:
Excel 列
于是代码会简单很多。
三、FastExcel 和 EasyExcel 的关系
FastExcel 和 EasyExcel 的关系非常特殊。
FastExcel 是原 EasyExcel 作者继续维护和演进的项目,设计和 API 与 EasyExcel 有很深的继承关系,因此从 EasyExcel 迁移到早期 FastExcel 的成本比较低。
早期 FastExcel 的典型 Maven 坐标为:
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>...</version>
</dependency>
代码包也由原来的:
com.alibaba.excel.*
迁移到了:
cn.idev.excel.*
并且官方更推荐使用:
FastExcel.read(...)
FastExcel.write(...)
作为统一入口。
需要特别注意的是,截至 2026 年,FastExcel 项目后续已经进入 Apache Incubator,并进一步更名为 Apache Fesod。
因此如果你现在新建项目,建议同时确认当前 Apache Fesod 的最新依赖和迁移文档;如果你的公司项目目前使用的是 FastExcel 1.x,那么本文介绍的 FastExcel 使用思路仍然很有参考价值。
四、为什么 FastExcel 特别适合后台管理系统?
后台系统经常有这样的功能:
用户管理
├── 新增
├── 修改
├── 删除
├── 查询
├── Excel 导入
└── Excel 导出
例如 HR 系统批量导入员工:
employee.xlsx
↓
读取 5 万行数据
↓
校验员工编号
↓
校验手机号
↓
校验部门
↓
批量写入 MySQL
如果 Excel 很大,一个非常危险的做法是:
整个 Excel
↓
一次全部读入 JVM
↓
List<Employee> 50 万条
↓
全部放在内存
数据量越来越大后:
JVM 内存占用不断增加
↓
频繁 GC
↓
Full GC
↓
甚至 OOM
FastExcel 这类 Excel 框架的重要设计思路之一就是:
不要一定等整个 Excel 读取完成之后再处理,而是尽可能边读取边处理。
也就是:
Excel
第 1 行 ──→ Java
第 2 行 ──→ Java
第 3 行 ──→ Java
第 4 行 ──→ Java
...
配合批量写数据库:
读取 1000 条
↓
批量 INSERT
↓
清空 List
继续读取 1000 条
↓
批量 INSERT
↓
清空 List
这样 JVM 中没有必要长期保存几十万条 Java 对象。
五、Spring Boot 引入 FastExcel
对于仍在使用 FastExcel 1.x 的项目,可以在 Maven 中添加对应依赖。
例如:
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>${fastexcel.version}</version>
</dependency>
版本建议根据自己项目 JDK、Spring Boot 版本以及官方当前稳定版本确定。
然后就可以通过:
FastExcel.read(...)
和:
FastExcel.write(...)
完成读取和写入。
六、先准备一个 Excel 实体类
假设我们需要导出用户信息。
定义:
@Data
public class UserExcelDTO {
@ExcelProperty("用户ID")
private Long id;
@ExcelProperty("用户名")
private String username;
@ExcelProperty("手机号")
private String phone;
@ExcelProperty("年龄")
private Integer age;
}
这里非常关键的注解是:
@ExcelProperty
例如:
@ExcelProperty("用户名")
private String username;
可以理解成:
Java:
username
↓ 映射
Excel:
用户名
最终生成的 Excel 就可能是:
| 用户ID | 用户名 | 手机号 | 年龄 |
|---|---|---|---|
| 1 | 张三 | 13800000001 | 20 |
| 2 | 李四 | 13800000002 | 21 |
七、FastExcel 导出 Excel
先来看最常见的场景。
后台有一个:
导出用户
按钮。
浏览器请求:
GET /user/export
后端查询用户,然后生成 Excel。
Controller 可以这样写:
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {
response.setContentType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
);
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder
.encode("用户列表", StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.setHeader(
"Content-Disposition",
"attachment;filename*=utf-8''" + fileName + ".xlsx"
);
List<UserExcelDTO> data = List.of(
new UserExcelDTO(1L, "张三", "13800000001", 20),
new UserExcelDTO(2L, "李四", "13800000002", 21)
);
FastExcel.write(
response.getOutputStream(),
UserExcelDTO.class
)
.sheet("用户列表")
.doWrite(data);
}
整个流程其实非常简单:
浏览器
↓
GET /user/export
↓
Controller
↓
准备 List<UserExcelDTO>
↓
FastExcel.write()
↓
response OutputStream
↓
浏览器下载
核心代码就是:
FastExcel.write(
response.getOutputStream(),
UserExcelDTO.class
)
.sheet("用户列表")
.doWrite(data);
八、FastExcel 到底帮我们做了什么?
如果没有 Excel 框架,我们可能需要手动:
创建 Workbook
创建 Sheet
创建 Header Row
创建每一个 Cell
遍历 Java 对象
处理 String
处理 Integer
处理 Date
写 OutputStream
关闭 Workbook
FastExcel 则把大量重复工作封装了起来。
我们只需要表达:
我要导出什么 Java 类
数据是什么
Sheet 叫什么
然后:
FastExcel.write(...)
即可。
这就是工具框架真正的价值:
不是 Java 做不了,而是帮我们减少重复代码。
九、FastExcel 导入 Excel
下面继续看 Excel 导入。
用户上传:
users.xlsx
Spring Boot 接收到:
MultipartFile file
然后 FastExcel 开始读取。
Controller:
@PostMapping("/import")
public String importExcel(
@RequestParam("file") MultipartFile file
) throws IOException {
FastExcel.read(
file.getInputStream(),
UserExcelDTO.class,
new UserExcelListener()
)
.sheet()
.doRead();
return "导入成功";
}
这里最重要的是:
UserExcelListener
因为 Excel 解析出来以后:
数据最终怎么处理?
需要开发者自己决定。
十、ReadListener 是什么?
可以定义一个监听器:
public class UserExcelListener
implements ReadListener<UserExcelDTO> {
@Override
public void invoke(
UserExcelDTO data,
AnalysisContext context
) {
System.out.println(
"读取到一行:" + data
);
}
@Override
public void doAfterAllAnalysed(
AnalysisContext context
) {
System.out.println(
"Excel 读取完成"
);
}
}
假设 Excel 有:
张三
李四
王五
那么可以把:
invoke()
理解为:
读取张三
↓
invoke(张三)
读取李四
↓
invoke(李四)
读取王五
↓
invoke(王五)
也就是说:
invoke 方法会在解析数据时持续接收到当前读取的数据。
这对于处理大 Excel 非常重要。
十一、为什么不建议把几十万条数据全部放 List?
很多初学者第一次写 Excel 导入会这样:
List<UserExcelDTO> list = new ArrayList<>();
@Override
public void invoke(
UserExcelDTO data,
AnalysisContext context
) {
list.add(data);
}
最后:
@Override
public void doAfterAllAnalysed(
AnalysisContext context
) {
userService.saveBatch(list);
}
小 Excel 没什么问题。
例如:
100 条
1000 条
但是如果:
50 万条
就意味着:
50 万个 Java 对象
↓
全部保存进 List
↓
一直占据 JVM Heap
Excel 越大:
内存占用越高
最后可能:
OutOfMemoryError
所以大文件导入更合理的设计是:
分批读取 + 分批写数据库。
十二、正确姿势:读取 1000 条就写一次数据库
例如:
public class UserExcelListener
implements ReadListener<UserExcelDTO> {
private static final int BATCH_COUNT = 1000;
private final List<UserExcelDTO> cache =
new ArrayList<>(BATCH_COUNT);
private final UserService userService;
public UserExcelListener(
UserService userService
) {
this.userService = userService;
}
@Override
public void invoke(
UserExcelDTO data,
AnalysisContext context
) {
cache.add(data);
if (cache.size() >= BATCH_COUNT) {
saveData();
cache.clear();
}
}
@Override
public void doAfterAllAnalysed(
AnalysisContext context
) {
if (!cache.isEmpty()) {
saveData();
cache.clear();
}
}
private void saveData() {
userService.saveBatch(cache);
}
}
整个过程就变成:
Excel 50 万条
↓
读取 1000 条
↓
List 1000
↓
批量 INSERT
↓
clear()
读取下一批 1000 条
↓
批量 INSERT
↓
clear()
......
于是 JVM 中同时保存的数据可能只有:
1000 条左右
而不是:
50 万条
这个思想特别重要。
它不仅适用于 Excel。
以后你看到:
Kafka Consumer
RabbitMQ Consumer
文件解析
数据库迁移
日志处理
ETL
很多大数据处理方案本质上都有类似思想:
不要无脑把所有数据同时塞进内存,要分批、流式处理。
十三、大数据量导出也不能直接 select *
Excel 导出同样存在这个问题。
假设数据库有:
100 万订单
非常危险的写法是:
List<Order> orders =
orderMapper.selectAll();
FastExcel.write(...)
.sheet()
.doWrite(orders);
看起来代码非常简单。
但是:
MySQL
↓
SELECT 100 万条
↓
MyBatis
↓
创建 100 万个 Order 对象
↓
全部进入 JVM
↓
FastExcel
Excel 还没开始真正写多少数据,Java 堆可能已经扛不住了。
十四、正确的大数据导出思路
应该考虑:
MySQL 分页查询
+
Excel 分批写入
例如:
第 1 批:
SELECT ...
LIMIT 10000
↓
写入 Excel
第 2 批:
SELECT ...
LIMIT 10000
↓
继续写 Excel
最终:
MySQL
↓
每次查询一部分
↓
Java
↓
FastExcel
↓
持续写 OutputStream
也就是说:
大文件导入和大文件导出的核心思想其实完全一样。
导入:
Excel
↓
分批读取
↓
MySQL
导出:
MySQL
↓
分批读取
↓
Excel
中间都应该尽量避免:
一次把所有数据放进 JVM。
十五、为什么批量 INSERT 比一条一条 INSERT 好?
假设 Excel 有 10 万条。
一种写法:
for (User user : users) {
userMapper.insert(user);
}
意味着:
10 万次数据库交互
例如:
Java → MySQL
Java → MySQL
Java → MySQL
Java → MySQL
...
网络交互、SQL 执行、事务处理都会产生额外开销。
所以更常见的是:
读取 500 / 1000 条
↓
批量 INSERT
例如:
INSERT INTO user
(username, phone, age)
VALUES
('张三', '138...', 20),
('李四', '139...', 21),
('王五', '137...', 22);
所以 FastExcel 大文件导入经常和:
批量写 MySQL
一起出现。
十六、Excel 导入千万不要忘记数据校验
还有一个非常现实的问题。
Excel 是用户提供的。
用户完全可能上传:
| 姓名 | 年龄 | 手机号 |
|---|---|---|
| 张三 | -100 | abc |
| 李四 | 10000 | 空 |
| 空 | 20 | 138xxx |
所以:
FastExcel 读取成功
并不等于:
业务数据合法
建议处理流程:
Excel
↓
FastExcel
↓
转换 DTO
↓
字段校验
↓
业务校验
↓
批量写 MySQL
例如:
private void validate(UserExcelDTO data) {
if (data.getUsername() == null
|| data.getUsername().isBlank()) {
throw new IllegalArgumentException(
"用户名不能为空"
);
}
if (data.getAge() != null
&& data.getAge() < 0) {
throw new IllegalArgumentException(
"年龄不能小于0"
);
}
}
实际企业项目里通常还要返回:
第 18 行:手机号格式错误
第 29 行:用户编号不存在
第 56 行:部门不存在
甚至会生成一个:
导入失败结果.xlsx
让用户下载修改。
这才是完整的 Excel 导入系统。
十七、Excel 实体不要直接使用数据库 Entity
还有一个很常见的设计问题。
例如数据库实体:
UserDO
不要为了省事直接:
FastExcel.read(
...,
UserDO.class,
...
);
更推荐单独定义:
UserExcelDTO
例如:
@Data
public class UserExcelDTO {
@ExcelProperty("用户名")
private String username;
@ExcelProperty("手机号")
private String phone;
@ExcelProperty("年龄")
private Integer age;
}
然后:
Excel
↓
UserExcelDTO
↓
参数校验
↓
业务转换
↓
UserDO
↓
MySQL
为什么?
因为:
Excel 字段
并不一定等于:
数据库字段
例如 Excel:
性别:男
数据库可能保存:
gender = 1
Excel:
状态:启用
数据库:
status = 1
所以需要转换层。
这也是 DTO 存在的意义之一。
十八、Excel 最大能有多少行?
.xlsx 单个工作表最大行数是:
1,048,576
也就是大约:
104 万行
因此如果业务真的需要导出:
300 万条数据
就不能全部塞到一个 Sheet。
可能需要:
Sheet1:100 万
Sheet2:100 万
Sheet3:100 万
或者更加常见:
拆分多个 Excel 文件
甚至进一步考虑:
CSV
因为当数据规模已经达到几百万、上千万时:
Excel 本身可能就不再是最合适的数据交换格式。
十九、100 万数据导出为什么通常要异步?
假设后台导出:
订单 100 万条
如果用户点击按钮以后:
HTTP 请求
↓
查询 100 万
↓
生成 Excel
↓
等待几十秒
↓
下载
这个 HTTP 请求可能持续非常久。
还可能出现:
Nginx 超时
Gateway 超时
浏览器断开
Tomcat 线程长时间占用
于是大型系统通常会设计成:
用户点击导出
↓
创建 export_task
↓
返回:
“导出任务已创建”
↓
后台异步生成 Excel
↓
上传 OSS / MinIO
↓
更新任务状态
↓
用户下载
甚至可以配合 MQ:
Controller
↓
创建导出任务
↓
RabbitMQ
↓
Export Consumer
↓
分页查询 MySQL
↓
FastExcel
↓
OSS
这时你前面学到的 RabbitMQ 就串起来了。
RabbitMQ 不负责 Excel。
它负责:
异步任务
FastExcel 负责:
生成 Excel
MySQL 负责:
保存业务数据
OSS / MinIO 负责:
保存生成的 Excel 文件
二十、FastExcel 的完整业务架构
一个比较完整的企业 Excel 导入流程:
Browser
│
上传 Excel
│
▼
Spring Boot
│
▼
FastExcel
│
流式读取数据
│
▼
Excel DTO
│
参数 / 业务校验
│
▼
Batch List
1000条
│
▼
MyBatis Plus
│
Batch Insert
│
▼
MySQL
导出流程:
Browser
│
点击导出
│
▼
Spring Boot
│
▼
MySQL
│
分页查询
│
▼
FastExcel
│
分批写入
│
▼
Excel / OSS
│
▼
下载
二十一、FastExcel 和 Apache POI 应该怎么选?
并不是用了 FastExcel,以后 POI 就没用了。
可以这样理解:
Apache POI
更加底层
功能更加全面
Excel 细节控制能力更强
而:
FastExcel
更偏业务开发
API 更简单
适合常规 Excel 导入导出
更加关注大数据量处理体验
如果需求只是:
导入员工
导出订单
导出用户
导出商品
批量数据处理
FastExcel 往往非常方便。
如果你需要做特别复杂的:
Excel 模板编辑
复杂公式
图表
复杂单元格样式
非常细粒度的 Office 控制
则可能仍然需要深入使用 POI,或者结合具体需求选择其他方案。
二十二、FastExcel 最容易踩的几个坑
最后总结几个实际项目里非常常见的问题。
1. 一次把所有数据读进 List
错误思想:
Excel 100 万条
↓
List 100 万
↓
再处理
正确思想:
读取 1000
↓
处理
↓
clear
↓
继续读取
2. 导出时一次查询全部数据库
错误:
SELECT * FROM orders;
然后:
List<Order> list;
正确方向:
分页查询
+
分批写 Excel
3. 只考虑 Excel 性能,不考虑数据库性能
Excel 解析很快,但如果:
每读取一行
↓
INSERT 一次
数据库一样会成为瓶颈。
所以需要:
Excel 分批读取
+
数据库批量写入
4. Excel DTO 直接当数据库 Entity
不推荐:
Excel → UserDO → MySQL
推荐:
Excel
↓
UserExcelDTO
↓
校验 / 转换
↓
UserDO
↓
MySQL
5. 超大导出仍然使用同步 HTTP
数据量很大时:
点击按钮
↓
浏览器一直等
用户体验和系统稳定性都不好。
可以考虑:
任务表
+
MQ
+
后台异步生成
+
OSS / MinIO
+
下载链接
二十三、FastExcel 最核心的思想
学完 FastExcel,其实不应该只记住:
FastExcel.read()
FastExcel.write()
真正应该掌握的是背后的数据处理思想。
Excel 导入:
Excel
│
│ 流式读取
▼
FastExcel
│
▼
Batch List
1000 records
│
▼
MySQL
Excel 导出:
MySQL
│
│ 分页查询
▼
Java List
10000 records
│
▼
FastExcel
│
▼
Excel
核心只有一句话:
面对大数据量时,不要一次性把所有数据加载进 JVM 内存,而应该通过流式读取、分页查询和分批处理控制内存占用。
二十四、FastExcel 和之前学的技术怎么串起来?
如果现在把 Java 后端常见组件放在一起:
MySQL
↓
业务数据持久化
Redis
↓
缓存
RabbitMQ
↓
异步任务 / 消息解耦
Elasticsearch
↓
全文搜索
Canal / CDC
↓
监听数据库数据变化
FastExcel
↓
Excel 导入导出
例如一个完整后台系统可能同时存在:
Spring Boot
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
MySQL Redis RabbitMQ
│
▼
Export Worker
│
▼
FastExcel
│
▼
OSS / MinIO
所以 FastExcel 本身并不是一个复杂的分布式组件。
它解决的是一个非常具体的问题:
Java 程序如何方便、高效地和 Excel 文件进行数据交换。
总结
最后用几句话把 FastExcel 彻底记住。
FastExcel 是 Java 生态中的 Excel 读写工具,特别适合后台管理系统里的 Excel 导入和导出。
对于小数据量:
MySQL
↓
List
↓
FastExcel
↓
Excel
完全够用。
对于大数据量,真正重要的是:
导入:
Excel
↓
流式读取
↓
分批处理
↓
批量 INSERT
↓
MySQL
以及:
导出:
MySQL
↓
分页查询
↓
分批写入
↓
FastExcel
↓
Excel
如果数据规模继续增加,则进一步考虑:
异步导出
+
RabbitMQ
+
任务表
+
OSS / MinIO
因此真正值得沉淀的并不是某一个 API,而是这样一个工程经验:
任何“大数据量文件处理”问题,都要警惕一次性加载全部数据。FastExcel 解决 Excel 的读写问题,而分页、批处理、异步化和内存控制,才决定了整个 Excel 导入导出系统能不能真正扛住生产环境。