简介:面向Intellij IDEA插件开发者的系统学习手册,基于JetBrains Runtime 17.0.9,兼容IDEA 2023+及2024版本,适合具备一定Java基础、希望进入插件开发领域的读者。上册围绕插件开发基础与图形化插件开发展开:从平台术语、IDE插件类型、开发环境要求、开发流程与参考网站入手,详细演示了第一个插件工程的创建与配置,并涉及工程测试等关键环节;同时为下册的语言类插件开发与附录的工具资料规划了清晰路径,便于读者按需选择框架集成、代码统计、效率工具,或代码自动完成、代码检查等方向的深入学习。资源为1个PDF文件,大小15.82MB,目录分级明确,知识点间配有示例与操作说明,可有效降低上手门槛。目前已有383人学习下载,适合希望通过系统手册快速搭建Intellij平台插件开发知识体系的Java开发者。
1. Intellij Platform PlugIn 插件开发手册“上”册:环境、骨架与第一次运行
我见过不少团队在 idea插件开发 上卡住的第一个晚上,不是卡在写那个 AnAction 类,而是卡在“手册看完了,runIde 却一直起不来”的翻车现场。Intellij Platform PlugIn 插件开发手册的“上”册,从标题也能猜到它的任务:把环境、工程骨架、plugin.xml、第一个 Action 和第一次打包串成一条可复现的路。它适合有 Java 或 Kotlin 基础的从业者,目标不是让你成为 PSI 专家,而是让你在三天内从零跑通一个能被 IDE 加载、能弹菜单、能打包分发的最小插件。如果你目标更激进,比如要改编辑器渲染或做语言服务集成,这份“上”册只负责打底,不负责带你进深渊。
2. 搭好 IntelliJ Platform 插件开发环境:JDK、Gradle 与 IDE 三者的版本对齐
开发 idea 插件步骤里,最容易劝退新人的不是 API 复杂度,而是三件套版本对不上:IDE 自带的 JBR、编译插件用的 JDK、以及 Gradle 插件自身的版本。手册“上”册通常会直接从 IntelliJ SDK 讲起,但我建议你先把下面这张经验表贴在终端旁边,再开始动手。
| IDE 版本 | 自带 JBR | 推荐编译 JDK | 推荐 Gradle | 说明 |
|---|---|---|---|---|
| 2021.3 | JBR 11 | JDK 11 | Gradle 6.8+ | 老工程常见组合 |
| 2022.3 | JBR 17 | JDK 17 | Gradle 7.5+ | 大量教程默认环境 |
| 2023.2 | JBR 17 | JDK 17 | Gradle 8.0+ | 我常用组合 |
| 2024.1 | JBR 21 | JDK 21 | Gradle 8.5+ | 新项目可以直接跟进 |
别把这张表当成官方兼容矩阵,它只是个人项目经验值,方向上参考。真正重要的是理解为什么需要对齐:插件最终跑在 IDE 自带 JBR 里,而不是跑在你系统安装的 JDK 上。
2.1 三个版本前提:JBR、JDK 与 Gradle 为什么必须对齐
IDE 2023.x 这个区间,自带的 JBR 是 17。插件字节码最终要加载进这个 JBR,所以编译目标不能比它新。你本地如果装的是 JDK 21,那就把编译 target 设成 17;直接 target 21,启动阶段就会报 UnsupportedClassVersionError。反过来,IDE 还是 2021.x 时代(JBR 11),却在 build.gradle.kts 里把依赖指向了 2023.1 的 Platform SDK,构建能过,启动时接口二进制不兼容,NoSuchMethodError 满天飞,这才是新手眼里最典型的“玄学”问题。
Gradle 版本影响的是 org.jetbrains.intellij 插件能否被正常加载。老项目常见 Groovy DSL 加 apply 语法,在 Gradle 8 下会碰到 apply method 被禁用的硬报错,这个坑我放到第 5 章专门说。我这边能稳定用的组合是 IDEA 2023.2、JDK 17、Gradle 8.0、org.jetbrains.intellij 1.15.0。如果你开新项目,可以去看看官方最新的 IntelliJ Platform Gradle Plugin 2.x,配置入口和 1.x 不一样;但“上”册里大量工程还是 1.x,先按 1.x 学,之后迁移成本不大。
2.2 搭建最小 Gradle 工程:build.gradle.kts 里的关键参数
先建一个空目录,然后放两个文件。第一个是 settings.gradle.kts,用于指定插件仓库和工程名:
pluginManagement { repositories { gradlePluginPortal() mavenCentral() } } rootProject.name = "my-first-ide-plugin"这段配置解决的是插件本身从哪下载。公司内网环境下,如果默认源拉不到 org.jetbrains.intellij,就在这里加内部 mirror,不要改 build.gradle.kts 里的 repositories,否则会绕过 pluginManagement 直接走全局源,行为更不可控。
第二个是 build.gradle.kts,核心内容如下:
plugins { java id("org.jetbrains.intellij") version "1.15.0" } group = "com.example" version = "0.1.0" intellij { pluginName.set("my-first-ide-plugin") version.set("2023.1") type.set("IC") downloadSources.set(true) } tasks.patchPluginXml { sinceBuild.set("231") untilBuild.set("241.*") }intellij 块里最值得盯的是 version 和 type。version 是插件要下载的 IDE SDK 版本,我这里用 2023.1;type 是发行版型号,IC 是社区版,IU 是旗舰版,常见还有 PC、PY、GO。个人学习阶段用 IC 最轻量,但如果你要依赖旗舰版才有的模块,type 就必须设成 IU,否则编译期找不到对应类。downloadSources 默认打开,这样断点能进 IDE 自己的实现类,代价是首次同步时间长,初次跑通时可以先关掉。
patchPluginXml 的 sinceBuild 和 untilBuild 直接决定打包出来的 zip 能被哪些 IDE 版本安装。sinceBuild 设 231,意思是只认 2023.1 起的 IDE;untilBuild 设 241.*,表示 2024.1 全版本。范围写太窄,团队里别人装不上;写太宽,用到的 API 在老 IDE 上不存在,启动直接挂。
2.3 目录结构:别自己发明存放位置
插件工程和普通 Java 工程的区别只在两个地方。src/main/java 放插件代码,src/main/resources/META-INF/plugin.xml 放清单。IDE 加载插件时先读这个 XML,再反射创建类,文件名和路径是默认约定,Gradle 插件不会帮你换位置。你如果自己创建一个 resources/plugin.xml 放在别的地方,构建能过,运行起来 IDE 完全不认识。
第一次跑通之前,还建议在 gradle.properties 里加一行 org.gradle.jvmargs=-Xmx2g。原因是 runIde 会拉起整个 IDE,内存不够会闪退。很多新手把这里当成构建无关配置直接跳过,结果 runIde 一启动 IDEA 就报 low memory,然后回头改各种虚拟机参数,绕一大圈。先 2G,不够再加,这是最稳的起点。
3. 认识插件骨架:plugin.xml、AnAction 与扩展点如何协作
“上”册最核心的章节,我认为是“清单驱动”这一节。Intellij Platform PlugIn 插件没有 main(),入口是一份 plugin.xml。IDE 加载插件时,先解析这个 XML,把 Action 类塞进菜单,把扩展实现挂到平台钩子上;不认识的节点就跳过并写警告。所以插件开发的第一定律:宁可 Java 代码写朴素一点,也要让 plugin.xml 完全对着官方 schema 写。
3.1 plugin.xml:插件如何被 IDE 识别
我一般会先写一个最小的 plugin.xml,确保能加载,再往里填 Action 和扩展点。一个能弹菜单的最小清单长这样:
<idea-plugin> <id>com.example.my-first-ide-plugin</id> <name>My First IDE Plugin</name> <version>0.1.0</version> <vendor email="dev@example.com">Your Team</vendor> <description>一个用于验证插件链路的最小工程</description> <depends>com.intellij.modules.platform</depends> <depends>com.intellij.modules.java</depends> <actions> <action id="com.example.ShowTimeAction" class="com.example.ShowTimeAction" text="Show Current Time" description="在状态栏显示当前时间"> <add-to-group group-id="ToolsMenu" anchor="first"/> </action> </actions> </idea-plugin>id 最好用公司域名反写,避免和 Marketplace 上既有插件撞名。撞名不会影响编译,但同一个沙箱里同时装两个同名插件,后装的那个不会被加载,而且 idea.log 里只有很隐晦的提示。vendor 不是必填,但企业内部分发时,联系人邮箱能省掉不少沟通成本。
depends 是新手最容易漏的。com.intellij.modules.platform 是基础依赖,几乎每个插件都要加;一旦你的代码要操作 Java 工程的 PSI,比如读类名、拿 import 列表,就必须加 com.intellij.modules.java,否则运行时会告诉你类不存在。少依赖的症状是启动报错或功能没反应,而不是编译器报警,所以特别难查。
actions 节点写在 XML 而不是注解里,这是 IntelliJ 的标准做法。IDE 根据 id 识别菜单项,class 必须指向一个继承 AnAction 的类,text 是菜单显示文字。不要在 Java 代码里再次设置文本,两边不一致会让人困惑。
3.2 写第一个 AnAction:跟手菜单的完整过程
AnAction 是插件交互的基石。下面这个类对应上面的清单,点击菜单后弹一个消息框:
package com.example; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.ui.Messages; public class ShowTimeAction extends AnAction { @Override public void actionPerformed(AnActionEvent e) { Messages.showInfoMessage( "Now: " + java.time.LocalTime.now(), "Current Time"); } @Override public void update(AnActionEvent e) { e.getPresentation().setEnabledAndVisible(true); } }actionPerformed 是唯一的业务入口,参数 e 能拿到当前项目 Project、编辑器 Editor、选中文本等数据。先用 Messages 弹窗验证最省事,弹窗能出现,说明类和 XML 都被正确加载了,后面再逐步换成真实功能。
update 方法会在菜单每次显示前被调用,用于控制按钮是否置灰。初学阶段最安全的行为是直接 setEnabledAndVisible(true),什么条件都不加。把重逻辑挪进 update 是典型自杀式写法——IDE 每次弹菜单都会执行一遍,用户会直观感受到 UI 卡顿。如果你要用当前编辑器的上下文去做判断,比如只在 Java 文件里启用,再把 update 里的逻辑收紧。和 Chrome 插件开发里注册 browser action 的感觉类似,AnAction 只需要“类 + 注册点”就能挂到菜单,不需要手动为按钮画界面。
3.3 扩展点:更隐蔽的插件形态
Action 面向用户主动点击,扩展点面向平台主动回调。光标移动、文件保存、编译完成这些事件发生时,IDE 会调用你注册的实现类。扩展点同样写在 plugin.xml 的 extensions 节点上,格式如下:
<extensions defaultExtensionNs="com.intellij"> <toolWindow id="MyToolWindow" anchor="right" factoryClass="com.example.MyToolWindowFactory"/> </extensions>toolWindow 是最容易肉眼确认的扩展点之一,IDE 右侧会出现一个 MyToolWindow 页签,页面内容由 factoryClass 创建。扩展点名称是平台写死的,不能自创。开发时打开 IDEA 自己的 Actions 面板,搜 Extensions,能看到当前 IDE 支持的所有扩展点清单,比凭记忆写可靠得多。
注册扩展点最容易犯的错误是:类存在、XML 也写了,但忘记在实现类上实现对应接口,或者类名少写一个字母。IDE 加载时会尝试把 factoryClass 强转成 ToolWindowFactory,转不成就抛异常,然后 idea.log 里出现一大段 stack trace。这跟 AnAction 的注册逻辑是两套体系,不要混在一起记。
下面这张表适合贴在笔记里,区分两类注册方式:
| 维度 | AnAction | Extension |
|---|---|---|
| 触发方式 | 用户点击菜单或快捷键 | 平台事件回调 |
| 注册位置 | <actions> | <extensions> |
| 实现要求 | 继承 AnAction | 实现扩展点接口 |
| 典型用途 | 菜单动作、快捷键 | 工具窗、行标记、补全 |
新学的时候,Action 的感知成本低;扩展点需要一点反向思考,但它是插件能力的上限。手册“上”册一般只要求你认识它,能注册一个 toolWindow 就算过关。
4. 把插件跑起来:runIde、断点日志与 buildPlugin 全流程
前两章把纸面东西搭好了,接下来进入真正的开发 idea 插件步骤——本地运行。整个流程只有三条命令:runIde、Debug runIde、buildPlugin。下面按顺序拆。
4.1 runIde 最小命令与首次下载
在项目根目录执行:
./gradlew runIde这条命令背后会做三件事:解析 intellij.version 并下载对应 IDE SDK;把插件工程编译成类,合并 plugin.xml;然后启动一个沙箱 IDE。沙箱的配置目录是 build/idea-sandbox/config,日志目录是 build/idea-sandbox/system/log,和你日常开发用的 IDE 配置互不干扰,这一点非常重要——在沙箱里调试插件,不会污染你平时的工作环境。
如果本地已经装了同版本 IDE,可以指定 ideaPath 跳过下载:
./gradlew runIde -PideaPath=/Applications/IntelliJ IDEA.app/Contents/MacOS/ideaWindows 下就写 idea64.exe 的完整路径。这样可以省掉下载时间,但代价是拿不到 Platform SDK 的源码,断点进不了 IDE 内部实现。我一般建议第一次跑通用下载方式,之后为了速度再用本地安装路径。
首次 runIde 会下载全部依赖,耗时较长,别急着反复停任务。如果下载失败,优先检查网络源或换一个更常见的 IDE 版本,而不是马上重试,否则容易留下残缺缓存,后面每次构建都卡在同一处。
4.2 断点与日志:插件排错的左右手
在 Gradle 工具窗里右键 runIde,选择 Debug,IDEA 会以调试模式启动沙箱。由于 runIde 本质是启动另一个 JVM,断点能否命中取决于 Gradle 是否把调试参数传给沙箱,正常情况下是可以的,但要确认断点打在插件类里,不是打在平台类里。
如果发现断点一直不生效,别急着换断点位置,先看 idea.log。日志路径稳定在 build/idea-sandbox/system/log/idea.log,类加载、Action 注册失败、扩展点转换失败都有记录。推荐在 Action 里加一段日志,观察调用时机:
import com.intellij.openapi.diagnostic.Logger; public class ShowTimeAction extends AnAction { private static final Logger LOG = Logger.getInstance(ShowTimeAction.class); @Override public void update(AnActionEvent e) { LOG.info("update: " + e.getPresentation().getText()); e.getPresentation().setEnabledAndVisible(true); } }日志会输出到 idea.log,也可以用终端 tail 实时看。System.out 不是不能用,但插件类加载器在某些场景会吞掉标准输出,排查起来不如 Logger 干净。用 Logger 还支持按类名过滤,比在控制台里翻乱码快很多。
4.3 buildPlugin 打包到安装
本地验证通过后,执行:
./gradlew buildPlugin产物在 build/distributions/my-first-ide-plugin-0.1.0.zip。注意不要把这个 zip 解压后再压缩,IDE 的插件安装器期待一个带 META-INF/plugin.xml 的顶层目录,你重新压缩成别的结构,安装时会提示 Invalid plugin descriptor。
安装路径是日常 IDE 的 Settings → Plugins → 齿轮 → Install Plugin from Disk → 选择 zip。如果 IDE 提示版本不兼容,大概率是 sinceBuild/untilBuild 没对齐,回到 patchPluginXml 改。团队分发还可以用菜单里的 Export、Import 或自建 update site,那是“上”册以外的话题,先不用碰。
还有一个容易被忽略的点:runIde 沙箱和日常 IDE 是两个环境,沙箱里能跑的插件,日常 IDE 不一定能装。每次发版前,一定用 buildPlugin 装到日常 IDE 里点一次,这是底线操作。
5. 插件开发避坑笔记:四个高频问题与修复路径
跑通基础流程之后,剩下的就是血泪经验。以下四类问题,每条我都至少见过一次,有的直到现在偶尔还会踩到。
5.1 Gradle 8 下 apply 老写法直接报错
现象:build.gradle 第一行写着 apply plugin: 'org.jetbrains.intellij',执行任何 Gradle 任务时抛错,提示 You are applying a plugin imperatively using the apply method,后面的 exit code 是 1。
原因:Gradle 8.x 开始收紧旧式 apply 方法,org.jetbrains.intellij 属于第三方插件,不能再用命令式加载。网上大量 2020 年的博客都是这种写法,抄的时候很容易翻车。这个报错不是 IntelliJ 特有的,只要用 Gradle 8 升级旧第三方插件的工程都会遇到。
解决:把 apply 写法改成 plugins 块:
plugins { id("org.jetbrains.intellij") version "1.15.0" }同时保留 settings.gradle.kts 里的 pluginManagement 仓库。改完后先执行 ./gradlew clean,再执行 runIde,避免旧的构建缓存把问题掩盖掉。
5.2 JDK 与 JBR 不匹配,弹 J2SE 版本警告
现象:runIde 或真实 IDE 打开插件时,IDE 弹窗提示 in order to access this application, you must install the J2SE plugin version 17;或者更常见的是 java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException。
原因:插件用高版本 JDK 编译后,字节码里的类版本高于运行时 IDE 自带的 JBR。另一种情况是 JDK 11 之后标准库移除了 JAXB,老插件代码里还在 import,于是 ClassNotFound。
解决:分两步。第一步在 build.gradle.kts 里固定 java toolchain:
java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } }第二步把 intellij.version、sinceBuild、untilBuild 三者调整到同一个大版本区间,比如 2023.1 + 231 + 241.*。改完别只重跑 build,要 clean 一次再 runIde,否则 Gradle 缓存里还是旧 class 文件。
5.3 菜单不显示或一直置灰
现象:Action 类编译通过,plugin.xml 也写了,但菜单里找不到,或者找到了却是灰色。
原因:三个方向排查。一是 class 全限定名写错,idea.log 里会有一条 ClassNotFoundException;二是 add-to-group 的 group-id 不对,IDE 只会把 Action 放进真实存在的菜单组;三是 update 方法里 setEnabledAndVisible(false),又被某次事件上下文触发把按钮关掉了。
解决:先用最保守的写法。在 plugin.xml 里 anchor 改成 first,在 update 里直接 setEnabledAndVisible(true),重跑 runIde 看菜单。别在 update 里判断太多 Editor 上下文,等链路确认通了再加条件控制。如果还是看不见,打开 IDE 的 Plugins 设置,确认插件处于 enabled 状态。
5.4 打包出来的 zip 装不进目标 IDE
现象:成功 buildPlugin,但在另一台相同版本 IDE 上安装失败,提示插件描述文件缺失或版本不兼容。
原因:常见是 patchPluginXml 没写,untilBuild 默认为空,某些 IDE 版本严格模式拒绝安装;或者 zip 内部结构不对,Gradle 生成的 zip 是插件目录为顶层,手工改过的 zip 可能破坏层级。
解决:显式配置 sinceBuild 和 untilBuild。团队统一 IDE 版本时可以写死一个区间,比如 231-241,别写太宽。另一点是尽量不要手工改 zip,用 Gradle 重新 build,避免结构变化。“上”册能做到这里,插件已经具备分发价值了。
5.5 插件崩溃:NoClassDefFoundError 指向自己的类
现象:runIde 刚起来,控制台或 idea.log 出现大段 stack trace,Caused by: java.lang.NoClassDefFoundError,类名指向你自己写的类,有时还会弹出插件 crash 对话框。
原因:插件引用了 IDE 模块之外的第三方库,比如 commons-io,但没有把依赖打进产物;或者 plugin.xml 里 class 字段对应的 jar 没有进入插件 classpath。
解决:在 build.gradle.kts 里把第三方依赖声明成 implementation,Intellij Gradle 插件会把它复制进 lib 目录:
dependencies { implementation("commons-io:commons-io:2.11.0") }不要用 compileOnly,那意味着编译期可见、运行期缺失。出包后用任意解压工具查看 zip,确认 lib/ 目录存在并且包含对应 jar,基本就能避开这个坑。
6. 用三个小实验验证“上”册学到的整套链路
读完“上”册,最重要的不是记住 API,而是形成验证闭环。我建议你花一个下午做下面三个实验,每一步都是前面章节的组合练习。
第一个实验:把 ShowTimeAction 的弹窗改成 DialogWrapper 子类,接收用户输入后写进当前工程目录。这要求你同时改动 Java 类和 plugin.xml,验证 Action 注册链路仍然完整。如果弹窗能被打开,说明从 plugin.xml 到类加载再到 UI 的整条路径是通的。改动时不要动 plugin.xml 里的 id,只改 class 内容,能少踩一个注册冲突。
第二个实验:把第 3.3 节里的 toolWindow 扩展点实现出来。factoryClass 里返回一个简单 JLabel 面板,并在 createToolWindowContent 方法里打一条日志。启动后右侧出现页签,日志出现对应行,这条扩展链路就是通的。这个实验能帮你建立“扩展点视图”的肌肉记忆,后面学 LineMarkerProvider、CompletionContributor 都是同一套“接口 + XML 注册”的逻辑。
第三个实验:跑一次 buildPlugin,把 zip 装到你日常开发用的同一个版本 IDE 里,连续用几天。期间刻意去点菜单、开工具窗,看有没有闪退或异常输出。只有在日常 IDE 里跑过一周的插件,才算真正“上”册毕业。平时顺手把 idea.log 里和插件相关的 WARN 都处理掉,比学更多扩展点重要得多。
我自己的教训是:第一次做插件时,觉得 plugin.xml 只是配一下而已,于是跳过它直接写 Action 代码,结果菜单一直不出现。后来发现 XML 里 class 的包名写错一位,IDE 连日志都懒得告诉你是哪个类找不到,只能靠逐行排查。现在我会先让 plugin.xml 最小化,再逐步加 Action、加扩展点,每加一个就 runIde 一次。把 runIde 当成单元测试工具而不是启动器,是“上”册之后我最推荐的姿势。希望帮到你。
本文还有配套的精品资源,点击获取