避免一次性将整个 Excel 文件加载到内存中,而是采用流式(SAX)或逐行读取的方式。

下面我将为你详细介绍几种主流的方案,从推荐到备选,并附上核心代码示例。
Apache POI (SAX Event Model) - 最推荐的方案
这是 Apache POI 官方推荐处理大数据的方式,它基于 SAX (Simple API for XML) 解析模型,虽然 Excel 不是 XML,但 POI 模拟了 SAX 的事件驱动模型,逐行读取数据,内存占用极低。
核心原理:
- 不加载整个文件:POI 不会将整个 Excel 文件读入内存。
- 事件驱动:它将 Excel 文件看作一个事件流,当解析器遇到一个新行、一个新单元格时,会触发相应的事件(如
startRow,endRow,startCell,endCell)。 - 回调处理:你需要实现
XSSFReader.SheetContentsHandler接口,并重写handleRow方法来处理每一行的数据。
优点:

- 内存占用极低:无论 Excel 有多大,内存占用都非常稳定,通常只与单行数据的复杂度有关。
- 性能好:因为是流式读取,速度也很快。
缺点:
- API 相对复杂,需要理解事件驱动模型。
- 不支持修改 Excel,只支持读取。
- 对合并单元格等复杂格式处理起来比较麻烦。
Maven 依赖
<dependencies>
<!-- Apache POI for .xlsx (Office 2007+) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<!-- You might need these for XML processing -->
<dependency>
<groupId>org.apache.xmlbeans</groupId>
<artifactId>xmlbeans</artifactId>
<version>5.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.21</version>
</dependency>
</dependencies>
核心代码示例
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFReader.SheetContentsHandler;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;
public class LargeExcelReader {
public static void main(String[] args) throws Exception {
String filePath = "path/to/your/large_file.xlsx";
try (OPCPackage pkg = OPCPackage.open(filePath)) {
XSSFReader reader = new XSSFReader(pkg);
SharedStringsTable sst = reader.getSharedStringsTable();
// 获取第一个 sheet
InputStream sheetStream = reader.getSheet("rId1"); // "rId1" 通常是第一个 sheet
XMLReader parser = XMLReaderFactory.createXMLReader();
parser.setContentHandler(new SheetHandler(sst));
// 开始解析
parser.parse(new InputSource(sheetStream));
}
}
/**
* 自定义 Sheet 处理器,用于逐行处理数据
*/
private static class SheetHandler implements SheetContentsHandler {
private final SharedStringsTable sst;
private int currentRow = -1;
private int currentCol = -1;
public SheetHandler(SharedStringsTable sst) {
this.sst = sst;
}
@Override
public void startRow(int rowNum) {
// 当新行开始时触发
this.currentRow = rowNum;
this.currentCol = -1;
System.out.println("开始处理第 " + (rowNum + 1) + " 行...");
}
@Override
public void endRow(int rowNum) {
// 当行结束时触发
// 在这里可以执行数据库保存等操作
// System.out.println("第 " + (rowNum + 1) + " 行处理完毕");
}
@Override
public void cell(String cellReference, String formattedValue, String stringValue) {
// 当单元格数据被读取时触发
// cellReference: "A1", "B2" 等
// stringValue: 单元格的原始字符串值
// formattedValue: 格式化后的值
this.currentCol++;
if (stringValue != null && !stringValue.isEmpty()) {
// System.out.printf("第 %d 行, 第 %d 列 (Cell: %s) 的值是: %s%n",
// this.currentRow + 1, this.currentCol, cellReference, stringValue);
// 在这里处理每个单元格的数据,例如存入 List 或直接写入数据库
// 注意:为了避免频繁IO,最好每 N 行或每批数据执行一次数据库批量插入
}
}
@Override
public void headerFooter(String text, boolean isHeader, String tagName) {
// 忽略页眉页脚
}
}
}
EasyExcel - 阿里巴巴开源,更易用的选择
EasyExcel 是阿里巴巴开源的项目,它在底层同样基于 POI 的 SAX 模式,但对其进行了深度封装,提供了极其简洁的 API,极大地降低了使用门槛,是目前国内处理大数据 Excel 的首选方案。
核心原理:
- 底层仍然是 POI 的 SAX 模式,保证了低内存占用。
- 通过监听器模式,你只需要创建一个监听器类,实现
AnalysisEventListener接口,并指定数据模型即可。
优点:

- API 极其简单:几行代码就能实现读取。
- 功能强大:支持多行头、数据转换、数据校验、异步导入等高级功能。
- 性能优秀:底层和 POI SAX 一样,性能和内存表现都很好。
- 社区活跃:文档完善,问题容易找到答案。
缺点:
- 相对 POI 是一个较新的项目,但经过阿里等大厂的验证,非常稳定。
Maven 依赖
<dependencies>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
核心代码示例
定义数据模型 (DTO)
import com.alibaba.excel.annotation.ExcelProperty;
// @Data 是 Lombok 注解,自动生成 getter/setter
@Data
public class UserData {
// "姓名" 是 Excel 中的表头
@ExcelProperty("姓名")
private String name;
@ExcelProperty("年龄")
private Integer age;
@ExcelProperty("邮箱")
private String email;
}
创建监听器
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import java.util.ArrayList;
import java.util.List;
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次new出来,然后里面用到spring可以构造方法传进去
public class UserDataListener extends AnalysisEventListener<UserData> {
// 可以通过构造函数传入 Spring 管理的 Bean,UserService
// private UserService userService;
// 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
private static final int BATCH_COUNT = 5;
private List<UserData> cachedDataList = new ArrayList<>(BATCH_COUNT);
// 可以使用构造函数注入需要的依赖
// public UserDataListener(UserService userService) {
// this.userService = userService;
// }
@Override
public void invoke(UserData data, AnalysisContext context) {
System.out.println("解析到一条数据: " + data.toString());
cachedDataList.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据过多,OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
System.out.println("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
System.out.println("{}条数据,开始存储数据库!", cachedDataList.size());
// TODO: 将 cachedDataList 中的数据批量插入数据库
// userService.saveBatch(cachedDataList);
System.out.println("存储数据库成功!");
}
}
启动读取
import com.alibaba.excel.EasyExcel;
public class EasyExcelDemo {
public static void main(String[] args) {
String fileName = "path/to/your/large_file.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();
}
}
传统 POI (DOM Model) - 仅适用于小数据量
警告:此方案仅适用于小文件(例如几万行以内),大数据量会导致内存溢出!
传统 POI 使用 DOM 模型,它会将整个 Excel 文件(包括所有数据)都加载到内存中的一个 Workbook 对象中,这种方式编程简单直观,但内存消耗巨大。
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
public class SmallExcelReader {
public static void main(String[] args) throws Exception {
String filePath = "path/to/your/small_file.xlsx";
List<List<String>> dataList = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = new XSSFWorkbook(fis)) {
Sheet sheet = workbook.getSheetAt(0); // 获取第一个 sheet
for (Row row : sheet) {
List<String> rowData = new ArrayList<>();
for (Cell cell : row) {
switch (cell.getCellType()) {
case STRING:
rowData.add(cell.getStringCellValue());
break;
case NUMERIC:
rowData.add(String.valueOf(cell.getNumericCellValue()));
break;
// ... 其他类型处理
}
}
dataList.add(rowData);
}
}
// dataList 中包含了所有数据
System.out.println("读取完成,共 " + dataList.size() + " 行数据");
}
}
大数据导入的完整流程与最佳实践
无论你选择哪种读取方案,完整的大数据导入流程通常包括以下几个步骤:
-
前端校验:
- 文件类型:限制只能上传
.xlsx文件。 - 文件大小:设置一个合理的最大文件大小限制(如 50MB, 100MB)。
- 行数限制:通过 API 可以先获取 Excel 的总行数,如果超过限制则直接拒绝。
- 文件类型:限制只能上传
-
后端处理:
- 接收文件:将文件上传到服务器,可以保存到临时目录。
- 使用流式读取:必须使用方案一(POI SAX)或方案二(EasyExcel)进行读取。
- 批量处理与数据库插入:
- 不要逐条插入:在读取循环中,每读取一条数据就执行一次
INSERT语句是性能杀手。 - 批量插入:将读取的数据放入一个
List缓冲区(如List<UserData>),当缓冲区达到一定数量(如 500 或 1000 条)时,执行一次批量插入(如 MySQL 的INSERT INTO ... VALUES (...), (...), ...)。 - 事务管理:将整个导入过程放在一个事务中,如果中途失败,可以回滚,保证数据一致性,但要注意,大事务会影响数据库性能,可以考虑每 N 批提交一次事务。
- 不要逐条插入:在读取循环中,每读取一条数据就执行一次
- 数据校验:在读取过程中或批量插入前,对每条数据进行业务逻辑校验(如格式、必填项、唯一性等)。
- 错误处理与日志:记录下哪些行、哪些数据因为什么原因导入失败,并最终返回给用户一个详细的错误报告。
-
异步处理:
- 对于超大规模的导入(几十万行以上),同步导入会导致 HTTP 请求长时间挂起,用户体验差。
- 推荐使用消息队列(如 RabbitMQ, RocketMQ):用户上传文件后,后端立即返回一个“已接收,正在处理中”的状态,并将文件路径和处理任务 ID 发送到消息队列,然后由一个或多个后台消费者 Worker 异步地去处理这个任务,处理完成后,再更新任务状态,并通知用户。
总结与选择建议
| 特性 | POI (SAX Event Model) | EasyExcel | POI (DOM Model) |
|---|---|---|---|
| 内存占用 | 极低 | 极低 | 极高,大数据量会OOM |
| API 复杂度 | 复杂,需理解事件驱动 | 非常简单 | 简单直观 |
| 性能 | 高 | 高 | 低(大数据量) |
| 功能 | 读取为主,功能基础 | 强大,支持读写、校验等 | 功能全面,支持读写 |
| 适用场景 | 对框架无依赖,追求极致性能控制的场景 | 绝大多数 Java 项目,尤其是中小型项目 | 仅适用于小文件(< 10万行) |
最终建议:
- 新项目:直接选择 EasyExcel,它为你解决了所有底层复杂性,让你能更专注于业务逻辑,是目前最主流、最高效的选择。
- 老项目/有特殊要求:如果你的项目已经深度集成了 POI,或者对框架有强依赖、不能引入新依赖,POI SAX 模型 是不二之选。
- 避免:除非你 100% 确定你的 Excel 文件永远不会变大,否则坚决不要使用传统的 POI DOM 模型。
