尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

IDEA插件实战:从Java实体类一键生成MySQL/Oracle建表语句与JSON请求体

IDEA插件实战:从Java实体类一键生成MySQL/Oracle建表语句与JSON请求体 简介这是一款基于Java开发的IntelliJ IDEA插件主要面向Java后端开发者用于将实体类一键转换为MySQL建表语句、Oracle建表语句以及JSON请求体省去手写SQL和报文模板的重复劳动。插件以右键菜单方式集成使用者在实体类中选中目标后即可调用ToMysql、ToOracle、ToJson功能生成的语句自动存入剪切板便于粘贴到数据库客户端或接口文档中。资源包共18个文件以Java源码4个、XML配置7个、Markdown说明、图片预览等为主整体体积仅63KB属于轻量级开发工具源码适合有IDEA插件开发基础或希望直接安装使用的Java工程师参考。压缩包内含完整的项目结构包括src目录、META-INF插件描述、UI设计文件及相关配置文件下载后可导入IDEA进行二次修改或按说明安装体验。目前已有773人学习下载对于需要高频输出建表语句和请求体的开发场景具有实用价值。 每天都在跟 Java 实体类、SQL 建表语句和 JSON 请求体打交道的开发大概都经历过这种场景改一个字段先动 DDL再动实体类还要在一大段 JSON 里找到对应的参数位置。这个 IDEA 插件的初衷特别朴素——选中一个实体类右键一键生成 MySQL、Oracle 建表语句和 JSON 请求体把这三件事从手工变成半自动。文章不是教你用现成的在线转换工具而是分享我自己用 Java 写这个插件时的完整思路包括 IDEA 插件工程怎么搭、实体类解析用什么 API 更靠谱、同一套元数据怎么同时适配 MySQL 和 Oracle 两套方言、JSON 请求体生成有哪些隐藏的边界问题。如果你有 IDEA 插件开发经验可以直接跳到后面的调试和打包部分如果是第一次写插件建议从头看每一节的坑我都标出来了。1. 为什么每天写完实体类就想摔键盘手工 DDL 和 JSON 请求体是连环痛点先说说我当时的手工流程。改完一个订单实体类要给 MySQL 写建表语句要给 Oracle 写一套还要给前端联调文档准备 JSON 请求示例。一个字段名要手动转三次下划线一个类型要对着 MySQL 的 VARCHAR、Oracle 的 VARCHAR2、JSON 的 string 各对应一遍。字段少还好二十个字段的实体类这种重复劳动基本占据了我每天固定半小时。1.1 手工转换的三个致命问题第一是类型映射错位。Java 的LocalDateTime在 MySQL 里是DATETIME到了 Oracle 应该是DATE而 JSON 里又是2024-01-01 00:00:00这种字符串。如果靠人肉记忆很容易在 Oracle 里手滑写成DATETIME数据库跑起来才报错返工成本极高。第二是命名不统一。Java 驼峰orderNo转 MySQL 下划线order_no很好理解但团队里不同人写 DDL 的习惯不一样有人写order_no有人写orderno还有人写ORDER_NO。一旦出现混用后续 ORM 映射排查起来相当耗时。第三是注解信息被忽略。实体类上明明写了TableField(order_no)和TableId手工建表时这些信息等于完全没被利用等于把有价值的设计信息白白丢掉。1.2 为什么现成的在线转换工具替代不了其实网上不是没有实体类转 SQL 的工具但真正用到生产环境就会发现不合适一类是在线网页工具实体类代码属于公司业务资产粘贴到第三方网站本身就是合规风险另一类是 IDEA 的 Database 插件自带的生成能力它确实能从表结构生成实体但方向是反的我要的是从实体反推 DDL它做不了还有一类是 MyBatis Generator 之类的代码生成器它们能生成建表脚本但配置一套 XML 模板比手工写 DDL 还累。所以最后结论是自己动手写一个插件最合适。只读取本地源码不涉及任何外部传输生成逻辑完全可控而且可以深度集成到 IDEA 的右键菜单和 Generate 菜单里不用切换任何工具。1.3 工具用起来的样子这个插件完成后的交互路径是这样的在编辑器里打开一个 Java 实体类右键呼出菜单找到 Generate会看到三个选项——Generate MySQL DDL、Generate Oracle DDL、Generate JSON Body。选中一个弹窗展示生成结果同时自动复制到剪贴板贴到任何地方就能直接用。整个过程从原来手工 30 分钟缩短到 10 秒而且只要实体类定义正确生成结果基本不会出错。2. IDEA 插件工程搭建Java 选型和 Gradle 配置里的版本坑2.1 为什么用 Java 而不是 KotlinIDEA 官方在新版本插件开发时默认推荐 Kotlin原因无非是 Kotlin 与 IntelliJ Platform 的 API 配合更顺滑DSL 语法也简洁。但我最终还是选了 Java理由很实际团队里的同事全都写 Java插件做出来不是给我一个人用的后续大家要维护、要加功能Java 版本的阅读门槛最低。另一个原因是IDEA 的 PSI API 本身就是 Java 写的用 Java 调用时所有方法签名一目了然IDE 自动补全的体验完全不差。Kotlin 的优势主要体现在 DSL 配置和空安全但对我这种以逻辑处理为主的小插件来说Java 完全够用。2.2 Gradle 构建脚本的关键配置IDEA 插件开发推荐用 Gradle IntelliJ Plugin核心配置如下plugins { id java id org.jetbrains.intellij version 1.13.0 } group com.example version 1.0.0 repositories { mavenCentral() } dependencies { implementation com.fasterxml.jackson.core:jackson-databind:2.15.2 } intellij { version 2020.3 type IC plugins [java] } patchPluginXml { sinceBuild 202.7660 untilBuild 231.* }这里有几个坑需要注意。version我设成2020.3不是因为我用的这个版本而是为了向下兼容。IDEA 插件有sinceBuild和untilBuild两个概念如果不设置插件装到不兼容版本时会直接提示无法加载。我这里设成202.7660到231.*意思是兼容 2020.2 到 2023.1 的所有 IDEA 版本。plugins [java]这一项也很关键因为要解析 Java 实体类必须依赖 IDEA 的 Java 插件模块少了它 PSI 的JavaPsiFacade等类在运行时会报 NoClassDefFoundError。2.3 Action 注册右键菜单和 Generate 菜单是两个入口IDEA 插件的最基本单元是 Action通过plugin.xml注册。我配了两个入口一个放在编辑器右键菜单的 Generate 分组里一个放在 EditorPopupMenu 里。actions action idEntityToSql.MySQLDdlAction classcom.example.generator.MySQLDdlAction textGenerate MySQL DDL description根据当前实体类生成 MySQL 建表语句 add-to-group group-idGenerateGroup anchorlast/ /action /actionsGenerateGroup是 IDEA 自带的 AltInsert 菜单组和EditorPopupMenu不同前者在代码编辑器里按快捷键就能触发后者是点击鼠标右键出现的菜单。两个都挂载用户习惯哪种方式都能用。Action 的具体实现只需要继承AnAction重写actionPerformed方法然后从AnActionEvent里拿当前编辑的PsiFile再解析出PsiClass就够了。3. 用 PSI 解析实体类反射在这里完全行不通3.1 反射做不到的事情PSI 能做到很多人第一次写 IDEA 插件时第一反应是用 Java 反射去读实体类字段。这个思路看着合理真正实现时会被两个问题卡死。第一插件拿到的是源码文件不是编译后的 class 文件。IDEA 插件运行在 IDE 进程里和用户项目的类加载器完全隔离反射拿到的不是当前项目的实体类。第二理想情况下应该在类还没编译时就生成建表语句反射只能对编译后的字节码操作天然做不到源码级分析。PSI 全称 Program Structure Interface是 IntelliJ Platform 对源码文件的抽象模型。它能把.java文件解析成一棵语法树树的节点就是类、方法、字段、注解这些编程语言要素。用 PSI 读源码不需要用户项目成功编译甚至源码有报错也不影响字段结构解析。3.2 解析核心流程拿到PsiClass之后整个解析流程是这样的public static EntityModel parse(PsiClass psiClass) { EntityModel model new EntityModel(); model.setTableName(parseTableName(psiClass)); model.setComment(parseComment(psiClass)); for (PsiField field : psiClass.getAllFields()) { // 跳过静态字段和 transient 字段 if (field.hasModifierProperty(PsiModifier.STATIC) || field.hasModifierProperty(PsiModifier.TRANSIENT)) { continue; } EntityField entityField new EntityField(); entityField.setFieldName(field.getName()); entityField.setFieldType(field.getType().getCanonicalText()); // 关键识别列名注解优先用注解值 entityField.setColumnName(parseColumnName(field)); model.getFields().add(entityField); } return model; }getAllFields()会包含父类继承过来的字段这个行为对建表很有用因为我们经常有BaseEntity里放id、createTime这种公共字段。默认包含继承字段比只拿当前类字段更合理。类型获取用的是field.getType().getCanonicalText()得到的是完整类名加包名比如java.lang.String、java.math.BigDecimal。后面做 SQL 类型映射时优先用完整类名判断避免Date到底是java.util.Date还是java.sql.Date被搞混。3.3 注解识别与中间模型生成 SQL 时实体类上的注解信息比字段名本身更可信。MyBatis-Plus 的TableName和TableField、JPA 的Table和Column我都做了兼容。private static String parseColumnName(PsiField field) { PsiAnnotation tableField field.getAnnotation( com.baomidou.mybatisplus.annotation.TableField); if (tableField ! null) { PsiNameValuePair value tableField.findAttribute(value); if (value ! null value.getValue() ! null) { return value.getValue().getText().replace(\, ); } } PsiAnnotation column field.getAnnotation(javax.persistence.Column); if (column ! null) { PsiNameValuePair name column.findAttribute(name); if (name ! null name.getValue() ! null) { return name.getValue().getText().replace(\, ); } } // 默认策略驼峰转下划线 return camelToUnderline(field.getName()); }注意这里读取注解值用的是findAttribute然后getValue().getText()得到的是带引号的原始文本需要手动去掉引号。IDEA 没有专门提供把注解值转成实际 Java 值的 API这是 PSI 解析时经常被忽略的小细节。我把解析结果统一封装成一个EntityModel中间模型后续生成 MySQL、Oracle、JSON 全部基于这个模型。这样做的好处是逻辑分层清晰解析只做一次三种输出各自消费哪怕以后要新增 PostgreSQL 方言也只需要写一个全新的生成器不用动解析逻辑。4. 两套 SQL 方言MySQL 和 Oracle 建表语句的差异处理4.1 类型映射表是核心资产实体类解析完之后最核心的工作就是类型映射。Java 类型到数据库类型没有绝对标准我根据自己几年的实践经验维护了一份映射表Java 类型MySQL 类型Oracle 类型StringVARCHAR(255)VARCHAR2(255)Integer / intINTNUMBER(10)Long / longBIGINTNUMBER(19)BigDecimalDECIMAL(18,2)NUMBER(18,2)Boolean / booleanTINYINT(1)NUMBER(1)Date / LocalDateTimeDATETIMEDATELocalDateDATEDATELocalTimeTIMEDATEbyte[]BLOBBLOB枚举类型VARCHAR(32)VARCHAR2(32)这份映射表里最值得说的是BooleanMySQL 里最常见的做法是TINYINT(1)Java 的布尔值可以无损存入Oracle 没有 BOOLEAN 列类型必须用NUMBER(1)字段值为 0 或 1这一条如果没做映射生成出来的 Oracle DDL 必然在数据库里执行失败。BigDecimal我默认给到DECIMAL(18,2)金额或数量够用。但如果你实体类用Column(precision 10, scale 2)指定了精度应该优先读取注解里的值而不是用默认值。这块我做了优先级注解精度 默认精度。4.2 命名策略与关键字处理列名默认采用驼峰转下划线。最简单的实现是用正则逐个字符判断public static String camelToUnderline(String str) { return str.replaceAll(([A-Z]), _$1).toLowerCase(); }orderNo转出来是order_nouserId转出来是user_id效果基本符合预期。但要注意orderNoStr这种连续大写字母的情况正则转出来是order_no_str和主流 ORM 框架的策略一致不用特别处理。关键字问题是另一个容易踩的坑。MySQL 里order、desc、group都是保留字列名直接套上会执行报错。解决方式是统一给列名加反引号order。Oracle 不认反引号但攻击面不同Oracle 的保留字相对少一些而且统一加双引号会改变列名的大小写敏感性所以 Oracle 生成时不加任何引号只对表名和列名做关键词检测命中时提示用户手工调整。4.3 MySQL 模板与 Oracle 模板的分歧点MySQL 生成相对简单所有列定义完成后统一加一个PRIMARY KEYCREATE TABLE order_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, order_no VARCHAR(64) NOT NULL COMMENT 订单号, total_amount DECIMAL(18,2) DEFAULT NULL COMMENT 总金额, PRIMARY KEY (id), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单信息表;AUTO_INCREMENT 放在列定义里只要检测到字段带TableId(type IdType.AUTO)注解就加上。索引的处理是另一套逻辑带TableIndex或字段名以orderNo这类高频查询字段结尾的我默认生成普通索引。Oracle 的生成要复杂很多因为列注释不能内联CREATE TABLE ORDER_INFO ( ID NUMBER(19) NOT NULL, ORDER_NO VARCHAR2(64) NOT NULL, TOTAL_AMOUNT NUMBER(18,2), CONSTRAINT PK_ORDER_INFO PRIMARY KEY (ID) ); COMMENT ON TABLE ORDER_INFO IS 订单信息表; COMMENT ON COLUMN ORDER_INFO.ID IS 主键ID; COMMENT ON COLUMN ORDER_INFO.ORDER_NO IS 订单号;关于主键自增Oracle 从 12c 开始支持IDENTITY列语法是ID NUMBER(19) GENERATED BY DEFAULT AS IDENTITY。但考虑到很多存量系统还在用 11g我默认不生成 IDENTITY 而是只做主键约束让用户根据自己数据库版本自行决定。这个取舍在生成结果上方用注释提示了避免不知情的同事拿到 SQL 就直接跑然后报错。生成 SQL 的具体实现没什么黑魔法就是按模板拼接字符串。我推荐把模板写成StringBuilder循环追加而不是用String.format硬拼因为字段数量多时String.format的可读性会快速下降。每个字段追加一行列定义用,分隔最后再统一拼接主键、索引、注释逻辑清晰也不容易漏逗号。5. JSON 请求体生成嵌套对象、集合和日期格式的深层处理5.1 从实体到 JSON 树生成 JSON 请求体比建表语句更依赖 Jackson 的树模型。我一开始想的是手写字符串拼接后来发现自己拼 JSON 会遇到转义地狱——字段值里有双引号、换行符时拼出来的 JSON 几乎没法检查。后来改成用ObjectMapper构建ObjectNode和ArrayNode最后调用writerWithDefaultPrettyPrinter()输出带缩进的格式。基础字段的映射逻辑是字符串映射成空字符串数字映射成 0布尔值映射成 false日期映射成2024-01-01 00:00:00格式。映射成空值而不是 null是为了让请求体示例对前端更有参考价值——前端拿到例子之后能直接复制改数据不用先处理 null。if (typeName.equals(java.lang.String)) { return jsonNodeFactory.textNode(); } if (typeName.equals(java.lang.Integer) || typeName.equals(java.lang.Long)) { return jsonNodeFactory.numberNode(0); } if (typeName.equals(java.math.BigDecimal)) { return jsonNodeFactory.numberNode(new BigDecimal(0.00)); }这里有几个细节。BigDecimal不能直接用numberNode(0)那样输出的 JSON 是0而不是0.00与业务前端预期的金额格式不一致所以用字符串构造new BigDecimal(0.00)。小数位数是 2 位和建表语句里DECIMAL(18,2)的精度保持统一。日期类型输出格式固定为yyyy-MM-dd HH:mm:ss这个格式同时兼容 Fastjson 和 Jackson 的默认处理方式前端和后端都能一眼看懂。5.2 嵌套对象、集合和泛型的递归处理实体类里最常见的不只是基础类型还有嵌套对象和集合。比如订单实体里包含一个ListItemInfo生成 JSON 时不能简单地把List映射成数组还需要递归解析ItemInfo的字段。实现这个逻辑要利用PsiClassType的泛型解析能力if (typeName.startsWith(java.util.List)) { PsiClassType classType (PsiClassType) psiType; PsiType[] parameters classType.getParameters(); if (parameters.length 0) { PsiClass itemClass ((PsiClassType) parameters[0]).resolve(); JsonNode itemNode generateJsonFromPsiClass(itemClass); ArrayNode arrayNode jsonNodeFactory.arrayNode(); arrayNode.add(itemNode); return arrayNode; } } return jsonNodeFactory.nullNode();调用getParameters()能拿到泛型参数类型。有一个问题值得一提resolve()返回的可能是 null比如泛型类型来自第三方 jar 且源码没在本地索引时就会解析失败。我自己的做法是对 null 做兜底直接生成空对象{}至少保证 JSON 输出完整不会因为单个类型问题导致整个生成失败。递归深度不是无限加深的我只处理了两层嵌套。原因很务实请求体的嵌套层级一旦过深可读性急剧下降而且与后端的实际接收结构往往有出入两层以内刚好覆盖绝大多数请求场景。5.3 输出方式剪贴板优先JSON 生成之后不是只显示在弹窗里而是同时写入系统剪贴板。这个设计来自实际使用经验弹窗里的 JSON 看起来很长人工选中再复制很容易漏行而且 IDEA 的弹窗有显示高度限制长 JSON 看一半就被截断了。直接写剪贴板用户切换到接口调试工具或文档编辑器里 CtrlV 粘贴就行。实现剪贴板写入只需要一行Toolkit.getDefaultToolkit().getSystemClipboard() .setContents(new StringSelection(json), null);这里有个权限细节插件运行在 IDE 进程内拿系统剪贴板不需要额外权限但最好放在后台任务里执行不要在 EDTEvent Dispatch Thread上做重操作。生成 JSON 时如果实体类嵌套很深解析耗时可能超过几十毫秒直接用ApplicationManager.getApplication().executeOnPooledThread()包一层更稳妥。弹窗我用的是Messages.showDialog配合自带滚动条不引第三方 UI 库因为各种平台样式适配问题会让简单弹窗变得不可控。6. 调试、打包与实测发布前必须处理的问题6.1 runIde 调试的漫长等待IDEA 插件调试比普通 Java 程序要痛苦得多。点runIde任务会启动一个全新的 IDEA 实例这个实例需要重新加载工作区插件和索引冷启动时间基本在两分钟以上。刚开始调试时我改一行代码就要重启一次效率极低。后来摸索出两个提升效率的方法。第一是尽量在调试实例里用临时测试项目项目体量越小IDEA 索引越快。测试项目里放十几个实体类就够了不要直接把公司的大项目拖进去。第二是用buildPlugin和installPlugin两个 Gradle 任务配合手动安装改完代码直接打包安装到日常使用的 IDEA 里比一遍遍重启调试实例快很多。缺点是日常 IDEA 里装的是旧版本需要在修复一个 bug 之后重新打包所以两个方法配合用逻辑改动用调试实例问题定位完的验证版本用日常 IDEA。6.2 版本兼容是永远绕不过去的IDEA 插件最大的隐形成本是版本兼容。我用sinceBuild 202.7660作为下限意味着 2020.2 到 2023.1 之间所有版本都能装。但从 2020.2 到 2023.1IntelliJ Platform 的 API 变更了好几次比如Messages.showInputDialog的签名有调整、EditorPopupMenu的注册方式也有微调。还好我的插件只依赖了稳定的 PSI 核心 API 和 Action 体系没有用com.intellij.ui.components.JBList这类经常变动的高级组件所以目前测下来各版本表现一致。如果你的插件要用到树形控件、编辑器侧边栏这些高级 UI建议在plugin.xml里的untilBuild写得保守一些不要一次性声明到无限版本。6.3 打包安装的完整链路最终交付不是上传到 Plugin Marketplace而是团队内部使用。插件打包执行./gradlew buildPlugin生成物在build/distributions/目录下是一个 zip 文件。团队成员安装时打开 IDEA 的 Settings — Plugins — 设置图标 — Install Plugin from Disk选中 zip 就可以装。这里有个容易被插件新手忽略的点buildPlugin产生的 zip 文件里如果插件依赖了jackson-databind这类第三方库需要确保它被完整打进 lib 目录。Gradle IntelliJ Plugin 默认会处理这个但如果你手动指定任务链容易漏掉 dependencies 的拷贝。我的做法是直接在dependencies块声明implementation project(:jackson-databind)这种显式依赖确保打包时资源被正确带入。打包后的插件会有个特点IDEA 启动时会弹出未知插件来源的确认框。这个不用处理是正常的确认即可。6.4 实测效果与后续可扩展方向用团队里一个真实订单实体做测试OrderInfo有 18 个字段包含一个ListOrderItem嵌套生成 MySQL 建表语句大约 8 秒大部分时间是 IDEA 索引解析生成 Oracle 语句涉及注释分行约 10 秒JSON 生成基本是瞬时的。把生成结果贴到数据库客户端执行一次通过。后续要扩展的话我计划加两块一是支持 PostgreSQL 方言把类型映射表再拉一份就好二是支持 FreeMarker 模板引擎让建表语句的格式可以由用户自定义而不是固定在代码里。目前插件用着稳定这个插件算是解决了 2024 年以来我一直被重复劳动困扰的那个具体问题。本文还有配套的精品资源点击获取
返回列表