Java Excel 批量导入 MySQL 实战:从选型到避坑的完整链路
简介这是一份面向Java初学者与后端开发者的实战型项目源码聚焦Excel表格数据与MySQL数据库之间的双向流转解决批量导入、重复数据更新及反向导出等常见业务需求。资源包共20个文件约1.31MB包含6个java源文件与6个class编译文件、2个jar依赖库如mysql-connector与jxl、1个sql建表脚本以及Eclipse工程配置与说明文档结构完整可直接导入运行。项目覆盖Java文件操作、Apache POI解析单元格、JDBC连接MySQL、INSERT与UPDATE条件判断、批量插入与事务处理等核心知识点并演示从数据库查询结果写回Excel的完整链路。目前已有1737人学习下载适合希望通过一个可运行案例打通数据导入导出、巩固JDBC与SQL实操能力的学习者参考。1. Java 把 Excel 灌进 MySQL一条被低估的批量导入链路电商后台导商品、教务系统导成绩、财务导流水几乎每个 Java 项目都会撞上「Excel 数据导入到 MySQL」这件事。标题里三个词——Java、Excel、MySQL——看着简单真做起来翻车点全在细节里几万行数据用 POI 的XSSFWorkbook直接 OOM日期列读出来变成一串数字手机号被 Excel 自动转成科学计数法导入一半报主键冲突整批回滚。我见过太多人第一版写完能跑通 500 行上线遇到 5 万行就崩。这篇笔记就围绕「Java 实现 Excel 数据导入到 MySQL」这条链路从选型、读文件、批量写库、事务边界一路讲到避坑。适合正在做数据导入功能的后端开发也适合被excel加载项被禁用、excel ctrl v 失效这类问题折腾过、想彻底搞明白数据怎么从表格进数据库的人。目标很明确给你一套能直接抄、能扛住几万行、参数知道怎么调的方案。2. 选型先定死POI、EasyExcel 还是流式读动手前先把「用什么读 Excel」定下来这一步选错后面全是补丁。Java 生态里读 Excel 主流就三样Apache POI 原生、阿里 EasyExcel、以及 POI 的流式 APISXSSF/事件模型。选型不看谁名气大看你的数据量和内存预算。2.1 三种读法的内存账要算清楚POI 的XSSFWorkbook是 DOM 模式把整个 xlsx 一次性加载进内存建对象树。一个 10 万行、20 列的 xlsx堆内存轻松吃掉 1G 以上OutOfMemoryError: Java heap space是标配。它的好处是 API 直观getRow、getCell随手就取适合几千行以内的小文件。EasyExcel 是阿里开源的封装底层做了 SAX 事件解析一行一行回调内存占用基本恒定跟文件行数无关。它的注解模型ExcelProperty对业务开发很友好读出来的对象直接映射成实体。代价是遇到复杂合并单元格、公式单元格时行为不如 POI 原生可控。POI 的XSSFReaderSheetContentsHandler事件模型是最底层的流式读法内存最省但代码量大每个单元格要自己判断类型、自己拼行写起来啰嗦。适合对性能极致敏感、又不想引第三方库的场景。我的默认选择是 EasyExcel中小项目省事大数据量也能扛。只有在需要精细控制公式求值、或者公司禁止引第三方依赖时才退回 POI 事件模型。方案内存占用开发成本适用行数复杂单元格XSSFWorkbook高随行数线性涨低几千行支持好EasyExcel低基本恒定低几万到百万一般POI 事件模型最低高百万级需自己处理2.2 依赖怎么引版本别乱跳Maven 里引 EasyExcel注意它内部依赖 POI别自己再引一个冲突版本。常见做法是只引 EasyExcel让它带 POI 进来dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version /dependency如果你项目里已经有 POI比如做导出功能要检查版本对齐。EasyExcel 3.x 一般配 POI 4.1.x 或 5.x混用容易出现NoSuchMethodError。排查方法很简单mvn dependency:tree | grep poi看最终生效的是哪个版本冲突就exclusions排掉旧的。提示xls老格式和 xlsx新格式底层实现不同EasyExcel 会自动识别但 xls 单表上限 65536 行超了直接报错导入前最好校验文件后缀和行数。2.3 数据库侧的准备表结构和字符集MySQL 这边导入表建议单独建别直接往业务主表灌。字符集统一用utf8mb4否则中文和 emoji 会出问题。一个典型的导入临时表CREATE TABLE t_import_staging ( id BIGINT PRIMARY KEY AUTO_INCREMENT, row_no INT COMMENT Excel行号用于定位错误, phone VARCHAR(20), name VARCHAR(64), amount DECIMAL(12,2), import_batch VARCHAR(32) COMMENT 批次号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;row_no这个字段很多人不加等出错要定位「第几行数据有问题」时就抓瞎了。import_batch用于批次回滚导入失败按批次删数据比逐行删干净。字段类型要和 Excel 列语义对齐金额用DECIMAL不用FLOAT避免精度丢失手机号用VARCHAR不用BIGINT防止前导零丢失。3. 读 Excel 的实操从注解映射到类型转换选型定了进入读文件环节。这一章把「怎么把一行 Excel 变成 Java 对象」讲透重点在类型转换和校验因为 90% 的导入 bug 都出在这里。3.1 用注解把列映射成实体EasyExcel 的核心是实体类加注解。假设 Excel 有「手机号、姓名、金额」三列Data public class ImportRow { ExcelProperty(index 0) private String phone; ExcelProperty(index 1) private String name; ExcelProperty(index 2) private BigDecimal amount; }index按列顺序从 0 开始比用value 手机号匹配表头更稳——表头文字改一个字按名字匹配就全错位按 index 不受影响。代价是列顺序变了要改代码所以导入模板要固定别让用户随便调列。读的时候用监听器模式别用EasyExcel.read(...).doReadSync()一次性读全部那个方法会把所有行攒在内存里等于白用流式EasyExcel.read(inputStream, ImportRow.class, new ImportListener()) .sheet() .doRead();3.2 监听器里做校验和攒批监听器是流式读的核心invoke每读一行调一次doAfterAllAnalysed全部读完调一次。攒批写库就靠这两个方法配合public class ImportListener extends AnalysisEventListenerImportRow { private static final int BATCH_SIZE 1000; private final ListImportRow buffer new ArrayList(BATCH_SIZE); private final ImportService service; public ImportListener(ImportService service) { this.service service; } Override public void invoke(ImportRow row, AnalysisContext ctx) { int rowNo ctx.readRowHolder().getRowIndex() 1; // 校验手机号非空且为11位数字 if (row.getPhone() null || !row.getPhone().matches(\\d{11})) { throw new IllegalArgumentException(第 rowNo 行手机号非法); } buffer.add(row); if (buffer.size() BATCH_SIZE) { service.batchInsert(buffer); buffer.clear(); } } Override public void doAfterAllAnalysed(AnalysisContext ctx) { if (!buffer.isEmpty()) { service.batchInsert(buffer); buffer.clear(); } } }BATCH_SIZE设 1000 是个经验值太小则 SQL 次数多、网络往返开销大太大则单条 SQL 过长可能撞max_allowed_packet。1000 行、每行几个字段拼出来的INSERT一般几十 KB安全。ctx.readRowHolder().getRowIndex()拿的是 0 基行号加 1 才是用户看到的行号报错信息里带上它用户能直接定位。3.3 类型转换的坑日期、数字、科学计数法Excel 单元格类型是玄学重灾区。日期在 xlsx 里存的是数字从 1900-01-01 起的天数POI 读出来是double直接toString会得到45123这种鬼东西。EasyExcel 用DateTimeFormat能自动转ExcelProperty(index 3) DateTimeFormat(yyyy-MM-dd) private Date orderDate;但前提是单元格本身是日期格式。如果用户把日期填成了文本「2024/1/5」注解转换会失败。稳妥做法是读成String自己在业务层用DateTimeFormatter多格式尝试解析兼容yyyy-MM-dd、yyyy/MM/dd两种写法。手机号、身份证这类长数字Excel 默认按数值处理超过 11 位就变科学计数法1.38E10。解决办法有两个一是要求用户导入前把该列设成文本格式二是读的时候统一按String接EasyExcel 对数值型单元格转 String 时会尽量保留原值但科学计数法已经发生的救不回来。所以模板里最好把手机号列预设成文本这是最省心的。注意金额列如果 Excel 里带千分位逗号1,234.56直接转BigDecimal会抛NumberFormatException。读成 String 后先replace(,, )再转。4. 批量写 MySQLJDBC 批处理与事务边界数据读进来了怎么写库是另一半。逐条insert在几万行面前就是灾难必须走批处理。这一章讲清楚 JDBC batch、事务边界、以及和 MyBatis 的配合。4.1 JDBC 原生批处理怎么写不用 ORM 的话PreparedStatement.addBatch()executeBatch()是最直接的批量插入public void batchInsert(ListImportRow rows) { String sql INSERT INTO t_import_staging(phone,name,amount,import_batch) VALUES(?,?,?,?); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (ImportRow row : rows) { ps.setString(1, row.getPhone()); ps.setString(2, row.getName()); ps.setBigDecimal(3, row.getAmount()); ps.setString(4, currentBatchNo); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { throw new RuntimeException(批量插入失败, e); } }关键在连接串上要开rewriteBatchedStatementstrue否则 MySQL 驱动会把 batch 拆成一条条发批量等于没批jdbc:mysql://localhost:3306/db?rewriteBatchedStatementstrueuseServerPrepStmtstruerewriteBatchedStatementstrue让驱动把多条INSERT重写成INSERT INTO ... VALUES (...),(...),(...)一条语句性能能差好几倍。useServerPrepStmtstrue让预处理在服务端做配合使用效果更好。这两个参数是批量导入的必调项很多人不知道写完发现慢还以为是数据库问题。4.2 事务边界整批一个事务还是分批提交事务粒度是个权衡。整批一个事务几万行一次 commit的好处是失败全回滚数据一致坏处是事务日志大、锁持有时间长、失败重试成本高。分批提交每 1000 行一个事务吞吐高但中途失败会留下半截数据。我的做法是导入到临时表用import_batch标记批次每批一个事务。全部导完后再用一条INSERT INTO 业务表 SELECT ... FROM 临时表 WHERE import_batch?做最终落库。这样即使中途失败按批次号删掉临时数据重来即可业务表始终干净。这个模式在数据量大的场景下比单事务更可控。4.3 和 MyBatis 配合时的写法项目用 MyBatis 的话别在 XML 里foreach拼几万行VALUESSQL 会超长。正确姿势是 Mapper 方法接收List用ExecutorType.BATCHAutowired private SqlSessionFactory sqlSessionFactory; public void batchInsert(ListImportRow rows) { try (SqlSession session sqlSessionFactory.openSession(ExecutorType.BATCH)) { ImportMapper mapper session.getMapper(ImportMapper.class); for (ImportRow row : rows) { mapper.insert(row); } session.commit(); } }ExecutorType.BATCH会把多次insert攒起来到 commit 时一起发。注意它和一级缓存、自增主键回填有交互如果业务依赖插入后拿自增 IDBATCH 模式下拿不到得改用其他方式。这是踩过的坑别等主键为 null 才发现。5. 避坑与排查导入功能最常见的 5 个翻车现场功能能跑通不代表能上线。这一章列 5 个我真实遇到过的坑按「现象 → 原因 → 解决」写都是血泪经验。5.1 现象几万行导入直接 OOM原因用了XSSFWorkbook或doReadSync()整个文件加载进内存。解决换 EasyExcel 监听器模式或 POI 事件模型确保读的时候不攒全量数据。检查方法把 JVM 堆调小到 256M 跑一次能过说明是真流式。5.2 现象导入后中文变问号原因数据库连接串没指定字符集或表字符集是latin1。解决连接串加characterEncodingutf8表统一utf8mb4。已经乱码的数据救不回来只能重导。排查时先SHOW CREATE TABLE看表字符集再看连接串。5.3 现象日期列读出来是 45123 这种数字原因单元格是日期格式POI 读成数值型。解决实体字段加DateTimeFormat或读成 String 后自己解析。如果用户填的是文本日期注解会失效所以业务层要有兜底解析逻辑。5.4 现象批量插入慢几万行要几分钟原因连接串没开rewriteBatchedStatementstruebatch 被拆成单条。解决加上这个参数配合BATCH_SIZE1000 左右。验证方法开 MySQL 的general_log看实际发出的 SQL如果是一条条INSERT就是没生效。5.5 现象导入一半报主键冲突整批回滚原因Excel 里有重复数据或和已有数据冲突。解决导入前用临时表 批次号冲突在临时表阶段暴露业务表不受影响对重复行做去重或标记跳过。别用INSERT IGNORE掩盖问题该报的错要报出来让用户改。6. 进阶把导入做成可复用、可回滚的组件前面讲的是一条能跑通的链路但真实项目里导入功能会被反复用值得把它抽成组件。我一般会做三件事统一模板校验、批次回滚、异步化。模板校验放在读之前校验表头列名和顺序是否匹配不匹配直接拒绝避免读到一半才发现列错位。批次回滚靠import_batch字段提供一个「撤销本次导入」的接口按批次号删临时表数据。异步化则是把导入任务丢进线程池或消息队列接口立即返回任务 ID前端轮询进度避免大文件导入把 HTTP 请求拖超时。进度反馈有个小技巧EasyExcel 的监听器里能拿到AnalysisContext但拿不到总行数。可以在读之前先用事件模型扫一遍 sheet 拿总行数或者干脆按已处理行数报进度前端显示「已处理 N 行」。别为了精确百分比再读一遍文件得不偿失。验证导入结果我习惯写一个对账 SQL临时表行数、业务表新增行数、Excel 原始行数三者对齐差一行都要查。这个习惯帮我抓到过好几次「监听器漏了最后一批 buffer」的 bug——doAfterAllAnalysed里忘了 flush 剩余数据前面全对就差最后不足 1000 行的那批。最后说个我自己的教训导入功能一定要在测试环境用真实量级的数据压一遍别拿 100 行测完就上线。我吃过一次亏500 行跑得飞快生产 8 万行直接把数据库连接池打满因为每批都新开连接没复用。后来改成批量方法内复用连接、控制并发才稳住。数据导入这活儿细节决定成败慢一点、稳一点比快更重要。希望帮到你。本文还有配套的精品资源点击获取