简介这是一份面向Java开发者与数据库运维人员的数据处理工具源码包用于将Excel表格数据自动解析并生成可执行的SQL脚本再通过JDBC连接写入目标数据库适用于数据迁移、批量导入与数据分析等场景。压缩包共56个文件约24.48MB以21个jar依赖、20个class编译文件、11个java源码为主另含classpath配置、xls测试模板与fatjar打包文件结构完整便于直接运行或二次开发。资源核心围绕Apache POI解析Excel、构造INSERT等SQL语句、配置数据库驱动并执行连接操作展开源码可帮助读者理解从工作簿读取、字段映射到语句执行的完整链路。目前已有979人学习下载适合希望掌握Excel到数据库自动化转换思路的开发者参考借鉴。1. 从手工导数到脚本直出这个工具到底解决什么问题做后台系统迁移或者数据初始化的时候最烦的往往不是写 SQL而是从 Excel 里一条条抠数据再拼 INSERT。产品给过来一张表几十列、几百行字段名和数据库列名还对不上手工拼到第三十条就开始眼花。这个项目就是冲着这个场景来的读一个 Excel 文件按配置好的映射关系自动生成可执行的 SQL 脚本顺带还能通过 JDBC 直连数据库把数据灌进去。它适合两类人——一类是经常做数据迁移、初始化脚本的后端开发另一类是测试同学需要批量造数据但不想手写 SQL。核心链路就三步解析 Excel、字段映射、生成或执行 SQL下面把它拆开讲清楚。2. 技术选型与整体架构为什么是 POI JDBC 这套组合2.1 技术栈拆解这个项目的技术栈不复杂属于典型的 Java 工具类应用选型逻辑也很务实。层次技术作用Excel 解析Apache POI读取 xls/xlsx兼容两种格式数据库连接JDBC直连执行 SQL不依赖 ORM配置读取Properties / YAML存字段映射和连接信息构建工具Maven依赖管理和打包日志SLF4J Logback记录解析和执行的每一步为什么不用 EasyExcel如果只是读EasyExcel 内存占用确实更小但这个项目需要拿到单元格的原始类型日期、数字、公式结果POI 的 Cell 对象控制粒度更细改起来也直观。为什么不用 MyBatis 批量插入因为项目的定位是「生成脚本」而不是「做业务写入」JDBC 的addBatch已经够用还少一层配置负担。2.2 整体架构与数据流整个项目可以拆成四个模块数据从 Excel 流到数据库中间经过映射层转换。Excel 文件 │ ▼ [ExcelReader] ── 读取行转成 ListMapString,Object │ ▼ [FieldMapper] ── 按配置把 Excel 列名映射成数据库列名 │ ▼ [SqlBuilder] ── 拼装 INSERT / UPDATE 语句 │ ├──► 输出 .sql 文件 │ ▼ [JdbcExecutor] ── 批量执行事务控制 │ ▼ 目标数据库这个分层的好处是每一层都能单独替换。比如你不想用 POI 了只改 ExcelReader不想生成 INSERT 想生成 UPDATE只改 SqlBuilder。模块之间通过ListMapString,Object这种朴素结构传递数据没有引入额外的 DTO减少理解成本。2.3 目录结构一个典型的工程目录长这样excel2sql/ ├── pom.xml ├── src/main/java/com/example/excel2sql/ │ ├── Main.java # 入口解析命令行参数 │ ├── reader/ │ │ └── ExcelReader.java # POI 读取封装 │ ├── mapper/ │ │ └── FieldMapper.java # 字段映射 │ ├── builder/ │ │ └── SqlBuilder.java # SQL 拼装 │ ├── executor/ │ │ └── JdbcExecutor.java # JDBC 执行 │ └── config/ │ └── AppConfig.java # 配置加载 └── src/main/resources/ ├── mapping.properties # 字段映射配置 └── db.properties # 数据库连接配置Main只做参数解析和流程编排真正的逻辑都在各自模块里。这种结构对二次开发很友好你想加一个「导出 CSV」的功能新建一个exporter包就行不用动现有代码。3. 核心模块拆解从读单元格到拼出 INSERT 语句3.1 ExcelReader把一行行单元格变成 MapPOI 读取的核心是处理单元格类型这是最容易翻车的地方——日期读成数字、数字读成科学计数法都是血泪经验。public ListMapString, Object read(String filePath, int sheetIndex, int headerRow) { ListMapString, Object rows new ArrayList(); try (Workbook workbook WorkbookFactory.create(new File(filePath))) { Sheet sheet workbook.getSheetAt(sheetIndex); Row header sheet.getRow(headerRow); // 先读表头作为 Map 的 key ListString columns new ArrayList(); for (Cell cell : header) { columns.add(getCellValue(cell).toString().trim()); } // 从表头下一行开始读数据 for (int i headerRow 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); if (row null) continue; MapString, Object map new LinkedHashMap(); for (int j 0; j columns.size(); j) { Cell cell row.getCell(j); map.put(columns.get(j), getCellValue(cell)); } rows.add(map); } } catch (Exception e) { log.error(读取 Excel 失败: {}, filePath, e); throw new RuntimeException(Excel 解析异常, e); } return rows; }WorkbookFactory.create会自动识别 xls 和 xlsx不用手动判断。headerRow参数指定表头在第几行因为有些 Excel 前面有标题行。用LinkedHashMap是为了保持列顺序后面拼 SQL 时字段顺序和 Excel 一致方便排查。getCellValue是类型转换的关键单独抽出来private Object getCellValue(Cell cell) { if (cell null) return null; switch (cell.getCellType()) { case STRING: return cell.getStringCellValue().trim(); case NUMERIC: // 日期类型必须单独判断否则会读成 45000 这种数字 if (DateUtil.isCellDateFormatted(cell)) { return new java.sql.Timestamp(cell.getDateCellValue().getTime()); } double d cell.getNumericCellValue(); // 整数去掉小数点避免 1 读成 1.0 if (d Math.floor(d) !Double.isInfinite(d)) { return (long) d; } return d; case BOOLEAN: return cell.getBooleanCellValue(); case FORMULA: return cell.getCellFormula(); default: return null; } }DateUtil.isCellDateFormatted这行是后悔药不加的话日期列会变成一串数字。整数判断是为了让id这种列生成1而不是1.0否则插入 int 列时数据库可能报类型不匹配。3.2 FieldMapperExcel 列名和数据库列名的翻译官Excel 里的表头往往是中文或者业务名数据库列名是英文下划线风格中间需要一层映射。配置写在mapping.properties里# Excel列名数据库列名 用户编号user_id 用户姓名user_name 手机号码phone 注册时间create_time映射逻辑本身很简单但要做容错public MapString, Object map(MapString, Object rawRow) { MapString, Object mapped new LinkedHashMap(); for (Map.EntryString, Object entry : rawRow.entrySet()) { String excelCol entry.getKey(); String dbCol mapping.getProperty(excelCol); if (dbCol null) { // 没有配置映射的列默认转小写下划线或者直接跳过 log.warn(列 [{}] 未配置映射已跳过, excelCol); continue; } mapped.put(dbCol, entry.getValue()); } return mapped; }这里有个设计取舍未配置的列是跳过还是报错我一般选择跳过加 warn 日志因为 Excel 里经常有备注列、说明列强制要求全配置反而累赘。但如果你的场景要求严格把continue改成throw就行。3.3 SqlBuilder拼 INSERT 时怎么防注入和转义拼 SQL 最怕两件事字符串里的单引号没转义导致语法错误以及 null 值处理不当。看核心代码public String buildInsert(String tableName, MapString, Object row) { StringBuilder cols new StringBuilder(); StringBuilder vals new StringBuilder(); for (Map.EntryString, Object entry : row.entrySet()) { cols.append(entry.getKey()).append(,); vals.append(toSqlValue(entry.getValue())).append(,); } // 去掉末尾逗号 cols.setLength(cols.length() - 1); vals.setLength(vals.length() - 1); return String.format(INSERT INTO %s (%s) VALUES (%s);, tableName, cols, vals); } private String toSqlValue(Object value) { if (value null) { return NULL; } if (value instanceof Number || value instanceof Boolean) { return value.toString(); } if (value instanceof java.sql.Timestamp) { return value ; } // 字符串转义单引号和反斜杠 String s value.toString() .replace(\\, \\\\) .replace(, ); return s ; }toSqlValue是安全底线。数字和布尔直接输出不加引号时间戳加引号字符串先转义反斜杠再转义单引号顺序不能反否则\会被二次转义。这里没有用 PreparedStatement因为目标是生成静态 SQL 文件但如果你走 JDBC 执行路径建议在 Executor 层用PreparedStatement再包一层。3.4 JdbcExecutor批量执行和事务控制生成 SQL 文件是一种用法直连数据库执行是另一种。批量插入必须开事务和批处理否则几千行数据能跑到天荒地老。public int executeBatch(String url, String user, String password, ListString sqlList) { int total 0; try (Connection conn DriverManager.getConnection(url, user, password)) { conn.setAutoCommit(false); // 关闭自动提交 try (Statement stmt conn.createStatement()) { for (String sql : sqlList) { stmt.addBatch(sql); // 每 500 条执行一次避免批次过大 if (total % 500 0) { stmt.executeBatch(); stmt.clearBatch(); } } stmt.executeBatch(); // 执行剩余 conn.commit(); } catch (SQLException e) { conn.rollback(); // 出错整体回滚 throw e; } } catch (SQLException e) { log.error(批量执行失败, e); throw new RuntimeException(e); } return total; }setAutoCommit(false)和commit/rollback是标配。每 500 条执行一次是经验值太小网络往返多太大内存压力大。clearBatch别忘了否则批次会一直累积。4. 跑起来配置、命令行参数和两种执行模式4.1 环境准备和依赖JDK 8 以上Maven 3.6 以上。pom.xml里核心依赖就三个dependencies dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.7/version /dependency /dependenciespoi-ooxml同时支持 xls 和 xlsx不用再单独引poi。数据库驱动按你实际用的换PostgreSQL 就换成postgresql驱动。4.2 配置文件怎么写db.propertiesjdbc.urljdbc:mysql://localhost:3306/test_db?useUnicodetruecharacterEncodingutf8 jdbc.userroot jdbc.passwordyour_password jdbc.tableuser_infomapping.properties就是前面说的字段映射。两个文件都放在resources下打包时会进 jar。4.3 两种运行模式模式一只生成 SQL 文件不连数据库。java -jar excel2sql.jar \ --file ./data/user.xlsx \ --sheet 0 \ --header 0 \ --mode file \ --output ./out/user.sql模式二直连数据库执行。java -jar excel2sql.jar \ --file ./data/user.xlsx \ --sheet 0 \ --header 0 \ --mode db参数说明--file是 Excel 路径--sheet是 sheet 索引从 0 开始--header是表头行号--mode选file或db--output只在 file 模式下需要。Main里用简单的循环解析args没有引第三方 CLI 库保持轻量。4.4 执行结果怎么看file 模式下打开生成的.sql文件检查三件事字段名是否和数据库一致、字符串是否正确转义、日期格式是否是yyyy-MM-dd HH:mm:ss。db 模式下看日志里的executeBatch输出成功会打印总条数失败会打印具体 SQL 和异常堆栈。5. 避坑与排查这几类问题我踩过不止一次5.1 日期列读出来是一串数字现象Excel 里明明是2024-01-01生成 SQL 变成45292。原因POI 把日期存成数字getNumericCellValue返回的是序列号没有走日期分支。解决在getCellValue里用DateUtil.isCellDateFormatted(cell)判断是日期就调getDateCellValue。如果 Excel 里日期是文本格式存的那读出来就是字符串不用特殊处理。5.2 中文乱码现象生成的 SQL 里中文变成???。原因读文件时没指定编码或者 JDBC URL 没加characterEncodingutf8。解决POI 读 Excel 本身不涉及编码问题乱码通常出在写 SQL 文件时。用Files.newBufferedWriter(path, StandardCharsets.UTF_8)明确指定 UTF-8。JDBC URL 里加上useUnicodetruecharacterEncodingutf8。5.3 批量执行到一半报主键冲突现象前 500 条成功第 501 条报Duplicate entry整个事务回滚。原因Excel 里有重复数据或者目标表已有相同主键。解决生成 SQL 时把INSERT INTO改成INSERT IGNORE INTOMySQL或ON CONFLICT DO NOTHINGPostgreSQL。或者在 Executor 里捕获单条异常记录失败行号后继续最后统一报告。我一般选后者因为能知道具体哪几行有问题。5.4 数字精度丢失现象Excel 里是123456789012345生成 SQL 变成1.23456789012345E14。原因POI 读大数字时用了 double超过 15 位有效数字就丢精度。解决在getCellValue的 NUMERIC 分支里如果数字很大用BigDecimal.valueOf(cell.getNumericCellValue()).toPlainString()转成字符串。更稳妥的做法是用DataFormatter它能按单元格显示格式输出。5.5 表头有空格或换行导致映射失败现象配置里写的是用户编号但 Excel 表头是用户编号末尾有空格映射不上。原因表头读取时没 trim。解决读表头时统一.trim()并且把全角空格也替换掉。配置文件的 key 也做同样处理两边对齐。6. 二次开发从单表到多表关联的改造思路这个项目目前是单表 INSERT但实际业务里经常遇到「Excel 一张表要拆到两张关联表」的情况。比如用户表和用户详情表Excel 里是平铺的入库要拆开。改造的核心思路是在 FieldMapper 和 SqlBuilder 之间加一层「表拆分器」。具体做法在mapping.properties里用前缀区分目标表。# user_info 表 user_info.user_id用户编号 user_info.user_name用户姓名 # user_detail 表 user_detail.user_id用户编号 user_detail.address地址 user_detail.email邮箱然后 FieldMapper 返回的不再是单个 Map而是MapString, MapString,Objectkey 是表名。SqlBuilder 遍历这个 Map为每张表生成独立的 INSERT。JdbcExecutor 里要注意执行顺序先插主表再插从表如果有外键约束还得把主表生成的主键回填到从表——这一步如果主键是自增的就需要用getGeneratedKeys拿回来。// 主表插入后拿自增主键 try (PreparedStatement ps conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) { ps.executeUpdate(); try (ResultSet rs ps.getGeneratedKeys()) { if (rs.next()) { long pk rs.getLong(1); // 把 pk 塞回从表的 Map 里 detailRow.put(user_id, pk); } } }另一个常见的改造点是加校验。比如手机号列必须符合格式、必填列不能为空。我一般会在 FieldMapper 之后加一个Validator链每个校验器实现同一个接口配置里声明要启用哪些。这样加规则不用改主流程。验证改造是否成功最直接的办法是拿一份包含边界数据的 Excel——空值、超长字符串、特殊字符、重复主键——跑一遍看生成的 SQL 能不能在测试库上完整执行。我现在的习惯是每次改完映射或拼装逻辑都强制走一遍这个边界用例集因为字符串转义和 null 处理这两个地方翻车概率实在太高。希望这些能帮到你。本文还有配套的精品资源点击获取