1.背景:
上传文件较大excel,使用WorkbookFactory.create(inputStream)全在内存处理导致OOM。文件数据不多,但包含了很多冗余信息。
2.原因分析:
-
WorkbookFactory.create(inputStream)是 Apache POI 中用于解析 Excel 文件的核心方法。然而,这个方法会将整个 Excel 文件加载到内存中。所以文件比较大的时候,会导致OOM。
-
HSSFWorkbook 和 XSSFWorkbook 的内存占用
-
如果是
.xls
文件(HSSFWorkbook),Apache POI 会在内存中构建一个完整的对象模型。 -
如果是
.xlsx
文件(XSSFWorkbook),尽管它是基于 XML 的格式,但仍然可能占用大量内存,尤其是在文件中存在大量空白单元格、样式、公式等。
-
一次性加载文件
-
WorkbookFactory.create(inputStream)
默认会一次性将整个文件加载到内存中,而不是流式读取。
-
文件冗余内容
-
即使数据量不多,文件中可能存在大量无用的空白行、隐藏列、样式等,导致内存占用过高。
3.解决方案:
1. 使用 SXSSFWorkbook(适用于 .xlsx
文件)
-
SXSSFWorkbook
是 Apache POI 提供的流式处理类,适合处理大文件。 -
它不会将整个文件加载到内存中,而是只保留一部分数据在内存中,其余数据写入磁盘。
-
示例代码:
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
// 将输入流转换为 SXSSFWorkbook
Workbook workbook;
if (fileName.endsWith(".xlsx")) {
workbook = new SXSSFWorkbook(new XSSFWorkbook(inputStream), 100); // 保留 100 行在内存中
} else {
workbook = WorkbookFactory.create(inputStream);
}
2. 使用 EasyExcel(推荐)
-
阿里开源的 EasyExcel 是一个轻量级的 Excel 解析库,支持流式读取,内存占用极低。
-
示例代码:
import com.alibaba.excel.EasyExcel;
String fileName = "example.xlsx";
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
-
DemoData
是数据模型类,DemoDataListener
是自定义的数据处理监听器。
3. 转换为 CSV 格式
-
如果可以接受用户上传 CSV 文件,建议优先使用 CSV 格式,因为它的解析效率更高,且不包含样式、公式等额外信息。
-
示例代码:
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
String[] data = line.split(",");
// 处理数据
}
4. 增加 JVM 堆内存
-
如果必须使用
WorkbookFactory.create(inputStream)
,可以增加 JVM 堆内存以避免 OOM。 -
修改 JVM 参数:
-
注意:这只是临时解决方案,可能会掩盖潜在的问题。
5. 检查并优化文件
-
清理空白行/列:确保文件中没有多余的空白行和列。
-
移除隐藏工作表:删除不需要的工作表。
-
清除格式化信息:减少样式、公式、注释等内容。
-
另存为新文件:保存为新的
.xlsx
文件,通常可以减少文件大小。
6. 分片处理
-
如果文件过大,可以在前端或后端实现分片逻辑,分批次读取和处理数据。
-
后端示例:
InputStream inputStream = new FileInputStream("large_file.xlsx");
Workbook workbook = WorkbookFactory.create(inputStream);
Sheet sheet = workbook.getSheetAt(0);
int batchSize = 1000; // 每次处理 1000 行
for (int i = 0; i <= sheet.getLastRowNum(); i += batchSize) {
List<Row> batchRows = new ArrayList<>();
for (int j = i; j < i + batchSize && j <= sheet.getLastRowNum(); j++) {
batchRows.add(sheet.getRow(j));
}
processBatch(batchRows); // 处理当前批次数据
}
总结
-
优先推荐使用 EasyExcel,它专为大文件设计,内存占用极低。
-
如果必须使用 Apache POI,改用
SXSSFWorkbook
或对文件进行预处理。 -
调整 JVM 参数是临时解决方案,无法从根本上解决问题。
-
在前端限制文件大小和格式,避免不必要的性能开销。