easy-plugin 插件化组件设计
本文档描述 easy-plugin 组件的设计与实现。该组件基于 PF4J (Plugin Framework for Java) 进行 Fat-Jar 场景的二次封装,解决 PF4J 原生假设”插件 = 外部独立 jar”而 Spring Boot Fat-Jar “所有插件打进单一可执行 jar”的矛盾。核心改造了 PF4J 的插件发现、加载、描述符解析三大环节,使 Fat-Jar 内部也能运行完整的 ExtensionPoint → Extension → ExecutionUtil 链路。
一、背景与问题
1.1 PF4J 原生的假设不适合 Fat-Jar
| PF4J 原生假设 | Fat-Jar 实际情况 | 矛盾 |
|---|---|---|
| 插件是外部独立 jar 文件 | 所有插件打进单一可执行 jar | 插件发现机制(扫描 jar 目录)失效 |
| 插件有独立 ClassLoader | 插件类被 Spring Boot 主 ClassLoader 加载 | 类加载机制失效 |
| 插件描述符从外部 jar 读取 | 描述符 (plugin.properties) 在 classpath 资源中 | 描述符加载路径失效 |
1.2 核心改造点
easy-plugin 重写了 PF4J PluginManager 的三大工厂方法:
| PF4J 工厂 | 原生实现 | Fat-Jar 重写 |
|---|---|---|
createPluginLoader() | 按插件路径加载独立 ClassLoader | FarJarPluginLoader → 从主 ClassLoader 取 |
createPluginRepository() | 扫描外部 jar 目录 | FatJarPluginRepository → 扫描 classpath 所有 plugin.properties |
createPluginDescriptorFinder() | 从外部 jar 文件读描述符 | FatJarPropertiesPluginDescriptorFinder → 从 classpath 资源读 |
1.3 ExecutionUtil 不是 PF4J 的一部分
PF4J 原生只负责插件生命周期和扩展注册,不提供「按节点路径 + Pattern 匹配查找扩展」的高阶函数。easy-plugin 在 PF4J 之上增加了:
Pattern枚举(ACCURATE / CHILDREN / DESCENDANTS / WILDCARD)PluggableUtil旁路缓存 + 节点路径过滤ExecutionUtil批量执行 / 首个匹配执行
二、组件定位与边界
定位:Spring Boot Fat-Jar 场景下的 PF4J 插件化增强框架。提供插件发现、扩展点注册、节点路径匹配、执行工具链。
边界:
| 做 | 不做 |
|---|---|
| PF4J Fat-Jar 三大工厂重写 | 不实现具体业务 ExtensionPoint |
| ExecutionUtil 高阶执行函数 | 不提供远程插件加载(只支持本地 Fat-Jar) |
| Pattern 节点路径匹配机制 | 不做插件热部署(Spring Boot Fat-Jar 本身不支持) |
| PluggableUtil 扩展旁路缓存 | 不替代 PF4J 原生 ExtensionFinder,只是旁路 |
| SpringPf4jConfiguration 自动配置 | 不自动扫描 Extension 实现(依赖 PF4J META-INF/extensions.idx) |
约束:
- PF4J 使用官方版本
pf4j-spring:0.8.0+ 传递的pf4j:3.10.0;原 ZTO 内部定制版的改动,以同 FQCN 源码 shadow(org/pf4j/...顶层包,非io/matrix/extension)形式内置在 easy-plugin 中,运行时 classloader 优先加载本组件类,从而不再依赖任何定制 jar(详见 五、包结构与核心类 的 shadow 覆盖层) PluggableUtil.extensionMap是静态全局缓存,插件 STARTED 时填充、STOPPED 时清除- 类名拼写不一致:
FarJarPluginLoader/FarJarPluginManager(Far)vsFatJarPath/FatJarPluginRepository(Fat)—— 历史遗留,不影响功能
三、总体架构
flowchart TD
subgraph "插件发现 (FatJarPluginRepository)"
A1["PathMatchingResourcePatternResolver<br/>扫描 classpath:plugin.properties"]
A2["为每个 properties 创建<br/>FatJarPath(delegate, descriptor, pluginClass)"]
A1 --> A2
end
subgraph "插件加载 (FarJarPluginLoader)"
B1["判断 path instanceof FatJarPath"]
B2["FatJarPath.getPluginClass().getClassLoader()<br/>直接用 Spring Boot 主 ClassLoader"]
B1 --> B2
end
subgraph "插件描述符解析 (FatJarPropertiesPluginDescriptorFinder)"
C1["FatJarPath 场景:直接返回<br/>已携带的 PluginDescriptor"]
C2["非 FatJarPath:从 pluginClass<br/>的 ClassLoader 读取 plugin.properties"]
end
subgraph "PF4J 插件生命周期"
D1["插件 STARTED"]
D2["FatJarPluginStateListener<br/>→ PluggableUtil.cache4Plugin()"]
D3["插件 STOPPED/DISABLED"]
D4["→ PluggableUtil.evict4Plugin()"]
D1 --> D2
D3 --> D4
end
subgraph "扩展缓存与路由 (PluggableUtil)"
E1["extensionMap<br/>ConcurrentMap<Class<?>, List<Wrapper>>"]
E2["getExtension() → 默认 Pattern.DEFAULT"]
E3["getExtensions() → 默认 Pattern.DESCENDANTS"]
E4["Wrapper 内含 pluginId + ExtensionWrapper + 按 ordinal 排序"]
end
subgraph "高阶执行工具 (ExecutionUtil)"
F1["execute(clazz, Consumer, nodes)<br/>遍历全部匹配扩展"]
F2["executeFirst(clazz, Function, nodes)<br/>取首个匹配扩展"]
end
subgraph "Pattern 匹配"
G1["DEFAULT / ACCURATE<br/>精准匹配:长度相同 + 每个元素相等"]
G2["CHILDREN<br/>调用方 nodes+1 == 扩展 nodes"]
G3["DESCENDANTS<br/>调用方 nodes 前缀匹配扩展 nodes"]
G4["WILDCARD<br/>永远返回 true"]
end
A2 --> B2
A2 --> C1
C2 --> B2
D2 --> E1
D4 --> E1
F1 --> E3
F2 --> E2
E2 --> G1 & G2 & G3 & G4
E3 --> G1 & G2 & G3 & G4
PluggableUtil 缓存结构:
flowchart TD
subgraph "extensionMap"
K1["key = ExtensionPoint 接口<br/>(如 Connector.class)"]
K2["key = ExtensionPoint 父类<br/>(如果是 abstract class)"]
end
subgraph "Wrapper 列表(按 ordinal 升序)"
W1["Wrapper#1<br/>pluginId + ExtensionWrapper"]
W2["Wrapper#2<br/>pluginId + ExtensionWrapper"]
W3["..."]
end
K1 --> W1
K1 --> W2
K1 --> W3
K2 --> W1
K2 --> W2
缓存触发时机:插件 STARTED 时从 FarJarPluginManager.getExtensionWrappers(pluginId) 拉取,按 ExtensionPoint 的所有父类和实现接口(递归)归类。所以同一个 Extension 可能出现在多个 key 下(如果它实现了多个 ExtensionPoint 接口)。
四、核心 API 草图
ExecutionUtil
| 方法 | 返回值 | 说明 |
|---|---|---|
execute(clazz, Consumer<T>, String... nodes) | int | 遍历所有匹配扩展,消费执行,返回执行次数 |
execute(clazz, Function<T,R>, String... nodes) | List<R> | 遍历匹配扩展,收集非空返回值 |
executeFirst(clazz, Consumer<T>, String... nodes) | boolean | 取首个匹配扩展执行 |
executeFirst(clazz, Function<T,R>, String... nodes) | Optional<R> | 取首个匹配扩展执行并返回结果 |
所有方法都有带 Pattern 参数的重载版本(显式指定匹配策略)。
Pattern 枚举
public enum Pattern {
DEFAULT, // 同 ACCURATE
ACCURATE, // 精准匹配:调用方与扩展 nodes 长度相同、每个元素相等(支持 "*" 通配符)
CHILDREN, // 扩展 nodes 长度 == 调用方 + 1,前缀相同
DESCENDANTS, // 扩展 nodes 长度 > 调用方,前缀相同(默认策略)
WILDCARD; // 永远返回 true
}
PluggableUtil
// 静态方法
Optional<T> getExtension(Class<T> clazz, Pattern pattern, String... nodes);
List<T> getExtensions(Class<T> clazz, Pattern pattern, String... nodes);
// 生命周期回调
void cache4Plugin(PluginWrapper wrapper); // STARTED 时
void evict4Plugin(PluginWrapper wrapper); // STOPPED/DISABLED 时
FatJarPluginManager
// 只暴露一个额外方法
List<ExtensionWrapper> getExtensionWrappers(String pluginId);
// 底层调用 extensionFinder.find(pluginId),供 PluggableUtil 缓存使用
五、包结构与核心类
easy-plugin/
└── io/matrix/extension/
├── ExecutionUtil.java # 高阶执行工具(抽象静态类)
├── Pattern.java # 节点路径匹配枚举
├── PluggableUtil.java # 扩展缓存 + 生命周期桥接
├── autoconfigure/
│ └── SpringPf4jConfiguration.java # Spring Boot 自动配置(注册 FarJarPluginManager)
└── pf4j/ # PF4J Fat-Jar 重写层
├── FarJarPluginLoader.java # 插件类加载(Far=Fat 拼写)
├── FarJarPluginManager.java # PF4J PluginManager 三大工厂重写
├── FatJarPath.java # 路径装饰器(携带 PluginDescriptor + pluginClass)
├── FatJarPluginRepository.java # 插件发现(classpath 扫描 plugin.properties)
├── FatJarPluginStateListener.java # 插件生命周期 → PluggableUtil 缓存桥接
└── FatJarPropertiesPluginDescriptorFinder.java # 插件描述符解析
| 类 | 职责 | 文件 |
|---|---|---|
| ExecutionUtil | 批量执行 / 首个匹配执行高阶函数 | ExecutionUtil.java |
| Pattern | 节点路径匹配策略枚举(5 值 + 通配符 *) | Pattern.java |
| PluggableUtil | 扩展旁路缓存 + 生命周期回调;维护 extensionMap 静态全局缓存 | PluggableUtil.java |
| SpringPf4jConfiguration | 自动配置入口:注册 FarJarPluginManager + 挂 FatJarPluginStateListener | SpringPf4jConfiguration.java |
| FarJarPluginManager | PF4J SpringPluginManager 子类;重写三大工厂(Loader/Repository/DescriptorFinder) | FarJarPluginManager.java |
| FatJarPluginRepository | 插件发现:扫描 classpath 所有 plugin.properties → 生成 FatJarPath 列表 | FatJarPluginRepository.java |
| FatJarPluginLoader | 插件类加载:FatJarPath 场景下直接用 pluginClass.getClassLoader() | FarJarPluginLoader.java |
| FatJarPropertiesPluginDescriptorFinder | 插件描述符解析:FatJarPath 场景下直接返回已携带的 PluginDescriptor | FatJarPropertiesPluginDescriptorFinder.java |
| FatJarPluginStateListener | 插件状态变化回调:STARTED → cache4Plugin,STOPPED → evict4Plugin | FatJarPluginStateListener.java |
| FatJarPath | PF4J Path 装饰器:包装 delegate + PluginDescriptor + pluginClass | FatJarPath.java |
Shadow 覆盖层(org/pf4j,非 io/matrix 包)
以下类与官方 PF4J 同包同名(FQCN 相同),源码直接放于 easy-plugin/src/main/java/org/pf4j/...。因 JVM 双亲委派 + 类路径顺序「先加载者胜出」,运行时 easy-plugin 的类优先于官方 jar 中的同名类,从而把原 ZTO 定制版的逻辑内置,彻底摆脱定制 jar:
| 类 | 相对官方改动 |
|---|---|
| Extension.java | 新增 nodes() / supplements() 注解属性,支撑场景节点路由 |
| ExtensionDescriptor.java | 新增 nodes 字段 + (int, Class, String[]) 三参构造;是官方 3.8.0(public 字段 ordinal/extensionClass)的超集,故二进制兼容 |
| AbstractExtensionFinder.java | createExtensionWrappers 依据 supplements() 为补充节点生成多个 ExtensionWrapper |
| SpringPluginManager.java | @PostConstruct init() → InitializingBean.afterPropertiesSet(),避免依赖 javax.annotation |
| FileUtils.java | isZipFile 不再校验 Files.isRegularFile |
shadow 铁律:只能做加法补丁(新增字段/方法/注解),不得覆盖或删改官方已有成员——否则官方其它未 shadow 的类按编译期契约调用会触发
NoSuchMethodError/NoSuchFieldError。官方ExtensionDescriptor恰好用 public 字段而非 getter,是本方案能干净扩展的关键前提。 现状:pf4j-spring / pf4j 均已回退官方(0.8.0/3.8.0),「pf4j-spring 版本非官方」的历史问题已移除。
依赖:
| 依赖 | scope | 版本 | 说明 |
|---|---|---|---|
pf4j-spring | compile | 0.8.0 | PF4J 官方 Spring 集成 |
pf4j(pf4j-spring 传递) | compile | 3.8.0 | PF4J 官方核心;nodes/supplements 逻辑由源码 shadow 提供 |
spring-boot-autoconfigure | provided | 父 POM | 自动配置注解 |
六、关键设计决策
| 决策 | 内容 | 理由 |
|---|---|---|
| 插件发现用 classpath 扫描 | FatJarPluginRepository 扫描 classpath*:plugin.properties | Fat-Jar 无外部 jar,所有插件类/资源都在主 jar 内 |
| 类加载用主 ClassLoader | FarJarPluginLoader 不创建独立 ClassLoader | Spring Boot Fat-Jar 机制下所有类已被主 ClassLoader 加载 |
| PluggableUtil 旁路缓存 | 不走 PF4J 原生 ExtensionFinder,插件 STARTED 时主动拉取 | 原生 ExtensionFinder 没有 ordinal 排序和节点路径过滤 |
| extensionMap 静态全局 | ConcurrentMap<Class<?>, List<Wrapper>>,按 ExtensionPoint 父类/接口递归归类 | 同一个 Extension 实现多个 ExtensionPoint 时可被多处查到 |
| 缓存生命周期绑定插件状态 | FatJarPluginStateListener 回调 cache4Plugin / evict4Plugin | 插件禁用时扩展自动从缓存清除 |
| Pattern 默认策略差异 | getExtensions 默认 DESCENDANTS(向上找所有祖先扩展),getExtension 默认 DEFAULT | 批量执行通常要全链路,首个匹配要精准 |
nodes 用通配符 * | 扩展声明的 nodes 中出现 * 时该位置任意匹配 | 方便声明通用扩展 |
七、已知问题与待改进
| 问题 | 严重度 | 说明 |
|---|---|---|
| Far/Fat 拼写不一致 | 低 | FarJarPluginLoader/FarJarPluginManager vs FatJarPath/FatJarPluginRepository,历史遗留 |
| shadow 依赖 classpath / fat-jar 排序 | 中 | org/pf4j shadow 类靠类路径顺序覆盖官方 jar;fat-jar 需保证 easy-plugin.jar 在 BOOT-INF/lib 中排在 pf4j 之前(一般 direct dep 在前,建议实包冒烟验证) |
| Extension 实现必须显式 implements 接口 | 高 | PF4J 反射检查直接接口,只靠父类继承会找不到扩展——USAGE.md 已警告 |
| 必须有无参构造函数 | 高 | PF4J 反射实例化,无参构造是硬约束 |
| 没有插件热部署 | 低 | Fat-Jar 模式本身不支持热替换,需要重启应用 |
| META-INF/extensions.idx 自动生成 | 低 | PF4J 编译时注解处理器生成;如果没有则扩展不被发现 |