ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

IntelliJ IDEA插件开发实战:从构建配置到签名发布

IntelliJ IDEA插件开发实战:从构建配置到签名发布 简介面向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 代码的 PSIProgram 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 idcom.example.myplugin/id nameMy Plugin/name vendor emaildevexample.com urlhttps://example.comExample Corp/vendor dependscom.intellij.modules.platform/depends depends optionaltrue config-filejava-dependency.xmlcom.intellij.modules.java/depends extensions defaultExtensionNscom.intellij toolWindow idMyToolWindow anchorright icon/icons/myToolWindow.svg factoryClasscom.example.MyToolWindowFactory/ applicationService serviceInterfacecom.example.MyService serviceImplementationcom.example.MyServiceImpl/ /extensions actions action idcom.example.MyAction classcom.example.MyAction textMy Action descriptionDo something add-to-group group-idToolsMenu anchorfirst/ keyboard-shortcut keymap$default first-keystrokectrl alt M/ /action /actions /idea-plugin这个配置里值得注意的有几个点depends标签里的optionaltrue表示 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.internaltrue然后重启 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 自研的 MPSMeta 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 FormGrammar-Kit 解析它并生成对应的 PSI 类。其核心是generateParser和generatePsi两个任务分别生成解析器代码和 PSI 节点类。定义一个.bnf文件时配置项需要注意几个关键参数。:language要配成你的自定义语言类generateTokenType用于生成词法单元类型类parserClass指定生成的解析器类位置。有一个重要经验.bnf的规则顺序会直接影响解析器的优先级把长规则放前面能避免短匹配先命中导致后面的分叉解析失败。遇到解析错误比如某个表达式总是解析不出预期结构优先排查.bnf里的规则顺序因为它直接决定回溯路径——这在语言插件开发中几乎是一个必备技能。3.3 插件测试从 Light Test 到 Heavy Test 的取舍测试是语言插件开发绕不开的环节因为 PSI 操作极易出错而且错误往往是运行时才暴露。IntelliJ Platform 提供两类测试基类LightPlatformTestCase和HeavyPlatformTestCase。前者运行在内存文件系统中启动快适合解析和 PSI 结构测试后者启动完整的 IDE 环境会落盘适合涉及文件索引、VFSVirtual File System事件的测试。选择标准是凡是操作只需 PSI 层的测试用 Light凡是涉及 Project 级别的服务、文件索引、真实磁盘读写的测试用 Heavy。测试数据目录通过getTestDataPath()指定推荐的目录结构是testData/下按测试类分目录。运行测试时注意idea.test.ist和idea.test.tmp两个系统属性的配置它们分别控制测试实例标识和临时目录否则多个测试进程可能互相干扰。我在开发语法高亮插件时遇到过测试偶发失败的问题后来排查发现是测试数据文件编码不一致导致的——.java测试文件多是 UTF-8但.bnf生成的文件可能是平台默认编码在中文 Windows 上就变成 GBK测试断言直接失败。从那以后凡是测试相关数据文件我都统一在.gitattributes里强制*.bnf text eollf encodingUTF-8。测试常见问题时还常遇到默认日志级别下无法看到 DEBUG/TRACE的现象可以在idea.log路径下用log4j.properties手动提升日志级别如果不想在测试输出里看到 std err 日志可以通过idea.test.err.disabledtrue关闭。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.internaltrue] systemProperties [idea.platform.prefix: Idea] }这里jvmArgs在运行时追加到 IDE 的启动参数中。注意jvmArgs会全局替换该任务的默认 JVM 参数-Xmx2g把堆内存上限调到 2G-Didea.is.internaltrue启用了内部工具。加了这批参数后用runIde启动 IDE再打开Help - About能确认参数是否生效。4.3 动态插件自动重新加载引发的诡异状态现象插件代码修改后 IDE 自动重新加载插件但界面停留在旧版本或者功能时好时坏。原因IntelliJ 平台对动态插件Dynamic Plugin支持热加载但热加载对代码结构有限制——不能新增或删除扩展点注册不能修改plugin.xml里已有的扩展声明。解决办法在plugin.xml的根节点加idea-plugin dynamictrue只是声明插件支持动态加载开发阶段如果频繁该扩展点结构建议先关闭自动重载。做法是修改build.gradlerunIde { systemProperties[idea.plugins.loader.skip.conflict.check] true systemProperties[idea.auto.reload.plugins] false }我实际开发时习惯用idea.auto.reload.pluginsfalse宁可每次手动重启 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 才不会卡死。从那以后我每次写涉及批处理、文件扫描或频繁触发功能的代码都会强制走一遍能否放后台线程、能否加缓存、能否加索引。这个习惯希望也能帮到你。本文还有配套的精品资源点击获取
返回列表