ARTICLE DETAIL

资讯详情

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

IntelliJ IDEA插件开发:状态持久化、ToolWindow与打包实战

IntelliJ IDEA插件开发:状态持久化、ToolWindow与打包实战 简介IDEA插件开发手册下是一份面向中高级IDE插件开发者的PDF指南针对基于JetBrains Runtime 17.0.9并兼容IDEA 2023/2024的插件开发环境系统讲解语言类插件开发的核心技术。手册承上启下重点剖析PSI程序结构接口、文件视图提供者、元素节点等核心概念覆盖自上而下与自下而上的结构浏览方法、引用搜索及多解析结果处理等实用操作并给出元素匹配模式、引用解析等进阶主题附录中汇总了开发所需工具与参考资料为读者提供从理论到实践的完整路径。尤其适合计划开发代码自动补全、依赖管理、代码检查等高级或收费插件的开发者也适用于希望深入理解IntelliJ平台内部机制的进阶学习者。整个资源包共1个PDF文件大小约9.73MB便于离线阅读与标记。截至目前已有218人学习该资源在IntelliJ平台插件开发主题下具有较好的参考价值。1. 从“能跑”到“能用”Intellij idea PlugIn插件开发手册(下)在补哪些坑如果前面那本手册带你跑通了一个最小 Action你已经能在编辑器里呼出弹窗、在日志里打出 Hello World。但真要把插件交到同事手里你会撞上三个绕不开的坎重启 IDE 后配置没了界面上的耗时操作把 UI 卡死换一台电脑安装直接报版本不兼容。这本《Intellij idea PlugIn插件开发手册(下)》就是为这些问题写的它把插件的“生命周期”补齐状态持久化、ToolWindow 界面、崩溃排查、发布打包。适合已经写过最小插件的人也适合只想用 IntelliJ IDEA 社区版给团队做个内部工具、不想碰付费市场的开发者。下文默认你会建 Plugin 工程、会改 plugin.xml不再重复基础操作。2. 给插件装上记忆PersistentStateComponent 与配置界面的正确写法插件并非“活着”的进程它只在 IDE 需要时才被加载。你写的一个普通 Java 类new 出来处理完 Action 请求后什么也留不下来。要让插件记住用户上次填的模板、勾选的开关最可靠的做法不是自己写文件而是交给 IDE 的配置机制Service PersistentStateComponent。2.1 插件重启后状态丢失的根源Service 生命周期很多第一次做插件的人会把“配置”直接放在 Action 的静态字段里public class MyAction extends AnAction { public static String template ; }重启后字段被清理模板自然是空的。IntelliJ IDEA 插件里的对象生命周期由平台管理你要保存一组配置必须挂到 Service 上。Service 分应用级和项目级applicationService 在 IDE 整个生命周期内只有一个实例适合全局偏好projectService 在每一次打开项目时创建一个项目关闭就销毁适合“只对这个项目生效”的设置。上面那个 Action 里的 static 字段既不会被平台保存也不会在插件卸载时清理纯属玄学现场。正确的注册方式是在 plugin.xml 的extensions里写extensions defaultExtensionNscom.intellij projectService serviceImplementationcom.example.TemplateStorageService/ /extensions这样 IDEA 会在项目打开时创建 TemplateStorageService并在项目保存时自动把它的getState()结果写到.idea下的配置文件里。你可以在代码里通过project.getService(TemplateStorageService.class)拿到同一个实例而不是自己去new。如果注册成了项目服务却用全局方式获取IDEA 会直接抛 IllegalArgumentException这个错一搜就懂但新手往往先怀疑是不是类加载问题其实只是注册类型与获取方式不匹配。2.2 用 PersistentStateComponent 存配置从 State 到 State 注解下面是一个最简单的可运行状态类。它会保存一个模板字符串和一个布尔开关State( name com.example.TemplateStorageService, storages Storage(StoragePathMacros.WORKSPACE_FILE) ) public class TemplateStorageService implements PersistentStateComponentTemplateStorageService.State { public static class State { public String template // generated by plugin; public boolean addTimestamp true; } private State myState new State(); public static TemplateStorageService getInstance(Project project) { return project.getService(TemplateStorageService.class); } Override public State getState() { return myState; } Override public void loadState(State state) { this.myState state; } }逻辑说明State里的 name 是这条配置在文件里的唯一标识务必使用类全限定名避免两个插件撞名。Storage指定存到哪里WORKSPACE_FILE 对应项目根目录下的.idea/workspace.xml适合装项目相关、非共享的临时状态如果存到 PROJECT_FILE则会写进项目自身的.idea/misc.xml适合真正需要提交到 VCS 的配置如果连Storage都不写默认写到 IDE 全局的 options 目录用于 application 级服务。这里我们把它设计成项目级所以用 WORKSPACE_FILE。参数说明PersistentStateComponentT要求泛型 T 是普通对象字段必须是 public而且不能是ListMap这种带泛型的复杂结构。序列化时 IDEA 会对 State 做反射读写如果你在字段上放了一个private int x它也能读但最好保持全 public。loadState里直接把新对象赋给内部字段最稳妥千万不要在拿到底层 State 对象后再从外部修改因为平台可能在多次触发之间复用同一个实例。我见过最离谱的翻车是把getState()写成返回null结果 IDE 每次都会用默认值覆盖已有的配置等于没存。如果你的插件会迭代好几个版本新增 State 字段时尽量给个默认值比如public String template ;这样旧的配置反序列化进来时不会因为字段缺失而出现空指针。2.3 把设置面板做出来Configurable 的注册回读与 apply配置要能让人改就得做一个 Settings 面板。常见做法是让一个类实现Configurable只负责 UI另一个类继续当 State 控制器。分开写的好处是 UI 的刷新逻辑不会污染状态类。下面是设置界面的最小骨架public class TemplateSettingsPanel { private final JTextField templateField new JTextField(30); private final JCheckBox addStampBox new JCheckBox(Insert timestamp, true); public JComponent getComponent() { JPanel panel new JPanel(new GridBagLayout()); panel.add(new JLabel(Template:), new GridBagConstraints()); panel.add(templateField, new GridBagConstraints()); panel.add(addStampBox, new GridBagConstraints()); return panel; } public void apply(TemplateStorageService service) { service.getState().template templateField.getText(); service.getState().addTimestamp addStampBox.isSelected(); } public boolean isModified(TemplateStorageService service) { return !templateField.getText().equals(service.getState().template) || addStampBox.isSelected() ! service.getState().addTimestamp; } }对应的Configurable类这样写public class TemplateConfigurable implements Configurable { private final TemplateStorageService service; private TemplateSettingsPanel panel; public TemplateConfigurable(Project project) { this.service TemplateStorageService.getInstance(project); } Override public JComponent createComponent() { if (panel null) { panel new TemplateSettingsPanel(); } return panel.getComponent(); } Override public void apply() { panel.apply(service); } Override public boolean isModified() { return panel ! null panel.isModified(service); } }在 plugin.xml 里注册项目级配置入口projectConfigurable idcom.example.TemplateConfigurable displayNameTemplate Probe instancecom.example.TemplateConfigurable/这里有一个容易忽略的细节createComponent()会在打开设置窗口时调用不要在里面做 IO 或读大文件。面板加载时从service.getState()读取字段不直接操作State的引用这样平台在 IDE 自身保存配置时不会和面板里的“脏值”打架。同时一定要实现isModified()否则用户没点“应用”你也把值写进去下次打开设置以为保存了实际又没生效。IntelliJ IDEA 2021.2 之后的版本对模块依赖更敏感如果你在做项目级设置且用到了 Java PSI需要在 plugin.xml 里显式声明dependscom.intellij.modules.java/depends漏掉这个声明时最容易出现的现象是设置面板能打开、点保存后却没有任何反应。3. 给插件开一扇窗ToolWindow 里的 Swing 与耗时任务许多插件开发手册写到 Action 就停了但真实内部工具往往需要一块常驻面板比如展示项目文件列表、实时输出日志、放一个小的操作表单。ToolWindow 就是干这个的它是 IntelliJ IDEA 的工作台窗口不是随便拉一个 JFrame 就能解决的。3.1 ToolWindowFactory 接入从 plugin.xml 到 Content 面板先注册窗口工厂extensions defaultExtensionNscom.intellij toolWindow idTemplateProbe anchorright icon/toolIcons/sample.svg factoryClasscom.example.TemplateProbeToolWindowFactory/ /extensions工厂实现public class TemplateProbeToolWindowFactory implements ToolWindowFactory { Override public void createToolWindowContent(NotNull Project project, NotNull ToolWindow toolWindow) { ContentManager contentManager toolWindow.getContentManager(); Content content contentManager.getFactory() .createContent(new TemplateProbePanel(project), Preview, false); contentManager.addContent(content); } }这里anchorright决定窗口方向可选left、right、bottomicon 路径要放在 resources 下的/toolIcons/目录而且必须是 svg 或 png 的 16x16 图。工厂类会被 platform 实例化所以必须有无参构造函数。createToolWindowContent一定在 EDT 上执行内容面板构建要快否则 IDE 启动会卡住。面板类可以是一个普通的 JPanel但我们建议用JB(Panel)风格组件public class TemplateProbePanel extends JPanel { private final Project project; private JListString fileList; public TemplateProbePanel(Project project) { super(new BorderLayout()); this.project project; JLabel title new JBLabel(Project files:); fileList new JList(); add(title, BorderLayout.NORTH); add(new JScrollPane(fileList), BorderLayout.CENTER); } }注意这里没有调用fileList.setModel因为数据要异步加载不要在构造函数里去遍历磁盘否则打开 ToolWindow 的瞬间就会卡住 IDE。我也见过有人把初始化逻辑写在createComponent里然后在 EDT 中 sleep 三秒钟模拟加载结果整个 IDE 窗口转圈同事直接抱怨“插件崩溃了”——这不是崩溃是 EDT 被堵死。3.2 别让 UI 卡死用 Backgroundable 把任务扔出 EDTSwing 是单线程模型所有 UI 更新必须回到 EDT。耗时操作只能放到后台线程。IntelliJ Platform 为此提供了Task.BackgroundableProgressManager.getInstance().run(new Task.Backgroundable(project, Scanning project files) { Override public void run(NotNull ProgressIndicator indicator) { indicator.setIndeterminate(false); indicator.setFraction(0.0); ListString paths scanProject(project); for (int i 0; i paths.size(); i) { if (indicator.isCanceled()) { break; } paths.set(i, normalize(paths.get(i))); indicator.setFraction((i 1.0) / paths.size()); } final ListString result paths; SwingUtilities.invokeLater(() - { fileList.setListData(result.toArray(new String[0])); }); } });逻辑说明run()里的代码在线程池中执行可以安全地做 IO、读取文件列表、解析 PSI。ProgressIndicator用来报告进度setFraction(0.0)到1.0之间。当用户点击取消indicator.isCanceled()会变为 true你要在循环里主动 break 退出平台才能及时取消任务。结束后要回到 EDT 更新JList不要在后台线程直接setModel否则会报Called on the wrong thread。参数说明new Task.Backgroundable(project, title)的第一个参数传项目实例用于在进度条上显示项目名如果你传null进度条会变成全局提示可能干扰用户。setIndeterminate(false)表示确定进度如果任务没有明确步数就保持 true。还有一个小坑如果run()里抛了异常默认只会写进 idea.log不会弹窗告知用户。你要在run()里 try/catch 并调用NotificationGroup发通知否则用户只看到任务结束什么反馈也没有这是插件“看起来没响应”的高频原因。如果你不想用进度条也可以直接用ApplicationManager.getApplication().executeOnPooledThread(() - { // 耗时操作 SwingUtilities.invokeLater(() - { // UI 更新 }); });它更轻量但没有取消机制适合两三秒内的短任务。真正要处理“用户点取消”的长任务还是Task.Backgroundable更稳。另一个实操细节后台扫描完数据后不要把整个项目对象传给invokeLater里的面板面板只需要最终结果列表传项目引用容易在 IDE 关闭时造成内存泄漏血泪经验。4. 四大避坑现场插件加载失败、崩溃与版本不兼容的排查这一章是踩坑记录。插件开发最大的问题是 IDE 把自己当成黑匣子报错信息要么在弹窗里一闪而过要么只出现在日志文件。一旦调用了一点点平台 API就可能因为版本差异导致加载失败。下面按现象写每条都是“现象 → 原因 → 解决”。4.1 现象一启动日志出现 Plugin failed to initialize窗口被禁用现象安装后 IDE 启动不报错但你点工具窗口时它不出现任务栏能看到有 ToolWindow点击后只显示一条 “Plugin X failed to initialize and will be disabled”。打开Help → Show Log in Explorer里的idea.log能看到ClassNotFoundException: com.example.TemplateProbeToolWindowFactory。原因ToolWindow 工厂类没有被插件加载器扫描到。最常见的是 plugin.xml 里factoryClass拼写错误、类在另一个模块里没被放进 release 包或者State类与 plugin.xml 里注册的服务不一致。另一个高发点是插件声明支持 2021.3但代码里调用了 2022 之后才有的 API平台在初始化时发现方法不存在直接禁用整个插件。解决先确认 plugin.xml 里的factoryClass全限定名与类实际包名完全一致再用Build Rebuild Project后看build/classes下是否真的存在这个.class文件。版本边界问题把idea-version since-build2021.3 until-build2023.1/临时改成since-build设为2022.1并更新 API 使用用两个不同版本的 IDE 跑runIde验证。如果你使用的是 IntelliJ IDEA 社区版某些平台模块默认不可用必须在 plugin.xml 里加dependscom.intellij.modules.java/depends否则也会提示初始化失败。4.2 现象二点按钮就抛 NoClassDefFoundError现象插件能加载按钮也能看到点击后弹窗NoClassDefFoundError: org/xml/sax/SAXException或者类似Could not initialize class com.xxx.Library。原因这是一个很经典的依赖打包问题。你只在 build.gradle 里加了依赖项但插件运行时用的类加载器只读插件自己的lib/目录Gradle 的compileOnly依赖不会被打进 zipimplementation依赖如果没有被 IntelliJ Gradle 插件的打包任务包含也可能漏掉。解决在 build.gradle.kts 中对需要随插件发布的依赖单独处理。关键点是保证运行时 classpath 里的第三方 jar 会被复制到插件 zip 的lib/目录dependencies { implementation(org.apache.commons:commons-lang3:3.12.0) } tasks { buildPlugin { dependsOn(runtimeClasspath) } withTypeZip { from(configurations.runtimeClasspath) { into(lib) } } }然后执行./gradlew buildPlugin解压build/distributions/xxx.zip确认lib/下面有没有 commons-lang3 的 jar。如果没有按上面的方式把 runtimeClasspath 强制打进去。另一个隐藏坑是如果两个插件都带了同一个第三方 jar类加载器可能优先加载先加载的那个插件的类从而产生LinkageError。遇到这种情况最省事的办法是换用平台自带的高版本库或者在插件说明里注明不要和同类插件一起装。4.3 现象三断点不命中或源码对不上现象你按教程开了 Debug 模式插件代码里打了断点结果怎么也不停偶尔停了看到的是反编译的字节码不是自己的 Java 源码。原因IntelliJ IDEA 插件调试默认是在一个单独的 JVM 里运行插件Debugger 会连接这个 JVM。新版 IDE 默认对插件的类加载器做了隔离如果插件依赖的类与 IDE 核心类重名或者你没有把源码文件同步到运行 classpath断点就定位不到行号。解决先执行Build Rebuild Plugin再点Run Edit Configurations确认 Plugin 运行配置的Use classpath of module选了正确的模块。把Usage of local IDE的勾选去掉让runIde启动一个干净的沙箱实例断点通常能命中。如果还是不中打开Build Build Plugin Modules后的build目录用javap看一下 class 文件里有没有行号信息。还有一个小技巧不要用方法断点方法断点命中率极低改成普通行断点成功率会高得多。如果断点直接断在PluginClassLoader.loadClass内部说明你的类没有被完整加载回到 4.2 检查依赖是否完整。4.4 现象四安装到别的环境时报 Incompatible、版本依赖缺失现象你把buildPlugin打出的 zip 发给同事对方在Settings Plugins Install from Disk选择 zip 后提示 “Incompatible because it requires version 2022.3 or older”或者 “Plugin not loaded: required module com.intellij.modules.java is not available”。原因plugin.xml 里的idea-version until-build2022.3/限制了版本边界同事的 IDE 是 2023.1自然会拒绝。until-build没写的插件会被新版 IDE 兼容但如果你用了比since-build更新的 API会在运行期爆MethodNotFoundException。依赖 module 缺失则是因为你在代码里 import 了 Java PSI但没有在 plugin.xml 里声明依赖。解决在 plugin.xml 里看上界idea-version since-build2021.3 until-build2023.1.*/或者干脆不写until-build让插件声明持续兼容。再补上模块依赖dependscom.intellij.modules.platform/depends dependscom.intellij.modules.java/depends然后执行./gradlew verifyPlugin它会解析 plugin.xml检查依赖项和类引用。在社区版上需要com.intellij.modules.java的插件会因为社区版没有 Java 模块而无法完整运行这是正常现象对内部分发来说你只要确认目标同事都用的旗舰版声明这个依赖反而更安全。如果不想把绑定做死也可以在代码里对IndexNotReadyException做降级处理但那是另一个话题。5. 从沙箱到市场打包、签名与发布前自检步骤插件最终要交付给其他人使用不能永远在runIde里跑。这一章讲打包和发布前要做的检查顺便把“本地好、远端炸”的常见元凶揪出来。5.1 用 Gradle IntelliJ 插件把插件打成 zip用gradle-intellij-plugin的标准构建。先给 build.gradle.kts 配一个最小骨架plugins { id(java) id(org.jetbrains.intellij) } intellij { version.set(2023.2.5) type.set(IC) plugins.set(listOf(com.intellij.java, org.intellij.intelli-js)) } tasks { buildPlugin { archiveFileName.set(template-probe.zip) } }注意plugins块里id(org.jetbrains.intellij)的具体插件版本号要根据你本地的 Gradle 与 JDK 选择这里不写死以免误导。intellij.type用IC表示社区版IU表示旗舰版如果用到了 Java 相关 PSI API社区版里也要在plugins.set(...)中声明com.intellij.java否则编译时找不到PsiJavaFile。打包命令是./gradlew buildPlugin产物在build/distributions/下。拿到 zip 后不要急着发出去先在自己电脑上用Install from Disk装一次重启 IDE再把窗口点一遍确认没有ClassNotFoundException。这一步能拦下七成发布事故。为了确认打包内容干净建议执行unzip -l build/distributions/template-probe.zip重点看lib/目录下有没有出现idea.jar、platform-api.jar这类与 IDE 自带类同名的文件。如果有插件加载时会先污染类加载器出现NoClassDefFoundError或更严重的IncompatibleClassChangeError而且往往只在别人机器上复现自己这边因为本地 IDE 先加载了对应类而看起来一切正常。这种“本地好、远端炸”的翻车场景多半就是打包时把 IDE 自己的 jar 拷贝进去了。5.2 plugin.xml 的“身份证”since-build、until-build 与 depends在发布前最容易被忽略的是 plugin.xml 头部的兼容性声明。它三个字段决定了插件能在哪些 IntelliJ IDEA 版本上活下来字段含义建议since-build最低支持版本按你实际测试过的最低版本写写低了会在老版本上运行时报 API 错误until-build最高支持版本写2023.1.*表示只支持 2023.1写*表示不设上限但风险自负depends所需模块至少声明com.intellij.modules.platform用到 Java 再声明com.intellij.modules.java一个反面案例是since-build写了2021.1但你用了FileEditorManager.openEditor的新签名在 2021.2 上运行直接NoSuchMethodError。所以since-build要按“实际验证过的最小版本”写不要在办公室里拍脑袋填数字。发布到 JetBrains Marketplace 时官方还会再做一次兼容性检查要求插件通过verifyPlugin但如果只是内部使用这一步可以放到最后再跑。5.3 未签名插件在 2020.1 的安装策略本地验证就够从 IntelliJ IDEA 2020.1 开始官方对插件安全策略做了强化没有签名私钥的插件只能通过Settings Plugins Install from Disk安装装完会有一个“未签名”提示但不影响使用。这意味着如果你只是给团队内几个同事用完全不需要注册插件商城账号本地安装 zip 即可。这也是我在内部工具上最常用的方案不碰 Marketplace、不做签名证书省掉上传审核流程。签名插件需要你在 JetBrains 账号里申请 token并在 Gradle 配置里使用signPlugin任务。流程是生成密钥对、上传公钥、打包时用私钥签名。这部分官方文档写得很细我不在这本手册里展开。你只需要知道要投 Marketplace 就必须签名只给自己人装zip 就能跑。还有一个容易被安全策略拦截的细节zip 包的根目录必须是插件的lib/与META-INF/不能多套一层文件夹。如果打成template-probe/template-probe/lib/...安装时 IDE 反而会认不出 plugin.xml。发布前把结构列出来看一眼比等同事装完再回来报错省事得多。6. 验证一个插件有没有“病”从 idea.log 里挖三次失败痕迹我习惯在插件“看起来能跑”之后故意做一次破坏性测试把运行配置切到稍老一点的 IntelliJ IDEA 版本然后反复点击插件入口再打开 idea.log 看有没有异常。这个习惯帮我抓出过好几个“本地好、远端炸”的问题。日志路径在 Linux/macOS 是~/.cache/JetBrains/IntelliJIdea2023.2/log/idea.logWindows 在%LocalAppData%\JetBrains\IntelliJIdea2023.2\log\idea.log。我常用的过滤命令是grep -nE Plugin|ERROR|Caused by|at com\\.your\\.plugin \ ~/.cache/JetBrains/IntelliJIdea2023.2/log/idea.log | tail -n 200不要一上来就抓全部 Exception那样会看到无数 IDE 自身的噪音。正确做法是记下当前时间去做一次操作再回来过滤这个时间窗前后的日志。如果看到你自己包名下的异常先看堆栈顶部的业务代码如果是第三方库去检查打包时是否漏了 jar如果异常出现在com.intellij.openapi.actionSystem里多半是 Action 执行时抛了未捕获的运行时异常要给run()整体加 try/catch。还有一个小经验插件首次打开 ToolWindow 特别慢不一定是线程问题很可能是项目索引还没就绪你在后台线程里直接调用了PsiManager相关 API。遇到这种情况优先用indexIsUpToDate或者DumbService做延迟处理别急着加线程池。Intellij idea PlugIn 开发这条路翻车不可怕怕的是不看证据瞎猜把 idea.log 当成第一现场基本能省下大半排查时间。希望这份手册下篇能帮到你。本文还有配套的精品资源点击获取
返回列表