
1. 插件开发到底在做什么从需求到方案的整体拆解很多人第一次听到“Android Studio 插件开发”或者“IDEA 插件开发”脑子里冒出来的第一个念头是这东西是不是只有做 IDE 的人才能碰其实不是。插件开发的本质是给一个已经成型的开发工具“挂”上你自己写的功能模块。它跟写业务代码最大的区别在于你面对的不是用户界面和网络请求而是一个庞大的、已经运行起来的编辑器内核你要做的是在它提供的扩展点上插入自己的逻辑。我最初接触这块是因为团队里有个很烦人的重复劳动每次新建一个模块都要手动去改一堆配置文件改完还要检查命名规范、包结构、资源目录是否齐全。一个人一天可能要做五六次每次三分钟看起来不多但积累起来非常消耗耐心。当时我就想能不能让 IDE 自己帮我做这件事于是就开始研究插件开发。这个方向能解决的问题其实非常集中我把它归纳为三类。第一类是自动化重复操作比如批量生成代码、自动补全模板、一键格式化特定结构。第二类是增强编辑体验比如自定义代码检查、实时提示、快速跳转。第三类是集成外部工具比如把某个命令行工具、某个内部平台的能力直接嵌到 IDE 里不用来回切换窗口。适合看这篇内容的人我觉得有三类。一类是已经写了几年 Android 或者 Java 业务代码想往工具链方向延伸的开发者一类是团队里负责基建、效率工具的同学需要给团队做定制化开发支持还有一类是纯粹对 IDE 内部机制好奇想搞清楚“我每天用的这个工具到底是怎么跑起来的”的人。不管你是哪一类只要你会写 Java 或者 Kotlin能看懂基本的 XML 配置这篇内容里的思路和步骤你都能跟着走一遍。方案选型上我一开始也纠结过是直接基于 IntelliJ Platform SDK 从零写还是用一些封装好的框架后来实测下来对于大多数团队内部工具直接基于官方 SDK 是最稳的。原因很简单官方 SDK 的文档虽然不算特别友好但它的扩展点最全社区案例最多遇到问题能搜到答案的概率最高。而且 Android Studio 本身就是基于 IntelliJ 社区版构建的你写的插件理论上可以同时跑在 IDEA、Android Studio、甚至其他基于 IntelliJ 的 IDE 上复用性很好。提示如果你只是想做一个小工具给自己用不打算发布到插件市场那完全不需要考虑复杂的发布流程和兼容性测试直接本地打包安装就行能省掉大量时间。2. 开发环境与项目骨架别在第一步就卡住2.1 工具链选择与版本匹配的坑插件开发的环境准备说简单也简单说坑多也真多。核心就三样东西一个基于 IntelliJ 的 IDEAndroid Studio 或者 IDEA 都行、Gradle 构建工具、以及 IntelliJ Platform SDK。但这里有个非常关键的细节你的开发 IDE 版本、Gradle 插件版本、以及目标平台版本三者必须匹配。我踩过的第一个坑就是版本不匹配。当时我用的是比较新的 Android Studio 版本但 Gradle 插件里写的目标平台版本比较旧结果插件写完了本地跑起来没问题打包安装到另一个版本的 IDE 上直接报错提示找不到某个类。后来查了半天才明白IntelliJ Platform 的 API 在不同大版本之间是有 breaking change 的你编译时用的 SDK 版本决定了你能调用哪些 API而运行时 IDE 的版本决定了这些 API 是否存在。我的建议是开发时就用你日常使用的那个 IDE 版本作为目标平台。比如你团队统一用某个版本的 Android Studio那你就用那个版本对应的 SDK 来编译。这样至少保证你自己的环境是通的。如果后续要发布给其他人用再考虑向下兼容的问题。具体操作上现在官方推荐的方式是用 Gradle 来管理整个插件项目。你不需要手动去下载 SDK 压缩包然后配置路径Gradle 插件会自动帮你拉取对应的依赖。下面是一个最简化的build.gradle.kts配置示例plugins { id(java) id(org.jetbrains.intellij) version 1.17.0 } group com.example.plugin version 1.0.0 repositories { mavenCentral() } intellij { version.set(2023.1) type.set(AI) // 对应 Android Studio plugins.set(listOf(android)) } tasks { patchPluginXml { sinceBuild.set(231) untilBuild.set(241.*) } }这里有几个参数需要解释一下。version指的是目标平台版本type指的是 IDE 类型AI代表 Android StudioIC代表 IDEA 社区版IU代表 IDEA 终极版。sinceBuild和untilBuild是控制插件兼容范围的数字对应 IDE 的构建号。如果你不确定构建号可以在 IDE 的关于页面里看到。注意plugins.set(listOf(android))这一行是告诉 Gradle 你需要依赖 Android 插件的能力。如果你只是做通用功能不需要操作 Android 特有的结构可以去掉这一行能减少不少依赖体积。2.2 项目目录结构怎么组织才不乱插件项目的目录结构跟普通 Java 项目差别不大但有几个关键文件必须放在正确的位置。我见过不少新手把plugin.xml放错地方结果 IDE 死活识别不到插件折腾半天以为是代码问题。标准的结构是这样的my-plugin/ ├── build.gradle.kts ├── settings.gradle.kts ├── src/ │ └── main/ │ ├── java/ # 你的 Java/Kotlin 代码 │ └── resources/ │ └── META-INF/ │ └── plugin.xml # 插件描述文件 └── gradle/ └── wrapper/plugin.xml是整个插件的入口它告诉 IDE我这个插件叫什么名字、有哪些扩展点、对应的实现类是什么。你可以把它理解成插件的“身份证加说明书”。一个最基础的plugin.xml长这样idea-plugin idcom.example.myplugin/id nameMy First Plugin/name vendorExample Team/vendor description一个用于演示的插件/description dependscom.intellij.modules.platform/depends dependsorg.jetbrains.android/depends extensions defaultExtensionNscom.intellij applicationService serviceImplementationcom.example.MyService/ /extensions actions action idMyAction classcom.example.MyAction text执行我的操作 description点击后执行自定义逻辑 add-to-group group-idToolsMenu anchorfirst/ /action /actions /idea-plugin这里面的depends标签很关键。com.intellij.modules.platform是基础平台模块几乎所有插件都要依赖。org.jetbrains.android是 Android 插件模块只有你需要操作 Android 项目结构时才需要加。如果你不加这个依赖代码里却引用了 Android 相关的类编译时可能不报错但运行时会直接抛ClassNotFoundException。2.3 第一个可运行的最小插件我建议每个新手都先做一个“最小可运行插件”不要一上来就搞复杂功能。最小插件只需要做一件事在菜单栏里加一个按钮点击后弹出一个提示框。这个流程跑通了你就理解了插件从注册到执行的完整链路。具体步骤是这样的。首先创建一个继承自AnAction的类public class MyAction extends AnAction { Override public void actionPerformed(NotNull AnActionEvent e) { Project project e.getProject(); if (project ! null) { Messages.showInfoMessage(project, 插件运行成功, 提示); } } }然后在plugin.xml里注册这个 Action就是上面配置里actions那一段。最后运行runIde这个 Gradle 任务它会启动一个全新的 IDE 实例里面已经安装了你正在开发的插件。你在那个实例里点击菜单就能看到你的按钮。这个流程看起来简单但里面有个细节值得说runIde启动的 IDE 是一个沙箱环境它用的是独立的配置目录不会污染你日常使用的 IDE 配置。你在这个环境里做的任何操作包括安装其他插件、修改设置都不会影响你的主环境。这个设计非常贴心可以放心折腾。3. 核心扩展点与实操要点插件到底能挂在哪里3.1 Action 系统最常用的入口Action 是插件开发里最基础也最常用的扩展点。你可以把它理解成“用户触发的一个操作”。它可以是菜单栏里的一项、工具栏上的一个图标、右键菜单里的一个选项甚至是一个快捷键绑定的命令。Action 的注册方式有两种。一种是在plugin.xml里静态注册就是上面示例里那种写法。另一种是在代码里动态注册通过ActionManager来添加。静态注册适合固定的、启动时就存在的操作动态注册适合根据运行时条件决定是否显示的操作。我个人的经验是能用静态注册就用静态注册。因为静态注册的 Action 在 IDE 启动时就会被加载响应速度更快而且配置清晰排查问题方便。动态注册虽然灵活但容易因为生命周期管理不当导致内存泄漏或者 Action 重复注册。Action 的放置位置由add-to-group标签控制。常用的 group-id 有这么几个group-id位置说明ToolsMenu顶部 Tools 菜单MainMenu主菜单栏ProjectViewPopupMenu项目视图右键菜单EditorPopupMenu编辑器右键菜单RunMenu运行菜单anchor属性控制插入位置first表示放在最前面last表示放在最后面before和after需要配合具体的 Action id 使用。提示如果你不确定某个菜单的 group-id 是什么可以在 IDE 里打开Settings - Keymap找到对应的操作右键选择“Copy ID”就能看到它的完整标识。3.2 代码生成与模板让重复劳动归零代码生成是插件开发里最能体现价值的方向之一。我做的那个自动生成模块结构的插件核心就是用了PsiFileFactory和WriteCommandAction这两个东西。PsiFileFactory用来创建新的 PSI 文件。PSI 是 Program Structure Interface 的缩写是 IntelliJ 对代码结构的抽象表示。你可以把它理解成“代码的 DOM 树”。每一个类、方法、字段、注解在 PSI 里都是一个节点。你通过操作这些节点来修改代码而不是直接操作文本。这样做的好处是IDE 会自动帮你处理格式化、导入语句、语法校验等问题。WriteCommandAction用来包裹写操作。IntelliJ 的 PSI 模型是支持多线程读写的但写操作必须放在WriteCommandAction里执行否则会抛异常。这个设计是为了保证代码修改的原子性和一致性。下面是一个生成 Java 类的核心代码片段WriteCommandAction.runWriteCommandAction(project, () - { PsiFileFactory factory PsiFileFactory.getInstance(project); String className GeneratedClass; String content public class className {\n}; PsiFile file factory.createFileFromText( className .java, JavaFileType.INSTANCE, content ); PsiDirectory directory PsiDirectoryFactory .getInstance(project) .createDirectory( PsiManager.getInstance(project) .findDirectory(targetDir) ); directory.add(file); });这段代码做了三件事创建文件内容、创建 PSI 文件对象、把文件添加到目标目录。看起来简单但里面有几个容易出问题的地方。第一createFileFromText的第二个参数必须是正确的文件类型如果你生成的是 Kotlin 文件却传了JavaFileType后续操作会出问题。第二add方法会自动处理文件冲突如果同名文件已存在它会抛IncorrectOperationException你需要提前检查或者捕获处理。3.3 代码检查与快速修复代码检查是另一个高频需求。团队往往有自己的编码规范比如“所有网络请求必须加超时设置”、“所有数据库操作必须放在事务里”。这些规范靠人工 review 很容易漏但写成插件检查就非常稳。IntelliJ 提供了LocalInspectionTool这个扩展点。你继承它实现buildVisitor方法返回一个PsiElementVisitor然后在 visit 方法里检查你关心的节点类型。如果发现问题就调用ProblemsHolder.registerProblem注册一个问题并附带一个LocalQuickFix来提供修复方案。我做过一个检查是要求所有SharedPreferences的edit()调用必须用apply()而不是commit()。实现思路就是找到所有方法调用表达式判断方法名是不是commit再判断调用者是不是SharedPreferences.Editor类型。如果是就注册一个问题并提供一个把commit替换成apply的快速修复。这个过程中有个细节类型判断不能只看方法名。因为不同类可能有同名方法你如果只判断方法名会误报。正确的做法是通过PsiMethod的getContainingClass来获取所属类再判断类的全限定名。这个坑我当时踩过误报了一堆无关代码后来加了类型判断才解决。3.4 与 Android 项目结构交互如果你的插件需要操作 Android 项目特有的结构比如AndroidManifest.xml、build.gradle、资源目录等就需要依赖 Android 插件提供的 API。这些 API 主要在com.android.tools.idea包下。一个常见的需求是读取当前项目的所有模块然后对每个模块做处理。你可以通过ProjectManager和AndroidModel来获取这些信息。但要注意Android 插件的 API 在不同版本之间变化比较大而且官方文档相对较少很多时候需要靠阅读源码或者反编译来搞清楚用法。我的建议是尽量用通用的 PSI API 来操作文件只在必要的时候才用 Android 特有的 API。比如你要修改AndroidManifest.xml完全可以用 XML 的 PSI 来处理不需要依赖 Android 插件。这样你的插件兼容性会更好也不容易因为 Android 插件升级而挂掉。4. 调试、打包与发布把插件交到别人手里4.1 调试插件的几种手段插件调试比普通应用调试要麻烦一些因为它跑在 IDE 内部你不能随便打断点。我常用的手段有三种。第一种是日志输出。用Logger.getInstance(MyClass.class).info(消息)来打日志日志会输出到runIde启动的那个 IDE 实例的日志文件里。你可以在那个实例的Help - Show Log in Explorer里找到日志文件。这种方式最简单适合快速定位问题。第二种是远程调试。在 Gradle 配置里加上jvmArgs参数开启调试端口然后用主 IDE 的 Remote Debug 功能连上去。这种方式可以打断点适合排查复杂逻辑。配置大概是这样tasks { runIde { jvmArgs(-agentlib:jdwptransportdt_socket,servery,suspendn,address5005) } }第三种是单元测试。IntelliJ 提供了一套测试框架可以让你在不启动完整 IDE 的情况下测试插件逻辑。这种方式跑得快适合做回归测试。但它的局限是很多依赖 IDE 运行环境的功能没法测比如 UI 相关的操作。注意远程调试时suspendn表示不等待调试器连接就启动。如果你希望 IDE 启动后就暂停等你的调试器连上再继续可以改成suspendy。但这样如果调试器没连上IDE 会一直卡住。4.2 打包与兼容性处理打包插件用buildPlugin这个 Gradle 任务它会生成一个 zip 文件里面包含了插件运行所需的所有内容。你可以直接把这个 zip 发给别人让对方在 IDE 的Settings - Plugins - Install Plugin from Disk里安装。但这里有个兼容性问题需要认真对待。你的插件在plugin.xml里声明的sinceBuild和untilBuild决定了它能安装在哪些版本的 IDE 上。如果你声明的范围太窄用户升级 IDE 后插件就用不了了如果范围太宽又可能因为 API 不兼容导致运行时崩溃。我的做法是在sinceBuild上保守一点在untilBuild上留有余地。比如你只在某个版本上测试过那sinceBuild就写那个版本的构建号untilBuild可以写一个更大的范围但要在插件描述里注明“已在某版本测试通过”。这样既保证了基本可用性又给了用户尝试的空间。另外如果你的插件依赖了其他插件比如 Android 插件一定要在plugin.xml里用depends声明。这样 IDE 在安装时会自动检查依赖是否满足不满足会提示用户而不是等到运行时才报错。4.3 发布到插件市场的流程如果你想让更多人用你的插件可以发布到 JetBrains 的插件市场。流程大概是注册账号、生成上传令牌、配置 Gradle 的publishPlugin任务、执行发布。发布后需要等待官方审核审核通过后就能在 IDE 的插件市场里搜到了。但我要提醒一句发布到市场意味着你要对插件的质量负责。用户会给你提 issueIDE 升级后你需要跟进适配恶意差评你也要处理。如果你只是团队内部使用完全没必要走这个流程直接打包分发就行省心很多。我自己的做法是内部工具就内部消化用共享目录或者内部仓库来分发。只有那些确实有通用价值、而且我愿意长期维护的插件才会考虑发布到市场。5. 常见问题与排查技巧实录5.1 插件不生效的排查思路插件写完安装上去结果菜单里找不到、功能没反应这是最常见的问题。我总结了一个排查顺序基本能覆盖九成以上的情况。第一步检查plugin.xml是否在正确的位置。它必须在src/main/resources/META-INF/目录下文件名必须是plugin.xml大小写敏感。我见过有人写成Plugin.xml或者放在src/main/resources/根目录下结果 IDE 完全识别不到。第二步检查id是否重复。如果你的插件 id 和已安装的某个插件冲突新插件可能不会加载。可以在Settings - Plugins里搜索你的插件名看看是否出现了两个。第三步检查depends是否满足。如果你依赖了某个模块但那个模块在当前 IDE 里不存在或者版本不对插件会加载失败。可以在 IDE 的日志里搜索你的插件 id通常会有明确的错误提示。第四步检查 Action 的 group-id 是否正确。如果 group-id 写错了Action 会被注册到一个不存在的位置自然就看不到。可以尝试把 Action 注册到ToolsMenu这个最通用的位置来验证。5.2 常见异常与解决方案速查下面这个表格是我在实际开发中遇到过的典型问题以及对应的解决思路异常信息可能原因解决思路ClassNotFoundException依赖模块未声明或版本不匹配检查 plugin.xml 的 depends确认目标平台版本Already disposed在项目关闭后访问了 PSI加项目生命周期监听及时释放资源Write access is allowed inside write-action only写操作没有包裹在 WriteCommandAction 里用 WriteCommandAction.runWriteCommandAction 包裹Cannot find action with idAction id 重复或未注册检查 plugin.xml 中 action 的 id 是否唯一Plugin not loaded兼容性范围不匹配调整 sinceBuild 和 untilBuild5.3 性能与内存的注意事项插件跑在 IDE 进程里如果你的代码有性能问题会直接影响整个 IDE 的流畅度。我踩过的一个坑是在 Action 里做了一次全项目文件扫描结果项目大的时候点击菜单要等好几秒期间 IDE 界面卡死。后来我改成了异步执行用ProgressManager来跑后台任务并且加了进度条提示。这样用户知道程序在干活不会以为卡死了。同时对于耗时的操作一定要支持取消否则用户只能强制杀进程。内存方面不要长期持有 Project 或 PsiFile 的引用。这些对象在项目关闭后会被回收如果你在某个静态变量或者单例里持有它们会导致内存泄漏。正确的做法是用ProjectManagerListener监听项目关闭事件在关闭时清理你的引用。提示如果你不确定自己的插件有没有内存泄漏可以在runIde启动的实例里反复打开关闭项目然后手动触发 GC观察内存是否持续增长。这是一个简单但有效的验证方法。5.4 版本升级导致插件失效的应对IDE 每次大版本升级都可能引入 API 变化导致插件失效。我经历过一次Android Studio 升级后我插件里用的某个 Android API 被移除了编译直接报错。应对这种情况我的经验是尽量使用稳定的、跨版本兼容的 API。比如 PSI 相关的 API 就非常稳定几年都不会变。而 Android 插件特有的 API 变化就比较频繁。如果你必须用这些 API可以考虑用反射来调用这样即使 API 签名变了也不会导致编译失败最多是运行时功能降级。另外建议在项目里保留多个版本的 SDK 配置方便快速切换测试。Gradle 的intellij配置块支持通过属性来动态设置版本你可以用-PplatformVersion2023.1这样的命令行参数来覆盖默认值方便做兼容性验证。6. 从能用到好用一些进阶思路6.1 配置持久化与用户设置一个插件如果只有硬编码的行为用起来会很别扭。用户往往希望能自定义一些参数比如生成的代码模板、检查规则的开关、快捷键绑定等。IntelliJ 提供了PersistentStateComponent来帮你做配置持久化。你只需要定义一个配置类加上State和Storage注解实现PersistentStateComponent接口IDE 就会自动帮你把配置保存到 XML 文件里并在下次启动时恢复。用户可以通过Settings - Tools - 你的插件名来修改这些配置。这个功能看起来不起眼但它是插件从“能用”到“好用”的关键一步。我做的那个代码生成插件最初模板是写死的后来加了配置界面允许用户自定义包名规则、文件头注释、是否生成测试类等使用率明显提升。6.2 与版本控制系统的联动如果你的插件需要跟 Git 等版本控制系统交互IntelliJ 也提供了相应的 API。你可以获取当前分支、查看文件修改状态、甚至执行提交操作。这对于做代码审查辅助、提交规范检查之类的插件非常有用。但要注意版本控制相关的操作往往涉及用户数据一定要谨慎。任何写操作都要有明确的用户确认不能静默执行。我见过一个插件自动帮用户提交代码结果把不该提交的文件也提交上去了用户非常恼火。这种教训要引以为戒。6.3 插件间的协作与扩展IntelliJ 的插件体系是支持插件之间互相扩展的。你可以在自己的插件里定义扩展点让其他插件来扩展你的功能。这适合做平台型插件比如你做了一个代码生成框架允许其他团队写模板插件来接入。实现方式是定义一个扩展点接口在plugin.xml里用extensionPoint声明然后在代码里通过ExtensionPointName来获取所有实现。这个机制跟 IDE 自身的扩展机制是一样的理解了一个其他的就都通了。不过我要提醒一句定义扩展点意味着你要维护一套 API。一旦发布了就不能随便改接口签名否则依赖你的插件都会挂掉。所以除非确实有必要否则不要轻易对外暴露扩展点。6.4 我个人的一些实操心得做了几个插件之后我最大的体会是不要追求大而全要做小而精。一个插件解决一个明确的问题比一个插件什么都能干但什么都干不好要强得多。用户安装插件是为了解决具体痛点不是为了找一个全能工具。另外日志和错误处理要比业务代码更用心。插件出问题时用户往往不知道怎么排查如果你的日志清晰、错误提示友好能省掉大量沟通成本。我习惯在每个关键路径上都加日志并且在捕获异常时给出明确的用户提示而不是只打一行堆栈。最后保持对 IDE 更新的关注。IntelliJ 平台每年会有几个大版本更新每次更新都可能带来新的 API 和废弃的旧 API。定期花点时间看看更新日志提前适配比等到用户反馈问题再动手要从容得多。这个方向后续还可以往很多地方延伸比如结合代码分析做智能重构建议、集成内部平台做一键发布、甚至用机器学习来做代码补全。但不管往哪个方向走核心都是理解 IDE 的扩展机制然后在这个基础上做加法。把基础打牢了后面的路会越走越宽。