下面我将从核心思想、常用技术选型、代码实现、性能优化等多个方面,为你提供一个全面且深入的指南。

核心思想:避免 OOM 的关键
处理大数据导出,最核心、最关键的思想是:不要一次性将所有数据加载到内存中。
传统的做法(如使用 Apache POI 的 XSSFWorkbook 或 SXSSFWorkbook 之前的旧版本)会创建一个包含所有数据的 Excel 对象在内存中,当数据量达到几十万行时,内存占用会飙升,直接导致 JVM 崩溃。
正确的做法是采用流式处理或分批处理,将数据分批写入到 Excel 文件中,而不是全部驻留在内存。
技术选型
Java 操作 Excel 主要有以下几类库,各有优劣,适用于不同场景。

| 库 | 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Apache POI | 功能强大 | 功能最全面,支持 .xls 和 .xlsx 格式,API 丰富。 |
- .xlsx 格式:XSSFWorkbook 在内存中操作,大数据导出时极易 OOM。- .xls 格式:HSSFWorkbook 有行数限制(65535行)。 |
小数据量导出、复杂 Excel 模板操作、.xls 格式兼容。 |
| EasyExcel | 推荐 | - 基于 POI,但做了深度优化。 - 内存占用极低,解决了 POI 大数据导出的 OOM 问题。 - API 简洁,上手快,性能好。 |
相对 POI 功能稍少,但对于 99% 的导出场景已经足够。 | 大数据量导出、数据导入、日常 Excel 处理。 |
| SXSSFWorkbook (POI) | 流式处理 | - 官方提供的大数据解决方案。 - 通过 windowSize 控制内存中的行数,超出部分写入临时文件。 |
- API 相对复杂,不如 EasyExcel 简洁。 - 依赖临时文件,需要清理。 |
对 POI 有强依赖,且不想引入第三方新库的场景。 |
| JXL | 旧库 | 轻量级,API 简单。 | 已停止更新,不支持 .xlsx 格式,功能有限。 |
遗留系统维护,仅处理 .xls 格式。 |
对于大数据导出,强烈推荐使用 EasyExcel,它在 POI 的基础上做了完美的封装,既解决了内存问题,又提供了非常友好的 API,如果项目已经深度使用 POI 且不想引入新依赖,则可以使用 SXSSFWorkbook。
代码实现(以 EasyExcel 为例)
EasyExcel 的核心是 ExcelWriter 和 WriteSheet,它通过回调函数逐行写入数据,完美避免了内存问题。
添加 Maven 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version> <!-- 请使用最新版本 -->
</dependency>
准备数据模型
创建一个与 Excel 表头对应的 Java 实体类。@ExcelProperty 注解用于指定列名。
import com.alibaba.excel.annotation.ExcelProperty;
// 假设我们要导出用户信息
public class UserData {
// value 对应 Excel 表头
@ExcelProperty("用户ID")
private Long id;
@ExcelProperty("用户名")
private String username;
@ExcelProperty("邮箱")
private String email;
@ExcelProperty("创建时间")
private Date createTime;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
}
模拟数据查询
这是最关键的一步。不要一次性查询出所有数据,而是分批查询。

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class UserDataService {
// 模拟从数据库分批查询数据
// page: 页码, pageSize: 每页大小
public List<UserData> queryDataByPage(int page, int pageSize) {
// 这里应该是你的 DAO 层调用, userMapper.selectPage(page, pageSize);
// 为了演示,我们模拟生成一些数据
List<UserData> list = new ArrayList<>(pageSize);
for (int i = 0; i < pageSize; i++) {
long index = (page - 1L) * pageSize + i;
UserData user = new UserData();
user.setId(index + 1);
user.setUsername("用户_" + index);
user.setEmail("user_" + index + "@example.com");
user.setCreateTime(new Date());
list.add(user);
}
return list;
}
}
核心导出逻辑
使用 EasyExcel.write() 进行流式写入。
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.WriteTable;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
public class BigDataExportDemo {
public static void main(String[] args) {
// 在 Web 环境中,通常是通过 HttpServletResponse 来输出
// 这里为了演示,我们直接写入到文件
String fileName = "big_data_export.xlsx";
// 1. 创建 ExcelWriter 对象
// fileName 是最终生成的文件名
ExcelWriter excelWriter = EasyExcel.write(fileName, UserData.class).build();
// 2. 创建 WriteSheet 对象,可以理解为 Excel 的一个 Sheet
WriteSheet writeSheet = EasyExcel.writerSheet("用户数据").build();
// 3. 模拟总数据量和分批大小
long totalDataCount = 1_000_000L; // 100万条数据
int pageSize = 5000; // 每批查询5000条
// 4. 循环分批查询并写入
UserDataService userService = new UserDataService();
for (int page = 1; page <= (totalDataCount / pageSize + 1); page++) {
System.out.println("正在处理第 " + page + " 页数据...");
// 4.1 分批查询数据
List<UserData> data = userService.queryDataByPage(page, pageSize);
// 4.2 将当前批次数据写入到 Sheet 中
// EasyExcel 会自动处理,不会将所有数据都加载到内存
excelWriter.write(data, writeSheet);
// 如果查询出的数据为空,说明已经处理完毕,可以提前退出
if (data.isEmpty()) {
break;
}
}
// 5. 关闭流,非常重要!
excelWriter.finish();
System.out.println("导出完成!");
}
// Web 环境下的导出方法示例
public void exportInWeb(HttpServletResponse response) throws IOException {
String fileName = "用户数据_" + System.currentTimeMillis() + ".xlsx";
// 设置响应头,告诉浏览器这是一个文件下载
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// URLEncoder.encode 处理中文文件名,防止乱码
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + encodedFileName + ";filename*=utf-8''" + encodedFileName);
// 1. 创建 ExcelWriter,直接输出到 response 的 OutputStream
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), UserData.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("用户数据").build();
// ... (分批查询和写入的逻辑与上面 main 方法相同) ...
long totalDataCount = 1_000_000L;
int pageSize = 5000;
UserDataService userService = new UserDataService();
for (int page = 1; page <= (totalDataCount / pageSize + 1); page++) {
List<UserData> data = userService.queryDataByPage(page, pageSize);
excelWriter.write(data, writeSheet);
if (data.isEmpty()) {
break;
}
}
// 2. 关闭流
excelWriter.finish();
}
}
性能优化技巧
除了流式写入,你还可以从以下几个方面进行优化:
-
SQL 优化
- 只查询需要的列:避免使用
SELECT *,明确指定列名,减少数据传输量。 - 添加索引:确保查询字段(如分页的
id或create_time)有索引,加快数据库查询速度。 - 使用游标:如果你的数据库(如 MySQL)支持,可以使用 JDBC 的
ResultSet游标,让数据库驱动逐行返回数据,而不是一次性加载到内存,这能进一步降低 JVM 内存压力。
- 只查询需要的列:避免使用
-
JVM 参数调优
- 增加堆内存:如果导出过程非常耗时且数据量极大,可以适当增加 JVM 的最大堆内存(
-Xmx)。 - 使用 G1 垃圾回收器:对于大内存应用,G1 回收器通常比 CMS 有更好的表现,可以减少 Full GC 的停顿时间。
-XX:+UseG1GC。
- 增加堆内存:如果导出过程非常耗时且数据量极大,可以适当增加 JVM 的最大堆内存(
-
服务器配置
- 增加线程池:如果你的数据查询是 I/O 密集型(比如查询远程数据库),可以考虑使用多线程并行查询,然后将结果写入同一个 Excel 文件(注意线程安全和写入顺序)。
- 使用临时文件:
SXSSFWorkbook和 EasyExcel 底层都会使用临时文件,确保应用的临时目录(java.io.tmpdir)有足够的磁盘空间,并在导出完成后清理这些临时文件。
-
用户体验优化 (Web 场景)
- 异步导出:对于耗时很长的导出任务,绝对不能在前端直接同步调用,应该采用异步任务模式。
- 前端点击“导出”按钮后,先向服务器发送一个请求,服务器生成一个任务 ID 并立即返回。
- 前端轮询任务状态,或者通过 WebSocket 接收任务进度更新。
- 任务完成后,前端再通过一个链接去下载生成的文件。
- 进度反馈:在异步任务中,可以记录已导出的行数,并通过 WebSocket 或轮询接口将进度反馈给前端,让用户了解导出进度。
- 异步导出:对于耗时很长的导出任务,绝对不能在前端直接同步调用,应该采用异步任务模式。
对比:EasyExcel vs. POI SXSSFWorkbook
| 特性 | EasyExcel | POI SXSSFWorkbook |
|---|---|---|
| 易用性 | 极高,API 简洁,像写普通 Java 代码。 | 一般,需要理解 SXSSFSheet、SXSSFRow 等概念。 |
| 内存控制 | 非常优秀,通过监听器逐行写入,内存占用恒定。 | 优秀,通过 windowSize 控制内存中可见的行数。 |
| 底层原理 | 基于 POI SXSSF,但做了更高层封装。 | 原生的 POI 流式 API。 |
| 功能扩展 | 方便,可以轻松集成自定义的转换器、监听器。 | 需要直接操作 POI 底层,较为复杂。 |
| 推荐度 | 强烈推荐,是当前大数据导出的首选方案。 | 可选,适用于不想引入新依赖的 POI 老项目。 |
对于任何新的 Java 项目,需要进行大数据 Excel 导出时,请毫不犹豫地选择 EasyExcel,它为你解决了所有关于内存和性能的难题,让你可以专注于业务逻辑本身。
