资讯详情

资讯详情

IntelliJ插件开发入门:从Gradle工程到Action与Inspection实战

简介这份《IntelliJ Platform Plugin 开发指导手册》面向 Java 开发者与 IDE 插件爱好者帮助读者从零起步掌握 IntelliJ IDEA 插件开发并逐步进阶到语言类高级插件。手册由上册、下册与附录三份文档组成内容划分为插件开发基础、图形化插件开发、语言类插件开发以及工具与参考资料四大部分涵盖平台架构、插件生命周期、事件监听、Action System、Tool Windows、语法高亮、代码补全与自定义语言解析等核心主题并配有 Gradle 构建、SDK 配置与社区资源指引。资源包共 1 个 PDF 文件大小约 3.99MB便于随身查阅与检索。目前已有 876 人学习关注适合希望定制开发环境、编写效率工具或收费插件的开发者按需选读兼顾理论梳理与实战参考。1. 从一次“插件装不上”说起Intellij platform plugin 开发到底在做什么很多人第一次接触 Intellij platform plugin 开发是因为在 IntelliJ IDEA 里装了一个第三方插件结果发现它和当前 IDE 版本不兼容或者干脆在插件市场里搜不到。于是想自己写一个解决团队内部的重复劳动——比如自动生成某类代码模板、在编辑器里做实时校验、给项目树加一个自定义视图。这个方向的门槛其实比想象中低你不需要修改 IDE 本身只需要写一个独立的 Java/Kotlin 模块打包成 jar丢进 IDE 的 plugins 目录就能跑。但真正动手后第一个卡点往往不是写代码而是搞不清 Intellij platform plugin 的工程结构、依赖坐标和运行方式。它不像写一个普通 Spring Boot 应用那样加个依赖就能启动。你需要理解 plugin.xml 的注册机制、Action 的触发链路、以及沙箱运行环境。这篇笔记就按“能复现”的标准把从建工程到打包验证的完整路径拆开讲适合已经会 Java、想给 IDE 加功能但还没跑通第一个插件的开发者。2. 搭一个能跑的最小插件工程Gradle 配置与目录结构2.1 为什么选 Gradle 而不是旧版 DevKit早期做 Intellij platform plugin 开发很多人用 IDE 自带的 Plugin DevKit 向导生成的是基于 IDEA 项目模型的工程。这种方式在 2020 年之后逐渐被 Gradle 插件取代原因是 Gradle 能更干净地管理依赖、支持多模块、并且和 CI 流水线天然兼容。常见做法是使用org.jetbrains.intellij这个 Gradle 插件它负责下载目标 IDE 的 SDK、配置沙箱运行任务、以及打包成可安装的 zip。我一般会先确认目标 IDE 的版本号因为插件必须声明兼容范围。比如你面向 2023.3 到 2024.2 的 IDEA就要在 build.gradle 里写清楚sinceBuild和untilBuild。这个范围写窄了用户升级 IDE 后插件直接失效写宽了又可能调用到不存在的 API。血泪经验是先用你团队大多数人用的那个版本作为开发基线再往上放宽一到两个大版本。2.2 最小 build.gradle 配置与参数说明下面是一个能跑通的最小配置我删掉了所有非必要项只保留让插件启动的核心部分。plugins { id java id org.jetbrains.intellij version 1.17.0 // 插件版本按需调整 } group com.example.demo version 0.1.0 repositories { mavenCentral() } intellij { version 2023.3 // 目标 IDE 版本决定 SDK API type IC // IC 表示 CommunityIU 表示 Ultimate plugins [com.intellij.java] // 依赖的官方插件比如 Java 支持 downloadSources true // 下载源码方便调试时看实现 } java { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } tasks { patchPluginXml { sinceBuild 233 // 对应 2023.3 untilBuild 242.* // 对应 2024.2 } buildSearchableOptions { enabled false // 本地开发时关掉加快构建 } }这段配置里intellij.version决定了你编译时用的 API 版本type决定下载的是社区版还是旗舰版 SDK。plugins数组里写的是你依赖的其他官方插件 ID比如你要做 Java 代码分析就必须加com.intellij.java否则编译时找不到 PsiClass 这类类。patchPluginXml里的sinceBuild和untilBuild是插件市场的兼容性门槛写错了用户装不上。buildSearchableOptions在本地反复构建时很拖时间关掉能省不少等待。2.3 目录结构与 plugin.xml 注册入口Gradle 插件的默认约定是源码放在src/main/java资源放在src/main/resources而plugin.xml必须位于src/main/resources/META-INF/下。这个文件是整个插件的入口所有 Action、Service、扩展点都要在这里注册。idea-plugin idcom.example.demo.firstplugin/id nameDemo First Plugin/name vendorexample/vendor dependscom.intellij.modules.platform/depends dependscom.intellij.modules.java/depends actions action idDemo.HelloAction classcom.example.demo.HelloAction textSay Hello descriptionA demo action add-to-group group-idToolsMenu anchorfirst/ /action /actions /idea-pluginid是插件的唯一标识不能和已有插件冲突。depends声明依赖的平台模块com.intellij.modules.platform是所有插件的基础com.intellij.modules.java表示你需要 Java 语言支持。actions里注册了一个 Actionclass指向实现类text是菜单里显示的文字add-to-group把它挂到 Tools 菜单下。注意anchorfirst控制它在菜单里的位置不写就默认追加到末尾。2.4 写一个能弹窗的 Action 并跑起来Action 是插件里最常见的交互入口。下面这个类继承AnAction点击菜单后弹出一个对话框。package com.example.demo; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.ui.Messages; import org.jetbrains.annotations.NotNull; public class HelloAction extends AnAction { Override public void actionPerformed(NotNull AnActionEvent e) { // 获取当前项目名称没有项目时显示默认值 String projectName e.getProject() ! null ? e.getProject().getName() : No Project; Messages.showInfoMessage( Hello from plugin! Project: projectName, Demo Plugin ); } }actionPerformed是点击后的回调AnActionEvent携带了当前上下文比如项目、编辑器、文件等。e.getProject()可能为 null因为 Action 可以在没有打开项目时触发所以要做空判断。Messages.showInfoMessage是平台提供的弹窗工具比直接调 Swing 的 JOptionPane 更符合 IDE 风格。写完代码后在终端执行./gradlew runIdeGradle 会下载对应版本的 IDE启动一个沙箱实例。你会在 Tools 菜单里看到 “Say Hello”点击就能看到弹窗。这个命令第一次跑会下载几百 MB 的 SDK耐心等。如果启动失败先看控制台有没有报PluginException多半是 plugin.xml 里的类名写错了或者依赖没加全。3. 理解 Action、Service 与扩展点插件能力的三个层次3.1 Action 的触发条件与更新机制Action 不只是菜单项它还可以出现在工具栏、右键菜单、甚至编辑器里。关键在于add-to-group的 group-id 和 anchor。比如EditorPopupMenu是编辑器右键菜单ProjectViewPopupMenu是项目树右键菜单。你还可以通过重写update方法控制 Action 的可用状态。Override public void update(NotNull AnActionEvent e) { // 只有当前有打开的项目时才启用 boolean enabled e.getProject() ! null; e.getPresentation().setEnabledAndVisible(enabled); }update会在界面刷新时被频繁调用所以里面不要写耗时逻辑。setEnabledAndVisible同时控制灰显和隐藏如果只想灰显就只用setEnabled。常见坑是把耗时判断放在update里导致 IDE 界面卡顿用户会以为插件有性能问题。3.2 Service 的生命周期与获取方式Service 用来存放跨 Action 共享的状态或逻辑分为应用级和项目级。应用级 Service 在整个 IDE 生命周期内只有一个实例项目级 Service 每个项目一个。注册方式是在 plugin.xml 里加applicationService或projectService。applicationService serviceImplementationcom.example.demo.CounterService/public class CounterService { private int count 0; public int increment() { return count; } }获取 Service 时不要直接 new而是通过ApplicationManager或project.getService()。CounterService service ApplicationManager.getApplication() .getService(CounterService.class); int current service.increment();项目级 Service 用project.getService(CounterService.class)。注意 Service 的构造函数不能有复杂逻辑因为 IDE 启动时就会初始化应用级 Service拖慢启动会被用户感知到。我一般把重活放到第一次调用时懒加载。3.3 扩展点不改源码就能插入 IDE 流程扩展点是 Intellij platform 最强大的机制之一。IDE 本身定义了很多扩展点比如com.intellij.fileType可以注册新文件类型com.intellij.codeInsight.inspection可以注册代码检查。你只需要在 plugin.xml 里声明扩展并提供一个实现类。extensions defaultExtensionNscom.intellij fileType nameDemoFile implementationClasscom.example.demo.DemoFileType fieldNameINSTANCE languageDemo extensionsdemo/ /extensions这段注册了一个新文件类型后缀是.demo。implementationClass指向LanguageFileType的子类fieldName是静态实例字段名。注册后IDE 会用你指定的语言解析这类文件你可以进一步绑定语法高亮、解析器等。扩展点的坑在于不同 IDE 版本的扩展点名称和属性可能变化升级 SDK 时要对照官方文档的变更记录逐项检查。4. 避坑与排查插件开发中最容易翻车的五个点4.1 现象runIde 启动后菜单里找不到 Action原因通常是 plugin.xml 里的id和类名不匹配或者add-to-group的 group-id 写错了。解决方法是先检查class属性是否指向完整包名再确认 group-id 是平台预定义的合法值。可以在沙箱 IDE 里按CtrlShiftA搜索 Action 的 id如果能搜到但菜单不显示就是 group 配置问题。4.2 现象编译时报 “Cannot resolve symbol PsiClass”这是因为没有在intellij.plugins里声明com.intellij.java。PsiClass 属于 Java 插件提供的 API不是平台核心的一部分。加上这个依赖后重新同步 Gradle 即可。如果还报错检查intellij.version是否和依赖插件版本匹配比如 2023.3 的 Java 插件不能用在 2022.1 的 SDK 上。4.3 现象插件在本地能跑打包后装到 IDE 里报兼容性错误先看patchPluginXml里的sinceBuild和untilBuild。sinceBuild写的是 IDE 构建号的前三位比如 2023.3 是 2332024.1 是 241。如果你写成了2023.3这种版本号格式插件市场会直接拒绝。另外打包命令是./gradlew buildPlugin产物在build/distributions/下是一个 zip 文件通过 IDE 的 “Install Plugin from Disk” 安装。4.4 现象Action 点击后 IDE 卡死几秒大概率是actionPerformed里做了耗时操作比如网络请求或大文件读写。Action 默认在 UI 线程执行阻塞超过几百毫秒用户就能感觉到。正确做法是用ApplicationManager.getApplication().executeOnPooledThread()包一层或者用ProgressManager.runProcessWithProgressSynchronously显示进度条。记住任何可能超过 100ms 的操作都不应该直接放在 Action 回调里。4.5 现象Service 里的状态在 IDE 重启后丢失这是正常的Service 实例不持久化。如果需要保存状态要用PersistentStateComponent接口配合State注解和Storage指定存储位置。常见做法是存到项目目录下的.idea文件夹里或者应用级配置目录。不要自己写文件到任意路径否则卸载插件后残留文件会让用户困惑。5. 进阶技巧用 Inspection 做实时代码检查与快速修复5.1 注册一个自定义 InspectionInspection 是 IDE 里那种波浪线提示的来源。你可以针对特定语言注册检查规则比如检测某个方法调用缺少参数校验。注册方式是在 plugin.xml 里加localInspection扩展。extensions defaultExtensionNscom.intellij localInspection languageJAVA shortNameDemoMissingCheck displayNameMissing null check groupNameDemo enabledByDefaulttrue levelWARNING implementationClasscom.example.demo.MissingCheckInspection/ /extensionslanguage指定生效的语言level是警告级别implementationClass继承AbstractBaseJavaLocalInspectionTool。这个类里重写buildVisitor方法返回一个JavaElementVisitor在访问方法调用时做判断。5.2 实现检查逻辑与快速修复下面是一个简化示例检测System.out.println并提示替换为日志。public class MissingCheckInspection extends AbstractBaseJavaLocalInspectionTool { Override public NotNull PsiElementVisitor buildVisitor( NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { Override public void visitMethodCallExpression( NotNull PsiMethodCallExpression expression) { String methodName expression.getMethodExpression() .getReferenceName(); if (println.equals(methodName)) { holder.registerProblem( expression, Avoid System.out.println, ProblemHighlightType.WARNING, new ReplaceWithLoggerFix() ); } } }; } }holder.registerProblem注册一个问题最后一个参数是快速修复。ReplaceWithLoggerFix实现LocalQuickFix接口在applyFix里替换 PSI 元素。注意 PSI 修改必须在写操作里进行通常用WriteCommandAction.runWriteCommandAction包起来否则会抛异常。5.3 验证 Inspection 是否生效启动沙箱 IDE 后打开一个 Java 文件写一行System.out.println(test)如果配置正确这行代码会显示黄色波浪线鼠标悬停能看到提示按AltEnter能看到快速修复选项。如果没生效先检查language属性是否写成了JAVA全大写再确认enabledByDefault是 true。另外Inspection 的shortName不能和已有检查重名否则会被覆盖。5.4 一个我常犯的错误早期我总想把 Inspection 写得特别复杂一次检查十几条规则结果buildVisitor里堆了几百行调试时根本不知道哪条规则触发了。后来改成每个 Inspection 只做一件事用groupName归类用户可以在设置里单独开关。这样既好维护也方便定位问题。插件开发这件事功能拆得越细翻车概率越低。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →