下面我将从 问题分析、最佳实践方案、代码示例 和 其他方案对比 四个方面,为你详细讲解如何高效地导出大量数据。

问题分析:为什么会 OOM?
-
Apache POI 的 SXSSF vs XSSF:
- XSSF: 用于处理
.xlsx格式,它会将整个 Excel 文档(包括所有行和单元格)都保存在内存中,如果你的数据有几十万行,内存消耗会非常巨大,直接导致OutOfMemoryError。 - SXSSF (Streaming Usermodel API): 这是 解决大数据量导出的核心,它采用了 流式处理 的思想,它只保留一部分行(保存在内存中)和临时文件(保存在磁盘上)在内存中,当行数超过预设的
window size时,最早写入的行就会被“刷”到临时文件中,从而释放内存,所有内存中的数据和临时文件会被合并成一个完整的.xlsx文件。
- XSSF: 用于处理
-
.xlsvs.xlsx:- HSSF: 用于处理旧的
.xls格式,它和 XSSF 类似,也是将整个文档加载到内存,不适合大数据量。 - 导出大量数据,必须使用
.xlsx格式,并配合 SXSSF API。
- HSSF: 用于处理旧的
最佳实践方案:Apache POI SXSSF
这是目前最主流、最推荐的方案,下面是详细步骤和代码示例。
添加 Maven 依赖
确保你的 pom.xml 文件中包含 POI 的相关依赖。

<dependencies>
<!-- Apache POI Core -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version> <!-- 建议使用较新版本 -->
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<!-- POI 对 Excel 2003 格式的支持 (可选) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.3</version>
</dependency>
</dependencies>
核心代码实现
关键点:
- 使用
SXSSFWorkbook而不是XSSFWorkbook。 - 可以设置
window size(SXSSFWorkbook(int rowAccessWindowSize)),这个值决定了内存中最多保留多少行,100 表示内存中只保留最新的 100 行,其余的写入磁盘,通常设置为 100 是一个不错的选择。 - 数据分批查询:不要一次性从数据库查出所有数据,而应该使用分页查询(如 MySQL 的
LIMIT offset, size),每次只查询一页的数据,然后写入 Excel,再查询下一页,这是最最关键的一步,可以极大地降低数据库和应用的内存压力。 - 使用 try-with-resources:确保
Workbook和OutputStream被正确关闭,避免资源泄漏。
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFRow;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class LargeDataExportExample {
// 模拟从数据库分页查询数据
public static List<User> queryUsersFromDB(int pageNum, int pageSize) {
// 这里应该是你的数据库查询逻辑,例如使用 MyBatis 或 JPA
// 为了演示,我们创建一些假数据
List<User> users = new ArrayList<>();
for (int i = 0; i < pageSize; i++) {
int userId = (pageNum - 1) * pageSize + i + 1;
users.add(new User(userId, "User-" + userId, "user" + userId + "@example.com", new BigDecimal("10000.00")));
}
return users;
}
public static void main(String[] args) {
// 1. 准备数据 - 模拟总共有 1,000,000 条数据
int totalRows = 1_000_000;
int pageSize = 1000; // 每页查询 1000 条
int totalPages = (int) Math.ceil((double) totalRows / pageSize);
// 2. 创建 SXSSFWorkbook 对象
// 参数 100 表示内存中最多保留 100 行,超过的会写入磁盘
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
// 3. 创建 Sheet
Sheet sheet = workbook.createSheet("Users");
// 4. 创建表头(仅创建一次)
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("ID");
headerRow.createCell(1).setCellValue("Name");
headerRow.createCell(2).setCellValue("Email");
headerRow.createCell(3).setCellValue("Salary");
// 5. 分批查询和写入数据
for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
System.out.println("正在处理第 " + pageNum + " / " + totalPages + " 页数据...");
List<User> users = queryUsersFromDB(pageNum, pageSize);
// 遍历当前页数据并写入 Excel
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
// 注意:行号是全局的,从 0 开始,表头是第 0 行,所以数据从第 1 行开始
int rowNum = (pageNum - 1) * pageSize + i + 1;
Row row = sheet.createRow(rowNum);
row.createCell(0).setCellValue(user.getId());
row.createCell(1).setCellValue(user.getName());
row.createCell(2).setCellValue(user.getEmail());
row.createCell(3).setCellValue(user.getSalary().doubleValue());
}
}
// 6. 写入文件
try (FileOutputStream out = new FileOutputStream("D:/temp/large_data_export.xlsx")) {
workbook.write(out);
System.out.println("Excel 文件导出成功!");
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 用户实体类
static class User {
private int id;
private String name;
private String email;
private BigDecimal salary;
// constructor, getters, and setters
public User(int id, String name, String email, BigDecimal salary) {
this.id = id;
this.name = name;
this.email = email;
this.salary = salary;
}
public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
public BigDecimal getSalary() { return salary; }
}
}
代码优化点
setCellValue的数据类型:尽量使用setCellValue(String)或setCellValue(double)等基本类型,而不是直接传入复杂对象,减少不必要的转换开销。- 样式处理:SXSSF 对样式的支持有限,如果你需要对大量单元格应用复杂的样式,这可能会影响性能,尽量保持样式简单,或者在创建
CellStyle后重复使用它。 - 列宽自动调整:
autoSizeColumn()方法在 SXSSF 中是不支持的,因为它需要遍历所有行来计算宽度,这与流式处理的初衷相悖,如果必须设置列宽,最好在创建表头后,根据已知内容或预估宽度手动设置。
其他备选方案
除了 Apache POI,还有其他一些工具也可以处理大数据量导出,各有优劣。
EasyExcel (阿里巴巴)
这是目前非常流行的一个选择,尤其在国内,底层也是基于 POI 的 SXSSF,但做了大量优化,API 更加简洁易用。
优点:

- API 简洁:注解驱动,代码量少,学习成本低。
- 性能好:对内存的控制非常好,同样是流式读写。
- 功能丰富:支持 Excel 模板、数据转换、自定义格式等。
缺点:
- 生态和社区相比 POI 较小,遇到某些复杂问题可能资料较少。
Maven 依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
代码示例 (非常简洁):
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
public class EasyExcelExample {
public static void main(String[] args) {
// 准备数据
List<User> dataList = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
dataList.add(new User(i + 1, "User-" + (i + 1), "user" + (i + 1) + "@example.com", new BigDecimal("10000.00")));
}
// 写入文件
String fileName = "D:/temp/large_data_export_easyexcel.xlsx";
EasyExcel.write(fileName, User.class).sheet("Users").doWrite(dataList);
System.out.println("EasyExcel 导出成功!");
}
// 实体类,使用 @ExcelProperty 注解映射列
public static class User {
@ExcelProperty("ID")
private Integer id;
@ExcelProperty("Name")
private String name;
@ExcelProperty("Email")
private String email;
@ExcelProperty("Salary")
private BigDecimal salary;
// constructor, getters, setters
public User(Integer id, String name, String email, BigDecimal salary) {
this.id = id;
this.name = name;
this.email = email;
this.salary = salary;
}
// ...省略getter/setter
}
}
注意:EasyExcel 虽然代码简单,但在处理极致大数据量(如数千万行)时,其内部缓冲机制可能导致内存占用略高于手动控制的 POI SXSSF,但对于绝大多数场景,EasyExcel 已经足够优秀且开发效率高。
JXL (Java Excel API)
一个比较古老的轻量级库,只支持 .xls 格式。
优点:
- 非常轻量,API 简单。
- 内存占用相对较低。
缺点:
- 不支持
.xlsx格式,这是致命缺陷。 - 停止更新维护多年,不推荐用于新项目。
方案对比与选择建议
| 特性 | Apache POI SXSSF | EasyExcel (Alibaba) | JXL |
|---|---|---|---|
| 支持格式 | .xlsx, .xls |
.xlsx, .xls |
仅 .xls |
| 内存管理 | 优秀,流式处理,可配置窗口大小 | 优秀,流式处理,自动优化 | 一般,仅支持旧格式 |
| API 易用性 | 较繁琐,需要手动创建 Row/Cell | 非常简洁,注解驱动 | 简单 |
| 性能 | 非常好,是行业标准 | 非常好,略优于 POI | 一般 |
| 功能丰富度 | 极其强大,支持所有 Excel 特性 | 丰富,支持模板、数据校验等 | 较少 |
| 社区与生态 | 非常成熟,文档齐全 | 非常活跃,国内社区好 | 已过时,社区小 |
| 推荐场景 | 对 Excel 兼容性、功能要求极高的项目 | 绝大多数新项目,追求开发效率和性能 | 维护旧项目或只导出 .xls |
如何选择?
- 新项目,追求快速开发:首选 EasyExcel,它的注解式开发能为你节省大量代码,性能也足够好。
- 新项目,需要极致性能或复杂操作:使用 Apache POI SXSSF,你可以对内存和性能进行更精细的控制。
- 维护旧项目,只支持
.xls:可以考虑 JXL,但更建议升级到.xlsx并使用 POI 或 EasyExcel。 - 需要导出超大数据(亿级):如果单文件太大,可以考虑分多个 sheet,或者考虑 CSV 格式(纯文本,体积小,解析快,但无格式),如果必须单文件,POI SXSSF 依然是最后的保障。
导出大量数据到 Excel 的核心思想是 “流式处理” 和 “分批查询”。
- 工具选型:抛弃旧的 HSSF 和 JXL,使用 SXSSF (POI) 或 EasyExcel。
- 关键代码:使用
SXSSFWorkbook,并设置合理的rowAccessWindowSize。 - 数据源:务必从数据库分页查询数据,一次性加载所有数据是导致 OOM 的主要原因。
- 资源管理:使用
try-with-resources确保Workbook和OutputStream被关闭。
遵循以上原则,你就可以稳定、高效地完成 Java 大数据量 Excel 导出任务。
