
简介这是一套面向IntelliJ IDEA插件开发者的系统化指导手册覆盖从入门到进阶的完整路径。内容按四部分组织上册包含平台架构、插件生命周期、事件机制与API基础上册图形化开发部分讲解Action System、Tool Window、弹窗菜单及UI组件构建下册聚焦语言类插件涉及语法解析、代码补全、检查与分析、DSL支持等高级技术附录汇总Gradle配置、SDK资源与社区参考。不同目标的开发者可按需选读例如偏重业务工具可精读第一、二、四部分想做代码级高级插件则可聚焦语言类部分。资源共1个文件为约3.99MB的PDF文档目前已有876人学习。手册基于官方资料与作者实践经验整理注重理论与实践结合适合想定制或扩展IntelliJ IDEA功能、提升开发效率的Java开发者系统查阅。1. 为什么要在 2025 年动手做一套 IntelliJ IDEA 插件开发不是锦上添花是降本工具做了五年 Java 开发工具链我越来越确认一件事IntelliJ IDEA 插件开发已经从极客玩具变成了 Java 团队的基建能力。你的团队可能已经投了 Prometheus、Arthas、代码规范平台但每天八小时的主战场始终是这个 IDE——让公共代码片段一键生成、让内网接口文档直接在编辑器里悬浮展示、让分支命名规范在提交前拦截这些需求用外部平台做绕路用 IntelliJ Platform Plugin 做才是顺路。这篇指导手册我按自己带人落地插件的路径来写先讲清工程骨架和 Gradle 配置再逐层拆 Action、Tool Window、PSI 这几个核心扩展点最后把我踩过的坑和一套能快速验证的调试习惯交给你。适合正在做 Java 开发、也想把手头重复动作固化成插件的工程师按步骤能跑通最小工程熟手也能拿走边界参数和排查清单。2. 搭工程的姿势Gradle 配置、插件 XML 与社区版/旗舰版的选型逻辑2.1 用 IntelliJ IDEA 社区版从零生成插件工程的前三步刚开始做 IntelliJ IDEA 插件开发很多人第一反应是去 GitHub 找模板这其实是最绕的路。官方路线是直接打开 IntelliJ IDEA 社区版新建项目左侧选择 IDE Plugin。这个工程类型内置了 Plugin DevKit 支持能省掉手动接 DevKit 的环节。这里有个关键选择开发插件本身推荐用 IDEA 社区版因为旗舰版对插件开发的辅助能力差异不大而社区版轻量、不用处理激活问题把精力留在代码上。毕竟插件开发踩得最多的不是 IDE 功能缺失而是 Gradle 和 JVM 版本纠缠。找到 IDE Plugin 入口后第二件事是确认 Gradle JVM 版本。IntelliJ Platform 从 2022.2 起对 Java 17 支持完好2024.1 后的版本建议直接选 Java 17至少也是 Java 11。选 Java 8 会得到一个能编译但运行期到处报 UnsupportedClassVersionError 的工程——这种问题在开发期不爆发布后用户一装就炸属于典型的后期难排查问题。工程生成后的目录结构里最值得先摸清楚的是这三个文件build.gradle.kts控制插件和 IDE 的绑定版本、plugin.xml声明扩展点和 Action 注册、src/main/java写你的 Java 逻辑。我不建议先去动 UI 界面先把这三者的关系在脑子里立住Gradle 负责把你的代码打包成可安装的 jarplugin.xml 告诉 IntelliJ Platform 你的插件在哪个时机挂到哪个扩展点Java 类做实际的事情。下面这个build.gradle.kts是我现在每个插件工程的起点。plugins { id(java) id(org.jetbrains.kotlin.jvm) version 1.9.24 id(org.jetbrains.intellij) version 1.17.3 } group com.example version 1.0.0 repositories { mavenCentral() } intellij { version.set(2024.1.7) type.set(IC) // 或 IU 对应旗舰版 plugins.set(listOf(com.intellij.java)) } tasks { patchPluginXml { sinceBuild.set(231) untilBuild.set(241.*) } }version.set(2024.1.7)是你本地开发时要加载的 IntelliJ Platform 版本它决定你编译时能用哪些 APItype.set(IC)声明基于社区版构建改成IU才能用旗舰版专属 APIsinceBuild和untilBuild是发布时给 JetBrains 市场用来做版本匹配的太宽会导致老版本用户装了跑不起来太窄会丢掉新版本用户。插件开发里 80% 的「装不上」问题都出在这两个版本号上后面的避坑章还会展开。2.2 plugin.xml 的骨架扩展点、Action 注册与 idea-plugin 依赖顺序Gradle 配置通过后IntelliJ Platform 真正读取的是plugin.xml它位于src/main/resources/META-INF/。很多新手会绕过它直接用注解注册 Action这在 DevKit 的注解模式下可行但一旦你要用 Tool Window 或 Listen 编辑器事件plugin.xml 仍然是主战场。我的习惯是主用 XML 注册注解作为调试期的补充理由是 XML 里能一眼看到这个插件挂了多少扩展点排查「为什么我的按钮不出现」时少绕弯。一个最小可用骨架长这样idea-plugin idcom.example.my-first-plugin/id nameMy First Plugin/name version1.0.0/version vendorYour Team/vendor dependscom.intellij.modules.platform/depends dependscom.intellij.modules.java/depends extensions defaultExtensionNscom.intellij toolWindow idMyToolWindow anchorright factoryClasscom.example.toolwindow.MyToolWindowFactory / /extensions actions action idcom.example.actions.ShowHelloAction classcom.example.actions.ShowHelloAction textShow Hello from Plugin descriptionShow a hello message add-to-group group-idEditorPopupMenu anchorfirst / /action /actions /idea-plugin第一个dependscom.intellij.modules.platform/depends声明插件依赖基础平台第二个dependscom.intellij.modules.java/depends才能使用 Java PSI 相关的 API。这里有个血泪经验漏掉com.intellij.modules.java的插件在开发机跑得好好的装到只有 Kotlin 插件的用户机器上运行时直接给你抛ClassNotFoundException指向的却是你明明引到的 JDK 类——原因就是依赖没声明打包器把 Java API 排除掉了。actions段里的add-to-group决定你的菜单项出现在哪里EditorPopupMenu是编辑器右键菜单MainMenu是顶部菜单栏Toolbar是主工具栏。我不建议一上来把 Action 挂到 MainMenu因为弹右键菜单是开发者的高频动作更容易验证你的插件到底有没有装上。2.3 用 runIde 任务把插件跑起来至少调试一次再谈开发工程生成后要做的第一件事不是写业务逻辑而是验证 Gradle 的runIde任务能拉起一个带插件的 IDE。这个任务会下载指定版本的 IntelliJ Platform然后启动一个开发实例。第一次跑会拉几百 MB 依赖网速一般时抽根烟的工夫很正常。下面这条命令在 IntelliJ IDEA 社区版终端里执行./gradlew runIdeWindows 上记得用./gradlew.bat或gradlew.bat runIdemacOS 和 Linux 直接./gradlew runIde。日志里如果出现Plugin com.example.my-first-plugin is already loaded这类输出不代表出错只是说 hot reload 插件包含了你当前开发的项目。真正拉起的 IDE 窗口标题会带[Plugin Development]字样这告诉你当前处于插件开发者模式。runIde用起来有个麻烦每次改代码都要手动重启 IDE。社区版环境里我一般先不做热部署开发期打开 IntelliJ Platform 的Help - Find Action输入Run Plugin用系统自带的 DevKit 方式跑虽然也会重启但省掉 Gradle 重新解析依赖的时间。等逻辑稳定了runIde才是正路因为它的 JVM 参数、IDE 版本都和生产环境更接近。两个方式切换时保持 Gradle 里的version一致否则你会在「开发环境能用但 runIde 不能编译」的怪圈里浪费好几个小时。3. 从 Action 到 Tool Window你写的第一批真正能用的 IntelliJ IDEA 插件代码3.1 写一个能监听编辑器选中事件的 ActionAnAction 的五个生命周期钩子IntelliJ Platform 里 Action 是最基础的扩展点几乎所有用户可见的菜单按钮都属于 Action。继承AnAction实现actionPerformed再注册到 plugin.xml这是每个 IntelliJ IDEA 插件开发者的第一课。但只写一个actionPerformed的人很快就会撞到边界菜单项是灰的或者点按钮毫无反应。原因在于 AnAction 还有两个关键钩子——update和getTemplatePresentation前者控制按钮的可用和显隐后者控制按钮的文案图标。下面这段代码是「选中一行代码右键菜单里显示『用 Javadoc 包裹』」的最小实现package com.example.actions; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; public class WrapWithJavadocAction extends AnAction { Override public void actionPerformed(AnActionEvent e) { Editor editor e.getData(CommonDataKeys.EDITOR); Project project e.getProject(); if (editor null || project null) { return; } SelectionModel selectionModel editor.getSelectionModel(); String selectedText selectionModel.getSelectedText(); if (selectedText null || selectedText.trim().isEmpty()) { Messages.showWarningDialog(project, 请先在编辑器里选中一段代码, 没有选中内容); return; } String javadoc /**\n * selectedText.replace(\n, \n * ) \n */; editor.getDocument().replaceString( selectionModel.getSelectionStart(), selectionModel.getSelectionEnd(), javadoc ); } Override public void update(AnActionEvent e) { Editor editor e.getData(CommonDataKeys.EDITOR); e.getPresentation().setEnabledAndVisible(editor ! null); } }update这是那个决定菜单项是否可点的钩子它会在 IDE 每次刷新 UI 时被回调在这里拿Editor数据拿不到就置灰避免用户点了没反应。CommonDataKeys.EDITOR是 IntelliJ Platform 的数据上下文中拿编辑器实例的入口类似的还有CommonDataKeys.PROJECT、CommonDataKeys.PSI_FILE。getSelectedText()拿到的是纯文本替换时走Document.replaceString而不是直接改Editor是因为 Document 层才触发布 undo 栈用户按 CtrlZ 能退回你的操作。有一点要注意update里不要做重活比如文件 IO 或远程调用它被调用的频率极高稍微慢一点就会让 IDE 掉帧。之前有个同事把查询数据库的请求塞进 update结果右键菜单每次弹出都要卡半秒被投诉后才移走。3.2 用 Tool Window 搭一个团队内部脚手架面板从工厂类到 Swing 组件挂载Action 解决「点按钮干活」Tool Window 解决「常驻侧边栏看信息」。Java 团队的内部 API 文档、代码生成模板、最近报错预览这类场景都适合做成 Tool Window。相比 ActionTool Window 需要多写一个工厂类它的任务是把你的 Swing 组件交还给 IntelliJ Platform。常见做法是直接用 JPanel 配合 Swing 组件不走复杂布局框架。下面这个工厂类会把一个带按钮的面板挂到右侧工具窗口package com.example.toolwindow; import com.intellij.openapi.project.Project; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactory; import org.jetbrains.annotations.NotNull; import javax.swing.*; public class MyToolWindowFactory implements ToolWindowFactory { Override public void createToolWindowContent(NotNull Project project, NotNull ToolWindow toolWindow) { JPanel panel new JPanel(); JButton generateButton new JButton(生成当前类 Mapper); generateButton.addActionListener(e - { // 这里可以调用 PSI 相关逻辑或你团队内部的模板引擎 JOptionPane.showMessageDialog(panel, Mapper 模板已生成); }); panel.add(generateButton); ContentFactory contentFactory ContentFactory.getInstance(); Content content contentFactory.createContent(panel, 脚手架, false); toolWindow.getContentManager().addContent(content); } }工厂方法签名看起来简单里面最有讲究的是Content这个概念createContent第二个参数是 tab 标签名第三个参数false表示不允许用户关闭这个 tab。团队工具窗口我一般设置成不可关闭避免同事误关后不知道去哪找。ContentFactory.getInstance()是单例持有别手动 newIDE 的 UI 生命周期管理需要走它的注册流程。做完工厂类回到 plugin.xml 的extensions段保证那条toolWindow注册还挂着然后重新runIde。启动后右边会出现「MyToolWindow」标签页。若标签没出现优先检查 plugin.xml 里的factoryClass是否拼对以及类是否为 public 且无参构造——这两点是 Tool Window 最常见的翻车点。3.3 从右键菜单到编辑器的数据流转DataContext 与新老 API 的取舍写插件做编辑器联动时绕不开一个概念DataContext。IntelliJ Platform 的 UI 组件没有像 Web 前端那样把全局状态挂在 window 上而是通过AnActionEvent.getData(CommonDataKeys.EDITOR)这类 key-value 方式取。你写的 Action 不管在哪个 UI 位置触发都能拿到「当前聚焦的编辑器」「当前项目」「当前 PSI 文件」——这套机制让插件的代码和 UI 解耦。但这里有个隐藏的坑在Editor和Document混用上。老 API 喜欢直接拿Document.getText()去正则匹配新 API 建议尽量走 PSI 而不是文本操作。用 PSI 能拿到结构化的类名、方法名、注解信息不怕格式变化。我一般这样取舍只对整段文本做替换、截取时用 Document要分析哪个方法是 getter、哪个字段加了某注解时用 PSI。// 用 PSI 获取当前 Java 文件里的所有方法名 PsiFile psiFile e.getData(CommonDataKeys.PSI_FILE); if (psiFile instanceof PsiJavaFile javaFile) { for (PsiClass psiClass : javaFile.getClasses()) { for (PsiMethod method : psiClass.getMethods()) { System.out.println(method.getName()); } } }PsiJavaFile是 Java 文件的 PSI 根节点getClasses()拿到文件里定义的类PsiMethod提供方法名、参数列表、注解信息。初学阶段从e.getData(CommonDataKeys.PSI_FILE)入手即可等熟悉了再接触PsiManager.getInstance(project).findFile()的路径解析方式。每次写完 PSI 遍历代码我的习惯是立刻在调试器里看psiClass.getMethods().length确认拿到的不是 0 再继续避免后面出现「明明文件里有方法你的插件说没有」的诡异问题。这多半是 PSI 文件没刷新调一下psiFile.getViewProvider().isPhysical()能帮助判断。4. 深入 IntelliJ PlatformPSI 操作、文件监听与打包发布前的实弹演练4.1 用 PSI 改写 Java 源码而不是正则替换三行代码保住屎山代码的格式插件一旦要做「给方法加注解」「给类自动生成 getter」这类重写代码的事最危险的诱惑是用正则和字符串拼接。你这么干之后大概率会遭遇代码缩进乱掉、泛型尖括号被吞、注释块被误删。IntelliJ Platform 理念里操作 Java 源码的正路是 PSI它是 IDEA 对源码的树状结构描述。看一个实际场景给当前 Java 文件的所有方法加上一个Deprecated注解。PSI 的写法是遍历 PSI 树上的PsiMethod节点然后通过PsiElementFactory创建注解元素并添加public static void addDeprecatedToAllMethods(PsiClass psiClass) { PsiElementFactory factory JavaPsiFacade.getElementFactory(psiClass.getProject()); PsiAnnotation annotation factory.createAnnotationFromText(Deprecated, psiClass); for (PsiMethod method : psiClass.getMethods()) { method.getModifierList().addAnnotation(annotation.getQualifiedName()); } }JavaPsiFacade.getElementFactory是 PSI 的工厂入口createAnnotationFromText(Deprecated, psiClass)相当于先写一段字符串再解析成 PSI 节点addAnnotation会把注解加到方法修饰符列表上。这里的优点在addAnnotation会自动处理注解位置Deprecated放方法上时不会插错到返回类型后边。如果我用字符串拼接第一版必然有人把它拼在返回类型和花括号之间IDE 能忍但编译报错。还有两点值得一提第一一行注释的插入可以psiElement.addAfter(comment, method)但注释内容里的换行和*字符不处理会被 IDE 按新格式自动重排一次于是多次运行插件代码格式出现变化这是 PSI 操作常见引发「格式化漂移」的原因第二PsiDocumentManager.getInstance(project).commitDocument(document)不要忘缺少提交IDEA 会在下一次 PSI 操作时报Assertion failed: PSI and document do not match。PSI 是 IntelliJ IDEA 插件开发里见效最慢但最值得投入的一块熟悉后你的插件能做「规范团队代码」这件事而这恰恰是保证插件价值最硬的地方。4.2 监听文件保存与编辑器事件别在 DocumentListener 里做重活的三个理由IntelliJ IDEA 插件开发第二个常用面是监听 IDE 内发生的事件比如「文件保存过后自动格式化代码」。实现事件监听有三条常用路径EditorFactory的addEditorFactoryListener、EditorEventMulticaster的addDocumentListener、messageBus订阅端点。我用的最多的是第三条因为它相对轻不依赖 UI 组件生命周期。project.getMessageBus().connect().subscribe( EditorFactoryListener.TOPIC, new EditorFactoryListener() { Override public void editorCreated(NotNull EditorFactoryEvent event) { Editor editor event.getEditor(); editor.getDocument().addDocumentListener(new DocumentListener() { Override public void documentChanged(NotNull DocumentChangeEvent e) { // 在这里做轻量逻辑比如统计行数 } }); } } );这套 API 本身不复杂复杂的在事件回调里能做什么、不能做什么。第一不要在documentChanged里做文件写入或网络 IOdocumentChanged每次键入都会触发重活会让输入延迟用户感受是「打字卡顿」第二不要在事件回调里修改同一个 Document会引发递归回调第二次修改又触发事件死循环。完整方案一般是把变更收集后走ApplicationManager.getApplication().invokeLater()异步执行第三监听器要懂得释放。项目关闭后connect()会自动断连但如果你用了addDocumentListener并且没持有 listener 对象内存泄露会随着时间积累。在真正做「保存即格式化」这类功能前先想清楚一个问题用户可能并不希望你每次修改都出手。更稳的姿势是用FileDocumentManagerListener里的beforeDocumentSaving只拦保存点同时给插件加设置项让团队可以关掉这个自动行为。插件是给开发者用的开发者最讨厌「不由分说帮你做事」的插件。4.3 用 Gradle 打包与 sandbox 验证buildPlugin 之后别急着提交市场开发期在runIde里一切正常并不代表你打包安装后还能活。IntelliJ Platform 插件开发有一个必备流程./gradlew buildPlugin产物在build/distributions/下是一个 zip 文件。JetBrains 市场不支持裸目录上架这个 zip 的目录结构就是插件等价的安装包。打包完成后的第一道验证不是用自己的主 IDE而是用一个干净的 IntelliJ IDEA 社区版通过Settings - Plugins - 齿轮 - Install Plugin from Disk安装这个 zip再重启试功能。这步能暴露几类问题依赖被漏打、plugin.xml 写错但开发环境碰巧兼容、JDK 版本不匹配。我建议在build.gradle.kts里显式声明最终制品不含编译期依赖用instrumentCode和jarJar控制是否需要把第三方库嵌入插件tasks { buildPlugin { // 默认打包当前模块如需引入第三方 jar 用 below 方式 // from(configurations.runtimeClasspath) { exclude(kotlin-*) } } patchPluginXml { version.set(project.version.toString()) } }把buildPlugin的 zip 看成一个能扔到用户机器上的完整交付物。藏在build/distributions里的 zip 不是最终名pluginName-version.zip只需要一个。发布到 JetBrains 市场前你至少要确认三件事zip 小于 20MB 左右新版支持大包但加载慢、plugin.xml 的since-build与until-build不误杀新版本、没有把test目录的编译文件打进 jar。这三条哪个翻车用户装上后都是一句话「你的插件坏了」但原因千差万别。5. 常见问题避坑排查手册JVM 版本冲突、PSI 断言失败、菜单灰掉的三类高频故障5.1 现象构建失败提示 Java/JVM 版本不支持原因Gradle JVM 与 IntelliJ Platform 运行库不一致解决统一到同一主版本新手在 IntelliJ IDEA 插件开发里撞的第一堵墙多是这个项目刚建好跑./gradlew buildPlugin直接报Unsupported class file major version。这类报错翻译成人话是你的 Gradle JVM 比 IntelliJ Platform 依赖的旧或者反过来你的 Java 太新而 IDE 平台还没适配。原因是开发插件的 Gradle JVM 必须至少高于或等于intellij.version平台的运行时要求。用 2024.1.7 平台建议 Gradle JVM 直接用 JDK 17。查当前 Gradle JVM 的方式是在build.gradle.kts同级执行./gradlew -version看JVM:行。出现版本冲突时不要去魔改 IDE 安装目录里的jbr那是 JetBrains Runtime改坏了会让整个 IDE 起不来。解决就三步Settings - Build Tools - Gradle - Gradle JVM下拉选择 17若本地没有 17先手动装 OpenJDK 17 再指过来为了保险在gradle.properties里加一行org.gradle.java.home/path/to/jdk-17。几个项目共用一台机器时建议始终显式配置org.gradle.java.home别依赖系统 PATH否则「我这能编他那不能编」就是第二天对话的主题。5.2 现象跑 PSI 代码抛 Assertion failed: PSI and document do not match原因修改 PSI 后未在正确时机同步 Document解决提交文档后再二次操作这是做「自动加注解」「说明文档生成器」这类功能几乎必踩的坑。你写了 PSI 遍历、改了 PsiElement然后想读Document.getText()结果 IDEA 直接给你Assertion failed似乎不肯让你读到自己刚改的代码。原因是 PSI 和 Document 两套模型平时是同步的你通过PsiElement.add()改完后Document 视图不会立刻更新。同步动作要由PsiDocumentManager来做。正确顺序是PsiDocumentManager documentManager PsiDocumentManager.getInstance(project); documentManager.commitDocument(document); // 之后再读 document.getText() 才会包含你的修改这里的commitDocument有点类似于把暂存区的东西刷进工作区不刷就读会拿到旧快照。另外还有一个极常见的误用add()之后立刻再delete()同一个元素你以为连续两次修改没事但 IDE 内部增量更新根本反应不过来。务实建议是每完成一组「增删改」就commitDocument一次宁可多提交几次也别相信「PSI 会自动同步」。还有一个变体坑在一个 PSI 修改操作的去重循环里你会收集PsiElement到 List然后遍历 List 执行delete()。当循环执行到一半前面的删除已经让后面的元素失效IDE 抛InvalidAccessException。解决是倒序遍历或者每次删除后重新获取 PSI 树引用。5.3 现象右键菜单项是灰色、不可点击原因update 里拿不到目标数据上下文解决用 DataContext 的键值校验并写出兜底提示Action 注册成功、plugin.xml 也没写错按钮却置灰这类问题在 IntelliJ IDEA 插件开发里属于「不报错但很上头」的故障。我用 Debugger 跑AnActionEvent然后看e.getData()返回了什么发现拿不到 PSI_FILE原因是这个 Action 被挂在了 Toolbar 上Toolbar 区域不承载编辑器上下文。解决思路有三个层级。第一层级把update里的setEnabledAndVisible条件换成多个 key 联合判断避免一票否决比如e.getData(CommonDataKeys.PSI_FILE) ! null e.getData(CommonDataKeys.EDITOR) ! null。第二层级把 Action 挂到更合适的位置右键菜单用EditorPopupMenu工具窗口内部的事件用EditorTabPopupMenu。第三层级如果确实需要在工具栏也生效就主动从e.getData(CommonDataKeys.EDITOR)递推拿到相关 PSI而不是坐等其他数据到位。实操中最实用的一招是给 Action 写一个 MessageDialog 兜底即使按钮置灰逻辑出了漏洞点上去也要给用户一句「请在编辑器里选中代码后再试」。插件是给开发者用的工具静默失效比报错更劝退。5.4 现象runIde 启动后插件列表里找不到自己的插件原因patchPluginXml 的 since-build 与当前平台版本不匹配解决用 sinceBuild 低于当前版本的值临时调试项目能被开发实例加载是插件开发的底线而「加载失败」往往是sinceBuild太新了。开发时我用 2024.1.7 平台而开发实例可能是 2024.1.7一般不会出问题但如果你在 2024.2 的 IDE 上跑旧插件sinceBuild231会被识别为未知版本。临时调试期可以用sinceBuild.set(222)这种明显更早的值它能加载到绝大部分新版 IDE。发布前再把sinceBuild改成你实际要支持的最低版本untilBuild同理。我的习惯是本地开发保持sinceBuild222提交换市场前再按适配范围收紧。别直接不写sinceBuild或者不设上限那样 IntelliJ Platform 会默认插件不支持任何版本反倒一装一个不吭声。5.5 现象插件第一次运行时能工作第二次后行为异常原因listen 在项目重开时重复注册解决在 plugin.xml 里显式声明depends替代 ApplicationListener 的重复挂载这个坑相对隐蔽你写了一个全局ApplicationListener在插件启动时注册EditorFactoryListener项目切换几次后插件开始重复执行动作。原因在于ApplicationListener不是为「项目级别订阅」设计的项目一开一关它会重复绑定。IntelliJ IDEA 插件开发里项目生命周期事件应该走 ProjectManager 的TOPIC或直接依赖messageBus的项目连接点而不是ApplicationListener。最好是用 plugin.xml 的extensions挂projectService让智能的 IDE 容器帮你管理生命周期。手动注册监听器不是不行但要保留下Disposable parentDisposable参数并在dispose()里注销。自查时先看目录结构如果代码里频繁写project.getMessageBus().connect()却没有给一个 parentDisposable那每个项目窗口都会积累一份。6. 让插件真正有人用的关键一步用本地 Noteworthy 文件与手动回归清单来验收在 IntelliJ IDEA 插件开发里写完功能只是第一步真正能让团队上手长期用的是「验收习惯」。做法是在插件工程里维护一份NOTEWORTHY.md按「目标用户是谁、解决什么重复劳动、操作入口在哪、失败时看什么」四个板块写清楚同时在本地 ide 里做一次完整的手动回归。我的回归清单只有五项一、新开一个无插件环境磁盘安装 zip确认主菜单栏出现插件入口右键菜单能正常触发核心功能二、对同一个项目反复触发插件五次确认无重复注册导致的内存增长三、切到另一个项目再切回来Tool Window 内容能刷新不能出现残留旧项目数据四、在无选中文本、选中空行、选中半个中文字符的情况下分别触发插件要给出统一提示而不是抛异常五、快捷键冲突检查——用Keymap里搜索你注册的 keymap确认没有覆盖用户常用键位。最后这个验收环节我吃过一次亏给团队做了一个自动生成 Mapper 的工具第一次演示后大家反馈按钮有时灵有时不灵最后定位到是 Tool Window 下的工厂类没有判空没有打开编辑器时点了按钮一个 NPE 把整个工具窗口搞崩了。从那以后我写 Action 的第一版代码永远先写空值兜底再写业务逻辑。写 IntelliJ IDEA 插件很多时候是在「顺手帮同事省五分钟」和「制造一个没人想再点的按钮」之间做选择把回归清单跑完再交付你的插件才真的可能沉淀成团队基建而不是技术债。这套流程就是我自己做 Java 集成开发环境插件的全部思路希望帮到你。本文还有配套的精品资源点击获取