乐于分享
好东西不私藏

PF4J:轻量级Java 插件化架构实现方案

PF4J:轻量级Java 插件化架构实现方案

背景

当系统从单体走向模块化,业务需求频繁变更却不想每次重启发版,插件化架构就成了必然选择。

今天介绍一款成熟、轻量的插件化架构方案——PF4J。


一、PF4J是啥

在传统的单体应用中,所有功能模块都打包在同一个 JAR/WAR 中,模块之间通过直接依赖耦合。这种架构在面对以下场景时会显得力不从心:

  • 多租户定制化:不同客户需要不同的业务逻辑,但又不想维护多套代码分支
  • 运行时热插拔:新增或下线某个功能模块,不希望重启整个应用
  • 第三方扩展:希望外部开发者能在不修改核心代码的前提下扩展系统能力

PF4J(Plugin Framework for Java) 应运而生,它提供了插件化的核心能力:

  • 纯 Java 配置,无 XML
  • 支持运行时动态加载、卸载、启停插件
  • 每个插件独立 ClassLoader,天然隔离
  • 与 Spring 生态无缝集成

二、PF4J 核心功能与组件

2.1 五大核心组件

组件
角色
说明
ExtensionPoint
扩展点
一个标记接口,定义宿主与插件之间的契约。任何继承 ExtensionPoint 的接口或抽象类都成为扩展点
Extension
扩展实现
用 @Extension 注解标记的类,是某个扩展点的具体实现
Plugin
插件本体
插件的基类,提供生命周期钩子(start()/stop()/delete()),是扩展点和扩展的容器
PluginManager
插件管理器
负责插件的加载、解析、启停、卸载等全生命周期管理
PluginLoader
插件加载器
负责从 JAR/ZIP/目录等来源加载插件的类和资源

它们之间的关系可以概括为:

Plugin 是容器,ExtensionPoint 是契约,Extension 是实现,PluginManager 是调度者。

┌─────────────────────────────────────────────┐
│              PluginManager                    │
│  (加载/启停/卸载/扩展检索)                     │
└──────────────┬──────────────────────────────┘
               │ 管理多个
    ┌──────────▼──────────┐
    │       Plugin         │
    │  (生命周期容器)       │
    │  ┌─────────────────┐ │
    │  │ ExtensionPoint  │ │  ← 宿主定义的契约接口
    │  │   (接口定义)     │ │
    │  └────────┬────────┘ │
    │           │ 实现       │
    │  ┌────────▼────────┐ │
    │  │   @Extension    │ │  ← 插件提供的具体实现
    │  │   (实现类)       │ │
    │  └─────────────────┘ │
    └──────────────────────┘

2.2 关键特性

1. 编译期注解处理

PF4J 使用 Java Annotation Processing 在编译时扫描所有 @Extension 注解的类,生成扩展点索引文件(META-INF/extensions.idx)。运行时无需扫描全量类,直接读取索引即可,性能远优于运行时反射扫描。

2. 插件依赖管理

插件可以在 MANIFEST.MF 中声明对其他插件的依赖,PF4J 会自动处理依赖顺序和类加载委托。

3. 版本约束

通过 Plugin-Descriptor 的 requires 属性,可以指定插件所需的最低系统版本,避免不兼容的插件被加载。

4. 开发模式

设置 -Dpf4j.mode=development 后,PF4J 会直接从 classpath 加载插件,便于开发调试,无需每次打包。


三、PF4J 工作原理深度解析

3.1 类加载机制:Parent Last 策略

类加载是插件框架的核心难题。PF4J 为每个插件创建一个独立的 PluginClassLoader,默认采用 Parent Last(子优先) 策略,这与 JVM 默认的 Parent First(双亲委派)正好相反。

默认加载顺序(PDA:Plugin → Dependencies → Application):

1. 如果是系统类(java. 开头)→ 委托系统类加载器
2. 如果是 PF4J 引擎类(org.pf4j 开头)→ 委托父类加载器
3. 尝试用当前 PluginClassLoader 从插件 JAR 中加载
4. 当前加载器找不到 → 委托给所依赖插件的 PluginClassLoader
5. 仍然找不到 → 委托给父类加载器(应用 ClassLoader)

为什么要用 Parent Last?

  • 插件可以携带自己版本的依赖库,与宿主或其他插件的同库不同版本共存
  • 避免插件类被宿主提前加载导致的 ClassCastException
  • 真正实现插件间的类隔离

如果需要切换为 Parent First(APD)策略,可以自定义 PluginManager:

new DefaultPluginManager() {
@Override
protected PluginClassLoader createPluginClassLoader(
            Path pluginPath, PluginDescriptor pluginDescriptor)
{
returnnew PluginClassLoader(
this, pluginDescriptor,
            getClass().getClassLoader(),
            ClassLoadingStrategy.APD  // Application → Plugin → Dependencies
        );
    }
};

3.2 扩展点检索机制

PF4J 的扩展检索分为两步:

第一步:编译期索引生成

pf4j 的注解处理器 ExtensionAnnotationProcessor 在编译时扫描所有 @Extension 类,将其类名写入 META-INF/extensions.idx

# extensions.idx 内容示例
org.pf4j.demo.welcome.WelcomePlugin$WelcomeGreeting
org.pf4j.demo.hello.HelloPlugin$HelloGreeting

第二步:运行时扩展发现

DefaultExtensionFinder 在插件加载后读取该索引文件,解析每个扩展类实现的扩展点接口,建立 ExtensionPoint → List<Extension> 的映射关系。调用 pluginManager.getExtensions(Greeting.class) 时直接从映射中返回实例。

扩展实例的创建由 ExtensionFactory 负责,默认是 DefaultExtensionFactory(每次 new 新实例)。在 Spring 集成场景下则使用 SpringExtensionFactory,由 Spring 容器管理扩展实例的生命周期和依赖注入。

3.3 插件生命周期

每个插件在运行中会经历明确的状态流转,PluginState 枚举定义了所有状态:

CREATED → RESOLVED → LOADED → STARTED → STOPPED
              ↑          │
              └── DISABLED(可从 RESOLVED 进入,不自动 START)
状态
含义
CREATED
插件描述符已创建,尚未解析依赖
RESOLVED
依赖已解析,类加载器已创建
LOADED
插件类已加载,Plugin 实例已创建
STARTEDPlugin.start()
 已执行,扩展可被检索和使用
STOPPEDPlugin.stop()
 已执行,扩展不再可用
DISABLED
被禁用的插件不会被自动启动,但类加载器仍然有效

关键规则:只有处于 STARTED 状态的插件才能贡献扩展实例。DISABLED 插件的类仍可被手动加载探索,但不会出现在扩展检索结果中。

通过 PluginStateListener 可以监听插件状态变化,实现插件热插拔后的联动逻辑(如刷新路由、重建缓存)。

3.4 插件描述符与打包

插件的元信息通过 JAR 的 MANIFEST.MF 声明:

Plugin-Id: greeting-plugin
Plugin-Version: 1.0.0
Plugin-Provider: your-name
Plugin-Class: org.example.greeting.GreetingPlugin
Plugin-Dependencies: other-plugin;version=1.0.0
Plugin-Requires: 2.0.0

PF4J 提供了 Maven/Gradle 插件来自动生成这些清单,无需手写。


四、从零搭建插件化系统

下面通过一个示例,演示如何在 SpringBoot3 中集成 PF4J,实现一个可动态插拔的「消息通知」插件系统。

4.1 项目结构

pf4j-demo/
├── pom.xml                    # 父 POM
├── notification-api/          # 扩展点定义模块(宿主和插件共同依赖)
│   └── src/main/java/.../NotificationChannel.java
├── notification-plugins/      # 插件实现模块
│   ├── email-plugin/          # 邮件通知插件
│   └── sms-plugin/            # 短信通知插件
└── notification-app/          # Spring Boot 3 宿主应用
    └── src/main/java/.../
        ├── NotificationApp.java
        ├── config/Pf4jConfig.java
        └── controller/NotificationController.java

4.2 定义扩展点(API 模块)

扩展点是宿主和插件之间的契约,必须放在独立的模块中,宿主和插件都依赖它。

<!-- notification-api/pom.xml -->
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.10.0</version>
</dependency>
package com.example.notification.api;

import org.pf4j.ExtensionPoint;

/**
 * 通知渠道扩展点
 * 所有通知插件都必须实现此接口
 */

publicinterfaceNotificationChannelextendsExtensionPoint{

/**
     * 渠道名称,用于标识和展示
     */

String getName();

/**
     * 发送通知
     * @param recipient 接收者
     * @param message 消息内容
     * @return 是否发送成功
     */

booleansend(String recipient, String message);
}

4.3 开发插件

每个插件是一个独立的 Maven 模块,打包为 JAR,包含插件类和扩展实现。

4.3.1 邮件通知插件

<!-- email-plugin/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>notification-api</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.10.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<!-- PF4J Maven 插件:自动生成 MANIFEST.MF 和 extensions.idx -->
<plugin>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-maven-plugin</artifactId>
<version>3.10.0</version>
<executions>
<execution>
<goals>
<goal>manifest</goal>
</goals>
</execution>
</executions>
<configuration>
<pluginId>email-notification</pluginId>
<pluginVersion>1.0.0</pluginVersion>
<pluginProvider>demo-team</pluginProvider>
<pluginClass>com.example.email.EmailNotificationPlugin</pluginClass>
</configuration>
</plugin>
</plugins>
</build>
package com.example.email;

import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;

/**
 * 邮件通知插件
 * 提供生命周期钩子,可以在 start/stop 中做资源初始化和清理
 */

publicclassEmailNotificationPluginextendsPlugin{

publicEmailNotificationPlugin(PluginWrapper wrapper){
super(wrapper);
    }

@Override
publicvoidstart(){
        System.out.println("[EmailPlugin] 启动,初始化 SMTP 连接池...");
// 实际场景中初始化 SMTP 客户端、连接池等
    }

@Override
publicvoidstop(){
        System.out.println("[EmailPlugin] 停止,释放 SMTP 连接池...");
// 清理资源
    }
}
package com.example.email;

import com.example.notification.api.NotificationChannel;
import org.pf4j.Extension;

/**
 * 邮件通知渠道实现
 * 必须用 @Extension 注解标记,PF4J 编译期会扫描并索引
 */

@Extension
publicclassEmailNotificationChannelimplementsNotificationChannel{

@Override
public String getName(){
return"email";
    }

@Override
publicbooleansend(String recipient, String message){
        System.out.printf("[Email] 向 %s 发送邮件: %s%n", recipient, message);
// 实际场景中调用 JavaMailSender 等发送邮件
returntrue;
    }
}

4.3.2 短信通知插件

创建短信插件:

package com.example.sms;

import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;

publicclassSmsNotificationPluginextendsPlugin{
publicSmsNotificationPlugin(PluginWrapper wrapper){
super(wrapper);
    }

@Override
publicvoidstart(){
        System.out.println("[SmsPlugin] 启动,初始化 SMS 客户端...");
    }

@Override
publicvoidstop(){
        System.out.println("[SmsPlugin] 停止,释放 SMS 客户端...");
    }
}
package com.example.sms;

import com.example.notification.api.NotificationChannel;
import org.pf4j.Extension;

@Extension
publicclassSmsNotificationChannelimplementsNotificationChannel{

@Override
public String getName(){
return"sms";
    }

@Override
publicbooleansend(String recipient, String message){
        System.out.printf("[SMS] 向 %s 发送短信: %s%n", recipient, message);
returntrue;
    }
}

4.4 宿主应用集成

4.4.1 引入依赖

<!-- notification-app/pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 扩展点 API -->
<dependency>
<groupId>com.example</groupId>
<artifactId>notification-api</artifactId>
<version>1.0.0</version>
</dependency>

<!-- PF4J 核心 -->
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.10.0</version>
</dependency>

<!-- PF4J Spring 集成 -->
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>0.10.0</version>
</dependency>
</dependencies>

4.4.2 PF4J 配置类

package com.example.app.config;

import org.pf4j.PluginManager;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import jakarta.annotation.PreDestroy;

@Configuration
publicclassPf4jConfig{

private SpringPluginManager pluginManager;

/**
     * 创建 SpringPluginManager
     * 它会自动使用 SpringExtensionFactory,让扩展实例由 Spring 容器管理
     * 默认从 ./plugins 目录加载插件 JAR
     */

@Bean
public SpringPluginManager pluginManager(){
// 可以通过构造函数指定插件目录
// new SpringPluginManager(Paths.get("my-plugins"));
        pluginManager = new SpringPluginManager();

// 加载并启动所有插件
        pluginManager.loadPlugins();
        pluginManager.startPlugins();

        System.out.println("已加载插件数量: " + pluginManager.getPlugins().size());
        System.out.println("已启动插件数量: " + pluginManager.getStartedPlugins().size());

return pluginManager;
    }

/**
     * 应用关闭时停止并卸载插件
     */

@PreDestroy
publicvoiddestroy(){
if (pluginManager != null) {
            pluginManager.stopPlugins();
            pluginManager.unloadPlugins();
        }
    }
}

SpringPluginManager 是 PF4J 与 Spring 集成的核心,它做了三件关键的事:

  1. 使用 SpringExtensionFactory 创建扩展实例,使扩展可以被注入 Spring Bean
  2. 通过 ExtensionsInjector 将所有扩展注册为 Spring 容器中的 Bean
  3. 支持 SpringPlugin,让每个插件拥有自己的 Spring ApplicationContext

4.4.3 服务层:使用扩展

package com.example.app.service;

import com.example.notification.api.NotificationChannel;
import org.pf4j.PluginManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
publicclassNotificationService{

privatefinal PluginManager pluginManager;

@Autowired
publicNotificationService(PluginManager pluginManager){
this.pluginManager = pluginManager;
    }

/**
     * 获取当前所有可用的通知渠道
     * 每次调用实时从 PluginManager 获取,反映插件热插拔后的最新状态
     */

public List<NotificationChannel> getAllChannels(){
return pluginManager.getExtensions(NotificationChannel.class);
    }

/**
     * 通过指定渠道发送通知
     */

publicbooleansendByChannel(String channelName, String recipient, String message){
        Map<String, NotificationChannel> channelMap = getAllChannels().stream()
                .collect(Collectors.toMap(
                        NotificationChannel::getName,
                        Function.identity(),
                        (a, b) -> a
                ));

        NotificationChannel channel = channelMap.get(channelName);
if (channel == null) {
thrownew IllegalArgumentException("未找到通知渠道: " + channelName
                    + ",可用渠道: " + channelMap.keySet());
        }

return channel.send(recipient, message);
    }

/**
     * 向所有已加载的渠道广播通知
     */

publicintbroadcast(String recipient, String message){
return (int) getAllChannels().stream()
                .filter(channel -> channel.send(recipient, message))
                .count();
    }
}

关键点pluginManager.getExtensions(NotificationChannel.class) 是实时查询,每次都会反映当前已启动插件的最新扩展列表。这意味着插件热插拔后,无需重启即可获取到新增或移除的渠道。

4.4.4 控制器:暴露 REST API

package com.example.app.controller;

import com.example.app.service.NotificationService;
import com.example.notification.api.NotificationChannel;
import org.pf4j.PluginManager;
import org.pf4j.PluginWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api")
publicclassNotificationController{

privatefinal NotificationService notificationService;
privatefinal PluginManager pluginManager;

@Autowired
publicNotificationController(NotificationService notificationService,
                                   PluginManager pluginManager)
{
this.notificationService = notificationService;
this.pluginManager = pluginManager;
    }

/** 查看所有可用通知渠道 */
@GetMapping("/channels")
public List<String> getChannels(){
return notificationService.getAllChannels().stream()
                .map(NotificationChannel::getName)
                .collect(Collectors.toList());
    }

/** 通过指定渠道发送通知 */
@PostMapping("/send/{channel}")
public Map<String, Object> send(@PathVariable String channel,
                                     @RequestParam String recipient,
                                     @RequestParam String message)
{
boolean success = notificationService.sendByChannel(channel, recipient, message);
return Map.of("channel", channel, "success", success);
    }

/** 广播通知 */
@PostMapping("/broadcast")
public Map<String, Object> broadcast(@RequestParam String recipient,
                                          @RequestParam String message)
{
int count = notificationService.broadcast(recipient, message);
return Map.of("sentCount", count);
    }

// ==================== 插件管理 API ====================

/** 查看所有插件及其状态 */
@GetMapping("/plugins")
public List<Map<String, String>> listPlugins() {
return pluginManager.getPlugins().stream()
                .map(wrapper -> Map.of(
"id", wrapper.getDescriptor().getPluginId(),
"version", wrapper.getDescriptor().getVersion(),
"state", wrapper.getPluginState().toString()
                ))
                .collect(Collectors.toList());
    }

/** 启动指定插件 */
@PostMapping("/plugins/{id}/start")
public String startPlugin(@PathVariable String id){
        pluginManager.startPlugin(id);
return"插件 " + id + " 已启动";
    }

/** 停止指定插件 */
@PostMapping("/plugins/{id}/stop")
public String stopPlugin(@PathVariable String id){
        pluginManager.stopPlugin(id);
return"插件 " + id + " 已停止";
    }

/** 重新加载所有插件(热插拔核心) */
@PostMapping("/plugins/reload")
public String reloadPlugins(){
        pluginManager.stopPlugins();
        pluginManager.unloadPlugins();
        pluginManager.loadPlugins();
        pluginManager.startPlugins();
return"插件已重新加载,当前已启动: "
                + pluginManager.getStartedPlugins().size() + " 个";
    }
}

4.4.5 启动类

package com.example.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
publicclassNotificationApp{
publicstaticvoidmain(String[] args){
        SpringApplication.run(NotificationApp.classargs);
    }
}

4.5 打包与运行

  1. 构建插件 JAR
mvn clean package -pl notification-plugins/email-plugin,notification-plugins/sms-plugin
  1. 将插件 JAR 放入宿主应用的 plugins 目录
mkdir -p notification-app/plugins
cp notification-plugins/email-plugin/target/email-plugin-1.0.0.jar notification-app/plugins/
cp notification-plugins/sms-plugin/target/sms-plugin-1.0.0.jar notification-app/plugins/
  1. 启动宿主应用
cd notification-app
mvn spring-boot:run

启动日志中会看到:

[EmailPlugin] 启动,初始化 SMTP 连接池...
[SmsPlugin] 启动,初始化 SMS 客户端...
已加载插件数量: 2
已启动插件数量: 2

4.6 运行效果验证

# 查看可用渠道
curl http://localhost:8080/api/channels
# 返回: ["email","sms"]

# 发送邮件
curl -X POST "http://localhost:8080/api/send/email?recipient=user@example.com&message=Hello"
# 返回: {"channel":"email","success":true}

# 广播
curl -X POST "http://localhost:8080/api/broadcast?recipient=user@example.com&message=Broadcast"
# 返回: {"sentCount":2}

# 查看插件状态
curl http://localhost:8080/api/plugins
# 返回: [{"id":"email-notification","version":"1.0.0","state":"STARTED"},...]

4.7 热插拔演示

不重启应用,动态新增一个「钉钉通知插件」:

# 1. 将新插件 JAR 放入 plugins 目录
cp dingtalk-plugin-1.0.0.jar notification-app/plugins/

# 2. 调用重载接口
curl -X POST http://localhost:8080/api/plugins/reload

# 3. 再次查看渠道,钉钉已出现
curl http://localhost:8080/api/channels
# 返回: ["email","sms","dingtalk"]

同样,删除某个插件 JAR 后调用 reload,对应渠道会自动消失。


五、插件中使用 Spring Bean

在上面的例子中,扩展实现是纯POJO。但实际场景中,插件内部往往需要使用 Spring 的依赖注入、配置管理等能力。pf4j-spring 提供了 SpringPlugin 来支持这一点。

5.1 让插件拥有自己的 Spring 上下文

package com.example.wechat;

import com.example.notification.api.NotificationChannel;
import org.pf4j.Extension;
import org.pf4j.PluginWrapper;
import org.pf4j.spring.SpringPlugin;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 微信通知插件 - 内部使用 Spring 管理 Bean
 */

publicclassWechatNotificationPluginextendsSpringPlugin{

publicWechatNotificationPlugin(PluginWrapper wrapper){
super(wrapper);
    }

@Override
publicvoidstart(){
        System.out.println("[WechatPlugin] 启动...");
    }

@Override
publicvoidstop(){
        System.out.println("[WechatPlugin] 停止...");
super.stop(); // 必须调用,关闭插件的 ApplicationContext
    }

/**
     * 创建插件独立的 Spring ApplicationContext
     * 注意设置 ClassLoader 为插件的 ClassLoader,否则无法加载插件内的类
     */

@Override
protected ApplicationContext createApplicationContext(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.setClassLoader(getWrapper().getPluginClassLoader());
        context.register(WechatPluginConfig.class);
        context.refresh();
return context;
    }

/**
     * 插件内部的 Spring 配置
     */

@Configuration
publicstaticclassWechatPluginConfig{
@Bean
public WechatClient wechatClient(){
returnnew WechatClient("appid""secret");
        }
    }

/**
     * 扩展实现 - 通过 @Autowired 注入插件内部的 Spring Bean
     */

@Extension
publicstaticclassWechatNotificationChannelimplementsNotificationChannel{

@Autowired
private WechatClient wechatClient;

@Override
public String getName(){
return"wechat";
        }

@Override
publicbooleansend(String recipient, String message){
return wechatClient.sendTemplateMessage(recipient, message);
        }
    }
}

5.2 原理说明

SpringPlugin 为每个插件创建一个独立的 ApplicationContext,其父上下文是宿主应用的 ApplicationContext。这意味着:

  • 插件可以注入自己内部定义的 Bean
  • 插件也可以访问宿主应用的 Bean(通过父上下文)
  • 插件停止时,其独立的 ApplicationContext 会被关闭,释放所有资源

SpringExtensionFactory 在创建扩展实例时,会从对应插件的 ApplicationContext 中获取或创建 Bean,从而支持 @Autowired 等注解。


六、插件间通信

PF4J 本身不提供插件间直接通信机制。推荐方案:

  • 通过扩展点:插件 A 定义扩展点,插件 B 实现它,宿主负责调度
  • 通过宿主中介:插件将事件发布到宿主的事件总线(如 Spring ApplicationEvent),其他插件订阅
  • 共享服务接口:在 API 模块中定义服务接口,宿主提供实现,插件通过扩展点上下文获取

参考资料

  • PF4J 官方文档:https://pf4j.org/
  • pf4j-spring GitHub:https://github.com/pf4j/pf4j-spring
  • PF4J Maven 坐标:org.pf4j:pf4j:3.10.0 / org.pf4j:pf4j-spring:0.10.0