背景
当系统从单体走向模块化,业务需求频繁变更却不想每次重启发版,插件化架构就成了必然选择。
今天介绍一款成熟、轻量的插件化架构方案——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 |
它们之间的关系可以概括为:
“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 | |
STARTED | Plugin.start() |
STOPPED | Plugin.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 集成的核心,它做了三件关键的事:
使用 SpringExtensionFactory创建扩展实例,使扩展可以被注入 Spring Bean通过 ExtensionsInjector将所有扩展注册为 Spring 容器中的 Bean支持 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.class, args);
}
}
4.5 打包与运行
构建插件 JAR:
mvn clean package -pl notification-plugins/email-plugin,notification-plugins/sms-plugin
将插件 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/
启动宿主应用:
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
夜雨聆风