使用 pf4j-spring 实现插件注入和 api 接口动态注册 | 插件系统构建(上)

grtsinry43
1/26/2025(更新于 1/28/2025
511 views
预计阅读时长 26 分钟

AI Summary

Powered By DeepSeek-R1
|

哪个男孩不想拥有一个自己的插件系统?(x)话说回来,这个我已经计划好久了,不过一直在学其他的东西,刚开始想的是微服务+注册中心,不过对个人来说写这个实在太消耗精力了。在群里大佬的推荐下我感觉可以用 pf4j 试一试,不过毕竟是新玩意儿,加上...它文档是真的抽象,更不必说中文互联网从头翻到尾都是依托答辩,所以最后的效果是经过了大半天的研究,断点,看源码,然后尝试归纳出来的,这东西本来以为前端注册和加载组件是难点,结果是熟悉框架(说实话用 Spring 和反射自己实现都没这么痛苦)...不过既然已经弄出来了,那这篇文章可能是你能找到的最详细的一步一步手把手整合框架的教程了。

(虽然我感觉是因为我太菜了 😭 搞这个才这么麻烦,大佬轻喷,有更好的想法恳请赐教)

长文及分篇提示

这篇文章是整个插件系统构建环节的第一步(插件实例注入,Restful API 管理,动态加载远程组件),全文较长因此分开书写,再加上本来后两部分还在 dev 分支,还没有深入测试

了解 pf4j

pf4j,也就是 Plugin Framework for Java,它足够轻量,足够具有拓展性,官方的介绍是:

PF4J is an open source (Apache license) lightweight (around 100 KB) plugin framework for java, with minimal dependencies (only slf4j-api & java-semver) and very extensible (see PluginDescriptorFinder and ExtensionFinder).

官方文档也是很好心的给出了 4 个扩展和非常简洁的一段使用方法,大意就是确定一个接口(抽象类)作为扩展接口,然后将扩展类打上 @Extension 注解

No XML, only Java.

You can mark any interface or abstract class as an extension point (with marker interface ExtensionPoint) and you specified that an class is an extension with @Extension annotation.

集成框架

在 pf4j-spring 的 github 仓库提供了一个示例(How to use),我们沿着这个思路分析其用法,于是尝试出了注册方法,那咱们就开始吧:先把 github 仓库贴上:pf4j-spring

安装依赖

首先到 Maven 中央仓库找到最新版本:pf4j-spring/0.9.0

复制到 pom.xml 即可:

java
1<dependency>
2    <groupId>org.pf4j</groupId>
3    <artifactId>pf4j-spring</artifactId>
4    <version>0.9.0</version>
5    <scope>provided</scope>
6</dependency>

我们首先为其创建配置文件:

java
1package com.grtsinry43.grtblog.config;
2
3import org.pf4j.spring.SpringPluginManager;
4import org.springframework.context.annotation.Bean;
5import org.springframework.context.annotation.Configuration;
6
7/**
8 * @author grtsinry43
9 * @date 2025/1/25 12:39
10 * @description 热爱可抵岁月漫长

这里的 SpringPluginManager,稍微追一下源码可以发现,其首先继承了默认管理接口(因为这本身也是一个扩展),提供了应用程序上下文及其 get set,提供一个 @PostConstruct 方法,初始化后会调用加载和运行插件,通过 BeanFactory 将应用上下文中的类信息实例化并注入,以实现插件的效果。

看一下源码

提示

这里与 DefaultPluginManager 不一样的,其提供了初始化行为,无需我们手动执行 runner

配置内容

我们不难在 DefaultPluginManager 中发现,可以在 application.yml 中指定插件目录

yml
1pf4j:
2  pluginsConfigDir: plugins

我们也可以同时开一下调试级别日志方便我们进行排查:

yml
1logging:
2  level:
3    org:
4      pf4j: DEBUG

创建扩展接口

按理来说,我们创建一个统一扩展接口,插件继承接口实现自定义方法,再提供主类集成 pf4j 的接口实现生命周期方法就可以圆满解决了,不过当你想在新项目中引用原来的扩展接口的时候,如何引用就成了问题...

先不着急,我们可以先写一下扩展接口,比如我命名为 BlogPlugin

java
1package com.grtblog;
2
3import org.pf4j.ExtensionPoint;
4import org.springframework.http.ResponseEntity;
5
6/**
7 * @author grtsinry43
8 * @date 2025/1/25 12:38
9 * @description 热爱可抵岁月漫长
10 */

问题来了,

如果你选择复制粘贴,那么 jvm 会说:我看这两个接口也不一样啊,这加载的类不是一模一样也可以说毫不相关了()

如果你选择创建一个共享包,那么就是以下的步骤:

创建一个新的 maven 项目,src/main/java/包名 还是方才相同的内容,然后去安装好依赖:

xml
1<dependencies>
2        <dependency>
3            <groupId>org.pf4j</groupId>
4            <artifactId>pf4j</artifactId>
5            <version>3.13.0</version>
6        </dependency>
7        <dependency>
8            <groupId>org.pf4j</groupId>
9            <artifactId>pf4j-spring</artifactId>
10            <version>0.9.0</version>

共享包的优雅解决

插件系统肯定要人人开发,方便贡献嘛,因此我们最好还是打包并开放,而 Github Packages 便成为了不错的选择:

还是先看文档捏:使用 Maven 发布 Java 包

首先在你的 pom.xml 中添加发布包相关信息:

xml
1<distributionManagement>
2    <repository>
3         <id>github</id>
4         <name>GitHub Packages</name>
5         <url>https://maven.pkg.github.com/USER/REPO</url>
6    </repository>
7</distributionManagement>

发布包的全过程都是由 Github Actions 实现的,我们可以创建 .github/workflows/deploy.yml(deploy 就是个名字,什么都可以)

yml
1name: Publish to GitHub Packages
2
3on:
4  push:
5    branches:
6      - main
7
8jobs:
9  build:
10    runs-on: ubuntu-latest
提示

其中的 permissions 十分重要,要赋予当前 token packages 的写入权限,这样我们才能发布包

add commit push 一气呵成,不出意外的话,你的包就发布成功了

发布之后看到

如果这个包是私有读的,我们需要配置好自己的 token 以获得权限,在你的家目录 .m2/settings.xml(没有就自己创建)

xml
1<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
2          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
4    <servers>
5        <server>
6            <id>github</id>
7            <username>name</username>
8            <password>your_token</password>
9        </server>
10    </servers>

于是我们在项目中安装,我们就可以继续啦。

创建插件

我们顺利到了创建插件这一步,还是创建新的 maven 项目,安装需要的依赖(与上一个几乎相同)

首先我们创建方法实现类:我们用一个网易云请求举例(其实根本没请求,就起了个名字)

java
1package com.qwerty;
2
3import com.grtblog.BlogPlugin;
4import org.pf4j.Extension;
5import org.springframework.http.ResponseEntity;
6import org.springframework.web.client.RestTemplate;
7
8/**
9 * @author grtsinry43
10 * @date 2025/1/25 13:20

然后再提供插件主类以实现生命周期方法和注册应用上下文

java
1package com.qwerty;
2
3import org.pf4j.PluginWrapper;
4import org.pf4j.spring.SpringPlugin;
5import org.springframework.context.ApplicationContext;
6import org.springframework.context.annotation.AnnotationConfigApplicationContext;
7
8/**
9 * @author grtsinry43
10 * @date 2025/1/25 13:20

这里的创建上下文挺重要的,我试了好久,直接包注册比较稳妥,当然按照官方方法也没有问题。

下面,为了标识这个包的信息,我们创建 resources/plugin.properties

properties
1plugin.id=sample-plugin
2plugin.version=1.0.0
3plugin.provider=YourName
4plugin.description=A sample plugin for testing PF4J
5plugin.class=com.qwerty.NetEasePluginMain

这里提供的要是继承了 SpringPlugin 的主类而不是接口实现类。

由于其要求 properties 文件在 jar 包根目录,我们简单配置一下即可

xml
1<build>
2        <resources>
3            <resource>
4                <directory>src/main/resources</directory>
5                <includes>
6                    <include>plugin.properties</include>
7                </includes>
8            </resource>
9        </resources>
10    </build>

于是便可以大笔一挥,打包咯:

shell
1mvn clean package

扩展接口

其实到这步,通过 debug 日志你就可以发现插件已经通过豆工厂(bushi)成功实例化了,不过还记得我们之前提供的 getEndPoint 方法吗,我们为了让其调用更方便,于是便将其动态注册到 SpringMVC 端点上

由于我们根据应用上下文来注册插件,同样地,我们也可以在 ContextRefreshedEvent 触发时动态修改当前的路由,于是我们可以创建 PluginRouteRegistrar,我们分析一下它要完成什么:

首先要有之前依赖注入的插件管理示例,要有应用上下文获取,触发时的处理,我们让其实现 ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>,于是就有了:

java
1package com.grtsinry43.grtblog.runner;
2
3import com.grtblog.BlogPlugin;
4import org.pf4j.spring.SpringPluginManager;
5import org.springframework.beans.BeansException;
6import org.springframework.beans.factory.annotation.Autowired;
7import org.springframework.context.ApplicationContext;
8import org.springframework.context.ApplicationContextAware;
9import org.springframework.context.ApplicationListener;
10import org.springframework.context.event.ContextRefreshedEvent;

为了避免重复注入,我们维护一个 Set,注册成功即填入。

我们重写 onApplicationEvent 方法,通过 getExtensions() 获取我们之前打注解的实现类并实例化,于是我们便可以拿到他的方法。

对于注册,我们可以用 SpringMVC 提供的 RequestMappingHandlerMapping,将端点与其方法绑定,实现了动态加载路由

总结

大体的思路就是共享类,各个插件公共实现方法,由注解找到其对应类并实例化,调用 get 方法拿到端点和方法,注册到 Spring。

但是到这里充其量只是提供了一个思路,接下来我们要实现上传,动态加载和卸载,以及前端远程加载打包的 js,最终实现想要的效果,路漫漫其修远兮,前后端还有很长的路...

COPYRIGHT
作者grtsinry43
版权年份© 2025
许可协议

使用 pf4j-spring 实现插件注入和 api 接口动态注册 | 插件系统构建(上)》采用知识共享署名 4.0 国际许可协议

转载请注明出处并遵循 CC BY 许可协议条款

相关推荐

使用 BeautifulSoup 配合请求库实现简单的爬虫程序

使用 BeautifulSoup 配合请求库实现简单的爬虫程序

事情起源于课内的课程实验作业...因为要求要用爬虫,~~不必说课内讲的一言难尽,更不必说就算讲了我也...

grtsinry43
1/7/2025
170
0
1

折腾记录|使用 Nuxt.js 重写个人主页,使用 SSR 优化 SEO ,实现一些期待已久的效果

在 22 年刚创建个人主页的时候,由于我的技术水平不够,只能用一些 wordpress type...

grtsinry43
9/19/2024
227
0
1

使用 Spring Boot + MyBatisPlus 提高效率,简化开发

最近一段时间,由于一些项目的需求,于是被迫用很快的速度学完了 Spring Framework,...

grtsinry43
8/11/2024
109
0
0

问题解决|使用Less变量和媒体查询实现深浅色模式适配

使用Less变量简化css写法,并不影响css变量和媒体查询在深浅模式切换时的效果 <!--m...

grtsinry43
6/10/2024
137
0
0
快速搭建专属域名邮箱服务,简单整合邮箱消息推送功能至 Spring Boot 应用程序

快速搭建专属域名邮箱服务,简单整合邮箱消息推送功能至 Spring Boot 应用程序

作为即时通信的重要方式,邮件在互联网互动中起到举足轻重的作用,而搭建邮件推送服务不仅可以做到实时的消...

grtsinry43
1/2/2025
295
2
0
COMMENT 7288953958905810944

发表评论

来这里畅所欲言吧!
支持 Markdown 语法 0 / 3000

网站运行时间

0
0
0
0

在风雨飘摇之中

感谢陪伴与支持

愿我们不负热爱,继续前行