ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java实现Excel导入MySQL:从POI解析到批量插入的完整方案

Java实现Excel导入MySQL:从POI解析到批量插入的完整方案 简介这是一套基于Java实现Excel数据导入MySQL数据库的完整示例项目适合正在学习JDBC、Apache POI/JXL文件解析及MySQL数据同步的Java开发者。项目支持将Excel工作表数据批量写入MySQL若数据库已存在相同数据可自动更新同时提供从数据库导出至Excel的反向操作。整套资源包含20个文件以Java源码、class编译文件为核心附带mysql-connector-java和jxl两个依赖JAR、SQL建表脚本、txt说明及Eclipse工程配置文件压缩包大小仅1.31MB目录结构清晰可直接导入IDE运行调试。目前已有1736人学习能作为企业数据处理场景的实用参考。通过阅读源码与运行示例可掌握Excel单元格解析、PreparedStatement批量操作、事务控制及结果集导出等关键技能理解文件读取到数据库落地的完整链路适合课程设计、毕业设计及日常开发借鉴。1. 从一张 Excel 到 MySQL这个 Java 导入方案解决的不只是“读文件”“java实现Excel数据导入到mysql数据库.zip”这个标题几乎是 Java 后端开发里被搜索最多的需求之一。它本质上是把一张或多张 Excel 工作表里的结构化数据通过 Java 程序解析、校验、转换最终写入 MySQL 的数据表。如果你做过 ERP、OA、电商后台或者任何带“批量导入”按钮的系统一定不会陌生运营部门每个月整理一份商品价格表、人事部导出考勤记录、财务给过来一堆对账单这些场景每天都在催着开发写导入功能。这个方案的核心价值不在于用 Java 读 Excel——那只是个解析动作也不在于往 MySQL 写数据——那只是个 JDBC 操作。真正让这个标题值钱的部分是中间那段“数据的映射与清洗”Excel 里的列名往往和数据库字段对不上日期格式千奇百怪数字列里混着空字符串甚至同一个表头在不同月份的文件里位置都会变。把这些脏数据在进库之前处理干净才是整个导入功能最费时间也最体现功力的地方。这篇文章我们从零搭一个最小可运行的导入工程然后再把话题往深了推大文件怎么优化、批量插入的 batch 参数怎么调、日期和空值的坑在哪里。你有 Java 基础就能跟着做没有 Spring 经验也没关系我会把依赖和配置写到能直接跑起来的程度。先说明一点这里不引入 Spring Boot用最朴素的 JDBC POI 组合把原理讲透你迁移到任何框架里都只需要改一层壳。2. 准备工程环境JDK、MySQL 与 Maven 依赖的选型理由2.1 为什么用 POI 而不是 EasyExcel先解决“用什么读 Excel”的问题。目前 Java 生态里主流的选择有两个Apache POI 和阿里开源的 EasyExcel。EasyExcel 的卖点是低内存占用适合几十万行的大文件但它的 API 封装程度高一旦遇到你完全没见过的单元格类型、合并单元格、复杂的公式计算结果排查起来反而不如 POI 透明。POI 是底层操作模型你对单元格的每一个属性都有直接控制权而且它同时支持 .xlsHSSF和 .xlsxXSSF两种格式兼容性更稳。如果你只想做一个工具类丢到项目里用POI 的依赖体积和复杂度可以接受如果你要处理的是上百万行的导出导入再考虑 EasyExcel 也不迟。我们这里用 POI因为它的数据读取逻辑最接近“逐行逐列”的直觉也最能暴露各种脏数据问题对理解整个链路最有帮助。2.2 建表 SQL 与最小工程目录我先给出一张用于测试的数据库表模拟一个“员工信息导入”的场景字段不多但覆盖面够广有字符串、有整数、有日期还有小数。导入之后你可以立刻验证数据对不对。CREATE DATABASE IF NOT EXISTS excel_import_demo DEFAULT CHARACTER SET utf8mb4; USE excel_import_demo; CREATE TABLE employee ( id INT AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(20) NOT NULL COMMENT 员工编号, emp_name VARCHAR(50) NOT NULL COMMENT 姓名, department VARCHAR(50) DEFAULT NULL COMMENT 部门, salary DECIMAL(10,2) DEFAULT 0.00 COMMENT 月薪, hire_date DATE DEFAULT NULL COMMENT 入职日期, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 导入时间, UNIQUE KEY uk_emp_no (emp_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工导入测试表;这个表结构里有两个关键设计。第一个是UNIQUE KEY uk_emp_no它让“重复导入同一员工编号”时数据库直接拒绝这是防止重复数据的第一道防线第二个是hire_date用 DATE 类型这要求你在 Java 侧必须把 Excel 里的日期格式解析干净否则很容易报DataTruncation错误。工程目录不需要多复杂Maven 单模块就够用excel-import-demo/ ├── pom.xml ├── src/main/java/com/example/excelimport/ │ ├── ExcelImporter.java │ ├── Employee.java │ ├── JdbcUtils.java │ └── Main.java └── src/main/resources/ └── employee_import.xlsx2.3 pom.xml 里必须锁定的依赖版本properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target poi.version5.2.3/poi.version /properties dependencies dependency groupIdorg.apache.poi/groupId artifactIdpoi/artifactId version${poi.version}/version /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version${poi.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependency /dependenciesPOI 5.x 的依赖比 4.x 多了poi-ooxml里的 XML 解析支持同时它需要commons-io作为传递依赖Maven 会自动拉取。MySQL Connector/J 8.0.x 对 MySQL 5.7 和 8.0 都兼容驱动类名是com.mysql.cj.jdbc.Driver如果你的项目里还在用com.mysql.jdbc.Driver记得改成新的否则连接会警告但能跑。JDK 版本不用刻意追新8 就足够跑通所有代码生产环境里多数老项目也还停在 8。3. 读取 Excel 的完整代码从 Workbook 到自定义实体类3.1 解析 .xlsx 表格的核心 API 与逐行读取逻辑POI 读取 Excel 的标准姿势是拿到Workbook然后通过getSheetAt(0)拿到第一个工作表再从getPhysicalNumberOfRows()确定总行数最后逐行getRow(i)、逐列getCell(j)取值。先不要急着写实体映射把“读出来”这一步做扎实你才能看到真实数据里到底藏了多少问题。import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.FileInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.List; public class ExcelImporter { public static ListEmployee parseExcelToEmployees(String filePath) throws Exception { ListEmployee employeeList new ArrayList(); try (InputStream fis new FileInputStream(filePath); Workbook workbook new XSSFWorkbook(fis)) { Sheet sheet workbook.getSheetAt(0); int lastRowNum sheet.getLastRowNum(); // 默认第一行是表头从第二行开始读数据 for (int i 1; i lastRowNum; i) { Row row sheet.getRow(i); if (row null || isRowEmpty(row)) { continue; } Employee emp new Employee(); emp.setEmpNo(getCellStringValue(row.getCell(0))); emp.setEmpName(getCellStringValue(row.getCell(1))); emp.setDepartment(getCellStringValue(row.getCell(2))); emp.setSalary(getNumericCellValue(row.getCell(3))); emp.setHireDate(getDateCellValue(row.getCell(4))); employeeList.add(emp); } } return employeeList; } private static boolean isRowEmpty(Row row) { for (int c 0; c row.getLastCellNum(); c) { Cell cell row.getCell(c); if (cell ! null cell.getCellType() ! CellType.BLANK) { return false; } } return true; } }这段代码里isRowEmpty是第一个容易忽视的细节。Excel 文件里经常会有“看起来是空的”行——用户为了排版按了很多回车或者格式刷把边框带到了空行上——如果你不跳过这些行后面入库时可能插入一堆 null 记录。另一个细节是getLastRowNum()返回的是索引比如你有 100 行数据它返回 99所以循环条件用千万不要写成否则最后一行永远读不到。3.2 三个单元格取值工具方法字符串、数字、日期下面这三个方法是我从多个项目里磨出来的通用基础版。你以后写任何格式的 Excel 导入都可以复用它们遇到新格式只需要调整这里的判断逻辑不用去改主流程。private static String getCellStringValue(Cell cell) { if (cell null) { return null; } switch (cell.getCellType()) { case STRING: return cell.getStringCellValue().trim(); case NUMERIC: double numericValue cell.getNumericCellValue(); // 处理科学计数法比如手机号、工号 if (numericValue Math.floor(numericValue)) { return String.valueOf((long) numericValue); } return String.valueOf(numericValue); case BOOLEAN: return String.valueOf(cell.getBooleanCellValue()); case FORMULA: return getCellStringValue(cell.getSheet().getWorkbook().getCreationHelper() .createFormulaEvaluator().evaluate(cell)); default: return null; } }字符串读取里的trim()太重要了。Excel 单元格里用户经常手滑敲出前后空格比如“张三 ”和“张三”在 Java 字符串比较里是两个东西但在数据库的唯一索引眼里它们可能是同一个人。数值转字符串时9.0会被String.valueOf出9.0而不是你想要的9所以我加了Math.floor判断把整数浮点数转成 long 再转字符串这样工号“1001”读出来就是干净的“1001”不是“1001.0”。日期读取是另一个重灾区。private static java.sql.Date getDateCellValue(Cell cell) { if (cell null) { return null; } if (cell.getCellType() CellType.STRING) { String value cell.getStringCellValue().trim(); if (value.isEmpty()) { return null; } // 支持多种常见格式按需扩充 String[] patterns {yyyy-MM-dd, yyyy/MM/dd, yyyy.MM.dd}; for (String pattern : patterns) { try { java.text.SimpleDateFormat sdf new java.text.SimpleDateFormat(pattern); java.util.Date parsed sdf.parse(value); return new java.sql.Date(parsed.getTime()); } catch (java.text.ParseException ignored) { // 继续尝试下一个格式 } } throw new IllegalArgumentException(无法解析的日期格式: value); } if (cell.getCellType() CellType.NUMERIC) { // POI 对日期单元格的隐藏判断 if (DateUtil.isCellDateFormatted(cell)) { return new java.sql.Date(cell.getDateCellValue().getTime()); } } return null; }DateUtil.isCellDateFormatted(cell)是 POI 判断一个数值单元格是否为日期的核心方法。它的原理是检查单元格的格式编码是否为日期类型比如内置格式 0x16yyyy/m/d或者自定义的 yyyy-mm-dd。很多新手踩过这个坑Excel 里明明显示2024-01-15用getNumericCellValue()读出来却是一个 45241 之类的数字——因为 Excel 的日期本质就是浮点数序列值没有通过isCellDateFormatted判断就直接把它当数字处理了。还有一种情况是日期列里混了几个文本格式的值单元格左上角有个绿色小三角POI 读出来是 STRING 类型所以我的代码里先判断了 STRING再用正则去逐格式匹配这是最稳妥的做法。4. 数据入库 MySQL批量插入、事务边界与连接参数调优4.1 用 PreparedStatement 构建批量插入与手动事务控制数据解析成 List之后就要写库了。这里有个性能分水岭如果你用Statement一条一条 executeUpdate一万条数据可能要几十秒而用PreparedStatement的addBatch()executeBatch()同样数据量可以压到一两秒。差距来自两个层面第一是 SQL 预编译只做一次第二是网络往返次数从“每条一次”变成“每批一次”。import java.sql.*; import java.util.List; public class JdbcUtils { private static final String URL jdbc:mysql://localhost:3306/excel_import_demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue; private static final String USER root; private static final String PASSWORD your_password; public static Connection getConnection() throws SQLException { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动未找到, e); } return DriverManager.getConnection(URL, USER, PASSWORD); } public static void batchInsertEmployees(ListEmployee employees) { String sql INSERT INTO employee (emp_no, emp_name, department, salary, hire_date) VALUES (?, ?, ?, ?, ?); Connection conn null; PreparedStatement ps null; try { conn getConnection(); conn.setAutoCommit(false); ps conn.prepareStatement(sql); int batchSize 500; int count 0; for (Employee emp : employees) { ps.setString(1, emp.getEmpNo()); ps.setString(2, emp.getEmpName()); ps.setString(3, emp.getDepartment()); ps.setBigDecimal(4, emp.getSalary()); ps.setDate(5, emp.getHireDate()); ps.addBatch(); count; if (count % batchSize 0) { ps.executeBatch(); // 每批提交一次避免大事务内存积压 conn.commit(); } } // 处理最后一批不满 500 条的剩余数据 if (count % batchSize ! 0) { ps.executeBatch(); conn.commit(); } } catch (SQLException e) { try { if (conn ! null) { conn.rollback(); } } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(批量插入失败: e.getMessage(), e); } finally { try { if (ps ! null) ps.close(); if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这个实现里最关键的是rewriteBatchedStatementstrue这个 URL 参数。MySQL 的 JDBC 驱动在默认情况下executeBatch()不会真的把多条 INSERT 语句合并成一条多 VALUES 的语句发送而是逐条发送只是省去了客户端的多次编译。加上rewriteBatchedStatementstrue之后驱动才会把INSERT INTO t VALUES (?)重写成INSERT INTO t VALUES (?), (?), (?)...性能提升非常明显。数据量到十万以上时有没有这个参数耗时差距可以到五倍以上。4.2 batchSize 参数怎么选500 条 / 1000 条还是 2000 条我见过很多博客直接把 batchSize 写死 500其实这个参数需要根据两个指标来调单行数据大小和数据库的max_allowed_packet限制。一行数据只有七八个短字段2000 条一批也就几十 KB 的 SQL 包体随便发。但如果有一行里有个超长 TEXT 字段2000 条一批的 SQL 包可能超过 1MB此时 MySQL 默认的max_allowed_packet4MB就可能被打爆报PacketTooBigException。我的建议是分三层调小数据量几百行直接全部提交一次不用分批逻辑代码更简单中等数据量一千到一万行用 5001000 条一批大数据量十万以上用 2000 条一批同时把max_allowed_packet调大到 64MB。另外注意executeBatch()执行后要clearBatch()否则同一批数据会重复追加到下一批。我上面代码里用count % batchSize判断已经隐式处理了这个问题因为每次只是 addBatch 到新的批次但如果你是先循环 addBatch 再统一 executeBatch就必须在每批结束后手动 clearBatch。还有一个细节是setAutoCommit(false)必须在拿到 Connection 后立刻执行不要等插入到一半再设置。事务边界不是从 commit 开始的而是从关闭自动提交那一刻开始的前置设置更符合“这个连接的生命周期就是这一个事务”的直觉。5. 四个月月踩坑实录日期、编码与 Excel 格式的边界问题5.1 现象插入全部成功但表里的日期是空值用户反馈导入后所有员工的入职日期都是 NULL日志里没有任何报错。排查思路是先看 Excel 里这个日期列到底是什么格式——结果发现它根本不是日期格式而是一串形如 “2024-01-15” 的文本。POI 在读取时这个单元格的类型是 STRING 而不是 NUMERIC我就顺着分支走到了日期字符串解析的逻辑里。按道理文本也能解析但模板里的日期格式是 “2024年1月15日”我的 SimpleDateFormat 列表里根本没加这个 pattern。原因找到了日期解析抛出的 IllegalArgumentException 又被上层某处 catch住吞掉了整条数据就变成了只有 null 的残缺记录。解决方法是两件事一是扩充日期 pattern 列表优先把中文日期格式加进去二是把解析失败的单元格值单独收集到一个错误报告里而不是静默置 null。从那次之后我的导入工具一律带一个“错误行导出”日志文件哪一行哪个字段为什么失败一眼就能定位。5.2 现象读取 .xls 老格式时 API 直接抛异常一套代码在测试环境导入employee_import.xlsx一切正常换了个 .xls 文件就开始报错错误指向XSSFWorkbook无法解析文件头。原因很直白我之前直接写了new XSSFWorkbook(fis)这个类只认 OOXML 格式.xlsx遇到 BIFF8 格式.xls就会直接崩溃。解决方法是把 Workbook 的创建改成按文件后缀分派。POI 提供了WorkbookFactory.create(InputStream)它会自动根据文件头识别是 HSSF 还是 XSSF底层帮你创建对应的实现类。不过要注意WorkbookFactory默认对每个文件弹一个“你确定要打开吗”的提示窗口需要额外加一个new WorkbookFactory().create(inputStream)的变体来禁用系统级提示。我在代码里通常写一个静态工具方法用文件名后缀来判断:.xls就走HSSFWorkbook.xlsx就走XSSFWorkbook遇到其他后缀直接抛出明确异常。5.3 现象导入中文全部变成问号数据库里中文变成了“???”或者直接乱码英文正常。这个问题的位置只有两处数据库连接串和表结构。先检查你的连接 URL 里有没有characterEncodingutf8没有就加上。然后检查表结构本身的字符集——如果建表语句里没有指定DEFAULT CHARSETutf8mb4而 MySQL 服务端的默认字符集是 latin1那连接字符集设对也会被表结构再次转错。还有一个隐蔽点如果表已经在库中存在ALTER TABLE employee CONVERT TO CHARACTER SET utf8mb4可以纠正但前提是库里没有已损坏的数据。5.4 现象Excel 第一行是合并单元格的表头很多业务模板为了让标题更醒目会把第一行做成跨列的合并单元格里面写着“XX 公司员工信息表”第二行才是真正的列名。我见过不止一次新手直接把第一行当作表头读结果导入后所有字段错位把表头里的“员工编号”四个字当成 emp_no 插进了数据库。处理逻辑要区分一般情况和复杂情况。一般情况是在代码里加一个startRowIndex配置项外部传入“数据起始行”比如第一行是标题就传 1从 0 开始计数第一行是表头就传 0。这个配置放到 properties 文件里不同模板可以随时改不用重新编译。复杂情况是表头里存在纵向合并单元格即一个列名横跨两列这通常意味着模板结构本身有问题建议推动业务方规范化模板不要指望代码去猜列名。6. 验证导入结果与进阶优化断点续导、幂等与可视化进度6.1 用 SELECT 与文件核对做导入完整性校验导入完成后不能只看“日志输出 success”你要有一套独立于代码逻辑的验证手段。最直接的方法是把数据拉出来比对两个样本行数一致性Excel 的有效数据行数 vs 数据库记录数关键字段抽样比对拿 Excel 里薪资最高的一行、日期最早的一行去库里找同一条。SELECT COUNT(*) AS total_count, COUNT(DISTINCT emp_no) AS unique_count, COUNT(salary) AS has_salary_count, COUNT(hire_date) AS has_hire_date_count FROM employee;当total_count和unique_count不一致时说明有重复的 emp_no 被插入大概率是 Excel 内部本身有重复行当has_salary_count小于total_count说明有空薪资被插入。这两个指标一查导入质量就能量化。还有一个实用技巧把导入前后查一次SELECT COUNT(*)差值就能确认本次脚本到底插入了多少行比应用层的日志可靠得多。6.2 生产环境的两个进阶设计幂等导入和可视化的导入进度在真实项目中导入功能很少是一次性跑完的。文件五万行跑到第两万行时数据库连接断了是重跑整个文件还是从断点继续“断点续导”的方案可以用一个导入批次表来实现在批次表里记录当前文件已处理到的行号每次启动导入时先查一下这个文件是否已有未完成的批次。如果存在直接从上一批的末尾行号继续读 Excel。幂等性则是靠数据库层的唯一索引兜底。如果你的表结构允许业务上有自然主键比如员工编号、商品编码就一定要建唯一索引然后导入时用INSERT ... ON DUPLICATE KEY UPDATE或者INSERT IGNORE。前者会更新冲突行的字段适合“重新导入修正数据”的场景后者直接跳过重复行适合“只补全新数据”的场景。SQL 改成下面这样就不用担心重复导入了String sql INSERT INTO employee (emp_no, emp_name, department, salary, hire_date) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE emp_name VALUES(emp_name), department VALUES(department), salary VALUES(salary), hire_date VALUES(hire_date);VALUES()函数在 MySQL 8.0.20 之后被标记为 deprecated官方推荐改用别名语法AS new ON DUPLICATE KEY UPDATE emp_name new.emp_name但考虑到大部分生产库还在 5.7上面这版兼容性最好。关于导入进度的可视化需求——如果任务超过十秒用户就想要一个进度条或者至少是百分比提示。简单方案是在分批提交时打印一行已完成 x 条 / 共 y 条z%到日志复杂方案是用 WebSocket 推送进度到前端。但核心逻辑不变进度等于“已提交的批次行数 / 总行数”每批次提交后更新一次进度不能每行更新否则进度消息本身会成为性能瓶颈。我的经验是一万行以下不用做前端进度条日志够用十万行以上再上 WebSocket成本收益才划算。7. 从最小实现到通用导入中间件的三步演进如果你只是要解决一次性的数据迁移前面的代码已经够用。但如果想把这套逻辑沉淀成团队内部通用的导入工具还有三个方向值得做。第一个是把 Excel 导入抽象成配置驱动。具体做法是用一个 Map或 JSON 来定义“列映射关系”第 0 列对应数据库字段 emp_no它的类型是 string是否必填是否唯一校验等。这样业务方来了新导入需求只需要提供一份配置文件不需要改代码。我见过一个中型公司做的通用导入平台就是基于 POI 反射 自定义注解实现了这个模式用起来很像 MyBatis 的Results注解但更简洁。第二个方向是引入校验框架。目前代码里的校验都散落在parseExcelToEmployees里业务规则一复杂就变成一团乱麻。引入 Bean Validation 规范在Employee实体类的字段上加NotNull、Pattern等注解解析完直接调用validator.validate()错误信息自动收集。这样做有三个好处校验逻辑和解析逻辑解耦错误信息能精确定位到字段新增校验规则只需要改注解。第三个方向是性能瓶颈的进一步压榨。当 Excel 行数超过五十万时POI 的 XSSF 模式会占满 JVM 堆内存因为它把整个工作表都加载进内存了。此时需要切换到 SAX 模式的XSSFReader或直接上 EasyExcel 的流式读取。这是另一个话题但从架构上看你只需要把 Excel 读取层的接口抽象出来底层替换成流式实现上层业务代码完全不用动。这三个方向按团队实际需要推进优先级从高到低。如果你目前还要手写一个导入功能先别急着上中间件——把这张 Excel 里的脏数据坑摸清楚比任何封装都更值钱。我最早做导入功能时花在调日期格式和空行处理上的时间比写整个插入逻辑还多三倍。这些经验值会沉淀成一笔恒久的资产下次再遇到任何导入需求看一眼文件就能预判出八成的问题。希望这篇笔记能帮你少踩几个我当年踩过的坑。本文还有配套的精品资源点击获取
返回列表