☰
Java实现Excel批量导入MySQL:EasyExcel与JDBC批处理实战
2026/9/28 1:31:40 网站建设 项目流程

简介:这份资源面向Java后端初学者与需要处理数据迁移的开发者,聚焦Excel与MySQL之间的双向数据流转问题。项目基于Apache POI解析xls/xlsx文件,通过JDBC建立MySQL连接,实现Excel数据导入数据库,并在检测到重复数据时执行更新操作,同时支持将库中数据反向导出为Excel表格,覆盖文件操作、单元格类型解析、SQL条件判断与批量处理等核心技能点。压缩包共20个文件,约1.31MB,包含6个java源码与6个class编译文件、2个依赖jar包(mysql驱动与jxl)、1个建表sql脚本及Eclipse工程配置,结构完整可直接导入运行。目前已有1737人学习下载,适合作为JDBC与POI综合练习的参考案例,帮助读者理解数据导入导出策略与工程目录组织方式。

1. Java 把 Excel 灌进 MySQL:一条被低估的脏活链路

电商后台的运营丢过来一个 8 万行的 Excel,说「今天下班前导进系统」。你打开一看,合并单元格、手机号被存成科学计数法、日期列一半是文本一半是日期格式,还有三行是空行夹在中间。这时候你才意识到,Excel 导入 MySQL 这件事,写个for循环读单元格谁都会,真正难的是让这条链路在脏数据面前不崩、不重复、不 OOM。

这个标题讲的就是这条链路:用 Java 把 Excel 文件解析出来,清洗成规整的行记录,再批量写进 MySQL。它解决的是「业务方只给 Excel,系统只认数据库」这个每天都在发生的对接问题。适合谁看?写过 JDBC 但没处理过万行级导入的 Java 后端、需要给运营做数据导入功能的全栈、以及被 POI 内存溢出坑过一次想搞清楚边界的人。下面按「选型 → 解析 → 入库 → 排错 → 调优」的顺序,把我实际跑通过的方案拆开讲。

2. 选型先立住:POI、EasyExcel 和 JDBC 批处理怎么搭

2.1 解析层为什么我最终选 EasyExcel 而不是裸 POI

裸 POI 的XSSFWorkbook会把整个 xlsx 一次性读进内存,一个 10 万行、20 列的文件,堆内存轻松吃掉 1G 以上,线上直接 OOM。POI 官方给的SXSSFWorkbook是写场景的流式方案,读场景要用XSSFReader+ SAX 自己写事件处理器,代码量陡增,还得自己维护共享字符串表(sharedStrings)和样式索引,稍不留神就解析错位。

EasyExcel 本质是把 POI 的 SAX 模式封装成了监听器回调,读的时候一行一行触发invoke,内存占用和行数基本无关。常见做法是继承AnalysisEventListener,在invoke里攒够一批就落库,doAfterAllAnalysed里处理最后一批。选它的核心理由不是「快」,而是「内存可控 + 代码可读」,团队里新人接手也能看懂。

如果你的文件是老的.xls格式(BIFF8),EasyExcel 底层还是走 HSSF,内存优势会打折,这种文件建议先让业务方另存为 xlsx。至于.csv,别用 Excel 解析库,直接按行读、按逗号切,性能高一个数量级,但要注意引号包裹的字段里可能含逗号,得用带状态的解析器而不是split(",")。

2.2 入库层:JDBC 批处理 + rewriteBatchedStatements 才是关键

很多人导入慢,问题不在解析,在入库。默认情况下 MySQL JDBC 驱动会把addBatch()的语句一条条发给服务端,1 万行就是 1 万次网络往返。开启rewriteBatchedStatements=true后,驱动会把多条 INSERT 合并成一条INSERT INTO t VALUES (...),(...),(...),吞吐能差 5 到 10 倍。

连接串我一般这么写:

jdbc:mysql://127.0.0.1:3306/import_demo?useUnicode=true&characterEncoding=utf8mb4&rewriteBatchedStatements=true&useServerPrepStmts=false&allowMultiQueries=true

参数逐个说清楚:rewriteBatchedStatements=true是批处理合并的开关,必须开;useServerPrepStmts=false让驱动走客户端预编译,配合批处理重写才生效,开了服务端预编译反而会绕过重写逻辑;characterEncoding=utf8mb4保证 emoji 和生僻字不乱码;allowMultiQueries在合并语句时可能被用到。这几个参数是导入性能的血泪经验,少一个都可能让你以为「MySQL 就这么慢」。

提示:rewriteBatchedStatements只对PreparedStatement的addBatch生效,用Statement拼字符串是享受不到的。

2.3 表结构:字段类型和索引要先想清楚

导入前先把目标表建好,别指望导入时动态建表。字段类型上,手机号、身份证、订单号这类「看起来是数字但永远不参与运算」的列,一律用VARCHAR,用BIGINT存手机号迟早被前导零和超长号段坑。金额用DECIMAL(18,4),别用DOUBLE,浮点误差在对账时是灾难。

索引是双刃剑:导入期间目标表上的二级索引会拖慢写入,因为每插一行都要维护索引树。如果是一次性大批量导入,常见做法是先ALTER TABLE ... DISABLE KEYS(仅 MyISAM 有效),或者干脆先删掉非唯一索引,导完再建。InnoDB 没有 disable keys,替代方案是先导入到一张无索引的临时表,再用INSERT INTO ... SELECT灌到正式表。

3. 动手跑通:从 Excel 到 MySQL 的完整代码链路

3.1 依赖和实体映射

Maven 里引两个核心依赖,EasyExcel 和 MySQL 驱动:

<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

实体类用注解把表头和字段绑起来,@ExcelProperty的 value 必须和 Excel 表头文字完全一致,包括空格。index属性可以按列序号映射,表头会变动的场景用 index 更稳:

public class UserRow { @ExcelProperty("姓名") private String name; @ExcelProperty("手机号") private String phone; @ExcelProperty("注册时间") private String registerTime; // 先接字符串,后面自己解析 // getter / setter 省略 }

这里有个刻意的设计:日期列先接成String。因为 Excel 里的日期可能是2024-01-01、2024/1/1、45000(序列号)三种形态,交给 EasyExcel 的LocalDateTime转换器遇到序列号会直接抛异常。先接字符串,在业务层用统一方法解析,容错性高得多。

3.2 监听器里做批量攒批和落库

核心逻辑在监听器,攒够BATCH_SIZE条就 flush 一次:

public class UserImportListener extends AnalysisEventListener<UserRow> { private static final int BATCH_SIZE = 1000; private final List<UserRow> buffer = new ArrayList<>(BATCH_SIZE); private final UserDao userDao; public UserImportListener(UserDao userDao) { this.userDao = userDao; } @Override public void invoke(UserRow row, AnalysisContext context) { // 空行过滤:姓名和手机号都为空直接跳过 if (isBlank(row.getName()) && isBlank(row.getPhone())) { return; } buffer.add(row); if (buffer.size() >= BATCH_SIZE) { userDao.batchInsert(buffer); buffer.clear(); // 必须清空,否则内存持续增长 } } @Override public void doAfterAllAnalysed(AnalysisContext context) { if (!buffer.isEmpty()) { userDao.batchInsert(buffer); buffer.clear(); } } }

逻辑说明:invoke每读一行触发一次,攒批到 1000 条就调 DAO 落库并清空缓冲。doAfterAllAnalysed处理最后不足一批的尾巴,这一步漏了就会丢数据,是新手最常见的翻车点。buffer.clear()不能省,否则 List 会一直涨,攒批就失去意义了。

参数说明:BATCH_SIZE设 1000 是个经验值。太小则网络往返多,太大则单条 SQL 过长可能撞上max_allowed_packet(默认 4M),1000 行 20 列大约几百 KB,比较安全。如果列特别多,降到 500。

3.3 DAO 层的批处理写法

DAO 用PreparedStatement的addBatch/executeBatch:

public void batchInsert(List<UserRow> rows) { String sql = "INSERT INTO user_info(name, phone, register_time) VALUES (?, ?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 关自动提交,手动控事务 for (UserRow row : rows) { ps.setString(1, row.getName()); ps.setString(2, row.getPhone()); ps.setString(3, normalizeDate(row.getRegisterTime())); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { throw new RuntimeException("批量插入失败", e); } }

逻辑说明:关掉autoCommit后,整批插入在一个事务里提交,减少 redo log 刷盘次数。executeBatch一次性把攒的语句发出去,配合连接串里的rewriteBatchedStatements=true,驱动会合并成多值 INSERT。

参数说明:normalizeDate是自定义的日期归一化方法,把2024/1/1、45000这类统一转成yyyy-MM-dd HH:mm:ss。序列号转日期用1900-01-01加天数,注意 Excel 有个 1900 闰年 bug,1900 年 3 月之前的日期要减一天,这个坑后面排错章节细说。

3.4 主流程串起来

public class ImportMain { public static void main(String[] args) { String filePath = "D:/data/user_import.xlsx"; UserDao userDao = new UserDao(); EasyExcel.read(filePath, UserRow.class, new UserImportListener(userDao)) .sheet() .doRead(); System.out.println("导入完成"); } }

EasyExcel.read指定文件、实体类、监听器,.sheet()默认读第一个 sheet,多 sheet 场景用.sheet(0)或.sheet("Sheet1")指定。.doRead()是同步阻塞的,读完整文件才返回。如果文件有多个 sheet 都要导,链式调多个.sheet()即可。

4. 避坑与排查:导入翻车的 5 个真实场景

4.1 手机号变成 1.38E+10

现象:导入后手机号列全是1.38E+10这种科学计数法,或者末尾几位变成 0。

原因:Excel 把纯数字单元格当数值存储,超过 11 位精度丢失,读取时 POI 拿到的是 double。这是 Excel 本身的存储机制,不是解析库的锅。

解决:读取时用DataFormatter或 EasyExcel 的@ExcelProperty配合字符串转换器,强制按文本读。更彻底的办法是让业务方在 Excel 里把该列设成文本格式再填。代码侧可以在invoke里对手机号做一次new BigDecimal(value).toPlainString()还原。

4.2 日期列一半能解析一半报错

现象:同一列日期,有的行正常入库,有的行抛DateTimeParseException。

原因:Excel 里日期可能是真日期(存为序列号)、文本日期、或者带时区的字符串,格式不统一。

解决:实体类里日期字段接String,在业务层写一个normalizeDate方法,按优先级尝试多种格式解析,全失败就记日志跳过该行而不是整批失败。序列号转日期记得处理 1900 闰年 bug:序列号小于 60 的加 1 天再算。

4.3 导入到一半 OOM

现象:小文件正常,几万行的大文件跑着跑着OutOfMemoryError。

原因:要么用了XSSFWorkbook全量读,要么监听器里的 buffer 没清空,要么在内存里攒了全部行才落库。

解决:确认用的是 EasyExcel 的监听器模式而非EasyExcel.read(file).doReadSync()(后者会把所有行读进 List)。检查buffer.clear()是否在每个 flush 分支都调用了。JVM 参数上给-Xmx留够余量,但根本解法是流式处理。

4.4 重复导入产生重复数据

现象:同一个文件导了两次,表里出现两份数据。

原因:没有唯一约束,也没有幂等控制。

解决:在业务主键(比如手机号或订单号)上建唯一索引,插入用INSERT ... ON DUPLICATE KEY UPDATE或INSERT IGNORE。或者在导入前按文件 MD5 记录一张导入日志表,同一文件不重复处理。唯一索引是最后一道防线,别省。

4.5 中文乱码

现象:姓名列入库后是问号或乱码。

原因:连接串没指定字符集,或者数据库/表的字符集是latin1。

解决:连接串加characterEncoding=utf8mb4,建库建表时用DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci。注意utf8在 MySQL 里是阉割版,存不了 emoji,一律用utf8mb4。

5. 进阶:把导入做成可复用、可观测的组件

5.1 用模板方法把导入流程抽象出来

每张表的导入逻辑大同小异,差异只在实体类、校验规则、DAO。我一般抽一个泛型基类:

public abstract class AbstractImportListener<T> extends AnalysisEventListener<T> { private static final int BATCH_SIZE = 1000; private final List<T> buffer = new ArrayList<>(BATCH_SIZE); protected abstract void batchSave(List<T> rows); protected abstract boolean validate(T row); @Override public void invoke(T row, AnalysisContext context) { if (!validate(row)) return; buffer.add(row); if (buffer.size() >= BATCH_SIZE) { batchSave(buffer); buffer.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { if (!buffer.isEmpty()) { batchSave(buffer); buffer.clear(); } } }

子类只需实现batchSave和validate,导入逻辑复用。validate里做必填校验、格式校验,返回 false 的行直接跳过并计数,最后统一报告「成功 N 行,跳过 M 行」。

5.2 加一层导入结果统计

光导入不够,得让调用方知道结果。在监听器里维护计数器:

指标含义用途
totalRows解析到的总行数和文件行数对账
successRows成功入库行数判断导入是否完整
skipRows校验失败跳过行数定位脏数据
failRows入库异常行数排查数据库问题
costMs总耗时性能基线

这几个数在doAfterAllAnalysed里汇总,返回给调用方或写进导入日志表。运营看到「跳过 3 行」会主动去查那 3 行是什么问题,比一句「导入完成」有用得多。

5.3 大文件的分片与断点续传思路

几十万行的文件,单次导入可能跑十几分钟,中途网络抖动就前功尽弃。常见做法是按 sheet 或按行号分片,每片导入成功后记录进度到一张import_progress表,失败重试时从上次成功的分片继续。EasyExcel 支持headRowNumber和自定义读取范围,配合分片逻辑能实现断点续传。这个方案复杂度不低,只有文件稳定超过 10 万行才值得上,小文件别过度设计。

5.4 一个我踩过的坑:别在监听器里开事务

早期我把事务开在监听器的invoke里,每行一个事务,结果 1 万行跑了 8 分钟。后来改成攒批后整批一个事务,同样的数据 20 秒跑完。事务的粒度要和批处理的粒度对齐,一行一事务是最慢的写法,整文件一事务又太大容易锁表,1000 行一批是平衡点。

导入这件事,写通一次不难,难的是在脏数据、大文件、重复执行这些真实条件下还能稳住。我现在做任何导入功能,第一件事就是建唯一索引和导入日志表,这两样是后悔药,出事时能救命。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询