5209 字
约 17 分钟
5
FastExcel 入门到实战:Java 后端如何优雅实现 Excel 导入导出

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 导入导出系统能不能真正扛住生产环境。

FastExcel 入门到实战:Java 后端如何优雅实现 Excel 导入导出
http://clxhxhhr.top/posts/430/
作者
clxstart
发布于
2026-09-03
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。