杰瑞科技汇

Java Excel导入数据库,如何高效实现?

技术选型

要实现这个功能,我们通常需要以下几个核心库:

Java Excel导入数据库,如何高效实现?-图1
(图片来源网络,侵删)
  • Excel 解析库:

    • Apache POI: 最经典、功能最强大的 Java 操作 Office 文件(如 .xls, .xlsx)的库,它是事实上的标准。
    • EasyExcel: 阿里巴巴开源的库,它基于 POI,但做了大量优化,内存占用极低,性能更好,特别适合处理大文件(几十上百万行)。强烈推荐使用 EasyExcel
  • 数据库连接:

    • JDBC: Java 数据库连接,是 Java 访问数据库的标准 API。
    • 连接池 (HikariCP, Druid): 在实际应用中,必须使用连接池来管理数据库连接,以提高性能和稳定性,HikariCP 是目前性能最高的连接池之一。
  • 构建工具:

    • MavenGradle: 用于管理项目依赖。

环境准备与项目搭建

这里我们以 MavenEasyExcel 为例进行演示。

Java Excel导入数据库,如何高效实现?-图2
(图片来源网络,侵删)

1 创建 Maven 项目

在你的 IDE(如 IntelliJ IDEA 或 Eclipse)中创建一个新的 Maven 项目。

2 添加依赖 (pom.xml)

pom.xml 文件中添加以下依赖:

<dependencies>
    <!-- EasyExcel 核心依赖 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.3.2</version> <!-- 请使用最新版本 -->
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version> <!-- 请根据你的 MySQL 版本选择 -->
    </dependency>
    <!-- HikariCP 连接池 -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.0.1</version> <!-- 请使用最新版本 -->
    </dependency>
    <!-- Lombok (简化 Java 代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

数据库准备

假设我们要导入一张用户信息表。

1 创建数据库和表

-- 创建数据库
CREATE DATABASE IF NOT EXISTS excel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用数据库
USE excel_db;
-- 创建用户表
CREATE TABLE IF NOT EXISTS `user_info` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` VARCHAR(50) NOT NULL COMMENT '姓名',
  `age` INT NULL COMMENT '年龄',
  `gender` VARCHAR(10) NULL COMMENT '性别',
  `email` VARCHAR(100) NULL COMMENT '邮箱',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) COMMENT = '用户信息表';

创建 Excel 文件

src/main/resources 目录下创建一个名为 user_info_template.xlsx 的 Excel 文件。

Java Excel导入数据库,如何高效实现?-图3
(图片来源网络,侵删)

注意

  • 第一行必须是表头
  • 表头的名称需要和 Java 实体类的字段名对应(或通过注解映射)。
姓名 年龄 性别 邮箱
张三 25 zhangsan@example.com
李四 30 lisi@example.com
王五 28 wangwu@example.com

Java 代码实现

我们将代码分为几个部分:实体类、数据库配置、Excel 读取逻辑和主程序。

1 创建实体类 (UserInfo.java)

这个类用于映射 Excel 中的一行数据。

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
@Data // Lombok 注解,自动生成 getter, setter, toString 等
public class UserInfo {
    // ExcelProperty 的 value 值需要和 Excel 表头完全一致
    // index 可以指定列的顺序,但通常不推荐,依赖顺序容易出错
    @ExcelProperty("姓名")
    private String name;
    @ExcelProperty("年龄")
    private Integer age;
    @ExcelProperty("性别")
    private String gender;
    @ExcelProperty("邮箱")
    private String email;
    // 数据库中需要,但 Excel 中没有的字段
    private Date createTime;
}

2 数据库配置 (DatabaseConfig.java)

我们将数据库连接信息提取出来,方便管理。

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class DatabaseConfig {
    private static final String JDBC_URL = "jdbc:mysql://localhost:3306/excel_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai";
    private static final String USERNAME = "root"; // 你的数据库用户名
    private static final String PASSWORD = "your_password"; // 你的数据库密码
    private static HikariDataSource dataSource;
    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(JDBC_URL);
        config.setUsername(USERNAME);
        config.setPassword(PASSWORD);
        // 连接池配置
        config.setDriverClassName("com.mysql.cj.jdbc.Driver");
        config.setMaximumPoolSize(10); // 最大连接数
        config.setMinimumIdle(5);      // 最小空闲连接数
        config.setConnectionTimeout(30000); // 连接超时时间
        dataSource = new HikariDataSource(config);
    }
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

3 Excel 读取与数据库写入逻辑 (ExcelImportService.java)

这是核心业务逻辑,使用 EasyExcel 的监听器模式。

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class ExcelImportService {
    // 定义批量插入的大小,可以提升性能
    private static final int BATCH_SIZE = 1000;
    public void importExcel(String filePath) {
        // 1. 准备一个临时列表,用于存放读取到的数据
        List<UserInfo> dataList = new ArrayList<>(BATCH_SIZE);
        // 2. 创建数据库连接
        try (Connection connection = DatabaseConfig.getConnection()) {
            // 3. 创建 PreparedStatement,SQL 语句中用 ? 作为占位符
            String sql = "INSERT INTO user_info (name, age, gender, email, create_time) VALUES (?, ?, ?, ?, ?)";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                // 4. 使用 EasyExcel 读取 Excel 文件
                EasyExcel.read(filePath, UserInfo.class, new AnalysisEventListener<UserInfo>() {
                    // 每解析完一行数据,就会调用这个方法
                    @Override
                    public void invoke(UserInfo data, AnalysisContext context) {
                        dataList.add(data);
                        // 当数据量达到 BATCH_SIZE 时,执行批量插入
                        if (dataList.size() >= BATCH_SIZE) {
                            saveData(ps, dataList);
                            // 清空列表,准备下一批数据
                            dataList.clear();
                        }
                    }
                    // 所有数据解析完成后,会调用这个方法
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext context) {
                        // 处理剩余不足 BATCH_SIZE 的数据
                        if (!dataList.isEmpty()) {
                            saveData(ps, dataList);
                        }
                        log.info("所有数据解析完成!");
                    }
                }).sheet().doRead(); // 读取第一个 sheet
            }
        } catch (Exception e) {
            log.error("导入 Excel 失败", e);
            throw new RuntimeException("导入失败", e);
        }
    }
    /**
     * 批量保存数据到数据库
     * @param ps PreparedStatement 对象
     * @param dataList 数据列表
     * @throws SQLException
     */
    private void saveData(PreparedStatement ps, List<UserInfo> dataList) throws SQLException {
        log.info("开始保存 {} 条数据到数据库...", dataList.size());
        for (UserInfo data : dataList) {
            // 设置 SQL 语句中的参数
            ps.setString(1, data.getName());
            ps.setInt(2, data.getAge());
            ps.setString(3, data.getGender());
            ps.setString(4, data.getEmail());
            ps.setTimestamp(5, new java.sql.Timestamp(System.currentTimeMillis())); // 设置当前时间作为创建时间
            // 将 SQL 语句添加到批处理中
            ps.addBatch();
        }
        // 执行批处理
        ps.executeBatch();
        log.info("成功保存 {} 条数据!", dataList.size());
    }
}

4 主程序 (Main.java)

我们编写一个主程序来调用服务。

import java.io.InputStream;
public class Main {
    public static void main(String[] args) {
        // Excel 文件路径,这里从 resources 目录读取
        String fileName = Main.class.getClassLoader().getResource("user_info_template.xlsx").getFile();
        ExcelImportService importService = new ExcelImportService();
        importService.importExcel(fileName);
    }
}

运行与验证

  1. 确保 MySQL 服务已启动excel_db 数据库和 user_info 表已创建。
  2. 检查 DatabaseConfig.java 中的数据库连接信息(URL, 用户名, 密码)是否正确。
  3. 运行 Main.java
  4. 查看控制台日志,你会看到类似 "开始保存 1000 条数据..." 的输出。
  5. 查询数据库,验证数据是否已成功导入。
-- 查询 user_info 表
SELECT * FROM user_info;

你应该能看到 Excel 中的数据已经成功插入到数据库中。


高级与优化(非常重要)

处理大文件(内存优化)

EasyExcel 的核心优势就是处理大文件,上面的监听器模式本身就是为处理大文件设计的,它不会一次性将所有数据加载到内存中,而是逐行读取,处理完一行就丢弃,内存占用非常低。

关键点

  • 必须使用 AnalysisEventListener,而不是 EasyExcel.read().doReadAll()
  • invoke 方法中处理数据,而不是将所有数据存入一个 List 中再统一处理(除非你的业务逻辑确实需要)。

错误处理与数据回滚

在批量导入时,如果某条数据因为格式错误(如年龄是字符串)导致插入失败,我们希望跳过这条错误数据,继续处理后续数据,而不是整个导入任务失败。

优化后的 saveData 方法:

// 在 ExcelImportService.java 中
private void saveData(PreparedStatement ps, List<UserInfo> dataList) {
    log.info("开始保存 {} 条数据到数据库...", dataList.size());
    try {
        for (UserInfo data : dataList) {
            try {
                ps.setString(1, data.getName());
                ps.setInt(2, data.getAge());
                ps.setString(3, data.getGender());
                ps.setString(4, data.getEmail());
                ps.setTimestamp(5, new java.sql.Timestamp(System.currentTimeMillis()));
                ps.addBatch();
            } catch (SQLException e) {
                // 捕获单条数据插入时的异常(如类型不匹配),记录日志并跳过
                log.error("数据格式错误,已跳过: {}", data, e);
                // 继续处理下一条
                continue;
            }
        }
        ps.executeBatch();
        log.info("成功保存 {} 条数据!", dataList.size());
    } catch (SQLException e) {
        log.error("批处理执行失败", e);
        // 可以在这里选择是否回滚,但通常批处理是自动提交的,所以手动回滚需要额外配置
        // connection.rollback();
    }
}

事务管理

对于要求“要么全部成功,要么全部失败”的业务场景,就需要使用数据库事务。

// 在 ExcelImportService.java 的 importExcel 方法中
public void importExcelWithTransaction(String filePath) {
    Connection connection = null;
    try {
        connection = DatabaseConfig.getConnection();
        // 关闭自动提交,开启事务
        connection.setAutoCommit(false);
        String sql = "INSERT INTO user_info (name, age, gender, email, create_time) VALUES (?, ?, ?, ?, ?)";
        try (PreparedStatement ps = connection.prepareStatement(sql)) {
            List<UserInfo> dataList = new ArrayList<>(BATCH_SIZE);
            EasyExcel.read(filePath, UserInfo.class, new AnalysisEventListener<UserInfo>() {
                @Override
                public void invoke(UserInfo data, AnalysisContext context) {
                    dataList.add(data);
                    if (dataList.size() >= BATCH_SIZE) {
                        saveDataWithTransaction(ps, connection, dataList);
                        dataList.clear();
                    }
                }
                @Override
                public void doAfterAllAnalysed(AnalysisContext context) {
                    if (!dataList.isEmpty()) {
                        saveDataWithTransaction(ps, connection, dataList);
                    }
                    // 所有数据成功处理,提交事务
                    connection.commit();
                    log.info("所有数据解析并保存完成,事务已提交!");
                }
            }).sheet().doRead();
        }
    } catch (Exception e) {
        log.error("导入 Excel 失败,准备回滚", e);
        if (connection != null) {
            try {
                // 发生异常,回滚事务
                connection.rollback();
                log.info("事务已回滚!");
            } catch (SQLException ex) {
                log.error("回滚事务失败", ex);
            }
        }
        throw new RuntimeException("导入失败,已回滚", e);
    } finally {
        if (connection != null) {
            try {
                // 恢复自动提交
                connection.setAutoCommit(true);
                connection.close();
            } catch (SQLException e) {
                log.error("关闭连接失败", e);
            }
        }
    }
}
private void saveDataWithTransaction(PreparedStatement ps, Connection connection, List<UserInfo> dataList) throws SQLException {
    log.info("事务模式下,开始保存 {} 条数据...", dataList.size());
    for (UserInfo data : dataList) {
        ps.setString(1, data.getName());
        ps.setInt(2, data.getAge());
        ps.setString(3, data.getGender());
        ps.setString(4, data.getEmail());
        ps.setTimestamp(5, new java.sql.Timestamp(System.currentTimeMillis()));
        ps.addBatch();
    }
    ps.executeBatch();
}
场景 推荐方案 关键点
小文件导入 EasyExcel + JDBC 简单直接,使用 doReadAll() 或监听器均可。
大文件导入 EasyExcel (监听器模式) + JDBC 必须使用 AnalysisEventListener,逐行读取,内存友好。
高性能批量插入 EasyExcel + JDBC + 批处理 (addBatch/executeBatch) 显著减少数据库 I/O 次数,大幅提升速度。
容错性要求高 EasyExcel + JDBC + 异常捕获 try-catch 中处理单条数据错误,避免整个任务中断。
数据一致性要求高 EasyExcel + JDBC + 数据库事务 使用 connection.setAutoCommit(false)commit()/rollback()

通过以上步骤和优化,你就可以构建一个健壮、高效、可扩展的 Java Excel 导入数据库功能。

分享:
扫描分享到社交APP
上一篇
下一篇