杰瑞科技汇

Java大数据导入Excel如何高效处理?

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

Java大数据导入Excel如何高效处理?-图1
(图片来源网络,侵删)

下面我将为你详细介绍几种主流的方案,从推荐到备选,并附上核心代码示例。


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 方法来处理每一行的数据。

优点:

Java大数据导入Excel如何高效处理?-图2
(图片来源网络,侵删)
  • 内存占用极低:无论 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 接口,并指定数据模型即可。

优点:

Java大数据导入Excel如何高效处理?-图3
(图片来源网络,侵删)
  • 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() + " 行数据");
    }
}

大数据导入的完整流程与最佳实践

无论你选择哪种读取方案,完整的大数据导入流程通常包括以下几个步骤:

  1. 前端校验

    • 文件类型:限制只能上传 .xlsx 文件。
    • 文件大小:设置一个合理的最大文件大小限制(如 50MB, 100MB)。
    • 行数限制:通过 API 可以先获取 Excel 的总行数,如果超过限制则直接拒绝。
  2. 后端处理

    • 接收文件:将文件上传到服务器,可以保存到临时目录。
    • 使用流式读取必须使用方案一(POI SAX)或方案二(EasyExcel)进行读取。
    • 批量处理与数据库插入
      • 不要逐条插入:在读取循环中,每读取一条数据就执行一次 INSERT 语句是性能杀手。
      • 批量插入:将读取的数据放入一个 List 缓冲区(如 List<UserData>),当缓冲区达到一定数量(如 500 或 1000 条)时,执行一次批量插入(如 MySQL 的 INSERT INTO ... VALUES (...), (...), ...)。
      • 事务管理:将整个导入过程放在一个事务中,如果中途失败,可以回滚,保证数据一致性,但要注意,大事务会影响数据库性能,可以考虑每 N 批提交一次事务。
    • 数据校验:在读取过程中或批量插入前,对每条数据进行业务逻辑校验(如格式、必填项、唯一性等)。
    • 错误处理与日志:记录下哪些行、哪些数据因为什么原因导入失败,并最终返回给用户一个详细的错误报告。
  3. 异步处理

    • 对于超大规模的导入(几十万行以上),同步导入会导致 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 模型
分享:
扫描分享到社交APP
上一篇
下一篇