尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
从零开发 IntelliJ 插件:PSI、Inspection 与代码生成实战
1. 为什么我要自己写一个 IDE 插件第一次动了写插件的念头是因为团队里反复出现同一类低级问题新建的页面文件总是漏掉某个注解日志里该带的追踪字段十次有八次忘记加接口定义和实现类之间的命名规范全靠口头约定。每次代码评审都要重复提醒提醒完下次还犯。我试过写脚本做检查但脚本跑在提交之后等发现问题代码已经进了仓库也试过配置静态检查规则可规则能覆盖的场景太死稍微带点业务语义的判断就无能为力。后来我把目光转向了 IDE 插件。原因很直接插件运行在编码现场开发者敲下回车的那一刻就能给出反馈甚至可以直接帮他把代码补全、把模板生成好。这种“贴着手指尖”的体验是任何提交后检查工具都给不了的。Android Studio 和 IntelliJ IDEA 同根同源底层都是 IntelliJ Platform一套插件代码稍作适配就能同时服务移动端和通用 Java/Kotlin 开发者投入产出比相当可观。这篇文章面向的是有 Java 或 Kotlin 基础、日常用 Android Studio 或 IDEA 写代码、想把自己脑子里那些“要是 IDE 能自动帮我做就好了”的想法落地的开发者。我会从项目结构讲起把插件从零到能跑、能发布的全过程拆开重点讲清楚那些官方文档一笔带过、但实际开发中一定会卡住你的细节。读完你至少能独立做出一个带自定义 Inspection、代码生成 Action 和配置界面的可用插件。2. 插件开发整体设计与技术选型2.1 先搞清楚插件到底能做什么很多人对插件的想象停留在“加个菜单项”或者“换个主题”。实际上 IntelliJ Platform 暴露的扩展点有几百个按能力大致可以分成这么几类能力类别典型扩展点能实现的效果代码检查LocalInspectionTool实时标黄标红、QuickFix 一键修复意图动作IntentionActionAltEnter 弹出建议操作代码生成GenerateAction、Builder生成模板代码、批量插入片段编辑器增强EditorFactoryListener、TypedHandler输入时自动补全、格式化项目结构ProjectComponent、StartupActivity项目打开时初始化、扫描文件界面扩展ToolWindowFactory、Configurable自定义侧边栏、设置页构建集成BeforeRunTaskProvider运行前自动执行任务我自己的经验是先想清楚你要解决的是“发现问题”还是“生成内容”。前者走 Inspection 路线后者走 Action 或模板路线。两者经常配合使用Inspection 发现缺失QuickFix 调用生成逻辑补上。2.2 技术栈与构建方式的选择早期写插件用的是 DevKit 加手动配置plugin.xml构建产物靠 IDE 自己打包流程繁琐且难以做依赖管理。现在主流做法是Gradle IntelliJ Platform Gradle Plugin这也是我强烈推荐的方式。选 Gradle 的理由很实在依赖声明清晰、能接入 CI、多版本适配方便。你可以在build.gradle.kts里声明目标平台版本插件会自动拉取对应的 SDK 依赖不用再手动下载几百兆的 IDE 安装包去配 SDK。关于语言选择插件主体用 Kotlin 写会比 Java 舒服很多。IntelliJ Platform 的很多 API 是围绕 PSIProgram Structure Interface设计的Kotlin 的空安全特性和扩展函数能让 PSI 遍历代码简洁一大截。当然如果你的团队全是 Java 背景用 Java 也完全没问题API 是同一套。提示目标平台版本的选择很关键。sinceBuild和untilBuild决定了插件能在哪些 IDE 版本上安装。设得太窄用户升级 IDE 后插件就失效设得太宽可能用到新版本才有的 API 导致崩溃。我的做法是sinceBuild设成当前稳定版往前推两三个大版本untilBuild留空表示兼容后续版本然后靠 CI 在多版本上跑测试来兜底。2.3 项目骨架的搭建思路一个结构清晰的插件项目目录大致长这样my-plugin/ ├── build.gradle.kts ├── gradle.properties ├── settings.gradle.kts └── src/ └── main/ ├── kotlin/ │ └── com/example/plugin/ │ ├── inspection/ │ ├── action/ │ ├── generate/ │ └── settings/ └── resources/ ├── META-INF/ │ └── plugin.xml └── icons/plugin.xml是整个插件的注册中心所有扩展点都要在这里声明。我见过不少新手把逻辑写完了却忘了注册结果调试半天找不到入口。记住一句话在 IntelliJ Platform 里没在 plugin.xml 里注册的东西等于不存在。3. 核心细节解析与实操要点3.1 PSI插件开发绕不开的核心概念PSI 是 IntelliJ Platform 对源代码的抽象表示。你可以把它理解成一棵语法树每个节点对应代码里的一个元素——类、方法、字段、注解、甚至一个空格。插件要做的绝大多数事情本质上是读 PSI、判断 PSI、修改 PSI。举个具体例子。假设我要检查所有继承自某个基类的子类是否都加了Serializable注解。用 PSI 写出来大概是这样override fun visitClass(klass: PsiClass) { super.visitClass(klass) val superClass klass.superClass ?: return if (superClass.qualifiedName ! com.example.BaseModel) return val annotation klass.getAnnotation(com.example.Serializable) if (annotation null) { // 注册问题 registerProblem(klass.nameIdentifier) } }这段代码里几个点值得展开。visitClass是 PSI 访问者模式的入口平台会遍历文件里所有类并回调你。superClass拿到父类引用qualifiedName是全限定名比直接比字符串更可靠。getAnnotation按全限定名查找注解注意这里传的必须是全名简写名在跨包场景下会漏判。注意PSI 的遍历是有性能代价的。Inspection 会在你每次敲键盘后触发如果你的检查逻辑里做了全项目扫描或者复杂的字符串匹配编辑器会明显卡顿。我的经验是把重活放到PsiModificationTracker的缓存里只在文件真正变化时重算。3.2 注册 Inspection 的完整流程写一个 Inspection 分三步写实现类、写plugin.xml注册、写描述文件。实现类继承AbstractBaseJavaLocalInspectionToolJava或KotlinInspectionToolKotlin重写buildVisitor方法返回你的访问者。这里有个容易踩的坑访问者里不要直接持有 PsiElement 的引用做异步操作PSI 在编辑过程中随时可能失效异步访问会抛PsiInvalidElementAccessException。正确做法是在访问时立即提取需要的信息比如文本、偏移量把纯数据传出去。plugin.xml里的注册长这样extensions defaultExtensionNscom.intellij localInspection languageJAVA shortNameMissingSerializable displayNameMissing Serializable annotation groupNameMyPlugin enabledByDefaulttrue levelWARNING implementationClasscom.example.plugin.inspection.MissingSerializableInspection/ /extensionsshortName是内部标识displayName是设置页里显示给用户看的名字level决定默认严重级别。这几个字段一旦发布就尽量别改改了会导致用户的启用/禁用状态丢失。描述文件放在resources/inspectionDescriptions/下文件名必须和shortName一致后缀是.html。这个文件会在设置页右侧显示写清楚“这个检查是干什么的、为什么这么规定、怎么修”能大幅降低用户困惑。3.3 QuickFix 的编写要点发现问题只是第一步让用户一键修复才是体验的关键。QuickFix 实现LocalQuickFix接口核心是applyFix方法。修改 PSI 必须包在写操作里override fun applyFix(project: Project, descriptor: ProblemDescriptor) { val element descriptor.psiElement ?: return val factory JavaPsiFacade.getElementFactory(project) val annotation factory.createAnnotationFromText( Serializable, element ) val modifierList (element.parent as? PsiModifierListOwner)?.modifierList WriteCommandAction.runWriteCommandAction(project) { modifierList?.addBefore(annotation, modifierList.firstChild) } }WriteCommandAction是必须的没有它平台会拒绝修改并抛异常。createAnnotationFromText比手动拼 PSI 节点省事得多平台会自动处理导入语句。这里还有个细节如果目标类还没导入Serializable注解所在的包addBefore之后需要调用JavaCodeStyleManager.shortenClassReferences来触发自动导入否则用户会看到一个红色的未解析注解。3.4 代码生成 Action 的实现代码生成类插件最实用的形态是挂在Generate菜单下快捷键 AltInsert。实现方式是继承GenerateAction在invoke里弹出对话框让用户填参数然后生成代码。生成代码时我推荐用模板 参数替换的方式而不是硬编码字符串拼接。IntelliJ Platform 提供了FileTemplateManager可以读取用户自定义的模板也支持内置模板。这样用户如果改了团队的代码风格模板你的插件生成的内容会自动跟着变。val template FileTemplateManager.getInstance(project) .getInternalTemplate(My Data Class) val result template.getText().replace(\${NAME}, className)生成后插入到当前编辑器的正确位置需要计算插入点。如果是往类里加方法用PsiClass.add并配合CodeStyleManager.reformat让格式符合用户设置。这一步不做的话生成的代码缩进会乱用户第一眼就觉得插件不专业。4. 完整实操流程与关键环节4.1 环境准备与项目初始化第一步是装好 JDK 17 或更高版本新版平台要求然后建一个空的 Gradle 项目。build.gradle.kts的核心配置如下plugins { id(org.jetbrains.intellij) version 1.17.0 kotlin(jvm) version 1.9.0 } intellij { version.set(2023.2) type.set(AI) // Android Studio 用 AIIDEA 用 IC plugins.set(listOf(com.intellij.java)) } tasks { patchPluginXml { sinceBuild.set(232) untilBuild.set() } }type字段决定拉取哪个平台的 SDK。做 Android 相关插件就设AI通用插件设IC。plugins里声明依赖的其他插件比如你要用 Java 相关的 PSI 类就得加上com.intellij.java。初始化完成后跑一次./gradlew build确认能编译通过。这一步如果卡在下载依赖检查网络和 Gradle 镜像配置别急着往下走。4.2 调试运行的正确姿势插件调试不是普通 Java 程序的main方法而是启动一个沙箱 IDE 实例。Gradle 任务runIde会拉起一个全新的 IDE 窗口你的插件已经装在里面。第一次启动会比较慢因为要初始化整个 IDE 环境。调试时几个实用技巧在插件代码里打断点沙箱 IDE 触发对应逻辑时会命中断点和普通调试一样。修改plugin.xml后必须重启沙箱改 Kotlin 代码可以热重载部分场景。沙箱的日志在build/idea-sandbox/system/log/idea.log插件抛异常时第一时间看这里。如果插件导致沙箱启动失败删掉build/idea-sandbox目录重新跑能解决大部分“莫名其妙”的问题。提示调试 Inspection 时沙箱里打开一个测试项目故意写出会触发检查的代码观察是否标黄。如果没反应先确认 Inspection 的language字段和文件类型匹配再确认enabledByDefault是 true。4.3 配置界面的实现插件如果需要用户配置比如自定义检查规则、模板路径得提供一个设置页。实现方式是继承Configurable接口然后在plugin.xml里注册applicationConfigurable idcom.example.plugin.settings displayNameMy Plugin Settings instancecom.example.plugin.settings.MyConfigurable/Configurable的核心是createComponent返回一个 Swing 面板以及isModified和apply方法处理保存逻辑。数据持久化用PersistentStateComponent平台会自动把状态序列化成 XML 存到配置目录。这里有个我踩过的坑配置数据的默认值处理。如果用户从没打开过设置页PersistentStateComponent返回的是空对象所有字段都是 null 或 0。你必须在 getter 里做兜底否则插件第一次运行就会因为空指针崩溃。我的做法是给每个字段设一个明确的默认值并在loadState里做字段级合并。4.4 打包与发布开发完成后./gradlew buildPlugin会在build/distributions/下生成一个 zip 包。这个包可以直接通过 IDE 的“从磁盘安装插件”功能安装也可以上传到插件市场。上传前有几件事必须做把plugin.xml里的version改成正式版本号别用1.0-SNAPSHOT。准备好插件图标40x40 和 80x80 两套放在resources/META-INF/下并在plugin.xml里引用。写清楚description和changeNotes用户安装前会看这些。确认sinceBuild覆盖了你承诺支持的最低版本。发布后如果发现严重 bug可以快速发一个补丁版本。插件市场对版本号没有严格限制但建议遵循语义化版本方便用户判断升级幅度。5. 常见问题与排查技巧实录5.1 插件不生效的排查顺序这是新手遇到最多的问题。按下面顺序排查基本能定位到原因现象可能原因排查方法菜单项不出现Action 未注册或 group 锚点错误检查 plugin.xml 的 action 配置Inspection 不标黄language 字段不匹配确认文件类型和 language 一致设置页打不开Configurable 未注册检查 applicationConfigurable 节点启动报错依赖插件未声明看 idea.log 的 ClassNotFound修改不生效沙箱缓存删 build/idea-sandbox 重启我个人的习惯是每次改完plugin.xml先跑一次runIde确认沙箱能正常启动再写业务逻辑。这样能把“配置错误”和“代码错误”分开排查效率高很多。5.2 PSI 操作中的典型异常PsiInvalidElementAccessException是最常见的异常几乎每个插件开发者都会遇到。根本原因是 PSI 元素在异步操作期间被编辑失效了。解决办法有两个一是所有 PSI 访问都在读操作内完成用ReadAction.compute包裹二是需要跨线程传递时只传文本和偏移量这类纯数据。另一个坑是IncorrectOperationException通常出现在没有WriteCommandAction包裹的情况下修改 PSI。记住一个原则读 PSI 用 ReadAction写 PSI 用 WriteCommandAction两者不能混。5.3 性能问题的定位插件导致 IDE 卡顿是最劝退用户的问题。定位方法是用 IDE 自带的性能分析工具或者简单粗暴地在可疑代码里打时间戳。常见的性能杀手有三个在 Inspection 里做全项目扫描、在TypedHandler里做复杂计算、在StartupActivity里同步做耗时初始化。对应的优化手段是用缓存 修改追踪器、把计算移到后台线程、初始化改成异步并显示进度。注意后台线程里访问 PSI 必须包ReadAction而且不能持有 PSI 引用到线程外。我一般在线程内把需要的数据提取成普通对象线程外只操作这些对象。5.4 多版本兼容的处理IDE 版本迭代很快API 时有变动。处理兼容性的实用做法是用sinceBuild声明最低支持版本低于这个版本的用户装不上避免运行时报错。对可能变动的 API用反射调用并做异常兜底而不是直接编译期引用。在 CI 里配置多个平台版本跑测试提前发现不兼容。我自己的插件就遇到过某个 PSI 方法在新版本被标记废弃的情况。当时靠反射加降级逻辑撑过了过渡期等用户普遍升级后再切到新 API。这种“渐进式迁移”比强制用户升级体验好得多。6. 我在这条路上踩过的几个坑第一个坑是低估了 plugin.xml 的重要性。刚开始写的时候我把逻辑写得漂漂亮亮结果运行起来毫无反应查了两个小时才发现是扩展点没注册。后来我养成了一个习惯每加一个功能先在 plugin.xml 里把注册写好再写实现这样至少能保证“入口是通的”。第二个坑是在 Inspection 里直接弹对话框。我一开始想做一个“发现严重问题就弹窗提醒”的功能结果用户每敲几个字就弹一次体验极差。后来改成只在编辑器里标红加 QuickFix用户想修的时候按 AltEnter 就行。这件事让我明白插件的交互要克制能静默解决的就别打扰用户。第三个坑是忽略了国际化。我第一版插件里所有提示文字都是硬编码中文后来有海外用户反馈看不懂。改成用Bundle加载资源文件后加英文翻译只需要多写一个 properties 文件。虽然多花了一点时间但插件的适用范围一下子扩大了。如果你也想动手写一个自己的插件我的建议是从最小的功能开始——哪怕只是一个检查某个注解是否存在的 Inspection。把它跑通、发布出去、收到第一条用户反馈这个过程带来的成就感会推着你继续往下做。插件开发的门槛没有想象中高真正难的是想清楚“用户到底需要什么”而这恰恰是你作为一线开发者最大的优势。
RELATED

相关推荐

数据仓库安全体系建设:从Kerberos认证到Ranger权限管控实战

数据仓库安全体系建设:从Kerberos认证到Ranger权限管控实战

数仓安全这话题,说多了都是泪。做了这么久大数据平台,见过太多“裸奔”式部署:底层Hadoop集群七八百台机器,上面跑着数万张表,业务方几百号人用一个公共账号在跑数仓,权限全靠自觉。直到某天审计过来要数据…

📅 2026/10/19 4:45:40
学大模型应用开发,从动手做起:提示词、RAG与Agent实战

学大模型应用开发,从动手做起:提示词、RAG与Agent实战

1. 为什么要从“动手”开始学大模型应用开发这两年大模型的热度有目共睹,各种新产品、新框架层出不穷。但如果你去翻技术社区,会发现一个很有意思的现象:真正能做出东西的人,往往不是那些把Transformer论文倒背如流的人&#xff0…

📅 2026/10/19 4:45:40
网课视频防盗录实战:5种方案实测对比与选型建议

网课视频防盗录实战:5种方案实测对比与选型建议

卖课的人和做内容的知识博主,应该都经历过这种糟心事:辛苦录了一门网课,被某个用户买走,转头就出现在二手平台或资源站,标题写着“全网最全”,价格还比你卖的便宜十杯奶茶钱。我自己就遇到过,而…

📅 2026/10/19 4:40:40
MORE NEWS

更多资讯

📰

物联网设备WiFi配网实战:ESP32/ESP8266 SoftAP与SmartConfig详解

上个月帮朋友调一块 ESP32 开发板,设备本身跑得好好的,最后却卡在“怎么让设备连上他家的 WiFi”这一步。我说你直接把 SSID 和密码写在代码里不就行了?他说你自己调试当然没问题,可这个设备是要交给一个完全不会用串口、不会看代…

📰

artcraft:基于WebGL的轻量级创意合成工具实战

1. 项目缘起与整体设计思路1.1 为什么会有 artcraft 这个想法做创意工具这些年,我最大的感受就是:灵感来得快,工具跟不上。脑子里冒出一个画面,想快速拼贴、调色、加纹理,结果打开专业软件要等半分钟加载,图…

📰

VS2017 64位下OSGQT整合实战:从编译到渲染的避坑指南

简介:这是一份面向Qt与OpenSceneGraph(OSG)三维渲染开发者的整合示例工程,基于Github上口碑较好的OSGtree项目整理而来,已在VS2017搭配64位OSG环境下完成调试,可直接运行,适合正在学习OSG与Qt混…

📰

曼哈顿距离在机器人路径规划中的原理与实战详解

前两天在调我们一台小型自动清扫机器人的任务调度时,又看到有同事对着坐标嘀咕:机器人停在(3,5),任务点在(7,2),两点间直线距离明明是5,为什么程序最后算出的是7?这个问题背后,正是曼哈顿距离&a…

📰

Vite打包常见报错与解决方案:从内存溢出到体积优化全记录

Vite用久了你会发现一个规律:开发环境(dev server)跑得飞起,一旦执行npm run build,各种奇奇怪怪的问题就冒出来了。不是内存溢出,就是资源路径不对,再不然就是打包体积大得离谱,警告…

📰

diagram-design:从随手画图到系统性图表设计,告别技术债

1. 从一张草图到一套系统:diagram-design 到底在解决什么问题很多人第一次听到 diagram-design 这个词,会下意识觉得它只是“画图”的另一种说法。但真正在项目里被图表折磨过的人知道,画图本身从来不是难点,难点在于图表的可维护…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬