在当今的软件开发领域,构建具有高度灵活性和可扩展性的应用程序是许多开发者追求的目标。尤其是在面对业务需求不断变化、功能持续迭代的情况下,如何让应用程序能够轻松地添加、修改或删除功能模块,同时保持系统的稳定性和可维护性,成为了一个关键问题。而 Spring Boot 与 PF4J 的结合,为我们提供了一个强大的解决方案。

一、PF4J:轻量级 Java 插件框架的强大力量

PF4J(Plugin Framework for Java)是一款备受瞩目的轻量级 Java 插件框架,它致力于简化应用程序插件的开发与管理流程,让开发者能够轻松创建模块化、可扩展的应用程序,实现核心功能与插件逻辑的完美分离。通过 PF4J,开发者可以将大型应用程序拆分成多个独立的模块或插件,每个模块都能独立开发、测试和部署,极大地提高了开发效率和代码的可维护性。

1、PF4J 的核心能力
  • 动态加载: 在应用程序运行时,无需重启即可动态加载插件,实现功能的实时扩展。
  • 插件隔离: 每个插件都在独立的类加载器中加载,有效避免类冲突问题,确保插件之间的独立性和稳定性。
  • 事件驱动: 插件能够响应应用程序中的各种事件,实现灵活的交互和功能扩展。
  • 生命周期管理: 插件具有明确的生命周期方法,如 start () 和 stop (),方便开发者对插件的运行状态进行精准控制。
  • 依赖管理: 插件之间可以定义依赖关系,确保插件按正确的顺序加载,避免因依赖问题导致的运行异常。
2、PF4J 的广泛应用场景
  • 模块化应用程序: 将大型应用程序分解为多个独立模块,提高开发效率和代码可维护性。
  • 可扩展的平台: 适用于需要持续扩展新功能或第三方集成的平台,如电子商务平台、内容管理系统(CMS)、集成开发环境(IDE)等。
  • 企业级应用: 满足企业级应用定制和扩展的需求,根据客户特定需求开发定制插件,无需修改核心应用程序。
  • 微服务架构: 用于构建和管理微服务中的插件,实现微服务的独立开发和扩展,提高灵活性和可扩展性。
  • 游戏开发: 方便添加新功能和内容,如开发新的关卡、角色、道具等模块和扩展。
  • 金融系统: 助力金融机构快速集成新的功能模块,以应对不断变化的市场需求和法规要求。
  • 物联网(IoT): 管理和集成不同的设备和传感器插件,实现系统的灵活配置和快速扩展。
  • 大数据和分析平台: 开发和管理各种数据处理和分析插件,实现平台的灵活扩展和功能增强。
3、PF4J 的核心组件

a、PluginManager

作用: PluginManager是PF4J的核心管理类,负责插件的加载、启动、停止和卸载。

主要方法:

  • loadPlugins(): 从指定路径加载所有插件。
  • startPlugins(): 启动所有已加载的插件。
  • stopPlugins(): 停止所有已加载的插件。
  • unloadPlugins(): 卸载所有已加载的插件。
  • loadPlugin(String pluginPath): 动态加载单个插件。
  • unloadPlugin(PluginWrapper pluginWrapper): 动态卸载单个插件。

b、Plugin

作用: Plugin是所有插件的基类,每个插件都需要继承这个类并实现其生命周期方法。

主要方法:

  • start(): 插件启动时调用的方法。
  • stop(): 插件停止时调用的方法。
  • getPluginId(): 返回插件的唯一标识符。
  • getPluginDescription(): 返回插件的描述信息。

c、PluginWrapper

作用: PluginWrapper封装了插件的信息和元数据,用于在PluginManager和Plugin之间传递信息。

主要属性:

  • pluginId: 插件的唯一标识符。
  • pluginClass: 插件的类对象。
  • pluginState: 插件的状态(如加载中、已加载、已启动、已停止等)。

d、Extension

作用: Extension是插件提供的扩展点,用于向主应用暴露功能或服务。

使用方式:

在插件类中使用@Extension注解标记扩展点。主应用可以通过PluginManager获取所有扩展点的实例。

e、 ExtensionPoint

作用: ExtensionPoint是扩展点的接口,定义了扩展点的方法。

使用方式:

主应用定义一个接口继承自ExtensionPoint。插件实现该接口并使用@Extension注解标记。

f、 PluginStateListener

作用: PluginStateListener用于监听插件的状态变化,如加载、启动、停止等。

主要方法:

pluginStateChanged(PluginWrapper pluginWrapper):插件状态变化时调用的方法。

g、 PluginDescriptor

作用: PluginDescriptor描述了插件的元数据信息,如插件名称、版本、作者等

使用方式:

插件类中可以提供一个plugin.xml文件,包含插件的元数据信息。
PluginManager会读取这些元数据信息并创建PluginDescriptor对象。

二、PF4J 快速入门指南

1、在项目的pom引入pf4j GAV
 <dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j</artifactId>
            <version>${pf4j.version}</version>
        </dependency>
2、在应用程序/插件中使用 ExtensionPoint 接口标记定义一个扩展点
public interface Greeting extends ExtensionPoint {

    String getGreeting();

}
3、使用 @Extension 注解创建一个扩展
    @Extension
    public  class WelcomeGreeting implements Greeting {

        public String getGreeting() {
            return "Welcome";
        }

    }
4、插件打包

插件打包时,需要往classes/META-INF/MANIFEST.MF写入插件信息。内容形如下

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: decebal
Build-Jdk: 1.6.0_17
Plugin-Class: com.github.lybgeek.welcome.WelcomePlugin
Plugin-Dependencies: x, y, z
Plugin-Id: welcome-plugin
Plugin-Provider: Decebal Suiu
Plugin-Version: 0.0.1

可以利用maven-jar-plugin插件生成如上信息

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.2.2</version>
  <configuration>
    <archive>
      <manifestEntries>
        <Plugin-Id>welcome-plugin</Plugin-Id>
        <Plugin-Version>0.0.1</Plugin-Version>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

注: MANIFEST.MF中Plugin-Id以及Plugin-Version是必填项。详见官方示例说明

5、加载并执行插件
public static void main(String[] args) {
    ...

    // 创建插件管理器
    PluginManager pluginManager = new JarPluginManager(); // 或 "new ZipPluginManager() / new DefaultPluginManager()"
    
    // 启动并加载应用程序的所有插件
    pluginManager.loadPlugins();
    pluginManager.startPlugins();

    // 检索 "Greeting" 扩展点的所有扩展
    List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
    for (Greeting greeting : greetings) {
        System.out.println(">>> " + greeting.getGreeting());
    }
    
    // 停止并卸载所有插件
    pluginManager.stopPlugins();
    pluginManager.unloadPlugins();
    
    ...
}

输出:

>>> Welcome

也可以加载并运行指定插件

public static void main(String[] args) {
        // 创建插件管理器
        PluginManager pluginManager = new DefaultPluginManager();
 
        // 加载指定路径插件
        pluginManager.loadPlugin(Paths.get("E:\springboot-pf4j\welcome-plugin-0.0.1-SNAPSHOT.jar"));
 
        // 启动指定插件
        pluginManager.startPlugin("welcome-plugin");
 
        // 执行插件
        List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
        for (Greeting greeting : greetings) {
            System.out.println(">>> " + greeting.getGreeting());
        }
 
        // 停止并卸载指定插件
        pluginManager.stopPlugin("welcome-plugin");
        pluginManager.unloadPlugin("welcome-plugin");
 
    }

输出:

>>> Welcome

6、创建 Plugin 类监听插件生命周期事件(可选)

public class WelcomePlugin extends Plugin {

    @Override
    public void start() {
        System.out.println("WelcomePlugin.start()");
    }

    @Override
    public void stop() {
        System.out.println("WelcomePlugin.stop()");
    }
    
    @Override
    public void delete() {
        System.out.println("WelcomePlugin.delete()");
    }
    
}

同时往MANIFEST.MF写入插件信息,可以通过maven-jar-plugin生成

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.2.2</version>
  <configuration>
    <archive>
      <manifestEntries>
        <Plugin-Id>welcome-plugin</Plugin-Id>
        <Plugin-Version>0.0.1</Plugin-Version>
        <!-- 新增 -->
        <Plugin-Class>com.github.lybgeek.welcome.WelcomePlugin</Plugin-Class>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

打包后运行输出

WelcomePlugin.start()
>>> Welcome
WelcomePlugin.stop()

以上示例摘自官网github,详情可以查看如下链接
https://github.com/pf4j/pf4j

三、Spring Boot 整合 PF4J,开启高效开发新旅程

1、项目中pom引入spring-pf4j gav
<dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j-spring</artifactId>
            <version>${pf4j-spring-version}</version>
</dependency>

注: 因为springboot项目默认引入logback作为log,如果遇到引入pf4j-spring,导致项目启动不起来,可以排除pf4j-spring自带的日志

 <dependencies>
        <dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j-spring</artifactId>
            <version>${pf4j-spring-version}</version>
            <exclusions>
                <exclusion>
                    <artifactId>log4j</artifactId>
                    <groupId>log4j</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>log4j-api</artifactId>
                    <groupId>org.apache.logging.log4j</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>log4j-to-slf4j</artifactId>
                    <groupId>org.apache.logging.log4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>

或者也可以把logback相关依赖,放在pf4j-spring之前(maven最短路径优先依赖)

2、创建相应插件

核心是创建出相应的 Spring 容器。

public class EchoServiceSpringPlugin extends SpringPlugin {
    public EchoServiceSpringPlugin(PluginWrapper wrapper) {
        super(wrapper);
    }

    @Override
    public void stop() {
        System.out.println("EchoServiceSpringPlugin stop...");
        super.stop();
    }

    @Override
    public void start() {
        System.out.println("EchoServiceSpringPlugin start...");
        super.start();
    }

    @Override
    public void delete() {
        System.out.println("EchoServiceSpringPlugin delete...");
        super.delete();
    }

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

    }
}
3、创建扩展点
@Extension
public class EchoSpringServiceImpl implements EchoService {

    @Autowired
    private EchoHelpler echoHelpler;

    private final ThreadLocalRandom random = ThreadLocalRandom.current();

    @Override
    @MethodCostTime
    public String echo(String msg) {
        System.out.println("this is a spring plugin test.....");
        try {
            TimeUnit.MILLISECONDS.sleep(random.nextLong(500));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "spring:" + echoHelpler.getEchoMsg(msg);
    }
}
4、创建SpringPluginManager自动装配
 @Bean
    @ConditionalOnMissingBean
    public SpringPluginManager springPluginManager(PluginProperties pluginProperties){
        if(StringUtils.hasText(pluginProperties.getDir())){
            return new SpringPluginManager(Paths.get(pluginProperties.getDir()));
        }

        return new SpringPluginManager();
    }
5、测试
@SpringBootTest(classes = Pf4jApplication.class)
@RunWith(SpringRunner.class)
public class Pf4jSpringTest {


      @Autowired
    @Qualifier("com.github.lybgeek.plugin.spring.EchoSpringServiceImpl")
    private EchoService echoService;


    @Autowired
    private List<EchoService> echoServices;


    @Test
    public void testSpringPlugin(){
        System.out.println(echoService.echo("test123"));
    }
}

控制台

可以看出来和平时我们使用spring没什么差异,有差异就在于存在多个实现的插件接口,如果需要进行依赖注入,得指定具体的beanName,pf4j-spring默认的beanName为插件名的全类路径类名

不过如果涉及到需要动态变更插件,则不能直接使用依赖注入的方式,而是采用

   List<EchoService> extensions = pluginManager.getExtensions(EchoService.class);

类似spring的依赖查找

四、总结

本文详细讲解了 PF4J 的常用功能以及如何与 Spring Boot 进行整合,帮助开发者实现插件的动态加载和灵活扩展。虽然 PF4J 已经是一个非常强大的插件框架,但它仍有一些优化点,比如与 Spring 集成时,如果用到 AOP,它可能不会生效,因为它注入 Spring 的方式没有走 Spring 的完整生命周期。此外,它的插件查找是基于文件的,如果需要基于 URL 查找,还需要进行扩展。而这些扩展在文末链接的 demo 中都有进行实现,感兴趣的朋友可以深入研究。

五、demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-pf4j


linyb极客之路
355 声望193 粉丝