ARTICLE DETAIL

资讯详情

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

利用JDBC元数据自动生成XML映射文件:从表结构到MyBatis配置

利用JDBC元数据自动生成XML映射文件:从表结构到MyBatis配置 简介一款面向Java开发者及需要将数据库数据导出为XML文件的数据处理工程师的自动生成工具旨在解决手动编写XML时效率低、易出错的问题。工具基于JDBC技术连接各类数据库自动完成加载驱动、建立连接、执行SQL查询并将结果集逐行转换为结构化XML文档覆盖从数据源读取到文件输出的完整流程。资源包共4个文件包含主程序exe、ini启动配置、XML示例文件以及properties数据库连接配置压缩包仅144KB轻量易部署适合本地快速验证。已有2350人学习下载可应用于系统间数据交换、配置文件批量生成、接口测试数据准备等实际场景。下载后可直接运行生成器并通过附带的配置示例快速掌握数据库表到XML结构的映射思路包括根元素、命名空间、编码格式等自定义选项同时可通过修改SQL和配置文件灵活适配不同业务表结构与输出要求。1. XML 自动生成工具表结构到 XML 配置这一步真的不该手写xml 自动生成工具听起来像个偷懒的小脚本但真正手写过几百行 mapper 配置的工程师都知道从 datasource 连接串到 resultMap 的每一行都藏着翻车点。我最早被逼着写这个工具是因为一次数据库迁移几十张表要重新生成 XML 映射手写花了整整两天最后还因为一个没转义的号让整个文件在解析阶段直接报废。这个工具的思路一句话就能说清用 JDBC 连上 datasource读数据库元数据再按模板输出 XML 文件。它解决的是配置生成里的重复劳动和低级错误适合后端开发、维护 MyBatis 或 ORM 映射的人也适合在项目初始化阶段需要快速搭出配置骨架的团队。如果你正被表结构和 XML 字段对不齐折磨这个工具就是为这个场景准备的。2. 生成原理JDBC 元数据怎么映射成 XML 节点先搞清数据从哪来要把表结构变成 XML第一步不是写 XML而是搞清楚 JDBC 能给你什么。很多人在这一步就卡住了以为要一条条 SELECT 信息再手动拼其实 Connection 对象上的 DatabaseMetaData 早就把表、列、主键、注释全部准备好了。这一章先把数据来源和映射规则拆清楚后面写代码才不会踩空。2.1 DatabaseMetaData 与 ResultSetMetaData生成 XML 的两层数据来源DatabaseMetaData 是 JDBC 里被严重低估的接口。它挂在 Connection 上连接建立后随时可用不需要额外查询。常用的读取方法就三个getTables 拿表清单getColumns 拿列明细getPrimaryKeys 拿主键。这三个 ResultSet 组合起来一张表的完整骨架就出来了。我一般这么读表清单DatabaseMetaData meta conn.getMetaData(); try (ResultSet rs meta.getTables(catalog, schemaPattern, %, new String[]{TABLE})) { while (rs.next()) { String tableName rs.getString(TABLE_NAME); String tableType rs.getString(TABLE_TYPE); String remarks rs.getString(REMARKS); // 只取普通业务表视图、系统表靠 TABLE_TYPE 过滤掉 } }这段代码的逻辑是先拿到元数据对象再按条件过滤出普通表。四个参数的含义要记牢catalog 和 schemaPattern 是第一个坑MySQL 里库名放在 catalog 位置Oracle 里库名放在 schema 位置写反了什么也查不到tableNamePattern 传%表示匹配全部types 数组只传 TABLE避免把视图和临时表一起拉进来。取列明细是工作量最大的一步每条列记录里有十几个字段可用try (ResultSet rs meta.getColumns(catalog, schemaPattern, tableName, %)) { while (rs.next()) { String colName rs.getString(COLUMN_NAME); int dataType rs.getInt(DATA_TYPE); // java.sql.Types 常量 String typeName rs.getString(TYPE_NAME); // 数据库原生类型名 int size rs.getInt(COLUMN_SIZE); int digits rs.getInt(DECIMAL_DIGITS); boolean nullable rs.getInt(NULLABLE) DatabaseMetaData.columnNullable; String colRemarks rs.getString(REMARKS); } }这里的 DATA_TYPE 是 JDBC 标准常量适合做跨数据库的 switch 分支TYPE_NAME 是厂商原生类型名MySQL 的 int、Oracle 的 NUMBER 都在这里。还有一个细节COLUMN_SIZE 在 MySQL 里对 char 是字符数对 blob 是字节数在 Oracle 里 varchar2 的 size 也是字节数所以映射到 XML 的长度字段时要结合 TYPE_NAME 一起判断不能只看数字。2.2 表结构到 XML 的映射规则类型、主键、命名三件事拿到元数据之后要做映射决策。这个工具生成的表结构 XML 按以下规则映射表变成根元素表名放 name 属性注释放 comment 属性列变成子元素 column携带 name、jdbcType、javaType、size、nullable、primaryKey 属性主键把 primaryKey 置 true多主键按 KEY_SEQ 排序顺序不能乱列名默认做驼峰转换USER_NAME 变成 userName。JDBC 类型到 javaType 再到 XML 表示的对应关系是映射规则里最核心的一张表JDBC 类型javaTypeXML 表示注意事项VARCHAR、CHAR、TEXTStringxs:string长度超 4000 建议不设 maxLengthINTEGER、SMALLINTInteger、Shortxs:int / xs:shortTINYINT(1) 要单独处理BIGINTLongxs:long最常见的主键类型DECIMAL、NUMERICBigDecimaldecimal保留精度金额字段别转 doubleTIMESTAMP、DATETimestamp、Datexs:dateTime / xs:date时区交给驱动处理BIT、BOOLEANBooleanxs:booleanMySQL 的 TINYINT(1) 会被驱动报成 BITBLOB、CLOBbyte[]、Stringbase64Binary一般不进默认查询列映射表看着简单真正决定工具好不好用的全是边界分支。比如 MySQL 的 TINYINT(1) 驱动会当成 BIT 返回你要是图省事直接映射成 Boolean后面 ORM 反序列化时值对不上就翻车。所以映射规则必须是可配置的不能写死在代码里这一点在第五章给具体做法。2.3 三种输出形态datasource 配置、mapper XML 与结构交换 XML同一个元数据源可以生成三种不同用途的 XML工具在设计上把「读元数据」和「写 XML」拆成两层换输出模板不动读取代码这是它覆盖多场景的关键。输出形态典型用途生成要点datasource 配置 XMLSpring、MyBatis 的数据源配置属性名要和框架占位符一致mapper XMLMyBatis 的 resultMap 与 SQL 片段SQL 特殊字符必须转义结构交换 XML给外部系统做表结构 import/export类型、注释信息要完整datasource 配置看起来最简单坑却在属性名Spring 的 driver-class-name、连接串参数顺序任何一处名字对不上启动时就是一堆初始化异常。mapper XML 是三类里最容易出低级错误的SQL 里的和不转义文件在解析阶段直接报废。结构交换 XML 是给外部系统消费的类型信息的完整度比好看更重要。提示MyBatis 初始化时用 XMLMapperBuilder 逐个解析 mapper 文件namespace 重复、id 重复、resultType 写错全在 SqlSessionFactoryBuilder.build 阶段抛异常。所以工具生成的 XML 必须保证 namespace 与接口全限定名一致、id 文件内唯一把这两条写成生成器的校验规则比写完再人工对账靠谱得多。3. 落地实现用 JDBC 连接 datasource把表结构一次性写成 XML这一章直接给代码。核心类只有一个 Generator读配置、连库、抓元数据、写 XML 全在一个 main 里跑完适合新手直接改也适合熟手拆开复用。完整源码在下载包里下面贴的是最关键的四段。3.1 工程结构与依赖工程结构刻意精简避免引入无关依赖xml-generator/ ├── pom.xml ├── config.properties ├── src/main/java/com/example/gen/ │ ├── Generator.java # 入口编排整个流程 │ ├── ColumnInfo.java # 列元数据封装 │ └── XmlWriter.java # DOM 构建与落盘 └── generated/ # 输出目录Maven 依赖只需要数据库驱动版本以本地仓库现存的为准不做强制锁定dependencies dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId /dependency dependency groupIdcom.oracle.database.jdbc/groupId artifactIdojdbc8/artifactId /dependency /dependencies只依赖 JDBC 驱动、其他全用 JDK 自带 API 的理由很实际工具可以打进任何环境跑不跟 Spring、MyBatis 的版本打架。ColumnInfo 就是几个字段的 POJOname、typeName、size、decimalDigits、nullable、primaryKey对应 getColumns 返回的列。3.2 读取 datasource 参数与建立连接配置文件用 properties 格式数据库连接串、账号、输出规则都在这里# MySQL 连接串记得带 useUnicode 和 characterEncoding jdbc.urljdbc:mysql://127.0.0.1:3306/order_db?useUnicodetruecharacterEncodingUTF-8 jdbc.usernameroot jdbc.passwordchange_me jdbc.drivercom.mysql.cj.jdbc.Driver # MySQL 的 catalog 就是库名Oracle 把 schema 填这里 schemaorder_db # 逗号分隔留空表示生成所有表 tablesuser,order_info # 输出目录 output.dir./generated # true 时列名自动转驼峰 camel.casetrue # true 时旧文件直接覆盖false 时备份为 .bak overwritefalse连接与入口public static void main(String[] args) throws Exception { Properties props new Properties(); try (InputStream in Files.newInputStream(Paths.get(config.properties))) { props.load(in); // properties 里 不用转义很多人在这才想起 xml 和 properties 的区别 } Class.forName(props.getProperty(jdbc.driver)); try (Connection conn DriverManager.getConnection( props.getProperty(jdbc.url), props.getProperty(jdbc.username), props.getProperty(jdbc.password))) { ListString tables resolveTables(conn, props); for (String table : tables) { generateOne(conn, props, table); } } }Class.forName 在现代驱动上可以省略但保留能让老驱动也能跑。try-with-resources 保证连接用完即关。resolveTables 负责解析 tables 参数留空时走 getTables 全量拉取。注意 properties 文件里连接串的不用像 XML 里那样转义成amp;这两种格式最容易搞混。3.3 抓取表结构与主键private static ListColumnInfo readColumns(Connection conn, Properties props, String table) throws SQLException { String schema props.getProperty(schema, ); boolean mysql props.getProperty(jdbc.url).contains(mysql); String catalog mysql ? schema : null; // MySQL 库名在 catalog String schemaPattern mysql ? null : schema; // Oracle 库名在 schema DatabaseMetaData meta conn.getMetaData(); ListColumnInfo columns new ArrayList(); try (ResultSet rs meta.getColumns(catalog, schemaPattern, table, %)) { while (rs.next()) { ColumnInfo c new ColumnInfo(); c.name rs.getString(COLUMN_NAME); c.typeName rs.getString(TYPE_NAME); c.size rs.getInt(COLUMN_SIZE); c.decimalDigits rs.getInt(DECIMAL_DIGITS); c.nullable rs.getInt(NULLABLE) DatabaseMetaData.columnNullable; c.remarks rs.getString(REMARKS); columns.add(c); } } try (ResultSet pkRs meta.getPrimaryKeys(catalog, schemaPattern, table)) { while (pkRs.next()) { String pkCol pkRs.getString(COLUMN_NAME); // 多主键按 KEY_SEQ 排序返回 columns.stream() .filter(c - c.name.equals(pkCol)) .forEach(c - c.primaryKey true); } } return columns; }这段把 MySQL 和 Oracle 的 catalog/schema 差异直接写死在判断里因为这是空输出的头号原因。getColumns 只取需要的字段性能和可读性都更好。主键读取单独走一个 ResultSet用列名做关联不改动原本的列顺序。3.4 用 DOM 生成 XML 并落盘public static void writeXml(File outFile, String tableName, String comment, ListColumnInfo columns, boolean camelCase) throws Exception { DocumentBuilderFactory factory DocumentBuilderFactory.newInstance(); // 安全习惯禁止 DOCTYPE 声明防 XXE生成器也适用 factory.setFeature(http://apache.org/xml/features/disallow-doctype-decl, true); Document doc factory.newDocumentBuilder().newDocument(); Element root doc.createElement(table); root.setAttribute(name, tableName); if (comment ! null !comment.isEmpty()) { root.setAttribute(comment, comment); } doc.appendChild(root); for (ColumnInfo c : columns) { Element col doc.createElement(column); col.setAttribute(name, c.name); col.setAttribute(property, camelCase ? toCamel(c.name) : c.name); col.setAttribute(jdbcType, c.typeName); col.setAttribute(javaType, javaTypeOf(c.typeName)); col.setAttribute(nullable, String.valueOf(c.nullable)); col.setAttribute(primaryKey, String.valueOf(c.primaryKey)); if (c.size 0) col.setAttribute(size, String.valueOf(c.size)); root.appendChild(col); } Transformer transformer TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, UTF-8); transformer.setOutputProperty(OutputKeys.INDENT, yes); transformer.transform(new DOMSource(doc), new StreamResult(outFile)); }用 DOM 而不是字符串拼接最大的好处是 setAttribute 会自动做属性值转义name 里出现引号、特殊字符都不会破坏 XML 结构。toCamel 的实现在第五章javaTypeOf 是类型映射函数后续可以换成配置驱动。Transformer 的 ENCODING 固定成 UTF-8既保证中文注释不乱码也避免写出带 BOM 的文件。跑一次生成的单表 XML 长这样?xml version1.0 encodingUTF-8? table nameorder_info comment订单表 column nameid propertyid jdbcTypeBIGINT javaTypejava.lang.Long nullablefalse primaryKeytrue/ column nameuser_id propertyuserId jdbcTypeBIGINT javaTypejava.lang.Long nullablefalse primaryKeyfalse/ column namestatus propertystatus jdbcTypeTINYINT javaTypejava.lang.Integer nullablefalse primaryKeyfalse/ column namecreate_time propertycreateTime jdbcTypeTIMESTAMP javaTypejava.sql.Timestamp nullablefalse primaryKeyfalse/ /table启动方式就是java -cp带上依赖和 Generator 类输出落在 generated 目录。如果你想打开生成的 XML 文件检查建议用带 XML 语法高亮的编辑器缩进和标签配对一眼就能看出来。第一次跑如果发现文件打不开先别怀疑生成逻辑大概率是旧文件没清理或编码问题这两个坑下一章单独讲。4. 常见问题与避坑从生成失败到配置不生效的五个典型坑工具写出来不难难的是让它生成的东西每次都被直接消费。下面五条都是我在真实项目里踩过的按现象、原因、解决三步拆开。4.1 生成的 XML 一解析就报 Content is not allowed in prolog现象文件用编辑器打开完全正常但 xmllint 或 DocumentBuilder 解析直接抛 Content is not allowed in prolog报错位置在第 1 行第 1 列。xml 解析器对这类问题零容忍而编辑器通常帮你隐藏了真相。原因输出文件被写成了带 BOM 的 UTF-8BOM 这个不可见字符被解析器当成非法内容。Windows 下用 new FileWriter 不指定编码就会出现某些 IDE 的保存动作也会自动加 BOM。解决写文件固定用 OutputStreamWriter构造时指定 StandardCharsets.UTF_8或者像 3.4 那样用 Transformer 的 OutputKeys.ENCODING 控制。如果你在 Windows 上开发这个问题几乎必现一次值得提前预防。4.2 SQL 里的小于号把 XML 截断整个文件报废现象mapper 里出现WHERE create_time 2024-01-01生成时看着正常MyBatis 一加载就报元素类型不匹配或者文件后半段全变红。原因手写字符串拼接 XML 时被解析器当成标签开始字符后面所有内容被吞进一个不存在的标签里。解决SQL 片段里的和必须转义成lt;和amp;MyBatis 场景还可以用 CDATA 包整段 SQL。用 DOM 生成只保证属性值安全SQL 文本本身仍要过一层转义函数private static String escapeXml(String s) { if (s null) return ; return s.replace(, amp;) // 顺序不能乱 必须最先处理 .replace(, lt;) .replace(, gt;) .replace(\, quot;) .replace(, apos;); }注意 必须第一个处理否则转出来的lt;里的 会被二次转义成amp;lt;结果彻底没法看。4.3 TINYINT(1) 被映射成 BooleanORM 反序列化翻车现象MySQL 的 is_deleted TINYINT(1) 被工具生成成 java.lang.Boolean查询结果是 0/1代码期望 true/false对不上直接抛异常报错经常是「orm 读取实体类的 xml 错误」那一类。原因MySQL 驱动会把 TINYINT(1) 报成 BITBIT 在标准映射里对应 Boolean。这是驱动行为不是工具写错。解决在类型映射函数里对 typeName 为 TINYINT 且 size 为 1 的列强制走 Integer/Short或者放开配置开关 mapping.bitAsBooleanfalse按团队规范来。4.4 元数据接口返回 non-xml response from server. response code: 400, content-type: text/xml现象工具如果是走 HTTP 元数据服务拿 schema很多团队会把表结构包成 XML 接口请求时收到 non-xml response from server. response code: 400, content-type: text/xml; charsetutf-8但接口文档明明说返回 XML。原因400 表示请求被服务端拒绝content-type 只是错误响应页的声明body 里是纯文本错误说明并不是 XML。常见诱因是 URL 里的库名没做 URL 编码、token 参数大小写不对、或者缺少必带头。解决先用 curl 带同样参数请求直接看 body 文本库名用 URLEncoder.encode 编码后再拼 URL。这个报错的字面意思就是「说好的 XML 没来」问题几乎都在请求侧。4.5 二次生成把手改过的文件整个覆盖现象第一次生成后手工补了几个特殊字段下次数据库变更重新跑工具手工内容全没了。原因工具默认无脑覆盖没有差异比较和备份。解决生成前检查输出文件是否存在存在就先复制一份 .bak更稳妥的做法是先解析旧文件对比结构只有结构变化才重写。下载包里的版本做了简化实现旧文件与新的列集合完全一致时跳过写入。备份代码File out new File(outputDir, tableName .xml); if (out.exists() !props.getProperty(overwrite, false).equals(true)) { File bak new File(out.getParent(), out.getName() .bak); Files.copy(out.toPath(), bak.toPath(), StandardCopyOption.REPLACE_EXISTING); }这段的作用是给旧文件留后悔药overwritetrue 时跳过备份直接覆盖适合刚生成完还没手改过的场景。5. 参数与边界类型映射、命名转换和增量生成怎么调才顺手工具要落到团队里用参数化程度决定它能活多久。这一章把三个最需要调的边界讲透。5.1 类型映射表做成配置不写死在代码里3.4 里的 javaTypeOf 是硬编码函数实际项目里 LocalDateTime 和 Timestamp 之争、String 和 CLOB 之争每个团队都有自己规范。所以我把映射表抽出来mapping.TINYINTjava.lang.Integer mapping.BITjava.lang.Boolean mapping.DATETIMEjava.time.LocalDateTime mapping.TIMESTAMPjava.time.LocalDateTimeprivate static String javaTypeOf(String dbType, Properties mappingProps) { return mappingProps.getProperty( mapping. dbType.toUpperCase(), fallbackType(dbType)); // 没配到的类型走默认兜底 }逻辑很简单先按数据库类型名大写去拿配置拿不到就进 fallbackType 兜底。好处是生成器不用改代码不同项目各带一份 mapping.properties 就行。兜底函数我一般把未知类型统一给 String并在日志里打警告这样不会默默生成错误映射。5.2 驼峰转换的边界缩写、数字和保留字private static String toCamel(String col) { String[] parts col.toLowerCase().split(_); StringBuilder sb new StringBuilder(parts[0]); for (int i 1; i parts.length; i) { if (!parts[i].isEmpty()) { sb.append(Character.toUpperCase(parts[i].charAt(0))) .append(parts[i].substring(1)); } } return sb.toString(); }这个实现能覆盖九成列名USER_NAME 变 userNameORDER_ITEM_ID 变 orderItemIdURL_ADDR 变 urlAddr 而不是 uRLAddr。处理不了的是数字段开头比如 2024_DATA 会变 2024Data属性名以数字开头在 Java 里不合法。这种列要么在规则里过滤掉首段数字要么生成后人工改一次工具不应该猜你的意图。保留字是另一个边界。列名叫 order、group、desc 的表在真实环境不少生成的 SQL 里不处理数据库直接报语法错。我一般按数据库加包围符MySQL 用反引号Oracle 用双引号这个开关也放进配置按团队库的类型来定。5.3 参数总表与增量生成参数默认值说明jdbc.url必填驱动连接串tables空全表逗号分隔支持 user* 前缀通配camel.casetrue列名转属性名开关overwritefalsetrue 直接覆盖false 备份 .bakoutput.dir./generated输出目录按表名分文件encodingUTF-8生成文件编码不建议改mapping.*见映射表自定义类型映射键值对增量生成的核心逻辑不复杂生成前读旧文件把旧文件的列名集合和新列集合比对完全一致就跳过写入。比的是列名加类型组合而不是整个文件字符串因为格式化差异会产生大量噪音。这样工具能天天跑不会污染手工维护过的内容。output.dir 我习惯按业务域分目录比如 generated/order、generated/user避免几百个文件堆在一起后面接 MyBatis 的 mapperLocations 通配符也好配。6. 进阶用法生成之后的 XML 校验与 MyBatis 初始化对接工具跑完不是终点生成的文件能被消费才是。我每次跑完都强制走三层验证。第一层是 XML 合法性命令行一条命令xmllint --noout generated/order_info.xml没有输出就代表文件结构合法这一步能在 3 秒内拦下 4.1 和 4.2 两类问题。第二层是 MyBatis 初始化加载直接用你项目的 SqlSessionFactoryBuilder build 一把namespace 重复、id 冲突、resultType 写错全在 build 阶段暴露这正好对应 MyBatis 基于 XML 配置的初始化工作原理——XMLMapperBuilder 逐个解析任何一个文件的错误都会让整个 SqlSessionFactory 起不来。第三层是字段对账拿生成的列名清单去和 information_schema 对比防止工具漏列。数据库结构变更是最常见的触发场景我现在的习惯是任何 DDL 变更之后跑一遍生成器git diff 看改动确认无误再提交。生成器本身是可重复执行的设计跑一次没变化就什么都不写这依赖第五章的增量判断。整个流程可以收成一句命令java -cp xml-generator.jar com.example.gen.Generator git diff --stat generated/下载包里就是这套完整源码和示例配置解压改一下 config.properties 里的连接串就能跑。从那以后我每次跑完生成器都强制走一遍 XML 校验和 MyBatis 初始化测试宁可多花十秒也不让一个坏文件流到测试环境这个习惯帮我堵掉了很多低级问题希望帮到你。本文还有配套的精品资源点击获取
返回列表