简介:这是一款基于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 而不是 Kotlin
IDEA 官方在新版本插件开发时默认推荐 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 id="EntityToSql.MySQLDdlAction" class="com.example.generator.MySQLDdlAction" text="Generate MySQL DDL" description="根据当前实体类生成 MySQL 建表语句"> <add-to-group group-id="GenerateGroup" anchor="last"/> </action> </actions>GenerateGroup是 IDEA 自带的 Alt+Insert 菜单组,和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 类型 |
|---|---|---|
| String | VARCHAR(255) | VARCHAR2(255) |
| Integer / int | INT | NUMBER(10) |
| Long / long | BIGINT | NUMBER(19) |
| BigDecimal | DECIMAL(18,2) | NUMBER(18,2) |
| Boolean / boolean | TINYINT(1) | NUMBER(1) |
| Date / LocalDateTime | DATETIME | DATE |
| LocalDate | DATE | DATE |
| LocalTime | TIME | DATE |
| byte[] | BLOB | BLOB |
| 枚举类型 | VARCHAR(32) | VARCHAR2(32) |
这份映射表里最值得说的是Boolean:MySQL 里最常见的做法是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_no,userId转出来是user_id,效果基本符合预期。但要注意orderNoStr这种连续大写字母的情况,正则转出来是order_no_str,和主流 ORM 框架的策略一致,不用特别处理。
关键字问题是另一个容易踩的坑。MySQL 里order、desc、group都是保留字,列名直接套上会执行报错。解决方式是统一给列名加反引号:`order`。Oracle 不认反引号,但攻击面不同,Oracle 的保留字相对少一些,而且统一加双引号会改变列名的大小写敏感性,所以 Oracle 生成时不加任何引号,只对表名和列名做关键词检测,命中时提示用户手工调整。
4.3 MySQL 模板与 Oracle 模板的分歧点
MySQL 生成相对简单,所有列定义完成后统一加一个PRIMARY KEY:
CREATE 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 嵌套对象、集合和泛型的递归处理
实体类里最常见的不只是基础类型,还有嵌套对象和集合。比如订单实体里包含一个List<ItemInfo>,生成 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 看一半就被截断了。直接写剪贴板,用户切换到接口调试工具或文档编辑器里 Ctrl+V 粘贴就行。
实现剪贴板写入只需要一行:
Toolkit.getDefaultToolkit().getSystemClipboard() .setContents(new StringSelection(json), null);这里有个权限细节:插件运行在 IDE 进程内,拿系统剪贴板不需要额外权限,但最好放在后台任务里执行,不要在 EDT(Event 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.1,IntelliJ 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 个字段,包含一个List<OrderItem>嵌套,生成 MySQL 建表语句大约 8 秒(大部分时间是 IDEA 索引解析),生成 Oracle 语句涉及注释分行,约 10 秒,JSON 生成基本是瞬时的。把生成结果贴到数据库客户端执行,一次通过。
后续要扩展的话,我计划加两块:一是支持 PostgreSQL 方言,把类型映射表再拉一份就好;二是支持 FreeMarker 模板引擎,让建表语句的格式可以由用户自定义,而不是固定在代码里。目前插件用着稳定,这个插件算是解决了 2024 年以来我一直被重复劳动困扰的那个具体问题。
本文还有配套的精品资源,点击获取