news 2026/10/23 4:47:59

Java实现Excel数据导入MySQL:jxl解析与JDBC批量upsert实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java实现Excel数据导入MySQL:jxl解析与JDBC批量upsert实战

简介:这是一份面向Java后端初学者与数据处理开发者的实战项目源码,聚焦Excel表格数据与MySQL数据库之间的双向流转,解决批量导入、重复数据更新及反向导出等常见业务需求。压缩包共20个文件,约1.31MB,包含6个java源文件与6个class编译文件、2个jar依赖库、1个sql建表脚本,以及Eclipse工程配置与说明文档,结构完整可直接导入IDE运行。项目以Apache POI解析xls与xlsx单元格内容,通过JDBC建立MySQL连接,结合INSERT与UPDATE语句实现存在即更新、不存在则插入的导入策略,并支持将库中数据回写至Excel。已有1737人学习下载,读者可借此掌握文件操作、数据解析、数据库连接与SQL操作等核心技能,理解批量插入与事务处理的优化思路,是一份适合练手与二次开发的参考范例。

1. 一个老掉牙的 Eclipse 工程,为什么还值得翻出来拆

前阵子帮一个做 ERP 二次开发的朋友看数据迁移的活儿,对方甩过来一个压缩包,名字就叫java实现Excel数据导入到mysql数据库.zip。解压之后我愣了一下——.classpath、.project、.settings一整套 Eclipse 工程配置,lib里躺着mysql-connector-java-5[1].1.7-bin.jar和jxl.jar,src下是com.javen包,根目录还有一份javenforexcel.sql。这明显是十年前那批 Java 培训班的实战作业风格,但恰恰是这种"土"工程,把 Excel 解析、JDBC 连接、批量入库、存在即更新这几件事串成了一条完整链路,比现在很多只会调 MyBatis-Plus 的年轻人写得更完整。

它解决的核心问题很具体:把 Excel 表格里的行数据读出来,写进 MySQL,如果库里已经有相同记录就更新而不是重复插入,反过来也能把库里的数据导出成 Excel。适合谁?做后台管理系统、做数据初始化脚本、做运营批量导入功能的 Java 开发者,尤其是还在维护老项目、用 JDK 8 甚至更低版本、不想引入 Spring 全家桶的场景。下面我按"能跑起来"的标准,把这个包从头拆一遍。

2. 工程结构与依赖:先看清 lib 里那两个 jar 的分工

2.1 jxl 和 mysql-connector 各管什么

这个工程最值得先讲清楚的是lib目录。里面有两个 jar,很多人一看就懵:不是有 Apache POI 吗,怎么用的是 jxl?

jxl.jar是 JExcelAPI,一个比 POI 更轻量的 Excel 读写库,专门处理.xls(也就是 Excel 97-2003 的二进制格式)。它的 API 比 POI 简单,Workbook、Sheet、Cell三层结构,读单元格直接sheet.getCell(col, row).getContents()拿字符串,不用像 POI 那样区分HSSFCell和XSSFCell。代价是它不支持.xlsx,也不支持 Excel 2007 之后的新特性。这个工程选 jxl,说明目标数据源就是老式.xls文件,或者导出时也生成.xls。

mysql-connector-java-5[1].1.7-bin.jar是 MySQL 的 JDBC 驱动,版本号里的[1]是 Windows 下载重名时系统自动加的后缀,实际就是 5.1.7。这个版本对应 MySQL 5.x 服务端,驱动类名是com.mysql.jdbc.Driver,注意不是后来 8.0 的com.mysql.cj.jdbc.Driver。连接串里如果没加useSSL=false,在某些 MySQL 5.7 环境会报 SSL 警告甚至连接失败,这是后面避坑章要展开的点。

.classpath和.project是 Eclipse 的工程元数据,.classpath里会声明lib下两个 jar 的路径和src源码目录,.settings里是编译级别、编码格式这些。如果你用 IDEA 打开,这两个文件基本会被忽略,需要手动在 Project Structure 里把 jar 加进 Libraries。

2.2 建表脚本 javenforexcel.sql 怎么读

根目录的javenforexcel.sql是初始化脚本,通常包含CREATE DATABASE和CREATE TABLE。导入前必须先执行它,否则 Java 代码连上去找不到表会直接抛SQLException。典型结构长这样:

CREATE DATABASE IF NOT EXISTS javenforexcel DEFAULT CHARSET utf8; USE javenforexcel; CREATE TABLE user_info ( id INT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50) NOT NULL, user_age INT DEFAULT 0, create_time DATETIME, UNIQUE KEY uk_user_name (user_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这里的关键是UNIQUE KEY uk_user_name (user_name)。工程要实现"存在即更新",靠的就是这个唯一索引配合INSERT ... ON DUPLICATE KEY UPDATE,而不是先SELECT再判断。没有唯一约束,更新逻辑就无从谈起,这是很多人抄代码时最容易漏的一步。

提示:执行脚本前确认 MySQL 服务端字符集。如果服务端是latin1,中文会变问号,建库时显式指定utf8或utf8mb4。

2.3 把工程跑起来的最小步骤

先别急着看业务代码,按这个顺序把环境搭好:

  1. 本地装 MySQL 5.x,用命令行或客户端执行javenforexcel.sql,确认表建出来了。
  2. 用 Eclipse 以Existing Projects into Workspace方式导入解压目录,它会自动识别.project。
  3. 右键工程 → Build Path → Configure Build Path,检查lib下两个 jar 是否在 Libraries 里,缺了就 Add JARs 补上。
  4. 打开src/com/javen下的主类,找到数据库连接那几行,把 URL、用户名、密码改成你本地的。
  5. Run As → Java Application,观察控制台输出。

这五步里第 3 步最容易翻车,因为.classpath里写的是相对路径,换台机器或换目录后路径对不上,jar 就丢了。

3. 读 Excel 与写库:jxl 解析 + JDBC 批量 upsert 的完整链路

3.1 用 jxl 把 .xls 逐行读成对象

jxl 读文件的核心就三行:拿Workbook、拿Sheet、遍历Cell。下面这段是工程里读取逻辑的典型写法,我按可复现的标准补全了类型判断和空行跳过:

import jxl.Workbook; import jxl.Sheet; import jxl.Cell; import java.io.File; import java.util.ArrayList; import java.util.List; public List<UserInfo> readExcel(String filePath) throws Exception { List<UserInfo> list = new ArrayList<>(); // 直接打开文件,jxl 只认 .xls,传 .xlsx 会抛 BiffException Workbook workbook = Workbook.getWorkbook(new File(filePath)); // getSheet(0) 取第一个工作表,多 sheet 场景要按名字取 Sheet sheet = workbook.getSheet(0); int rows = sheet.getRows(); // 从第 1 行开始,第 0 行是表头,跳过 for (int i = 1; i < rows; i++) { Cell[] cells = sheet.getRow(i); // 整行为空则跳过,避免把空行写进库 if (cells.length == 0 || cells[0].getContents().trim().isEmpty()) { continue; } UserInfo u = new UserInfo(); u.setUserName(cells[0].getContents().trim()); // 年龄列可能为空,做一次兜底解析 String ageStr = cells.length > 1 ? cells[1].getContents().trim() : ""; u.setUserAge(ageStr.isEmpty() ? 0 : Integer.parseInt(ageStr)); list.add(u); } workbook.close(); // 必须关,否则文件句柄泄漏 return list; }

逻辑说明:Workbook.getWorkbook(File)是静态工厂方法,内部根据文件头判断格式,只支持 BIFF 格式即.xls。sheet.getRow(i)返回该行所有Cell,注意 jxl 的getRow返回数组长度等于该行实际有内容的列数,不是固定列数,所以取列前要判cells.length。getContents()永远返回字符串,日期单元格会返回格式化后的文本,数字返回原样字符串,这也是 jxl 比 POI 省事的地方——不用自己判断CellType。

参数说明:filePath建议用绝对路径,相对路径在 Eclipse 里是相对工程根目录,打包成 jar 后又是相对 jar 所在目录,很容易找不到文件。getSheet(0)的下标从 0 开始,如果 Excel 有多个 sheet,要改成getSheet("SheetName")。

3.2 JDBC 连接与 ON DUPLICATE KEY UPDATE

读出来之后就是写库。工程用的是原生 JDBC,没有连接池,这对小批量导入够用。核心 SQL 用INSERT ... ON DUPLICATE KEY UPDATE一条语句同时覆盖新增和更新:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public int saveToMysql(List<UserInfo> list) throws Exception { // 5.1.x 驱动类名,8.0 驱动要换成 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/javenforexcel" + "?useUnicode=true&characterEncoding=utf8&useSSL=false"; Connection conn = DriverManager.getConnection(url, "root", "123456"); // 关闭自动提交,配合批量执行提升吞吐 conn.setAutoCommit(false); String sql = "INSERT INTO user_info(user_name, user_age, create_time) " + "VALUES(?, ?, NOW()) " + "ON DUPLICATE KEY UPDATE user_age = VALUES(user_age), " + "create_time = NOW()"; PreparedStatement ps = conn.prepareStatement(sql); for (UserInfo u : list) { ps.setString(1, u.getUserName()); ps.setInt(2, u.getUserAge()); ps.addBatch(); // 加入批次,不立即执行 } int[] result = ps.executeBatch(); conn.commit(); // 手动提交 ps.close(); conn.close(); return result.length; }

逻辑说明:ON DUPLICATE KEY UPDATE依赖表上的唯一索引或主键。当插入的user_name已存在,MySQL 不报错,转而执行UPDATE子句。VALUES(user_age)是 MySQL 特有语法,取的是本次INSERT想插入的值,不是表里旧值。addBatch()把多条语句攒起来,executeBatch()一次性发给服务端,比循环executeUpdate()快一个数量级。

参数说明:useSSL=false在 MySQL 5.7 上能消掉 SSL 握手警告,5.1.7 驱动默认走的是老协议,不加这个参数在部分环境会卡在连接阶段。characterEncoding=utf8保证中文不乱码,要和建库时的字符集一致。setAutoCommit(false)加commit()是事务控制,批量中途出错可以rollback()回滚,避免写一半。

3.3 导出方向:从 MySQL 查出来写回 .xls

反向导出用的是 jxl 的写 API,WritableWorkbook和Label:

import jxl.Workbook; import jxl.write.Label; import jxl.write.WritableSheet; import jxl.write.WritableWorkbook; import java.sql.*; public void exportToExcel(String outPath) throws Exception { WritableWorkbook wwb = Workbook.createWorkbook(new File(outPath)); WritableSheet ws = wwb.createSheet("用户数据", 0); // 写表头 ws.addCell(new Label(0, 0, "用户名")); ws.addCell(new Label(1, 0, "年龄")); Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/javenforexcel?useSSL=false", "root", "123456"); Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT user_name, user_age FROM user_info"); int row = 1; while (rs.next()) { // Label 的构造是 (列, 行, 内容),注意顺序是列在前 ws.addCell(new Label(0, row, rs.getString("user_name"))); ws.addCell(new Label(1, row, String.valueOf(rs.getInt("user_age")))); row++; } wwb.write(); wwb.close(); // 不 close 文件是空的 conn.close(); }

逻辑说明:Label是文本单元格,数字也可以用jxl.write.Number,但导出场景用Label更省事,Excel 里照样能识别。Label(col, row, content)的参数顺序是列在前行在后,和很多人直觉相反,写反了数据会错位。wwb.write()把内存中的工作簿刷到文件,wwb.close()释放资源,两个都不能省,只write不close会得到一个 0 字节文件。

参数说明:createSheet(name, index)的 index 是 sheet 顺序,从 0 开始。导出路径同样建议绝对路径。如果数据量大,Label逐个 add 会慢,可以攒一批再写,但 jxl 本身对大数据量支持一般,超过几万行建议换 POI 的 SXSSF。

4. 避坑与排查:这套老工程最容易翻车的五个点

4.1 报 BiffException:文件根本不是 .xls

现象:运行读取代码,控制台抛jxl.read.biff.BiffException: Unable to recognize OLE stream。

原因:jxl 只认.xls二进制格式。如果文件实际是.xlsx(本质是 zip 包),或者是从网页导出的伪装成.xls的 HTML 表格,jxl 都读不了。

解决:用 Excel 打开文件另存为"Excel 97-2003 工作簿 (*.xls)"。如果是 HTML 伪装的,另存时选真正的 xls 格式。要处理.xlsx就得换 Apache POI 的XSSFWorkbook,jxl 这条路走不通。

4.2 中文乱码:连接串和建库字符集不一致

现象:导入后库里中文显示成???或乱码。

原因:三个环节的字符集没对齐——Excel 文件编码、JDBC 连接串的characterEncoding、MySQL 库表的字符集。任何一环是latin1都会出问题。

解决:建库时DEFAULT CHARSET utf8,连接串加useUnicode=true&characterEncoding=utf8,MySQL 服务端my.cnf里character-set-server=utf8。三处统一后重启 MySQL 再试。

4.3 连接被拒:驱动版本和 SSL 参数

现象:Communications link failure或SSL connection error。

原因:5.1.7 驱动连 MySQL 5.7/8.0 时,服务端默认要求 SSL,老驱动握手失败。或者 URL 里端口、主机写错。

解决:连接串加useSSL=false。如果服务端是 MySQL 8.0,5.1.7 驱动可能连认证都过不了(8.0 默认caching_sha2_password),需要把驱动换成mysql-connector-java-8.0.x,同时驱动类名改成com.mysql.cj.jdbc.Driver,URL 加serverTimezone=Asia/Shanghai。

4.4 更新不生效:唯一索引没建

现象:明明库里已有同名记录,导入后却多了一条重复数据,ON DUPLICATE KEY UPDATE像没起作用。

原因:ON DUPLICATE KEY UPDATE只在触发主键或唯一索引冲突时才走更新分支。如果user_name上没建唯一索引,每次插入都被当成新记录。

解决:ALTER TABLE user_info ADD UNIQUE KEY uk_user_name (user_name);。加之前先清理已有重复数据,否则加索引会失败。

4.5 文件句柄泄漏:Workbook 没 close

现象:批量处理多个 Excel 文件时,跑到第几十个报Too many open files,或者文件被占用无法删除。

原因:Workbook和WritableWorkbook底层持有文件流,不close()就不释放。异常路径下更容易漏。

解决:用 try-finally 或 try-with-resources 包住,确保close()一定执行。jxl 的Workbook实现了Closeable,JDK 7 以上可以直接 try-with-resources。

5. 进阶:把逐条 upsert 换成真正的批量,以及怎么验证没写漏

上面那套代码能跑,但数据量上到几万行就会暴露问题:addBatch攒的是同一条 SQL 的多组参数,MySQL 对单次 batch 的包大小有限制,max_allowed_packet默认 4MB,超了直接断连。我一般会做两件事。

第一,分批提交。把 list 按 500 或 1000 条切块,每块执行一次executeBatch加commit,既控制包大小,又避免一个超长事务锁表太久:

int batchSize = 500; for (int i = 0; i < list.size(); i += batchSize) { int end = Math.min(i + batchSize, list.size()); for (UserInfo u : list.subList(i, end)) { ps.setString(1, u.getUserName()); ps.setInt(2, u.getUserAge()); ps.addBatch(); } ps.executeBatch(); conn.commit(); ps.clearBatch(); // 清空批次,准备下一轮 }

clearBatch()这行很多人漏掉,不清的话下一轮addBatch会接着上一轮的参数继续攒,越滚越大。

第二,验证导入结果。别信控制台打印的"成功 N 条",直接查库对账:

-- Excel 总行数(减表头)应该等于这个 SELECT COUNT(*) FROM user_info; -- 检查有没有年龄为 0 的异常数据,可能是解析失败 SELECT * FROM user_info WHERE user_age = 0; -- 检查重复,正常应该为空 SELECT user_name, COUNT(*) c FROM user_info GROUP BY user_name HAVING c > 1;

我自己的习惯是,每次导入前先把目标表SELECT COUNT(*)记下来,导入后再查一次,差值应该等于 Excel 里新增的行数;再跑一遍同样的导入,总数不变才说明 upsert 真的生效了。这个"跑两遍看总数"的验证法,比看任何日志都靠谱。

还有个边界:jxl 读日期单元格时getContents()返回的是格式化字符串,如果 Excel 里日期格式是2024/1/5,直接当字符串入库没问题,但要转成Date就得用DateCell强转再取getDate(),否则SimpleDateFormat解析会因格式不匹配抛异常。这个坑我在做考勤数据导入时踩过,Excel 里同列日期格式不统一,一半能解析一半报错,最后是统一在 Excel 侧先规范格式才过的。

从那以后我每次接 Excel 导入的活儿,都强制先做三件事:确认文件真实格式、确认目标表唯一索引、跑两遍验证总数。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 15:37:43

UltraScale系列时钟资源核心解析:BUFGCE_DIV与MMCM应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 20:12:34

深入理解 C# 接口:从基础语法到实战应用

1. 引言在 C# 面向对象编程中&#xff0c;接口&#xff08;Interface&#xff09;是一种重要的抽象机制&#xff0c;它定义了类或结构体必须实现的一组方法、属性和事件。接口本身不包含实现代码&#xff0c;只声明成员签名&#xff0c;由实现它的类或结构体提供具体实现。本文…

作者头像 李华
网站建设 2026/10/23 16:10:02

继承基类与实现接口:功能对比与代码示例

一、引言在面向对象编程中&#xff0c;继承“基类”和实现“接口”都能实现某些相同的功能&#xff0c;但有些接口能够完成的功能是只用基类无法实现的。本文将通过对比和代码示例&#xff0c;帮助读者理解两者的区别与适用场景。二、接口的核心作用1、接口用于描述一组类的公共…

作者头像 李华
网站建设 2026/10/23 19:17:57

pstack 合并安全机制:为什么 SHA 一过期就拒绝合并?

pstack 合并安全机制&#xff1a;为什么 SHA 一过期就拒绝合并&#xff1f; 【免费下载链接】pstack-claude Claude Code, Codex, Copilot, Pi, OpenCode, Gemini, and Prime Agent versions of Potetos pstack. Rigorous agent workflows with Cursor primitives translated f…

作者头像 李华
网站建设 2026/10/23 10:46:26

基于MCP协议与GPT-4o的AI旅游规划产品实战:3天上线全记录

1. 为什么我盯上了MCP这个协议先说结论&#xff1a;MCP&#xff08;Model Context Protocol&#xff09;本质上就是给大模型装了一双手。以前我们用GPT-4o写代码、查资料&#xff0c;它只能“说”&#xff0c;不能“做”。你想让它帮你查个航班、订个酒店、调个地图API&#xf…

作者头像 李华
网站建设 2026/10/23 16:08:22

AI工程师必备:7天搞懂大模型核心概念与工程实践

1. 这不是词典&#xff0c;是技术人国庆假期的实战认知地图“AI概念大全&#xff1a;技术人的国庆7天扫盲指南”——看到这个标题&#xff0c;我第一反应不是去翻 glossary&#xff08;术语表&#xff09;&#xff0c;而是立刻掏出手机查了查日历&#xff1a;今年国庆七天假&am…

作者头像 李华