简介:面向Java开发者的IntelliJ Platform插件开发指导手册,以IntelliJ IDEA为对象,系统覆盖从基础概念、图形界面到语言类扩展的完整插件构建流程,适合希望提升开发环境可定制性的初中高级程序员。压缩包内共1个PDF文档,大小约3.99MB,由上册、下册及附录组成:上册讲解插件架构、生命周期、事件监听、XML配置,以及Action System、Tool Windows等图形化开发;下册深入自定义文法、解析器、语法高亮、代码补全与代码分析;附录汇总Gradle/Maven构建配置、SDK下载链接及社区资源。手册依据官方资料与作者实践经验编写,对UI类插件给出第一、二、四部分的学习重点,对代码级或商业插件给出第一部分与下册的进阶路线,并建议配合动手调试加深理解;整体按目标分层编写,不同基础读者均可找到合适起点。目前已有876人学习下载,整体目录清晰,可作系统学习或按需查阅的参考手册。
1. 从语言国际化改造到 IntelliJ IDEA 插件开发:这份手册解决了什么
如果你接手过 300 多个老应用的国际化改造,就会明白 IntelliJ IDEA 插件开发这件事根本不是锦上添花,而是救命稻草。笔者当初面对的就是这样一个局面:几十个研发要翻译 5 种语言,僵尸应用遍地都是,翻译遗漏还可能踩当地文化雷区,纯粹堆人力根本完不成。后来花两周时间做了一个能扫描自定义文件类型、调用翻译 API、自动生成 .properties 和 Excel 的插件,勉强顶过了考核,但第一版没有图形界面、翻译有遗漏,在公司推广时被吐槽得厉害。真正开始优化才发现,网上关于 IntelliJ Platform 插件开发的资料少得可怜,官方文档零散,GitHub 上的开源插件源码又看不懂,一个按钮的交互可能要试半天。这份手册就是笔者把官方文档重新整理、融合个人实践经验后的产物,上册讲 UI 界面类插件,下册讲语言类插件,附录收齐了术语、工具和社区资源。适合两类人:想写框架集成、代码统计这类带界面的工具的开发者,以及想做代码补全、语法高亮这类基于代码的插件的进阶开发者。说句实在话,如果你只靠官方文档自己摸索,一个简单插件可能也要磨一个月;有这份手册照着走,一周左右就能跑通。
2. 插件开发的底层骨架:从依赖库到 plugin.xml 完整配置
2.1 看懂 IntelliJ Platform 的插件体系结构
IntelliJ IDEA 本身是一个平台,插件就是挂在这个平台上的功能模块。理解这一点是开发的第一步:你写的插件不是一个独立程序,而是通过平台暴露的扩展点(Extension Point)和动作系统(Action System)嵌入 IDE 的。插件生命周期由平台管理,从加载、初始化到卸载,都有对应的回调接口。开发插件时做的最多的两件事:一是注册 Action 到菜单或工具栏,二是实现 Extension Point 提供的接口,让 IDE 在特定时机调用你的代码。
这里有个关键概念需要先搞清楚:插件依赖(Plugin Dependencies)。插件不是随便声明依赖就行,得看你的插件要兼容哪些 IDE 产品。如果你的插件只调用 IntelliJ Platform 最基础的 API,那它可以兼容所有 JetBrains 产品,包括 IntelliJ IDEA、PyCharm、WebStorm 等,这种情况下依赖声明为com.intellij.modules.platform就够了。但如果你的插件要操作 Java 代码的 PSI(Program Structure Interface),就必须声明com.intelij.modules.java,这就意味着你的插件只能运行在支持 Java 的 IDE 上。选错依赖声明,插件在别的 IDE 上要么报错要么直接不加载,这是新手最容易踩的坑之一。
2.2 Gradle IntelliJ Plugin:构建配置与任务说明
构建插件用的是 Gradle IntelliJ Plugin,这是官方维护的构建工具。在build.gradle里配置intellij块时,需要指定type和version,前者决定用哪个 IDE 作为依赖基线,后者决定版本。配置示例:
plugins { id 'java' id 'org.jetbrains.intellij' version '1.15.0' } intellij { version = '2023.2.5' type = 'IC' // IC: IntelliJ Community, IU: IntelliJ Ultimate pluginName = 'MyPlugin' updateSinceUntilBuild = false } dependencies { implementation 'com.google.code.gson:gson:2.10.1' }这段配置的逻辑是:version和type决定了你编译时依赖的 IDE SDK 版本,pluginName是打包后的插件文件名,updateSinceUntilBuild设为false意味着不限制 IDE 版本区间,插件可以在较新的 IDE 上运行。实际进行插件开发时,我一般会把updateSinceUntilBuild在开发阶段设为false,省得 IDE 版本一升级就加载不了插件;发布时再改成true并精确指定版本区间,因为市场审核会检查这个字段。这里还涉及buildSearchableOptions任务,执行后会在build目录下生成searchableOptions.xml,里面存放插件的搜索选项,发布前最好执行一遍,检查插件在 IDE 设置中能否被正常搜索到。
2.3 plugin.xml:插件的配置文件到底要写什么
plugin.xml是插件的核心配置文件,位于src/main/resources/META-INF/目录下。它声明了插件 ID、名称、版本、依赖、动作、扩展点等全部元信息。注意一个细节:插件 ID 一旦发布到市场就不能修改,否则老用户升级时会识别成两个不同的插件。
下面是一个包含多个扩展点的plugin.xml配置片段:
<idea-plugin> <id>com.example.myplugin</id> <name>My Plugin</name> <vendor email="dev@example.com" url="https://example.com">Example Corp</vendor> <depends>com.intellij.modules.platform</depends> <depends optional="true" config-file="java-dependency.xml">com.intellij.modules.java</depends> <extensions defaultExtensionNs="com.intellij"> <toolWindow id="MyToolWindow" anchor="right" icon="/icons/myToolWindow.svg" factoryClass="com.example.MyToolWindowFactory"/> <applicationService serviceInterface="com.example.MyService" serviceImplementation="com.example.MyServiceImpl"/> </extensions> <actions> <action id="com.example.MyAction" class="com.example.MyAction" text="My Action" description="Do something"> <add-to-group group-id="ToolsMenu" anchor="first"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl alt M"/> </action> </actions> </idea-plugin>这个配置里值得注意的有几个点:depends标签里的optional="true"表示 Java 模块依赖是可选的,这样插件在没有 Java 支持的 IDE 里也能加载,只是相关的功能不生效,实际操作中这算是一种优雅降级方案;toolWindow声明了一个右侧的工具栏窗口;keyboard-shortcut给 Action 绑定了快捷键。还有一点容易被忽略:插件图标要放在resources目录下,并在plugin.xml里用/icons/xxx.svg引用,SVG 格式的图标是官方推荐的,因为 IDE 的 Darcula 和 Light 主题对图标有适配要求。这里建议引用官方图标库提供的标准图标,而不是随便找一套,否则深色主题下图标会糊成一片。
2.4 内部工具:开发者模式下的调试利器
IntelliJ IDEA 提供了内部工具(Internal Actions),用于调试插件 UI。启用方式是Help -> Edit Custom Properties,在配置文件中加一行idea.is.internal=true,然后重启 IDE,菜单栏里就会出现Tools -> Internal Actions。这个工具有很强的调试价值,UI -> UI Inspector可以像浏览器开发者工具一样查看 IDE 界面的组件树,定位 Tool Window 在界面上的位置;UI -> Duplicate Line这类功能可以辅助测试编辑器行为。我在做 Tool Window 开发时遇到过面板布局不对的问题,用 UI Inspector 一看,发现是ContentManager的addContent时机不对,面板在初始化前就被填充了内容。内部工具还有一个用处是看当前 IDE 的扩展点列表,比去官网查 SDK 文档要准,因为它是从当前运行实例的类加载器里直接读取的,不会有版本偏差。
3. 动手开发语言类插件:Grammar-Kit 与 PSI 解析的开发要点
3.1 自定义语言开发向导与前置条件
下册的核心是语言类插件,目标是支持自定义语言或 DSL 的解析、语法高亮、代码补全、代码检查等功能。这类的插件和 UI 插件有本质差别:UI 插件主要操作视图层,语言插件直接操作 IDE 的文件系统和编辑器底层,也就是要处理 PSI——IntelliJ Platform 对源代码文件建立的树状结构模型。PSI 可以理解成是代码的逻辑表示,IDE 里的语法高亮、代码折叠、重构、导航都基于它工作,它的底层实现与 JetBrains 自研的 MPS(Meta Programming System)密切相关,MPS 负责把文法定义转换成可执行的解析器。开发语言插件之前需要明确一件事:你的目标语言是被 IDE 已有语言支持,还是完全没有支持的新语言?如果是已有语言,重点是利用 PSI 和扩展点挂新功能;如果是新语言,那么文法定义、Parser、Lexer、Annotator 这一整套链路都得自己写。手动编写解析器容易出错,因此官方推荐的做法是用 Grammar-Kit 插件,它在 IDE 中提供图形化的.bnf文法文件编辑界面,自动生成 PSI 类和解析器代码,再配合 JFlex 生成词法分析器。可以说 Grammar-Kit 是语言类插件开发中的核心工具,我接触下来发现它生成的代码完成度很高,自己再补少量手工逻辑就可以了。
3.2 Grammar-Kit 插件配置与 .bnf 文法生成流程
Grammar-Kit 是一个独立的 IntelliJ IDEA 插件,需要先在 IDE 中安装,它专门用于生成 Language 插件所需的 PSI 类和解析器。它的工作方式是你编写.bnf文件(Backus-Naur Form),Grammar-Kit 解析它并生成对应的 PSI 类。其核心是generateParser和generatePsi两个任务,分别生成解析器代码和 PSI 节点类。定义一个.bnf文件时,配置项需要注意几个关键参数。:language要配成你的自定义语言类,generateTokenType用于生成词法单元类型类,parserClass指定生成的解析器类位置。有一个重要经验:.bnf的规则顺序会直接影响解析器的优先级,把长规则放前面能避免短匹配先命中导致后面的分叉解析失败。遇到解析错误,比如某个表达式总是解析不出预期结构,优先排查.bnf里的规则顺序,因为它直接决定回溯路径——这在语言插件开发中几乎是一个必备技能。
3.3 插件测试:从 Light Test 到 Heavy Test 的取舍
测试是语言插件开发绕不开的环节,因为 PSI 操作极易出错,而且错误往往是运行时才暴露。IntelliJ Platform 提供两类测试基类:LightPlatformTestCase和HeavyPlatformTestCase。前者运行在内存文件系统中,启动快,适合解析和 PSI 结构测试;后者启动完整的 IDE 环境,会落盘,适合涉及文件索引、VFS(Virtual File System)事件的测试。选择标准是:凡是操作只需 PSI 层的测试用 Light,凡是涉及 Project 级别的服务、文件索引、真实磁盘读写的测试用 Heavy。测试数据目录通过getTestDataPath()指定,推荐的目录结构是testData/下按测试类分目录。运行测试时注意idea.test.ist和idea.test.tmp两个系统属性的配置,它们分别控制测试实例标识和临时目录,否则多个测试进程可能互相干扰。我在开发语法高亮插件时遇到过测试偶发失败的问题,后来排查发现是测试数据文件编码不一致导致的——.java测试文件多是 UTF-8,但.bnf生成的文件可能是平台默认编码,在中文 Windows 上就变成 GBK,测试断言直接失败。从那以后凡是测试相关数据文件,我都统一在.gitattributes里强制*.bnf text eol=lf encoding=UTF-8。测试常见问题时还常遇到默认日志级别下无法看到 DEBUG/TRACE的现象,可以在idea.log路径下用log4j.properties手动提升日志级别;如果不想在测试输出里看到 std err 日志,可以通过idea.test.err.disabled=true关闭。
3.4 代码补全与检查:Annotator 和 CompletionContributor 的配合
完成语法解析后,接下来的功能通常是在Annotator和CompletionContributor上做文章。Annotator负责在代码上标记错误和警告,CompletionContributor实现代码补全提示。两者配合的简单场景是:在注解处理器里识别出某个标识符类型不匹配时给出错误标记,同时在补全贡献者里按上下文提供候选符号。补全的上下文判断通常是看PsiElement的父节点类型和位置,例如当光标前是一个.符号时,补全对象应该是成员。这一类功能开发容易出现的问题是补全结果重复显示或不显示,原因多见于CompletionResultSet.addElement时没有设置Priority,导致排序混乱;或者getPrefixMatcher没有正确设置,匹配时把大小写敏感默认值带错了。
4. 常见问题避坑指南:错误定位与排查思路
4.1 IntelliJ Platform 插件的依赖冲突
开发插件过程中遇到最多的坑不是代码逻辑问题,而是依赖冲突。现象:插件加载后 IDE 直接报NoClassDefFoundError或ClassNotFoundException,但代码编译是正常的。原因:插件打包时把 IDE 自带的类库也打进去了,或者依赖的第三方库版本和 IDE 内部版本不一致。解决办法:在build.gradle里检查依赖的是implementation还是compileOnly。凡是 IDE 自身提供的类,一律用compileOnly,只有插件特有且 IDE 不提供的第三方库才用implementation。如果插件用了 Gson,但 IDE 里已经有旧版 Gson,就要用implementation('com.google.code.gson:gson:2.10.1') { transitive = false }防止传递依赖把 IDE 的类覆盖掉。
4.2 runIde 任务的 JVM 参数配置
现象:插件代码里用了大量内存做缓存,runIde启动的 IDE 频繁卡顿或直接 OOM。原因:runIde默认使用的 JVM 参数和正式 IDE 不同,内存上限偏低。解决办法:在build.gradle里对runIde任务单独配置 JVM 参数:
runIde { jvmArgs = ['-Xmx2g', '-Xms256m', '-Didea.is.internal=true'] systemProperties = ['idea.platform.prefix': 'Idea'] }这里jvmArgs在运行时追加到 IDE 的启动参数中。注意jvmArgs会全局替换该任务的默认 JVM 参数,-Xmx2g把堆内存上限调到 2G,-Didea.is.internal=true启用了内部工具。加了这批参数后用runIde启动 IDE,再打开Help -> About能确认参数是否生效。
4.3 动态插件自动重新加载引发的诡异状态
现象:插件代码修改后 IDE 自动重新加载插件,但界面停留在旧版本,或者功能时好时坏。原因:IntelliJ 平台对动态插件(Dynamic Plugin)支持热加载,但热加载对代码结构有限制——不能新增或删除扩展点注册,不能修改plugin.xml里已有的扩展声明。解决办法:在plugin.xml的根节点加<idea-plugin dynamic="true">只是声明插件支持动态加载,开发阶段如果频繁该扩展点结构,建议先关闭自动重载。做法是修改build.gradle:
runIde { systemProperties['idea.plugins.loader.skip.conflict.check'] = 'true' systemProperties['idea.auto.reload.plugins'] = 'false' }我实际开发时习惯用idea.auto.reload.plugins=false,宁可每次手动重启 IDE 跑一次全量加载,也不愿意在热加载的诡异 bug 上耗时排查。
4.4 工具窗口初始化时机错误
现象:插件启动后 Tool Window 内容是空的,但过一会儿手动触发刷新又正常。原因:Tool Window 的createToolWindowContent在 IDE 启动早期就会被调用,此时项目索引还没构建完,你订阅的异步数据还没准备好。解决办法:把数据加载逻辑放进ProjectManagerListener的projectOpened回调里,等项目完全打开后再填充 ToolWindow 内容;或者使用ApplicationManager.getApplication().invokeLater把任务丢到 EDT(事件分发线程)队列尾部。这里有一个关键点:PSI 的访问必须在 EDT 上执行,不能在后台线程里直接调用 PSI 方法,否则会抛出ReadAccess异常或直接死锁。
4.5 PSI 修改后必须提交并刷新
现象:在插件里修改了 PSI 节点,紧接着执行查询时读到的是旧数据,或者在编辑器里看到的内容和 PSI 不一致。原因:PSI 修改后,文档变更事件尚未提交,IDE 的索引和编辑器视图还没感知到变更。解决办法:在批量修改 PSI 时,用WriteCommandAction.runWriteCommandAction包裹所有修改操作,修改结束后调用PsiDocumentManager.getInstance(project).commitDocument(document)强制提交文档。若修改量较大,还需要调用CodeStyleManager.reformat做一次格式化。注意提交文档是一个耗时操作,在非 EDT 线程调用会抛异常;如果必须异步执行,用ReadAction配合ApplicationManager.invokeLater切回 EDT。
4.6 测试环境区分不清导致测试失败
现象:测试在本地跑通过,CI 上偶发失败;或者测试在 IDE 内跑成功,命令行跑失败。原因:Light 测试和 Heavy 测试的测试环境不同,Light 测试用内存虚拟文件系统,不触发文件监听,也不支持真实文件 IO;如果你在 Light 测试里写了new File()这种代码,它在 CI 环境的表现就会不稳定。解决办法:测试设计阶段就明确测试类型——涉及 VFS、文件索引、Project 模块结构的测试一律用 Heavy,纯解析逻辑用 Light。写测试时还用到一个技巧:测试数据放在testData/目录下,用getTestDataPath()拼接相对路径,不要写绝对路径,这样开发者本地和 CI 的路径差异就不会影响测试结果。
5. 插件签名与发布:市场审核绕不开的流程
5.1 插件签署原理与为什么要签名
IntelliJ 插件市场从 2020 年起要求插件必须签名,签名的作用是保证插件在传输和安装过程中未被篡改,同时验证作者身份。JetBrains 官方提供了一套基于非对称加密的签名机制:你生成密钥对,私钥用来签名插件 JAR 包,公钥随插件一起发布,IDE 安装时用公钥验证签名完整性。签名流程主要涉及三个动作:生成私钥、签名、验证。不签名会怎么样?插件无法提交到 JetBrains 插件市场,本地开发不受影响,但用户无法通过 IDE 内的插件市场安装你的插件。实际开发中我建议在build.gradle里签名的参数用环境变量注入,不要硬编码私钥路径和密码到代码里,因为插件市场审核员能看到你的构建配置,私钥一旦泄露任何人都可以伪造你的插件签名。
5.2 生成私钥与签名插件
签名用的是 JAR 签名工具jarsigner,它需要你先用keytool生成一个包含私钥的keystore文件。命令如下:
keytool -genkeypair -alias intellijplugin -keyalg RSA -keysize 2048 -validity 3650 -keystore myplugin.jks-alias指定别名,签名时要用同一个别名;-keyalg RSA是密钥算法;-validity 3650表示密钥有效期为 10 年,到期后必须重新签名;myplugin.jks是输出的密钥库文件,里面包含私钥和证书。它生成时会提示输入密码,密码要记住,签名和后续验证都要用。生成密钥库后,在build.gradle里配置签名:
intellij { // 已有其他配置 } signPlugin { certificateChain = files('certificates/chain.crt') privateKey = files('certificates/private.key') password = System.getenv('PLUGIN_SIGN_PASSWORD') } publishPlugin { token = System.getenv('JETBRAINS_TOKEN') }certificateChain是证书链文件,privateKey是私钥文件,这两个文件前置要求是私钥要从 JKS 中导出为 PEM 格式;password是私钥密码;publishPlugin.token是发布时调用 JetBrains 市场 API 的身份凭证。签名完成后执行buildPlugin任务,生成的 ZIP 包就是用私钥签名过的插件。验证签名是否有效,使用官方提供的插件验证器,它还能顺便检查插件兼容的 IDE 版本范围、插件描述是否符合规范,这些审核项如果不通过,插件提交后会被市场拒绝。
5.3 发布前的完整验证清单
发布前建议把以下检查走一遍,因为每一条都踩过坑。第一,plugin.xml里的since-build和until-build版本范围是否合理,范围太大可能被市场警告不兼容,范围太小则影响下载量,参考值是精确到你测试过的最近几个版本。第二,插件图标和描述是否符合市场要求,描述中不能出现其他市场或品牌的名字。第三,插件验证器跑一遍,确认无错误。第四,用buildPlugin打出的包在干净环境里的 IDE 上安装测试,而不是只在runIde的开发环境里测。第五,如果你设置了updateUntilBuild = false,发布到市场后要关注用户反馈,因为用户 IDE 版本太新而插件 API 不兼容时,插件会直接失效且 IDE 没有明显提示。
6. 进阶路线:从简单插件到成熟插件的几个关键习惯
6.1 把源码看懂的优先级排在文档之上
当你打算做代码检查、重构这类高级功能时,最好的资源不是文档,而是 IntelliJ Platform 本身的源码,这是因为官方 SDK 文档的覆盖范围有限,很多 API 的边界行为,通过源码能看得更直观。我看源码的高效路径是先定位一个功能对应的扩展点,然后在源码仓库里搜索扩展点类的实现,看官方插件是怎么处理的——比如想知道CodeStyleManager.reformat的行为边界,直接看 Java 插件里对它的调用方式,比干读 Javadoc 有用得多。IDE 自带的插件源码一般关联在 SDK 里,用Go To -> Implementation就能跳进去。
6.2 插件代码结构设计
插件代码达到一定规模后,推荐按模块拆分,而不是把所有代码堆在一个包下。这里分享一套个人习惯的分层:api存放插件的对外服务接口,internal存放核心实现,ui存放所有界面相关的类,lang放语言插件相关的 PSI 和解析逻辑。每个模块之间做到单向依赖:ui层只依赖api不依赖internal,这样后续重构时不需要全链路改。难点在于 IntelliJ 插件的模块化和普通的 Java 项目的模块化是有区别的——插件加载器对类的隔离是基于插件边界的,同一个类在插件 A 和插件 B 之间是不可见的,所以模块之间的依赖必须靠plugin.xml中声明的依赖关系来建立。
6.3 快速迭代:runIde、插桩与日志
runIde是本地调试最快的路径,但它有一个缺点:每次改代码都要重新编译和启动 IDE。后来我摸索出更高效的模式:runIde配合idea.log的实时日志输出,把关键逻辑的日志级别调到 DEBUG,用tail -f在终端实时看。代码里尽量避免用System.out.println输出调试信息,统一用LoggerFactory.getLogger,否则日志无法分级,生产环境也会打印一大堆无用信息。一旦遇到 UI 事件线程阻塞或卡死,第一时间看日志里有没有 EDT 线程相关字样。这类问题基本都是因为你在 EDT 上执行了耗时任务——正确的做法是异步任务在后台线程执行,拿到结果后再通过invokeLater回 EDT 更新 UI。
6.4 性能问题:缓存和索引的合理使用
插件功能越来越复杂后,性能就成了用户是否留存的判断标准。语法高亮和代码检查这类功能会在每次按键后触发,如果处理逻辑耗时长,IDE 就会反映为明显卡顿,因此这类操作必须高效。常见做法是:将解析结果缓存在PsiElement的UserData里,或者实现IndexedFileSet级别的文件级索引。我在做国际化扫描插件时犯过一个错误——扫描 300 个项目的文件时直接在 EDT 上遍历所有文件并打开每个文件、读取 PSI,这个操作用时几十秒,IDE 直接变白板。后来把文件遍历放到ProgressManager的后台任务中执行,并且每处理完一个文件就调用ProgressIndicator.checkCanceled()检查取消状态,IDE 才不会卡死。从那以后我每次写涉及批处理、文件扫描或频繁触发功能的代码,都会强制走一遍:能否放后台线程、能否加缓存、能否加索引。这个习惯,希望也能帮到你。
本文还有配套的精品资源,点击获取