一佳技术

PF4J 扩展点使用规范与最佳实践

本模块基于 PF4J(Plugin Framework for Java)进行二次封装,提供扩展点的注册、发现和执行能力。本文档描述如何正确定义和使用扩展点。


一、核心概念

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 ⚠️ 关键注意事项

  1. 必须显式实现扩展点接口

即使父类已经实现了 ExtensionPoint 接口,子类也必须显式声明 implements 该接口。否则 PF4J 无法识别:

// ✅ 正确
public class RestConnector extends BaseConnector implements Connector { ... }

// ❌ 错误(PF4J 找不到这个扩展)
public class RestConnector extends BaseConnector { ... }

原因:PluggableUtil.getExtensionPointClass() 在查找扩展点时,会检查类的直接接口和父类。如果只通过继承获得接口,而没有在类上显式声明,可能导致扩展点注册失败。

  1. 必须有无参构造函数

PF4J 通过反射实例化扩展,要求必须提供无参构造函数:

public class RestConnector implements Connector {
    public RestConnector() {
        // 无参构造函数(必需)
        this.type = "rest";
    }
}
  1. 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.DESCENDANTS
  • executeFirst() 默认使用 Pattern.DEFAULT(精确匹配)

6.3 匹配示例

假设扩展的 nodes 为 ["GATEWAY", "CONNECTOR", "REST"]

调用时 nodesPattern是否匹配
["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 正确的执行顺序

  1. **启动顺序:Spring 上下文刷新 → PF4J 插件加载 → 扩展点可用
  2. **使用顺序:先获取扩展 → 执行扩展方法
  3. **关闭顺序:先关闭插件 → 清理扩展缓存

7.4 调试技巧

  1. 检查扩展是否被正确注册

启动时查看日志,应该看到:

Plugin 'gateway-plugin@0.0.1' resolved
Start plugin 'gateway-plugin@0.0.1'

如果扩展没被找到,检查:

  • 类是否显式 implements 了扩展点接口
  • @Extension 注解的 nodes 是否正确
  • plugin.properties 是否配置正确
  • META-INF/extensions.idx 是否包含该扩展类(编译时自动生成
  1. 检查匹配失败

如果 ExecutionUtil.execute 没有找到预期扩展:

  • 确认调用时的 nodes 数组与扩展的 nodes 数组能否按指定 Pattern 匹配
  • 使用 Pattern.WILDCARD 测试是否能匹配到所有扩展
  • 检查 ordinal 值是否导致其他扩展排在前面

八、常见问题 FAQ

Q1: 为什么我的扩展没有被 PF4J 找到?

A: 检查以下几点:

  1. 类是否显式 implements 了扩展点接口(不能只靠继承)
  2. 是否有无参构造函数
  3. @Extension 注解是否正确添加
  4. 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 自动配置