
简介Intellij Platform插件开发手册上册PDF面向基于Intellij IDEA 2023兼容2024进行插件开发的工程技术人员定位是帮助读者补齐Intellij Platform插件开发基础并以图形化插件为主线走通从零到一的开发流程。手册先从Intellij Platform术语、IDE插件类型、开发环境与技术要求、开发流程及参考网站入手为后续编码打好基础随后以开发首个插件为目标详细讲解IDEA工具配置、插件工程创建、工程参数配置与测试方法等关键步骤并进一步延伸到图形化插件界面设计。资源为单个PDF文件大小15.82MB目录结构清晰、章节划分明确便于快速对照查阅目前已有382人学习下载。内容基于JetBrains Runtime 17.0.9编写兼容IDEA 2023作者结合官方指导与个人实践经验整理适合希望快速上手界面类插件开发的读者系统学习。本册聚焦界面类插件开发语言类插件与附录工具资料请参见下册及附录。 做了这么多年Java开发我每天有大半时间泡在IntelliJ IDEA里。绝大多数情况下这个IDE只是个更聪明的文本编辑器直到有一天你发现某个重复操作官方怎么都做不到顺手比如批量给几十个文件加注解、统一调整模板代码这时候你才会认真考虑动手写一个自己的插件。这篇内容定位是一套Intellij Platform PlugIn插件开发手册的上半部分目标是让一个从没接触过插件开发的Java工程师能在一个晚上跑通第一个可调试、可打包的最小插件项目。整篇内容围绕插件工程搭建、Action机制、PSI体系这几个核心模块展开配套的代码和配置我会直接贴出来你看完至少能做到自己上手写一个能改菜单、能改快捷键、能分析源码的插件。1. 为什么劝你直接上手而不是先啃文档1.1 IntelliJ平台的分层逻辑很多第一次接触Intellij Platform的人都会被官网那张架构图绕晕到底什么是Platform其实可以把它理解成一栋已经装修好的写字楼IDEA、PyCharm、WebStorm这些产品都是入驻在这栋楼里的公司楼层里的水电网、电梯、消防系统都属于平台层。具体来说IntelliJ平台本身提供的是窗口管理、菜单、文件系统抽象、编辑器、版本控制集成这些通用能力而语言引擎负责Python、Java、Kotlin这些具体语言的解析和索引最外层才是各个产品基于前两层包装出来的业务形态。插件开发和你平时写业务系统的差别在于你不是从零搭框架而是往一个拥有大量现成能力的平台里挂自己的逻辑模块。这个分工决定了插件开发的思维方式先找平台已经有的能力再找对应的扩展点最后做最小量的代码接入。很多让新手觉得“怎么这么简单”的功能本质上都是平台早就做好了你只是用一个Action或者一个Listener把业务挂上去。所以学习路径不是把官方文档从头翻到尾而是先写一个能改菜单按钮的插件再逐步深入到PSI、搜索、索引这些底层能力。官方文档更适合当字典用遇到具体API再回头查。1.2 插件和SDK到底是什么关系这里要先厘清一个概念IntelliJ Platform SDK不是你在Oracle官网下载的那种独立SDK包而是一套由IDEA发行版提供的API依赖。你在构建插件时构建脚本里声明依赖某个版本的IDEA实际就是把那一整套公共接口引入到你的工程里。日常代码里会频繁用到的Project、PsiFile、VirtualFile、ActionManager都来自这套接口。Intellij平台还分成社区版和终极版两套API社区版开源免费终极版里有一部分商业化功能对应的API。如果你的插件不需要依赖终极版的专属能力完全可以基于社区版做开发这样后续分发也不会遇到授权问题。为什么说“直接上手”比“先啃文档”更高效因为插件开发和普通后端开发不一样它的反馈回路很短改一行代码、运行一个实验实例、马上看到效果。这个反馈过程比阅读API文档更能帮助建立体系化认知。我自己带新人的经验是先让他们搭工程再写一个弹出消息的Action半小时内就能把平台的核心流程跑通之后再去深入解析PSI模型和线程模型效率会高很多。2. 搭建开发环境Gradle工程三件套2.1 新建项目和最小配置以IntelliJ IDEA 2023.1以上版本为例安装好IDE之后你可以在新建项目向导里直接选择“IntelliJ Platform Plugin”IDEA会自动生成一个Gradle工程包含build.gradle、settings.gradle、gradle.properties、一个src/main/java目录和一个plugin.xml。这个自动生成的过程其实帮你省掉了不少麻烦因为不同版本的IDEA对Gradle IntelliJ Plugin的版本要求不一样自动生成的版本号通常已经对齐。如果你想要手动控制也可以去GitHub拉官方的插件模板来改但第一次接触还是建议用IDE自动生成减少环境变量层面的困扰。一个最小可用的build.gradle大致长这样plugins { id java id org.jetbrains.intellij version 1.15.0 } group com.example version 1.0-SNAPSHOT repositories { mavenCentral() } dependencies { testImplementation junit:junit:4.13.2 } intellij { version.set(2023.2.5) type.set(IC) plugins.set([java]) } patchPluginXml { sinceBuild.set(232) untilBuild.set(241.*) }这里需要注意一个容易混淆的点plugins块里那个org.jetbrains.intellij指的是Gradle插件它解决的是把IntelliJ平台依赖拉进工程、编译插件、生成描述文件、运行实验实例这一系列构建任务而不是你插件里要用到的功能插件。下面intellij块里的plugins.set([java])才是声明当前插件依赖了IDEA自带的Java插件模块一旦你写Java代码分析功能这个依赖几乎就是必须的。2.2 关键参数怎么调初学者最常卡住的地方有两个一是intellij.version到底填多少二是sinceBuild和untilBuild怎么设置。拿我上面写的2023.2.5来说这是IDEA的发行版本号对应2023年8月前后的版本。Intellij Platform的构建号有一套自己的规则2023.1对应2312023.2对应2322024.1对应241这个编号会直接体现在sinceBuild和untilBuild里。sinceBuild表示插件能兼容的最低构建号untilBuild表示最高兼容构建号写成241.*意味着到2024.1的最新小版本都能装。这里有个经验值得记下来untilBuild不要写得太严除非你的插件用到了某个很快就会变的内部API。因为用户用的IDEA版本五花八门写得过窄会导致明明代码没问题的插件在低版本上根本无法安装。另一个常见的坑是type属性IC表示IntelliJ IDEA Community EditionIU表示Ultimate。如果你用IC当SDK却在代码里引用了IU才有的API编译期就会直接报错反过来虽然能编译过但最终用户如果只有社区版运行时也可能因为缺类而崩溃。一般情况下建议选IC除非确实需要Ultimate专属功能。2.3 第一次点运行配置完成之后在Gradle面板里找到Tasks - intellij - runIde双击执行Gradle会下载对应版本的IntelliJ平台并启动一个实验性的IDE实例。这个实例里会自动安装当前开发中的插件你可以在这个窗口里点菜单、看效果、打断点调试。第一次启动通常比较慢因为要下载完整的平台依赖再加上仓库访问速度不稳定这一步是最容易让人打退堂鼓的地方。如果卡在下下载阶段建议提前在Gradle配置里设置好国内镜像源或者手动把依赖下载到本地Gradle缓存里。从这时开始你才真正进入了IntelliJ插件开发的日常节奏改代码、运行实验实例、验证效果、再改。有一点要特别提醒实验实例和主IDEA默认共用同一套用户配置目录插件开发中很容易发生配置互串。我自己的做法是在运行配置里加一个JVM参数比如-Didea.home.path/tmp/idea-plugindev把实验实例的工作目录隔离开这样就不会出现实验把界面布局改掉之后主IDEA也变了的情况。3. 从菜单到代码Action机制实战3.1 写一个最简单的Action类插件开发里最基本也最常用的扩展点就是Action。你可以把Action理解成一个封装好的命令对象点击菜单、点击工具栏按钮、按下快捷键最后都会触发某一个Action的actionPerformed方法。先来看一个最简单的例子新建一个Java类继承AnActionpublic class HelloAction extends AnAction { Override public void actionPerformed(NotNull AnActionEvent e) { Project project e.getProject(); Messages.showInfoMessage(project, Hello from plugin, Info); } }AnAction默认有一个无参构造函数你可以在构造函数里通过super传入菜单显示的文本、描述和图标比如super(Hello, 这是描述, AllIcons.Actions.Execute)。这些信息也可以在plugin.xml注册时单独设置效果类似。actionPerformed是真正执行逻辑的地方e这个AnActionEvent可以拿到当前项目Project、当前文件VirtualFile、编辑器Editor、数据上下文DataContext等关键对象。这个事件参数是整个插件开发的入口后面几乎所有和IDE交互的地方都会用到它。需要注意一点Action承担的角色更接近Controller它只负责响应UI事件并调度后续逻辑不应该在里面写太多重业务。如果你在actionPerformed里直接做全项目扫描再弹结果UI线程会被耗住用户会感到整个IDEA卡死。平台对这类操作审查很严格正确做法是把耗时任务放进后台线程再通过ReadAction或WriteAction访问PSI这部分我在下一节会展开讲。3.2 在plugin.xml里注册写好的Action必须注册到META-INF/plugin.xml里才能被平台识别。plugin.xml可以说是插件的配置文件它描述了这个插件的id、名称、版本、依赖、扩展点、Action注册等信息。注册Action的典型片段如下idea-plugin idcom.example.helloplugin/id nameHello Plugin/name version1.0.0/version vendor urlhttps://example.comexample/vendor actions action idcom.example.helloplugin.HelloAction classcom.example.HelloAction textHello descriptionShow hello message add-to-group group-idToolsMenu anchorfirst/ keyboard-shortcut keymap$default first-keystrokectrl alt H/ /action /actions /idea-pluginid属性在全局必须唯一它也是插件系统管理Action时的索引键。class指向Action类的全限定名text和description会显示在菜单或快捷键设置界面。add-to-group的意思是把Action挂到某个已有的菜单组里这里挂到了ToolsMenu也就是顶部Tools菜单anchorfirst表示排在菜单最前面。keyboard-shortcut里的keymap$default表示注册到默认键位映射方案first-keystroke就是你想绑定的快捷键组合。好还有一类细节经常被新手忽略插件ID一旦发布到插件市场就尽量不要改动因为市场会把插件ID作为唯一标识来关联下载和更新。开发阶段无所谓但发布前要把ID、名称、Vendor这些元信息都确定下来否则后续改ID会让老用户收到“插件不兼容”的提示。3.3 菜单项的动态可见性如果希望菜单只在某些条件下出现比如只选中Java文件时显示就要重写AnAction的update方法Override public void update(NotNull AnActionEvent e) { VirtualFile file e.getData(CommonDataKeys.VIRTUAL_FILE); e.getPresentation().setEnabledAndVisible(file ! null file.getName().endsWith(.java)); }update方法会在菜单弹出前被平台自动调用你可以在里面根据当前上下文动态控制菜单项的可见性和可用性。setEnabledAndVisible方法一次控制两个状态设置为false时菜单直接隐藏。这里有个性能上的讲究update不是只在点击时才调用而是在菜单渲染、快捷键查找等很多场景下都会被频繁调用。如果你的update里做了高开销操作比如扫描整个项目文件IDE就会明显变卡。所以动态判断尽量使用e.getData拿到的轻量数据不要在这里做重量级计算。我见过不少插件把一大坨逻辑写进update里结果用户反馈说打开菜单要等两三秒排查下来就是这里的问题。一个实用的设计原则是update只做基于上下文的快速判断真正的工作全部放到actionPerformed里。4. 别被PSI吓到源码结构是插件的心脏4.1 PSI、VirtualFile和Document之间的关系PSI全称是Program Structure Interface程序结构接口它是IntelliJ平台对源代码文件建立的一套树形结构化模型。Java代码在PSI里会被解析成PsiJavaFile、PsiClass、PsiMethod、PsiField这些节点类似XML解析成DOM树。IDE的跳转定义、查找引用、重构、代码高亮全部建立在PSI之上。如果你的插件要做任何和源码分析、代码生成相关的事情PSI就绕不开。除了PSI还有两个基础概念要一起理解VirtualFile和Document。VirtualFile是平台对文件系统的抽象层它不屑于区分文件在磁盘上还是在jar包里也不关心具体编码格式只提供一个统一读取接口。Document则是对一块可编辑文本缓冲区的抽象编辑器里显示的内容就是一个Document。三者的关系可以这样记VirtualFile描述文件系统的文件Document描述可编辑的文本内容PSI把文本内容进一步解析成程序语法树。修改代码时通常先改PSI或Document再通过平台机制同步回VirtualFile最终落盘。4.2 如何安全地扫描项目里的Java类最常见的插件需求之一就是扫描项目里所有Java类找到特定注解或方法做处理。平台提供了一个严谨的读写锁机制读PSI需要在ReadAction里进行写PSI需要在WriteAction里进行。一个典型的只读扫描是这样写的ReadAction.run(() - { JavaPsiFacade javaPsiFacade JavaPsiFacade.getInstance(project); GlobalSearchScope scope GlobalSearchScope.projectScope(project); PsiClass psiClass javaPsiFacade.findClass(com.example.MyService, scope); if (psiClass null) return; PsiMethod[] methods psiClass.getMethods(); // 遍历方法做处理 });ReadAction.run是同步阻塞读锁。它的作用是在你读取PSI的时候其他线程不会同时修改这棵树从而保证数据安全。JavaPsiFacade是一个门面类集中提供了查找类、包、元素工厂这些核心能力。findClass接收全限定类名再结合GlobalSearchScope限定搜索范围可以只在当前项目、某个模块、某个依赖库或者测试源码里搜索。需要特别注意的是ReadAction.run适合短期读取如果操作时间较长比如遍历几百个文件做统计强烈建议把它挪到后台任务里让操作异步执行并通过ProgressManager展示进度条。IntelliJ平台对UI线程卡顿零容忍一旦插件在Event Dispatch Thread上执行了耗时操作IDE整体都会失去响应这类问题在上架审核时也容易被重点提出。4.3 代码生成和修改的正确姿势另一类常用场景是代码生成比如批量给类插入getter/setter方法。这类操作会修改PSI树所以必须放在WriteAction中通常配合WriteCommandAction来使用WriteCommandAction.runWriteCommandAction(project, () - { PsiElementFactory factory JavaPsiFacade.getInstance(project).getElementFactory(); PsiMethod method factory.createMethodFromText(public String getName() { return name; }, clazz); clazz.add(method); });这里用PsiElementFactory的createMethodFromText从一个字符串直接创建PsiMethod节点再通过clazz.add把节点挂到目标类上。对新手来说这是最直观的写法因为你不需要手动构造PSI树的每个子节点。但这种用法有一个隐藏风险字符串里的代码必须符合当前语言的语法规范而且最好能编译。如果字符串里引用了某个尚未import的类型生成的代码虽然能插入但用户使用时可能面临编译错误。更稳妥的做法是先创建方法签名节点再用其他API逐层构造参数和注释或者直接调用JavaPsiCodeStyleHelper这类辅助类做格式化处理。无论哪种方式都要记得在WriteAction执行完成后再做PSI的重新解析不要一边修改一边持有旧的PSI引用去操作否则很容易触发ConcurrentModificationException一类的问题。平台对PSI修改有一套事务和撤销机制建议把每次修改看作一个原子操作能包在一个WriteCommandAction里就尽量包在一个里。5. 开发中的高频坑与排错记录5.1 工程构建卡住和插件市场刷新不出来插件开发的第一步就劝退不少人的是Gradle首次构建时下载IntelliJ平台依赖特别慢。这个问题常见原因是默认仓库访问不稳定处理思路是给Gradle配置国内镜像仓库或者手动把distributionUrl和依赖文件预置到本地。另一个经常被忽略的原因是Gradle JVM没有指向完整的JDK。有人机器上只装了JREGradle也能启动但编译时会出现各种诡异失败建议在IDEA的Gradle设置里明确指定一个JDK 17或更高版本。还有一个非常常见的环境问题是IntelliJ IDEA里插件市场列表刷不出来。很多人以为这影响插件开发其实它影响的只是IDE内部浏览插件市场的功能和你用Gradle构建插件工程是两条线。如果市场面板刷新不出来通常检查系统能否正常访问官方站点以及是否设置了离线模式。开发插件本身最关键的依赖是IntelliJ Platform SDK这个通过Gradle下载和市场面板关系不大。两件事分开看能省去不少无谓的折腾。5.2 改动不生效热加载失灵怎么办插件开发有一个很舒服的体验代码改动后点击Build再切回实验实例大部分情况下不需要重启。但偶尔会遇到改动不生效的情况这时先别急着重建整个工程按照这个顺序排查先看底部构建日志有没有编译错误再确认Gradle的build目录下新生成的class时间戳是否已更新最后再重新运行runIde。很多时候问题就出在增量编译没有正确把改动同步出去Rebuild Project基本能解决。但要明确一个边界热加载并不能覆盖所有场景。修改plugin.xml里新增Action、修改扩展点、调整插件依赖这类结构性改动通常需要重启实验实例才能生效。我自己的习惯是只改方法体或私有逻辑时依赖热加载一旦动了描述文件或加依赖就老实重启别跟平台过不去。踩过几次坑之后你会发现重启实验实例的成本比反复猜测“怎么没生效”低得多。5.3 版本兼容性和类加载隔离Intellij平台每年发布两次大版本API总体稳定但高频使用的API偶尔会调整或者被标记为Deprecated。新用户最容易踩的坑是照着一个旧教程写代码用的是早已废弃的接口编译能过运行时却抛ClassNotFoundException或NoSuchMethodError。这类问题排查起来最费时间因为报错信息往往和现场操作没有直接关系。比较好的习惯是开发前先确认目标sinceBuild和untilBuild范围开发中优先用官方文档推荐的API每次升级IDEA版本后跑一遍完整的插件回归测试。类加载隔离也是一个隐蔽的问题。IntelliJ插件运行在独立的ClassLoader里插件依赖的第三方库与IDEA自带的同名库版本冲突时平台会优先加载插件自带的类但个别场景下还是可能出现运行时类来自平台导致方法签名对不上的情况。遇到这种情况通常就是两个库的版本不统一处理方式是把插件依赖的库版本对齐到IDEA内置的那个版本或者在plugin.xml里显式声明依赖关系避免平台和插件各有一套类定义。最后再分享一个我自己常用的排查表格遇到问题的时候对照着看能更快定位症状常见原因处理方向首次构建卡在下载平台依赖下载慢、Gradle JVM配置不对配置镜像仓库、检查JDK版本编译报找不到APISDK类型选错或版本太低检查intellij块的type和version改动后运行结果不变增量编译未同步或结构性改动未重启先Rebuild再重启实验实例运行时NoClassDefFoundError类加载隔离或依赖冲突对齐依赖版本、调整插件依赖声明说实话我刚开始写IDEA插件那会儿也在环境搭建和版本兼容上浪费过大量时间。后来慢慢摸清门路之后插件开发反而成了我最愿意跟人分享的一个方向因为它反馈直接、成就感强而且能把IDE里那些习以为常的功能真正变成自己的工具。如果你想继续往下走建议下一步去读几个知名开源插件的源码或者对照官方code samples做一个小而完整的项目。这篇上半部分只是把工程搭建、Action和PSI这条主线拉通下半部分我计划重点写自定义语言支持、代码检查与意图动作以及复杂UI组件的实现到时候再接着聊。本文还有配套的精品资源点击获取