扩展点、监听器与动态插件¶
IntelliJ Platform 的插件模型主要由三类机制组成:扩展、监听器和服务。扩展用于把能力挂到平台定义的扩展点;监听器用于订阅事件;服务用于保存运行时状态和封装长期逻辑。
Extensions¶
扩展是插件接入 IDE 功能最常见的方式。平台或其他插件定义扩展点,当前插件提供实现类或配置数据。
<extensions defaultExtensionNs="com.intellij">
<toolWindow
id="Example"
anchor="right"
factoryClass="com.example.ExampleToolWindowFactory"/>
<completion.contributor
language="JAVA"
implementationClass="com.example.MyCompletionContributor"/>
</extensions>
defaultExtensionNs 的选择:
| 值 | 含义 |
|---|---|
com.intellij |
扩展 IntelliJ Platform 核心能力 |
| 其他插件 ID | 扩展某个依赖插件暴露的扩展点 |
官方文档提到平台和捆绑插件提供了 1700 多个扩展点。实际查找扩展点时,优先使用:
plugin.xml中的代码补全和 Quick Documentation。- IntelliJ Platform Extension Point and Listener List。
- IntelliJ Platform Explorer,查看开源插件如何实现。
- 依赖插件自己的
plugin.xml。
扩展实现的约束¶
扩展类应保持轻量和无状态:
- 不要在构造函数做耗时初始化。
- 不要做静态初始化。
- 不要用 Kotlin
object作为扩展实现类。 - 不要把同一个类同时注册为 extension 和 service。
- 运行时状态放到 Service。
- 需要按场景禁用时,可以在构造函数抛出
ExtensionNotApplicableException。
这些规则不是形式主义。扩展类加载过早或持有状态,会增加启动成本,并影响动态卸载。
自定义 Extension Point¶
如果你的插件希望被其他插件扩展,可以声明自己的扩展点。
接口型扩展点:
<extensionPoints>
<extensionPoint name="exampleProvider" interface="com.example.ExampleProvider"/>
</extensionPoints>
Bean 型扩展点:
<extensionPoints>
<extensionPoint name="exampleRule" beanClass="com.example.ExampleRuleBean"/>
</extensionPoints>
其他插件使用时:
<depends>com.example.myPlugin</depends>
<extensions defaultExtensionNs="com.example.myPlugin">
<exampleProvider implementation="com.other.OtherProvider"/>
</extensions>
读取扩展:
private val EP_NAME =
ExtensionPointName.create<ExampleProvider>("com.example.myPlugin.exampleProvider")
fun providers(): List<ExampleProvider> = EP_NAME.extensionList
如果扩展点涉及 Dumb Mode,基类应标记 PossiblyDumbAware,读取时用 DumbService.getDumbAwareExtensions() 过滤可在 Dumb Mode 使用的实现。
Listeners¶
监听器通过消息总线订阅事件。官方建议优先使用声明式注册,因为实例会延迟到第一次事件发送时创建,减少启动和项目打开开销。
应用级监听器:
<applicationListeners>
<listener
class="com.example.MyVfsListener"
topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
</applicationListeners>
项目级监听器:
<projectListeners>
<listener
class="com.example.MyToolWindowListener"
topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener"/>
</projectListeners>
监听器要求:
- 实现类应无状态。
- 不要实现生命周期接口,例如
Disposable。 - 项目级监听器可以声明接收
Project的单参数构造函数。 - 可用
os、activeInTestMode、activeInHeadlessMode限制激活条件。
需要手动订阅时,务必把连接挂到合适的 Disposable:
project.messageBus.connect(disposable).subscribe(
VirtualFileManager.VFS_CHANGES,
object : BulkFileListener {
override fun after(events: List<VFileEvent>) {
// handle events
}
}
)
Components 已废弃¶
旧插件中的 application/project/module components 不适合新项目:
- 会影响动态插件能力。
- 生命周期和初始化时机不如 Service、Extension、Listener 清晰。
- 新插件应使用 Service 管理状态,Extension 接入功能,Listener 订阅事件。
迁移原则:
| 旧用途 | 新方案 |
|---|---|
| 保存状态 | Service + PersistentStateComponent |
| 运行时逻辑 | Application/Project Service |
| 订阅事件 | 声明式 Listener |
| 扩展平台行为 | Extension |
| 项目打开后执行 | ProjectActivity |
动态插件¶
动态插件指安装、更新、卸载插件时不需要重启 IDE。现代插件应尽量支持动态加载和卸载。
官方限制包括:
- 不使用 Components。
- 所有
<group>都有唯一id。 - 只使用支持动态加载的扩展点。
- 自定义扩展点如满足动态规则,应声明
dynamic="true"。 - 不使用
overrides="true"的 Service。 - Configurable 若依赖动态扩展点,应实现
Configurable.WithEpDependencies。
自定义动态扩展点:
<extensionPoints>
<extensionPoint
name="exampleRule"
beanClass="com.example.ExampleRuleBean"
dynamic="true"/>
</extensionPoints>
动态卸载安全规则:
- 不在长期对象里保存裸
PsiElement,用SmartPsiElementPointer。 - 不用
FileType或Language对象作 Map key,改用getName()或getID()。 - Service 需要清理资源时实现
Disposable。 - 插件卸载相关日志在
com.intellij.ide.plugins.DynamicPlugins分类下。 - 本地运行 Plugin DevKit 的 dynamic plugin verification inspection。