多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

IntelliJ IDEA 插件开发实战:从环境搭建到 PSI 操作与避坑指南

IntelliJ IDEA 插件开发实战:从环境搭建到 PSI 操作与避坑指南 简介这份《IntelliJ Platform Plugin 开发指导手册》面向 Java 开发者与 IDE 插件爱好者帮助读者从零起步掌握 IntelliJ IDEA 插件开发并逐步进阶到语言类高级插件。手册由上册、下册与附录三份独立文档组成内容划分为四部分插件开发基础、图形化插件开发、语言类插件开发以及汇总工具与参考资料的附录涵盖平台架构、插件生命周期、事件监听、Action System、Tool Windows、自定义语言解析与代码补全等核心主题。资源包共 1 个 PDF 文件大小约 3.99MB便于随身查阅与打印学习。目前已有 876 人学习关注说明其在插件开发学习群体中具有一定参考价值。读者可借助手册理清插件体系结构按需选择 UI 增强或代码分析方向深入并结合附录中的 Gradle 配置、SDK 链接与社区资源动手实践逐步积累可上架的插件开发经验。1. 从一次插件翻车说起IntelliJ IDEA 插件开发到底在做什么很多人第一次接触 IntelliJ IDEA 插件开发是因为一个很朴素的需求团队里每个人都在用 IDEA但有些重复动作实在受不了——比如每次新建类都要手动补一段固定注释或者要在几十个模块里来回跳转找某个配置项。于是想着写个插件一劳永逸。结果打开官方文档看到 Plugin SDK、Extension Point、PSI、VFS、Action System 这一堆名词直接劝退。更玄学的是照着示例抄了一个 Action运行起来发现菜单里根本找不到入口日志里也没有任何报错。这个标题讲的就是这件事在 Java 集成开发环境 IntelliJ IDEA 上做插件开发。它解决的不是“写个 Hello World 弹窗”这种玩具需求而是让你能把日常开发中那些重复、机械、容易出错的环节固化成 IDE 内部的一个功能点。适合两类人一是对 Java 和 IDEA 已经用得很熟、想进一步定制工作流的后端或客户端工程师二是需要把内部工具链代码检查、脚手架、配置同步嵌进 IDE 的研发效能同学。前提是你得能接受一个事实——插件开发本质是在别人的框架里做扩展你得先理解它的扩展模型再谈写代码。2. 环境搭建与最小可运行插件从零到能在菜单里点一下2.1 选 Gradle 还是 DevKit两条路线的取舍IntelliJ 插件开发目前主流有两条构建路线一是传统的 DevKit 项目IDEA 里直接 New Project → IDE Plugin二是基于 Gradle 的 IntelliJ Platform Plugin 模板。我一般会直接推荐 Gradle 路线原因有三个。第一依赖管理清晰你需要的 platform 包、第三方库都在 build.gradle.kts 里声明不会出现 DevKit 那种“SDK 配好了但运行时找不到类”的黑匣子问题。第二Gradle 路线天然支持多模块插件稍微复杂一点就要拆出 core、ui、test 几个模块DevKit 做这件事很别扭。第三调试和打包流程可以脚本化CI 上跑构建不用手动点 IDE。代价是 Gradle 路线前期配置项多一些尤其是 platformVersion 和 sinceBuild/untilBuild 这几个参数写错了会出现“插件能编译但装不上”或者“装上了但菜单不显示”的翻车现场。下面直接给一份能跑通的最小配置。// build.gradle.kts plugins { id(java) id(org.jetbrains.intellij) version 1.17.0 // 插件开发专用 Gradle 插件 } group com.example.demo version 0.0.1 repositories { mavenCentral() } intellij { // 本地调试时使用的 IDE 版本建议和你日常开发用的版本一致 version.set(2023.2) type.set(IC) // IC CommunityIU Ultimate plugins.set(listOf()) // 需要依赖的其他插件 ID没有就留空 } tasks { patchPluginXml { // 声明插件兼容的 IDE 版本区间写错会导致装不上 sinceBuild.set(232) untilBuild.set(241.*) } buildSearchableOptions { enabled false // 本地调试阶段关掉能省不少构建时间 } }这段配置里最关键的三个参数version决定你编译时依赖的 platform API 版本sinceBuild和untilBuild决定插件能被哪些版本的 IDEA 加载。常见坑是 version 写了 2023.2sinceBuild 却写了 231结果在 2023.1 上装不上在 2023.2 上又因为 API 差异报 NoSuchMethodError。稳妥做法是 version、sinceBuild、untilBuild 三者对齐调试阶段 untilBuild 可以留空表示不限制上限。2.2 写第一个 Action菜单入口为什么找不到插件最基础的扩展点就是 Action。一个 Action 代表用户能触发的一个操作可以挂在菜单、工具栏、右键菜单上。下面是一个最小 Action功能是在编辑器里弹出一个通知。package com.example.demo; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.ui.Messages; import org.jetbrains.annotations.NotNull; public class HelloAction extends AnAction { Override public void actionPerformed(NotNull AnActionEvent e) { // 从事件里拿当前编辑器拿不到说明触发场景不对 Editor editor e.getData(CommonDataKeys.EDITOR); if (editor null) { Messages.showInfoMessage(没有打开的编辑器, 提示); return; } // 取选中文本没选中就提示 String selected editor.getSelectionModel().getSelectedText(); Messages.showInfoMessage( selected null ? 未选中任何文本 : 选中内容 selected, Hello Plugin ); } Override public void update(NotNull AnActionEvent e) { // 控制 Action 是否可见/可用这里只在有编辑器时启用 Editor editor e.getData(CommonDataKeys.EDITOR); e.getPresentation().setEnabledAndVisible(editor ! null); } }逻辑说明actionPerformed是触发时执行的逻辑update是每次菜单展开或工具栏刷新时调用的用来决定这个 Action 当前是否可用。很多人只写了actionPerformed忘了update结果 Action 一直灰着以为是注册失败其实是update里没设置可见性。参数说明CommonDataKeys.EDITOR是平台提供的标准数据键能拿到当前焦点编辑器如果 Action 挂在项目视图右键菜单上这个键可能拿不到值需要改用CommonDataKeys.VIRTUAL_FILE。2.3 plugin.xml 注册入口不显示九成是这里的问题Action 写完了还得在src/main/resources/META-INF/plugin.xml里注册否则平台根本不知道它的存在。idea-plugin idcom.example.demo/id nameDemo Plugin/name vendorexample/vendor dependscom.intellij.modules.platform/depends actions action idcom.example.demo.HelloAction classcom.example.demo.HelloAction textHello Plugin description演示用 Action !-- 挂到编辑器右键菜单 -- add-to-group group-idEditorPopupMenu anchorfirst/ !-- 绑定快捷键可选 -- keyboard-shortcut keymap$default first-keystrokectrl alt H/ /action /actions /idea-plugin这里最容易踩的坑是group-id写错。EditorPopupMenu是编辑器右键菜单MainMenu是主菜单栏ProjectViewPopupMenu是项目视图右键菜单。写错 group-id 不会报错Action 就是不出现。另一个坑是depends只写了com.intellij.modules.platform如果你用到了 Java 相关的 PSI API还得加com.intellij.modules.java否则运行时会抛ClassNotFoundException。跑起来的命令很简单./gradlew runIde这个任务会启动一个带插件的沙箱 IDE你在里面点右键就能看到 Hello Plugin。调试阶段建议一直用 runIde不要急着 buildPlugin 打包安装沙箱环境能看到完整日志打包安装后日志会被吞掉一部分。3. PSI 与文件操作让插件真正读懂代码结构3.1 PSI 是什么把源码变成可遍历的树PSIProgram Structure Interface是 IntelliJ 平台最核心的概念之一。简单说平台会把每个源文件解析成一棵语法树每个节点都是一个 PsiElement比如 PsiClass、PsiMethod、PsiField、PsiStatement。你写插件想“找到所有没有注释的 public 方法”或者“把所有 System.out.println 替换成日志调用”靠正则是不靠谱的必须走 PSI。我一般会先用 PSI Viewer 看结构。在沙箱 IDE 里打开 Tools → View PSI Structure能看到当前文件的树形结构。这一步很关键因为 PSI 的类名和层级不是凭直觉能猜出来的比如方法体是 PsiCodeBlock参数列表是 PsiParameterList注解是 PsiAnnotation。先看清楚再写代码比反复试错快得多。3.2 遍历与修改一个批量加注释的完整例子下面这个例子实现一个实际需求遍历当前 Java 文件里所有 public 方法如果方法上没有 Javadoc就自动补一个占位注释。package com.example.demo; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.psi.*; import org.jetbrains.annotations.NotNull; public class AddJavadocAction extends AnAction { Override public void actionPerformed(NotNull AnActionEvent e) { PsiFile file e.getData(CommonDataKeys.PSI_FILE); if (!(file instanceof PsiJavaFile)) { return; } PsiJavaFile javaFile (PsiJavaFile) file; // 所有 PSI 修改必须包在 WriteCommandAction 里否则会抛异常 WriteCommandAction.runWriteCommandAction(e.getProject(), () - { for (PsiClass psiClass : javaFile.getClasses()) { for (PsiMethod method : psiClass.getMethods()) { if (!method.hasModifierProperty(PsiModifier.PUBLIC)) { continue; } // 已经有 Javadoc 就跳过 if (method.getDocComment() ! null) { continue; } PsiElementFactory factory JavaPsiFacade .getElementFactory(e.getProject()); PsiDocComment doc factory.createDocCommentFromText( /**\n * 待补充说明\n */ ); // 插到方法第一个修饰符之前 method.addBefore(doc, method.getFirstChild()); } } }); } }逻辑说明WriteCommandAction.runWriteCommandAction是所有 PSI 写操作的入口它保证修改能被撤销栈记录也保证线程安全。直接调method.addBefore而不包这一层运行时会抛IncorrectOperationException这是新手最常见的翻车点之一。参数说明method.hasModifierProperty(PsiModifier.PUBLIC)判断修饰符method.getDocComment()返回已有的 Javadoc 节点factory.createDocCommentFromText从字符串创建注释节点。注意addBefore的第二个参数是锚点这里用method.getFirstChild()保证注释插在最前面如果锚点选错注释可能插到方法体内部编译直接报错。3.3 虚拟文件与读写别直接碰 java.io.File插件里操作文件优先用 VFSVirtual File System而不是java.io.File。原因是 IDEA 有自己的文件缓存和事件机制你用原生 IO 改了文件IDE 可能感知不到导致编辑器里显示的还是旧内容。正确做法是通过VirtualFile和Document来读写。// 通过 PSI 拿虚拟文件 VirtualFile vFile psiFile.getVirtualFile(); // 读内容 String content new String(vFile.contentsToByteArray(), StandardCharsets.UTF_8); // 写内容同样要包 WriteCommandAction WriteCommandAction.runWriteCommandAction(project, () - { try { VfsUtil.saveText(vFile, content \n// appended); } catch (IOException ex) { // 实际项目里应该用日志而不是打印 ex.printStackTrace(); } });这里有个细节VfsUtil.saveText会触发文件系统事件编辑器会自动刷新。如果你用FileWriter直接写编辑器不会刷新用户会以为插件没生效。另外读大文件时不要一次性contentsToByteArray应该用Document分段读否则内存会飙。4. 避坑与排查插件开发里那些让人怀疑人生的时刻4.1 插件装上了但菜单不显示现象buildPlugin 打包后在 IDE 里安装成功重启后菜单里找不到 Action。原因通常是 plugin.xml 里的group-id写错或者sinceBuild/untilBuild和当前 IDE 版本不匹配导致插件被静默禁用。解决先在 Settings → Plugins 里确认插件是启用状态再看 idea.log 里有没有 “PluginException” 或 “disabled” 关键字。如果日志里没有明显报错把 Action 临时挂到MainMenu下测试能显示说明是 group-id 问题。4.2 运行时报 NoClassDefFoundError现象编译通过runIde 启动后触发 Action 时抛NoClassDefFoundError指向某个 platform 类。原因是你依赖的模块没有在 plugin.xml 的depends里声明。比如用了com.intellij.psi.PsiJavaFile就必须加com.intellij.modules.java。解决对照你 import 的包名platform 的模块划分在官方文档里有列表Java 相关的基本都在com.intellij.modules.java下。4.3 PSI 修改后编辑器内容没变现象Action 执行了日志也打了但编辑器里的代码没变化。原因有两种一是没包WriteCommandAction修改被回滚了二是直接改了 PSI 但没提交文档。解决确认所有写操作都在WriteCommandAction里并且修改后调一下PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document)强制刷新文档。4.4 沙箱里正常打包安装后行为不一致现象runIde 里一切正常buildPlugin 安装到正式 IDE 后功能失效或报错。原因通常是沙箱环境和正式环境的插件依赖不同或者你用了只在沙箱里存在的类。解决打包前跑一遍./gradlew buildPlugin然后在正式 IDE 里用 “Install Plugin from Disk” 安装不要用 runIde 的结果当最终验证。另外检查 build.gradle.kts 里plugins.set(listOf())是否漏了运行时需要的插件依赖。4.5 线程问题导致 IDE 卡死现象触发 Action 后整个 IDE 卡住几秒甚至更久。原因是在 EDT事件调度线程上做了耗时操作比如遍历大项目、网络请求、读大文件。解决把耗时逻辑放到后台线程用ApplicationManager.getApplication().executeOnPooledThread或者ProgressManager.runProcessWithProgressSynchronously。PSI 读操作可以在后台线程做但写操作必须在 EDT 或 WriteCommandAction 里。5. 进阶技巧让插件从能用变成好用5.1 用 Inspection 做代码检查而不是 ActionAction 是用户主动触发的Inspection 是平台自动触发的。如果你要做的是“发现代码里的坏味道”应该用 Inspection 而不是 Action。Inspection 会在编辑器里直接标黄标红用户体验好得多。实现方式是继承LocalInspectionTool在buildVisitor里返回一个PsiElementVisitor在 visit 方法里调ProblemsHolder.registerProblem。注册到 plugin.xml 的localInspection标签下。注意 Inspection 的性能要求很高遍历逻辑里不要做重操作否则打开大文件会卡。5.2 用 Service 管理插件状态插件里如果有全局状态比如配置缓存、连接池不要用静态变量应该用 Service。平台提供了Service注解分应用级和项目级两种。应用级 Service 在整个 IDE 生命周期内只有一个实例项目级 Service 每个项目一个。用 Service 的好处是平台帮你管理生命周期项目关闭时会自动释放不会出现内存泄漏。定义方式Service(Service.Level.APP) public final class MyPluginService { // 应用级单例通过 ApplicationManager.getApplication().getService(MyPluginService.class) 获取 }5.3 调试插件的三个实用手段第一个手段是runIde时开远程调试在 build.gradle.kts 里加jvmArgs配置然后用 IDEA 的 Remote JVM Debug 连上去能断点调试插件代码。第二个手段是看idea.log沙箱环境的日志在build/idea-sandbox/system/log/idea.log里面能看到插件加载、异常、警告的完整记录。第三个手段是用Logger.getInstance(MyAction.class).warn(...)打日志比System.out.println好在日志级别可控正式环境不会刷屏。5.4 一个我常用的验证习惯每次写完一个 PSI 修改类 Action我不会直接在大项目里试而是先建一个只有几行代码的测试文件跑一遍看结果再用CtrlZ撤销确认撤销栈正常。这个习惯帮我省了很多后悔药——PSI 写操作一旦没包 WriteCommandAction在大项目里触发可能导致整个文件被回滚用户正在编辑的内容直接丢失这种事故在团队内部插件里是致命的。另外插件发布前一定要在至少两个不同大版本的 IDEA 上装一遍API 兼容性这东西官方文档说兼容不代表真兼容跑一遍最踏实。希望帮到你。本文还有配套的精品资源点击获取
返回列表