简介:这是一份面向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 把工程跑起来的最小步骤
先别急着看业务代码,按这个顺序把环境搭好:
- 本地装 MySQL 5.x,用命令行或客户端执行
javenforexcel.sql,确认表建出来了。 - 用 Eclipse 以
Existing Projects into Workspace方式导入解压目录,它会自动识别.project。 - 右键工程 → Build Path → Configure Build Path,检查
lib下两个 jar 是否在 Libraries 里,缺了就 Add JARs 补上。 - 打开
src/com/javen下的主类,找到数据库连接那几行,把 URL、用户名、密码改成你本地的。 - 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 导入的活儿,都强制先做三件事:确认文件真实格式、确认目标表唯一索引、跑两遍验证总数。希望帮到你。
本文还有配套的精品资源,点击获取