easy-plugin 插件化扩展点使用文档
本模块基于 PF4J(Plugin Framework for Java)进行 Fat-Jar 场景二次封装,提供扩展点的注册、发现和执行能力。本文档描述如何正确定义 ExtensionPoint 接口、标记 Extension 实现、声明 Nodes 场景标识,以及通过 ExecutionUtil + Pattern 进行节点路径匹配查找和执行。
一、核心概念
1.1 扩展点(ExtensionPoint)
扩展点是一个接口,继承 org.pf4j.ExtensionPoint,定义了可被扩展的业务能力契约。
public interface Connector extends ExtensionPoint {
void init(GatewayService service);
void open();
void close();
// ...
}
1.2 扩展(Extension)
扩展是扩展点的具体实现,通过 @Extension 注解标记,并指定 nodes(业务场景标识)和 ordinal(优先级,越小越优先)。
@Extension(nodes = {"GATEWAY", "CONNECTOR", "REST"}, ordinal = 10)
public class RestConnector extends BaseConnector implements Connector {
// ...
}
1.3 节点(Nodes)
Nodes 是扩展点的场景标识数组,用于精确匹配扩展。按层级组织:
[业务域, 用例, 场景, 扩展1, 扩展2, ...]
示例:
["GATEWAY", "CONNECTOR", "REST"]— 网关模块的 REST 连接器["GATEWAY", "CONNECTOR", "MQTT"]— 网关模块的 MQTT 连接器["GATEWAY", "EVENT", "STORAGE", "MEMORY"]— 网关的内存事件存储
二、扩展点定义规范
2.1 定义扩展点接口
import org.pf4j.ExtensionPoint;
public interface MyExtension extends ExtensionPoint {
// 定义扩展方法
Result doSomething(Params params);
}
2.2 定义场景枚举(推荐)
建议定义一个枚举类,集中管理所有扩展点的 nodes,避免硬编码:
@Getter
public enum GatewayExtPts {
CONNECTOR("GATEWAY", "CONNECTOR"),
EVENT_STORAGE("GATEWAY", "EVENT", "STORAGE"),
;
private final String biz;
private final String usecase;
private final String scene;
// ... 其他字段
public String[] nodes() {
// 返回 nodes 数组
}
public String[] nodes(String node1) {
// 在基础 nodes 上追加场景节点
}
}
使用方式:
// 获取所有类型的连接器
ExecutionUtil.execute(Connector.class, connector -> {
// ...
}, GatewayExtPts.CONNECTOR.nodes());
// 获取特定类型的连接器
ExecutionUtil.executeFirst(EventStorage.class, storage -> {
// ...
}, GatewayExtPts.EVENT_STORAGE.nodes("MEMORY"));
三、扩展实现规范
3.1 实现扩展点
import org.pf4j.Extension;
@Slf4j
@Extension(nodes = {"GATEWAY", "CONNECTOR", "REST"}, ordinal = 10)
public class RestConnector implements Connector {
// 必须显式 implements Connector(扩展点接口)
// 即使父类已实现,也要显式声明!
}
3.2 ⚠️ 关键注意事项
- 必须显式实现扩展点接口
即使父类已经实现了 ExtensionPoint 接口,子类也必须显式声明 implements 该接口。否则 PF4J 无法识别:
// ✅ 正确
public class RestConnector extends BaseConnector implements Connector { ... }
// ❌ 错误(PF4J 找不到这个扩展)
public class RestConnector extends BaseConnector { ... }
原因:
PluggableUtil.getExtensionPointClass()在查找扩展点时,会检查类的直接接口和父类。如果只通过继承获得接口,而没有在类上显式声明,可能导致扩展点注册失败。
- 必须有无参构造函数
PF4J 通过反射实例化扩展,要求必须提供无参构造函数:
public class RestConnector implements Connector {
public RestConnector() {
// 无参构造函数(必需)
this.type = "rest";
}
}
- nodes 必须与查询匹配
@Extension(nodes = {...}) 中的节点数组,决定了该扩展能否被 ExecutionUtil 的 nodes 参数匹配到。详细匹配规则见下文。
四、插件配置
4.1 plugin.properties
每个插件(Plugin)必须在 src/main/resources/ 下提供 plugin.properties:
plugin.id=gateway-plugin
plugin.class=io.matrix.gateway.GatewayPlugin
plugin.version=0.0.1
plugin.provider=iot-care
plugin.dependencies=
4.2 插件类
import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;
public class GatewayPlugin extends Plugin {
public GatewayPlugin(PluginWrapper wrapper) {
super(wrapper);
}
}
4.3 Spring Boot 集成
通过 SpringPf4jConfiguration 自动配置类会自动集成 PF4J 与 Spring 的整合。确保:
@EventListener(ContextRefreshedEvent.class)
public void onContextRefreshed() {
// 延迟启动,确保 PF4J 插件加载完成后再使用扩展点
}
五、ExecutionUtil API
5.1 批量执行(execute)
执行所有匹配的扩展,返回执行的扩展数量:
// 无返回值的批量执行
int count = ExecutionUtil.execute(Connector.class, connector -> {
connector.init(gatewayService, config);
connector.open();
}, GatewayExtPts.CONNECTOR.nodes());
// 有返回值的批量执行(自动过滤 null)
List<Result> results = ExecutionUtil.execute(MyExtension.class, ext -> {
return ext.doSomething(params);
}, nodes);
5.2 单个执行(executeFirst)
只执行第一个匹配的扩展(按 ordinal 排序,越小越优先):
// 无返回值
boolean executed = ExecutionUtil.executeFirst(EventStorage.class, storage -> {
eventStorage = storage;
}, GatewayExtPts.EVENT_STORAGE.nodes(type));
// 有返回值
Optional<Result> result = ExecutionUtil.executeFirst(MyExtension.class, ext -> {
return ext.doSomething(params);
}, nodes);
5.3 方法签名总览
| 方法 | 返回值 | 说明 |
|---|---|---|
| `execute(clazz, Consumer, nodes) | int | 批量执行,返回执行数量 |
| `execute(clazz, Function, nodes) | List<R> | 批量执行,收集非空返回值 |
| `execute(clazz, Pattern, Consumer, nodes) | int | 指定匹配模式的批量执行 |
| `execute(clazz, Pattern, Function, nodes) | List<R> | 指定匹配模式的批量执行 |
| `executeFirst(clazz, Consumer, nodes) | boolean | 执行第一个匹配扩展 |
| `executeFirst(clazz, Function, nodes) | Optional<R> | 执行第一个并返回结果 |
| `executeFirst(clazz, Pattern, Consumer, nodes) | boolean | 指定匹配模式 |
| `executeFirst(clazz, Pattern, Function, nodes) | Optional<R> | 指定匹配模式 |
六、节点匹配规则(Pattern)
6.1 Pattern 枚举
| 枚举值 | 说明 |
|---|---|
DEFAULT / ACCURATE | 精确匹配,长度必须相等,对应位置节点必须匹配 |
CHILDREN | 子节点匹配,扩展 nodes 比输入 nodes 多 1 个 |
DESCENDANTS | 后代节点匹配,扩展 nodes 比输入 nodes 多 N 个(默认) |
WILDCARD | 通配匹配,匹配所有 |
6.2 默认行为
execute()默认使用Pattern.DESCENDANTSexecuteFirst()默认使用Pattern.DEFAULT(精确匹配)
6.3 匹配示例
假设扩展的 nodes 为 ["GATEWAY", "CONNECTOR", "REST"]
| 调用时 nodes | Pattern | 是否匹配 |
|---|---|---|
["GATEWAY", "CONNECTOR"] | DESCENDANTS | ✅ |
["GATEWAY", "CONNECTOR", "REST"] | ACCURATE | ✅ |
["GATEWAY", "CONNECTOR", "REST"] | CHILDREN | ❌(长度相同 |
["GATEWAY"] | DESCENDANTS | ✅ |
["GATEWAY", "CONNECTOR", "MQTT"] | DESCENDANTS | ❌(最后一个节点不匹配 |
七、最佳实践
7.1 推荐项目结构
src/main/java/com/example/
├── extension/
│ ├── MyExtension.java # 扩展点接口(extends ExtensionPoint)
│ ├── MyExtPts.java # 场景枚举(定义 nodes)
│ └── impl/
│ ├── MyExtensionImplA.java # 扩展实现 A
│ └── MyExtensionImplB.java # 扩展实现 B
└── MyPlugin.java # 插件入口
7.2 定义扩展点枚举
集中定义,避免魔法字符串:
@Getter
public enum MyExtPts {
MY_EXTENSION("MY_APP", "MY_USECASE"),
;
private final String biz;
private final String usecase;
MyExtPts(String biz, String usecase) { this(biz, usecase, null); }
MyExtPts(String biz, String usecase, String scene) { ... }
public String[] nodes() { return new String[]{biz, usecase}; }
public String[] nodes(String scene) { return new String[]{biz, usecase, scene}; }
}
7.3 正确的执行顺序
- **启动顺序:Spring 上下文刷新 → PF4J 插件加载 → 扩展点可用
- **使用顺序:先获取扩展 → 执行扩展方法
- **关闭顺序:先关闭插件 → 清理扩展缓存
7.4 调试技巧
- 检查扩展是否被正确注册
启动时查看日志,应该看到:
Plugin 'gateway-plugin@0.0.1' resolved
Start plugin 'gateway-plugin@0.0.1'
如果扩展没被找到,检查:
- 类是否显式
implements了扩展点接口 @Extension注解的 nodes 是否正确plugin.properties是否配置正确META-INF/extensions.idx是否包含该扩展类(编译时自动生成
- 检查匹配失败
如果 ExecutionUtil.execute 没有找到预期扩展:
- 确认调用时的 nodes 数组与扩展的 nodes 数组能否按指定 Pattern 匹配
- 使用
Pattern.WILDCARD测试是否能匹配到所有扩展 - 检查
ordinal值是否导致其他扩展排在前面
八、常见问题 FAQ
Q1: 为什么我的扩展没有被 PF4J 找到?
A: 检查以下几点:
- 类是否显式
implements了扩展点接口(不能只靠继承) - 是否有无参构造函数
@Extension注解是否正确添加plugin.properties是否在 classpath 中
Q2: execute 和 executeFirst 改怎么选?
A:
- 需要执行所有匹配扩展时用
execute(如加载所有连接器) - 只需要第一个匹配扩展时用
executeFirst(如获取指定类型的存储实现)
Q3: nodes 怎么设计?
A: 按业务层级设计 nodes 节点:业务节点 → 用例 → 场景 → 具体实现。 保持一致,不要混用不同层级的语义。
Q4: 多个扩展的执行顺序能控制吗?
A: 通过 @Extension(ordinal = 10) 控制。ordinal 越小优先级越高,排在前面。executeFirst 会取 ordinal 最小的那个。
Q5: Spring Bean 能注入到扩展中吗?
A: 可以。PF4J 与 Spring 集成后,扩展实例会被自动注册为 Spring Bean,可以使用 @Autowired 注入依赖。
九、参考代码
ExecutionUtil.java— 扩展执行工具PluggableUtil.java— 扩展点注册与查找Pattern.java— 节点匹配模式SpringPf4jConfiguration.java— Spring 自动配置