springboot插件式开发 springboot-plugin-framework-v2.4.5使用文档
功能介绍
简介
文章备份自:https://www.yuque.com/starblues
介绍
此框架可在SpringBoot项目上开发出用于扩展项目的插件,可在插件模块中单独定义接口、静态文件、mybatis-xml等扩展功能。
核心功能
- 插件配置式插拔于springboot项目。
- 在springboot上可以进行插件式开发, 扩展性极强, 可以针对不同项目开发不同插件, 进行不同插件jar包的部署。
- 可通过配置文件指定要启用或者禁用插件。
- 支持上传插件和插件配置文件到服务器, 并且无需重启主程序, 动态部署插件、更新插件。
- 支持查看插件运行状态, 查看插件安装位置。
- 无需重启主程序, 动态的安装插件、卸载插件、启用插件、停止插件、备份插件、删除插件。
- 在插件应用模块上可以使用Spring注解定义组件, 进行依赖注入。
- 支持在插件中开发Rest接口。
- 支持在插件中单独定义持久层访问等需求。
- 可以遵循主程序提供的插件接口开发任意扩展功能。
- 支持注解进行任意业务场景扩展, 并使用定义的坐标进行场景命中。
- 插件可以根据生产和开发环境自定义独立的配置文件。目前只支持yml文件。
- 支持自定义扩展开发接口, 使用者可以在预留接口上扩展额外功能。
- 支持插件之间的通信。
- 支持插件接口文档:
Swagger
、SpringDoc
。 - 插件支持
拦截器
的定制开发。
快速入门
新建项目
Maven目录结构下所示
-example
- example-runner
- pom.xml
- example-main
- pom.xml
- example-plugin-parent
- pom.xml
- plugins
- example-plugin1
- pom.xml
- example-plugin2
- pom.xml
- pom.xml
- pom.xml
结构说明:
- pom.xml 代表maven的pom.xml
- example 为项目的总Maven目录。
- example-runner 在运行环境下启动的模块。主要依赖example-main模块和插件中使用到的依赖包, 并且解决开发环境下无法找到插件依赖包的问题。可自行选择是否需要。(可选)
- example-main 该模块为项目的主程序模块。
- example-plugin-parent 该模块为插件的父级maven pom 模块, 主要定义插件中公共用到的依赖, 以及插件的打包配置。
- plugins 该文件夹下主要存储插件模块。上述模块中主要包括example-plugin1、example-plugin2 两个插件。
- example-plugin1、example-plugin2 分别为两个插件Maven包。
主程序模块集成
主程序为上述目录结构中的 example-main
模块
1 在主程序中新增maven依赖包
<dependency>
<groupId>com.gitee.starblues</groupId>
<artifactId>springboot-plugin-framework</artifactId>
<version>2.4.5-RELEASE</version>
</dependency>
2 配置插件
@Configuration
@Import(AutoIntegrationConfiguration.class)
public class PluginBeanConfig {
@Bean
public PluginApplication pluginApplication(){
// 实例化自动初始化插件的PluginApplication
return new AutoPluginApplication();
}
}
3 在主程序Springboot配置文件新增如下配置:
# 插件的配置
plugin:
# 插件运行模式. dev: 开发环境, prod: 生产环境
runMode: dev
# 插件存放的目录, 可配置多个
pluginPath:
- 指定到插件的目录存储目录(也就是上面描述的`plugins`目录)
集成插件模块
1 插件包pom.xml配置说明
引入主程序的包
<dependency>
<groupId>example</groupId>
<artifactId>example-main</artifactId>
<version>${version}</version>
<scope>provided</scope>
</dependency>
2 在插件模块的 resources
目录下新建插件引导文件 plugin.properties
文件中新增如下内容:
plugin.id=example-plugin1
plugin.class=插件引导类全包路径. 也就是继承 BasePlugin 类的全包路径
plugin.version=版本号
plugin.provider=StarBlues(可选)
plugin.description=插件描述(可选)
plugin.requires=主程序需要的版本(可选)
配置说明:
plugin.id: 插件id
plugin.class: 插件引导类全包路径。也就是步骤三的类包名
plugin.version: 插件版本
plugin.provider: 插件作者(可选)
plugin.description: 插件描述(可选)
plugin.requires: 主程序需要的版本, 搭配主程序配置的 version 来校验插件是否可安装
3 继承 com.gitee.starblues.realize.BasePlugin
包
import com.gitee.starblues.realize.BasePlugin;
import org.pf4j.PluginWrapper;
public class DefinePlugin extends BasePlugin {
public DefinePlugin(PluginWrapper wrapper) {
super(wrapper);
}
}
4 新增HelloPlugin1 controller
此步骤主要验证环境是否加载插件成功。
@RestController
@RequestMapping(path = "plugin1")
public class HelloPlugin1 {
@GetMapping()
public String getConfig(){
return "hello plugin1 example";
}
}
项目启动
- 手动将插件模块进行编译, 确保
target
目录下存在编译的class
文件 - 启动主程序, 观察日志出现如下信息, 说明插件加载启动成功
Plugin 'example-plugin1@version' resolved
Start plugin 'example-plugin1@version'
Plugins initialize success
3 访问插件中的Controller 验证。
浏览器输入:http://ip:port/plugins/example-plugin1/plugin1
地址说明:
plugins: 为插件地址前缀(可使用pluginRestPathPrefix进行配置)
example-plugin1: 为插件id
plugin1: 为插件中地址的接口地址
响应并显示: hello plugin1 example
说明集成成功!
推荐项目目录
生产环境建议目录
-main.jar
-main.yml
-plugins
-plugin1.jar
-plugin2.jar
-plugin-configs
-plugin1.yml
-plugin2.yml
结构说明:
- main.jar 为主程序。
- main.yml 为主程序配置文件。
- plugins 为插件存放的目录。plugin1.jar、plugin2.jar 分别为两个插件。
- plugin-configs 插件的配置文件存放位置。plugin1.yml、plugin2.yml 分别为 plugin1.jar、plugin2.jar 的配置。
- 不一定每个插件都需要配置文件,可根据需求来。如果代码中定义了配置文件,则启动时需要将配置文件存放到plugin-configs目录。
开发环境建议目录
-example
- example-runner
- pom.xml
- example-main
- pom.xml
- example-plugin-parent
- pom.xml
- plugins
- example-plugin1
- pom.xml
- plugin.properties
- example-plugin2
- pom.xml
- plugin.properties
- pom.xml
- pom.xml
结构说明:
- pom.xml 代表maven的pom.xml
- plugin.properties 为开发环境下, 插件的元信息配置文件, 配置内容详见下文。
- example 为项目的总Maven目录。
- example-runner 在运行环境下启动的模块。主要依赖example-main模块和插件中使用到的依赖包, 并且解决开发环境下无法找到插件依赖包的问题。
- example-main 该模块为项目的主程序模块。
- example-plugin-parent 该模块为插件的父级maven pom 模块, 主要定义插件中公共用到的依赖, 以及插件的打包配置。
- plugins 该文件夹下主要存储插件模块。上述模块中主要包括example-plugin1、example-plugin2 两个插件。
- example-plugin1、example-plugin2 分别为两个插件Maven包。
主程序配置
主程序集成
配置PluginApplication
配置PluginApplication
自动初始化配置方式(AutoPluginApplication): 该配置方式可以将启动插件的初始化工作交由Spring容器初始化完成后自动调用。具体配置如下:
@Bean
public PluginApplication pluginApplication(){
// 实例化自动初始化插件的PluginApplication
PluginApplication pluginApplication = new AutoPluginApplication();
return pluginApplication;
}
- 手动(默认)初始化配置(PluginApplication):该配置方式可在必要的时候手动初始化插件。具体配置如下:
@Configuration
public class PluginConfig {
@Autowired
private ApplicationContext applicationContext;
@Bean
public PluginApplication pluginApplication(){
// 实例化自动初始化插件的PluginApplication
PluginApplication pluginApplication = new DefaultPluginApplication();
return pluginApplication;
}
@PostConstruct
public void init(){
pluginApplication.initialize(applicationContext, new PluginInitializerListener() {
@Override
public void before() {
}
@Override
public void complete() {
}
@Override
public void failure(Throwable throwable) {
}
});
}
}
设置配置
主程序可通过三种方式进行插件配置, 三种方式如下:
集成式配置文件
1 导入配置文件对应的Bean
@Import(AutoIntegrationConfiguration.class)
2 配置文件定义
# 插件的配置
plugin:
# 插件运行模式. dev: 开发环境, prod: 生产环境
runMode: dev
# 是否启用插件
enable: true
# 插件存放的目录, 可配置多个
pluginPath:
- D:\example\plugins
# 生产环境下, 插件文件的路径. 只在生产环境下生效
pluginConfigFilePath: D:\example\plugin-config
# 插件rest接口前缀
pluginRestPathPrefix: /plugins
# 是否启用插件id作为rest接口前缀, 默认为启用
enablePluginIdRestPathPrefix: true
# 是否启用Swagger刷新机制. 默认启用
enableSwaggerRefresh: true
# 在卸载插件后, 备份插件的目录
backupPath: backupPlugin
# 上传的插件所存储的临时目录
uploadTempPath: temp
# 当前主程序的版本号, 用于校验插件是否可安装, 插件中可通过插件配置信息 requires 来指定可安装的主程序版本
version: 0.0.0
# 插件版本校验是否允许准确匹配
exactVersionAllowed: false
# 停止插件时, 是否停止当前插件所依赖的插件. 默认: false
stopDependents: false
# 启用的插件id. 为Set集合
enablePluginIds:
- example
# 禁用的插件id, 禁用后系统不会启动该插件. 为Set集合
disablePluginIds:
- example1
# 系统初始化启动时,设置插件的启动顺序. 为 List 集合
sortInitPluginIds:
- example1
- example2
# 是否启用webSocket的功能. 如需启用, 则需要引入springboot支持的WebSocket依赖
enableWebSocket: false
使用Builder
模式代码式进行配置
@Bean
public IntegrationConfiguration configuration(){
List<String> pluginPath = new ArrayList<>();
pluginPath.add("D:\example\plugins");
return ConfigurationBuilder.toBuilder()
.runtimeMode(RuntimeMode.byName("dev"))
.pluginPath(pluginPath)
.pluginConfigFilePath("D:\example\plugin-config")
.uploadTempPath("temp")
.backupPath("backupPlugin")
.enablePluginIdRestPathPrefix(true)
.enableSwaggerRefresh(true)
.build();
}
继承默认配置类进行配置
1 继承 com.gitee.starblues.integration.DefaultIntegrationConfiguration
该类。在初始化插件时需要传入。该类主要时插件的核心配置。具体说明如下:
@Component
public class Config extends DefaultIntegrationConfiguration {
@Override
public RuntimeMode environment() {
return RuntimeMode.byName("dev");
}
@Override
public String pluginPath() {
List<String> pluginPath = new ArrayList<>();
pluginPath.add("D:\example\plugins");
return pluginPath;
}
@Override
public List<String> pluginConfigFilePath() {
return "D:\example\plugin-config";
}
@Override
public String uploadTempPath() {
return "temp";
}
@Override
public String backupPath() {
return "backupPlugin";
}
@Override
public String pluginRestPathPrefix() {
return "/api/plugins";
}
@Override
public boolean enablePluginIdRestPathPrefix() {
return true;
}
@Override
public boolean enable() {
return true;
}
}
配置详细说明
配置项 | 值类型 | 适用环境 | 配置说明 |
---|---|---|---|
runMode | String | dev&prod | 运行模式, 开发环境: dev/development; 生产环境: prod(deployment) |
enable | Boolean | dev&prod | 是否启用插件功能, 默认启用 |
pluginPath | List | dev&prod | 插件的路径, 可配置多个, 开发环境下配置为插件模块上级目录; 生产环境下配置到插件jar包存放目录。建议配置绝对路径. 默认: /plugins |
pluginConfigFilePath | String | prod | 插件对应的配置文件存放目录, 只用于生产环境下. 默认: /plugin-config |
pluginRestPathPrefix | String | dev&prod | 统一配置访问插件rest接口前缀. 默认: /plugins |
enablePluginIdRestPathPrefix | Boolean | dev&prod | 是否启用插件id作为rest接口前缀, 默认为启用. 如果为启用, 则地址为 /pluginRestPathPrefix/pluginId, 其中pluginRestPathPrefix: 为pluginRestPathPrefix的配置值, pluginId: 为插件id |
enableSwaggerRefresh | Boolean | dev&prod | 是否启用Swagger刷新机制. 默认启用 |
backupPath | String | prod | 在卸载插件后, 备份插件的目录 |
uploadTempPath | String | prod | 上传的插件所存储的临时目录 |
version | String | dev&prod | 当前主程序的版本号, 用于校验插件是否可安装. 插件中可通过插件配置信息 requires 来指定可安装的主程序版本。如果为: 0.0.0 的话, 表示不校验 |
exactVersionAllowed | Boolean | dev&prod | 设置为true表示插件设置的requires的版本号完全匹配version版本号才可允许插件安装, 即: requires=x.y.z; 设置为false表示插件设置的requires的版本号小于等于version值, 插件就可安装, 即requires<=x.y.z。默认为false |
enablePluginIds | Set | dev&prod | 启用的插件id |
disablePluginIds | Set | dev&prod | 禁用的插件id, 禁用后系统不会启动该插件, 如果禁用所有插件, 则Set集合中返回一个字符: * |
sortInitPluginIds | Set | dev&prod | 设置初始化时插件启动的顺序 |
enableWebSocket | Boolean | dev&prod | 是否启用webSocket的功能. 默认禁用 |
stopDependents | Boolean | dev&prod | 停止插件时, 是否停止当前插件所依赖的插件. 默认: false |
监听器
监听器1: 插件初始化的监听器
1 实现接口 com.gitee.starblues.integration.listener.PluginInitializerListener
@Component
public class PluginListener implements PluginInitializerListener {
@Override
public void before() {
System.out.println("初始化之前");
}
@Override
public void complete() {
System.out.println("初始化完成");
}
@Override
public void failure(Throwable throwable) {
System.out.println("初始化失败:"+throwable.getMessage());
}
}
2 注册监听器
● 该监听器只能设置一个
● 自动初始化监听器的设置
@Bean
public PluginApplication pluginApplication(PluginListener pluginListener){
AutoPluginApplication autoPluginApplication = new AutoPluginApplication();
autoPluginApplication.setPluginInitializerListener(pluginListener);
return autoPluginApplication;
}
- 手动初始化监听器的设置
@Configuration
public class PluginBeanConfig {
/**
* 定义出 DefaultPluginApplication
* @return PluginApplication
*/
@Bean
public PluginApplication pluginApplication(){
PluginApplication pluginApplication = new DefaultPluginApplication();
return pluginApplication;
}
}
@Component
public class Init {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private PluginListener pluginListener;
@Autowired
private PluginApplication pluginApplication;
@PostConstruct
public void init(){
// 开始执行初始化
pluginApplication.initialize(applicationContext, pluginListener);
}
}
监听器2: 对插件启动、停止的监听器
1 实现接口 com.gitee.starblues.integration.listener.PluginListener
public class Listener implements PluginListener {
/**
* 注册插件
* @param pluginId 插件id
* @param isStartInitial 是否随着系统启动时而进行的插件注册
*/
@Override
public void registry(String pluginId, boolean isStartInitial) {
// 注册插件
}
@Override
public void unRegistry(String pluginId) {
// 卸载插件
}
@Override
public void failure(String pluginId, Throwable throwable) {
// 插件运行错误
}
}
2 注册监听器
- 该监听器可以注册多个
@Bean
public PluginApplication pluginApplication(){
PluginApplication pluginApplication = new AutoPluginApplication();
pluginApplication.addListener(Listener.class);
// 或者使用 pluginApplication.addListener(new Listener());
}
3 添加监听器说明
- pluginApplication.addListener(Listener.class);
该方式新增的监听器,可以使用注入,将Spring容器中的bean注入到 Listener 类中。 该方式新增监听器时, 在监听类上不要使用@Component、@Service 等注解,内部会自动将它注入成Spring bean, 否则系统会报错:重复注入bean的错误。
- pluginApplication.addListener(new Listener());
该方式新增的监听器,无法使用Spring的注入功能。因为是手动 new 出的对象。
监听器3: pf4j原生监听器
2.4.4-RELEASE
版本以上支持
1 实现接口 org.pf4j.PluginStateListener
public class Pf4jPluginListener implements PluginStateListener {
@Override
public void pluginStateChanged(PluginStateEvent event) {
PluginState pluginState = event.getPluginState();
log.info("Plugin {} {}", event.getPlugin().getPluginId(), pluginState.toString());
}
}
2 注册监听器
- 该监听器可以注册多个
@Bean
public PluginApplication pluginApplication(){
PluginApplication pluginApplication = new AutoPluginApplication();
pluginApplication.addPf4jStateListener(Pf4jPluginListener.class);
// 或者使用 pluginApplication.addPf4jStateListener(new Pf4jPluginListener());
}
3 添加监听器说明
pluginApplication.addPf4jStateListener(Listener.class);
该方式新增的监听器,可以使用注入,将Spring容器中的bean注入到 Listener 类中。
该方式新增监听器时, 在监听类上不要使用@Component、@Service 等注解,内部会自动将它注入成Spring bean, 否则系统会报错:重复注入bean的错误。
pluginApplication.addPf4jStateListener(new Listener());
该方式新增的监听器,无法使用Spring的注入功能。因为是手动 new 出的对象。
动态部署插件
介绍
● 可通过配置文件指定要启用或者禁用插件。
● 支持上传插件和插件配置文件到服务器, 并且无需重启主程序, 动态部署插件、更新插件。
● 支持查看插件运行状态, 查看插件安装位置。
● 无需重启主程序, 动态的安装插件、卸载插件、启用插件、停止插件、备份插件、删除插件。
● 只能用于主程序
操作方式
主要调用如下类进行插件的动态部署功能
com.gitee.starblues.integration.operator.PluginOperator
通过 PluginApplication 获取。
private final PluginOperator pluginOperator;
@Autowired
public PluginResource(PluginApplication pluginApplication) {
this.pluginOperator = pluginApplication.getPluginOperator();
}
PluginOperator 接口说明
/**
* 初始化插件。该方法只能执行一次。
* @param pluginInitializerListener 插件初始化监听者
* @return 成功: 返回true; 失败: 抛出异常或者返回false
* @throws Exception 异常信息
*/
boolean initPlugins(PluginInitializerListener pluginInitializerListener) throws Exception;
/**
* 校验插件jar包
* @param jarPath 插件包的路径
* @return 成功: 返回true; 失败: 抛出异常或者返回false
* @throws Exception 校验异常
*/
boolean verify(Path jarPath) throws Exception;
/**
* 通过路径安装插件(会启用), 该插件文件必须存在于服务器 [适用于生产环境]
* 如果在插件目录存在同名的插件包, 系统会自动备份该插件包。备份文件命名规则为;[install-backup][时间]_原jar名.jar
* @param jarPath 插件路径
* @return 成功: 返回插件信息PluginInfo; 失败: 抛出异常或者返回null
* @throws Exception 异常信息
*/
PluginInfo install(Path jarPath) throws Exception;
/**
* 加载插件, 但不启动 [适用于生产环境]
* @param jarPath 插件路径
* @return 成功: 返回插件信息PluginInfo; 失败: 抛出异常或者返回null
* @throws Exception 异常信息
*/
PluginInfo load(Path jarPath) throws Exception;
/**
* 加载插件, 但不启动 [适用于生产环境]
* @param pluginFile 插件文件
* @return 成功: 返回插件信息PluginInfo; 失败: 抛出异常或者返回null
* @throws Exception 异常信息
*/
PluginInfo load(MultipartFile pluginFile) throws Exception;
/**
* 配合load使用. 针对load的插件进行unload [适用于生产环境]
* @param pluginId 插件id
* @param isBackup 是否备份原来的插件。备份文件命名规则为;[uninstall][时间]_原jar名.jar
* @return 成功返回true.不成功抛出异常或者返回false
* @throws Exception 异常信息
*/
boolean unload(String pluginId, boolean isBackup) throws Exception;
/**
* 卸载插件 [适用于生产环境]
* @param pluginId 插件id
* @param isBackup 是否备份原来的插件。备份文件命名规则为;[uninstall][时间]_原jar名.jar
* @return 成功: 返回true; 失败: 抛出异常或者返回false
* @throws Exception 异常信息
*/
boolean uninstall(String pluginId, boolean isBackup) throws Exception;
/**
* 启用插件 [适用于生产环境、开发环境]
* @param pluginId 插件id
* @return 成功返回true.不成功抛出异常或者返回false
* @throws Exception 异常信息
*/
boolean start(String pluginId) throws Exception;
/**
* 停止插件 [适用于生产环境、开发环境]
* @param pluginId 插件id
* @return 成功: 返回true; 失败: 抛出异常或者返回false
* @throws Exception 异常信息
*/
boolean stop(String pluginId) throws Exception;
/**
* 上传插件并启用插件。[适用于生产环境]
* 如果在插件目录存在同名的插件包, 系统会自动备份该插件包。备份文件命名规则为;[install-backup][时间]_原jar名.jar
* @param pluginFile 配置文件
* @return 成功: 返回插件信息PluginInfo; 失败: 抛出异常或者返回null
* @throws Exception 异常信息
*/
PluginInfo uploadPluginAndStart(MultipartFile pluginFile) throws Exception;
/**
* 通过路径安装插件的配置文件。该文件必须存在于服务器。[适用于生产环境]
* 如果配置文件目录存在同名的配置文件, 系统会自动备份该配置文件。备份文件命名规则为;[install-config-backup][时间]_原jar名.jar
* @param configFilePath 配置文件路径。
* @return 成功: 返回true; 失败: 抛出异常或者返回false
* @throws Exception 安装异常
*/
boolean installConfigFile(Path configFilePath) throws Exception;
/**
* 上传配置文件。[适用于生产环境]
* 如果配置文件目录存在同名的配置文件, 系统会自动备份该配置文件。备份文件命名规则为;[upload-config-backup][时间]_原jar名.jar
* @param configFile 配置文件
* @return 成功: 返回true; 失败: 抛出异常或者返回false
* @throws Exception 异常信息
*/
boolean uploadConfigFile(MultipartFile configFile) throws Exception;
/**
* 通过路径备份文件。可备份插件和插件的配置文件。[适用于生产环境]
* @param backDirPath 备份的目录路径
* @param sign 备份文件的自定义标识
* @return 成功: 返回true; 失败: 抛出异常或者返回false
* @throws Exception 异常信息
*/
boolean backupPlugin(Path backDirPath, String sign) throws Exception;
/**
* 通过插件id备份插件。[适用于生产环境]
* @param pluginId 插件id
* @param sign 备份文件的自定义标识
* @return 成功: 返回true; 失败: 抛出异常或者返回false
* @throws Exception 异常信息
*/
boolean backupPlugin(String pluginId, String sign) throws Exception;
/**
* 获取插件信息 [适用于生产环境、开发环境]
* @return 返回插件信息列表
*/
List<PluginInfo> getPluginInfo();
/**
* 根据插件id获取插件信息 [适用于生产环境、开发环境]
* @param pluginId 插件id
* @return 插件信息
*/
PluginInfo getPluginInfo(String pluginId);
/**
* 得到插件文件的路径 [适用于生产环境]
* @return 返回插件路径列表
* @throws Exception 异常信息
*/
Set<String> getPluginFilePaths() throws Exception;
/**
* 得到所有插件的包装类 [适用于生产环境、开发环境]
* @return 返回插件包装类集合
*/
List<PluginWrapper> getPluginWrapper();
/**
* 通过插件id得到插件的包装类 [适用于生产环境、开发环境]
* @param pluginId 插件id
* @return 返回插件包装类集合
*/
PluginWrapper getPluginWrapper(String pluginId);
PluginUser对象说明
介绍
●该接口用于在主程序操作Spring管理的插件bean. 主要用途是: 在主程序定义接口, 插件中实现该接口做扩展, 主程序通过接口class可以获取到插件中的实现类。
●只能在主程序使用
接口位置
com.gitee.starblues.integration.user.PluginUser
使用
通过 PluginApplication 获取 PluginUser。
private final PluginUser pluginUser;
@Autowired
public PluginResource(PluginApplication pluginApplication) {
this.pluginUser = pluginApplication.getPluginUser();
}
接口说明
/**
* 通过bean名称得到bean。(Spring管理的bean)
* @param name bean的名称。spring体系中的bean名称。可以通过注解定义,也可以自定义生成。具体可百度
* @param <T> bean的类型
* @return T
*/
<T> T getBean(String name);
/**
* 通过aClass得到bean。(Spring管理的bean)
* @param aClass class
* @param <T> bean的类型
* @return T
*/
<T> T getBean(Class<T> aClass);
/**
* 通过bean名称得到插件中的bean。(Spring管理的bean)
* @param name 插件中bean的名称。spring体系中的bean名称。可以通过注解定义,也可以自定义生成。具体可百度
* @param <T> bean的类型
* @return T
*/
<T> T getPluginBean(String name);
/**
* 在主程序中定义的接口。
* 插件或者主程序实现该接口。可以该方法获取到实现该接口的所有实现类。(Spring管理的bean)
* 使用场景:
* 1. 在主程序定义接口
* 2. 在主程序和插件包中都存在实现该接口, 并使用Spring的组件注解(@Component、@Service)
* 3. 使用该方法可以获取到所以实现该接口的实现类(主程序和插件中)。
* @param aClass 接口的类
* @param <T> bean的类型
* @return List
*/
<T> List<T> getBeans(Class<T> aClass);
/**
* 得到主函数中定义的类。
* 使用场景:
* 1. 在主程序定义接口
* 2. 在主程序和插件包中都存在实现该接口, 并使用Spring的组件注解(@Component、@Service)
* 3. 使用该方法可以获取到主程序实现该接口的实现类。
* @param aClass 类/接口的类
* @param <T> bean 的类型
* @return List
*/
<T> List<T> getMainBeans(Class<T> aClass);
/**
* 在主程序中定义的接口。获取插件中实现该接口的实现类。(Spring管理的bean)
* 使用场景:
* 1. 在主程序定义接口
* 2. 插件包中实现该接口, 并使用Spring的组件注解(@Component、@Service)
* 3. 使用该方法可以获取到插件中实现该接口的实现类(不包括主程序)。
* @param aClass 接口的类
* @param <T> bean的类型
* @return 实现 aClass 接口的实现类的集合
*/
<T> List<T> getPluginBeans(Class<T> aClass);
/**
* 在主程序中定义的接口。获取指定插件中实现该接口的实现类。(Spring管理的bean)
* 使用场景:
* 1. 在主程序定义接口
* 2. 插件包中实现该接口, 并使用Spring的组件注解(@Component、@Service)
* 3. 使用该方法可以获取到指定插件中实现该接口的实现类。
* @param pluginId 插件id
* @param aClass 接口的类
* @param <T> bean的类型
* @return 实现 aClass 接口的实现类的集合
*/
<T> List<T> getPluginBeans(String pluginId, Class<T> aClass);
/**
* 生成一个新的Spring实例Bean.
* 使用场景:主要用于非单例对象的生成。
* @param object 旧实例对象
* @param <T> 实例泛型
* @return 新实例对象
*/
<T> T generateNewInstance(T object);
/**
* 使用场景:
* 1. 在主程序定义接口(该接口需要继承 ExtensionPoint 接口)。
* 2. 插件包中实现该接口
* 3. 在主程序可以使用该方法获取到实现该接口的实现类。(实现类可以配合 @Extension 控制顺序)
* 注意: 该场景用于非Spring管理的bean, 使用Spring注解无效
* @param tClass bean的类型
* @param <T> bean的类型
* @return List
*/
<T> List<T> getPluginExtensions(Class<T> tClass);
集成接口文档
集成Swagger
- 自行引入
Swagger
依赖 - 配置启用
Swagger
接口刷新机制
将配置项enableSwaggerRefresh
设置为true
(框架已经默认为启用了) - 配置
Swagger
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
.paths(PathSelectors.any())
.build();
- 注意: 接口扫描路径设置为
PathSelectors.any()
或者RequestHandlerSelectors.withClassAnnotation(Api.class)
集成SpringDoc
1自行导入SpringDoc依赖
2使用如下注解进行导入集成注册刷新插件的接口信息
@Import(SpringDocControllerProcessor.class)
打包说明
由于SpringBoot提供的打包插件存在的某些Bug, 会导致插件动态安装、卸载时会出现各种问题,所以建议使用如下步骤对主程序进行打包
1 删除SpringBoot打包插件, 也就是如下打包插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2 新增如下打包插件, 目的就是将依赖包统一打入到lib
目录, 然后启动时使用Classpath
指定依赖包进行启动
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<useUniqueVersions>false</useUniqueVersions>
<mainClass>此处修改当前主程序Main类</mainClass>
</manifest>
<manifestEntries>
<Class-Path>./</Class-Path>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 具体使用方式可参考案例: springboot-plugin-framework-example
集成插件步骤
集成步骤
1 在插件目录下新建maven模块
2 新建插件引导类
●在当前模块的根包下面新建类, 然后继承com.gitee.starblues.realize.BasePlugin类即可, 例如:
import com.gitee.starblues.realize.BasePlugin;
import org.pf4j.PluginWrapper;
public class DefinePlugin extends BasePlugin {
public DefinePlugin(PluginWrapper wrapper) {
super(wrapper);
}
}
3 新建插件引导配置文件
●当前模块的 src/main/resources 下新建插件文件 plugin.properties, 内容如下:
plugin.id=example-plugin1
plugin.class=BasePlugin的继承类全包路径
plugin.version=版本号
plugin.provider=StarBlues(可选)
plugin.description=插件描述(可选)
plugin.requires=主程序需要的版本(可选)
plugin.configFileName: 配置文件名称(可选)(2.4.5版本以上支持)
plugin.configFileProfile: 配置文件环境后缀 (可选)(2.4.5版本以上支持)
配置说明:
plugin.id: 插件id
plugin.class: BasePlugin的继承类全包路径。也就是步骤2的DefinePlugin类包名
plugin.version: 插件版本
plugin.provider: 插件作者(可选)
plugin.description: 插件描述(可选)
plugin.requires: 主程序需要的版本, 搭配主程序配置的 version 来校验插件是否可安装
plugin.configFileName: 配置文件名称. 比如: plugin.yml
plugin.configFileProfile: 配置文件环境后缀, 可根据不同环境来设置加载不同文件. 比如配置为 dev, 则最终加载文件名称为 plugin-dev.yml
扫描包配置
- 插件中默认扫描的
plugin.class
定义的类的包, - 如果想配置其他包下作为插件扫描的根包, 则重写
com.gitee.starblues.realize.BasePlugin
类的scanPackage
方法即可
重要提示: 插件运行主程序前一定要手动编译插件模块, 确保target
目录下存在编译的class
文件
自定义配置文件
实现步骤
1 在插件包的 resources 目录下定义配置文件 plugin1.yml
name: plugin1
plugin: examplePlugin1
setString:
- set1
- set2
listInteger:
- 1
- 2
- 3
subConfig:
subName: subConfigName
2 在代码中定义对应的bean
import com.gitee.starblues.annotation.ConfigDefinition;
import java.util.List;
import java.util.Set;
@ConfigDefinition(fileName="plugin1.yml")
public class PluginConfig1 {
private String name;
private String plugin;
private Set<String> setString;
private List<Integer> listInteger;
private String defaultValue = "defaultValue";
private SubConfig subConfig;
// 自行提供get set 方法
}
public class SubConfig {
private String subName;
public String getSubName() {
return subName;
}
// 自行提供get set 方法
}
该bean必须加上 @ConfigDefinition(fileName="plugin1.yml") 注解。其中值为插件文件的名称。
3 其他地方使用时, 可以通过注入方式使用。
例如:
@Component("plugin2HelloService")
public class HelloService {
private final PluginConfig1 pluginConfig1;
private final Service2 service2;
@Autowired
public HelloService(PluginConfig1 pluginConfig1, Service2 service2) {
this.pluginConfig1 = pluginConfig1;
this.service2 = service2;
}
public PluginConfig1 getPluginConfig1(){
return pluginConfig1;
}
public String sayService2(){
return service2.getName();
}
}
分环境进行配置
@ConfigDefinition 设置为:
@ConfigDefinition(fileName="plugin1.yml",devSuffix="-dev", prodSuffix="-prod")
fileName
: 文件基本名称devSuffix
: 开发环境下文件名称后缀,最终映射为:plugin1-dev.yml
prodSuffix
: 生产环境下文件名称后缀,最终映射为:plugin1-prod.yml
在resources下新增plugin1-dev.yml
和plugin1-prod.yml
文件,分别对应开发环境和生产环境下的配置文件。
开发环境配置文件读取说明
开发环境的配置文件将会从resources
下读取,因此一定记着将配置文件存放在当前插件的resources
目录下,而且保证运行时target->classes
目录下存在该配置文件,否则启动阶段会报找不到配置文件的异常。
读取的文件为: plugin1-dev.yml
生产环境下配置文件读取说明
在生产环境下配置文件会有如下读取顺序:
- 从当前配置的插件jar目录下加载配置文件。加载不到走2步骤。
- 从
pluginConfigFilePath
配置目录下加载配置文件。加载不到走3步骤。 - 从当前jar包里面加载配置文件。
读取的文件为: plugin1-prod.yml
插件中Bean初始化
方式1
直接使用@Configuration 配合 @Bean 注解进行定义(2.4.0.RELEASE版本以上支持)
@Configuration
public class ConfigBean {
@Bean
public ConfigBeanTest config(PluginUtils pluginUtils){
PluginConfiguration mainBean = pluginUtils.getMainBean(PluginConfiguration.class);
ConfigBeanTest configBeanTest = new ConfigBeanTest();
configBeanTest.name = "hello";
configBeanTest.age = 16;
return configBeanTest;
}
}
- 注意:该方式定义的Bean如果需要卸载时进行一些关闭资源的动作,比如连接数据库等,则需要搭配插件监听器
OneselfListener
进行手动关闭
方式2
- 实现
ConfigBean
接口 - 使用场景:有时候在插件启动阶段,我们需要在插件中进行某些Bean的初始化,然后调用该Bean的某些方法,就类似于Spring中使用 @Bean 标记的方法所返回的Bean对象。我们就可以使用如下方法进行插件中的Bean
- 使用说明
实现接口 com.gitee.starblues.realize.ConfigBean
即可。它有两个方法,如下:
public interface ConfigBean {
/**
* 初始化。所有bean的初始化工作在此处实现
* @throws Exception 初始化异常
*/
void initialize() throws Exception;
/**
* 销毁实现
* @throws Exception 销毁异常
*/
void destroy() throws Exception;
}
- 例如:
public class DatabaseConfigBean implements ConfigBean {
private DataSource dataSource;
// 可注入配置文件
@Autowired
private Config config;
@Override
public void initialize() throws Exception {
// 初始化实现
dataSource = ...;
}
@Override
public void destroy() throws Exception {
// 销毁实现
dataSource = ...;
}
/**
* 在其他地方注入该对象, 然后调用get方法获取初始化的bean
*/
public getDatasource(){
return dataSource;
}
}
- 注意: 在其他地方获取该方式初始化的bean时,注入该对象,调用get方法即可, 不能在构造器中调用, 只能在使用时调用
插件Aop集成
2.4.3-RELEASE版本 以上支持
说明
该功能可在插件中定义Aop
切面来定制一些非业务功能。比如日志打印、接口访问统计等功能。用法和Spring-Aop
一致。
集成步骤
1 在主程序依赖spring-boot-starter-aop
包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2 在插件中使用注解EnableAspectJAutoProxy
来启用Aop
功能. 例如:
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class EnableAop {
}
3 新增切面
@Component
@Aspect
@Order(10)
@Slf4j
public class TestAdvice {
@Pointcut("execution(public * com.gitee.starblues.example.basic.service..*.*(..))")
public void pointCut1() {
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void pointCut2() {
}
@Before(value = "pointCut2()")
public void controllerBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("controller aop = 请求URL:" + request.getRequestURL().toString());
}
@After(value = "pointCut1()")
public void serviceAfter(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("service aop = 请求URL:" + request.getRequestURL().toString());
}
}
- 具体集成案例见: springboot-plugin-framework-example
插件之间方法调用
原理
插件之间的数据交互功能, 是在同一JVM运行环境下, 基于代理、反射机制完成方法调用。使用说明如下:
1 被调用类需要使用注解 @Supplier(""), 注解值为被调用者的唯一key, (全局key不能重复) 供调用者使用。例如:
@Supplier("SupplierService")
public class SupplierService {
public Integer add(Integer a1, Integer a2){
return a1 + a2;
}
}
2 另一个插件中要调用1步骤中定义的调用类时, 需要定义一个接口,新增注解@Caller(""), 值为1步骤中被调用者定义的全局key。其中方法名、参数个数和类型、返回类型需要和被调用者中定义的方法名、参数个数和类型一致。例如:
// 全局扫描调用
@Caller(value="SupplierService")
public interface CallerService {
Integer add(Integer a1, Integer a2);
}
// 可指定调用具体的插件
@Caller(value="SupplierService", pluginId="example1")
public interface CallerService {
Integer add(Integer a1, Integer a2);
}
3 被调用者和调用者也可以使用注解定义被调用的方法。例如:
被调用者:
@Supplier("SupplierService")
public class SupplierService {
@Supplier.Method("call")
public String call(CallerInfo callerInfo, String key){
System.out.println(callerInfo);
return key;
}
}
调用者:
@Caller("SupplierService")
public interface CallerService {
@Caller.Method("call")
String test(CallerInfo callerInfo, String key);
}
该场景主要用于参数类型不在同一个地方定义时使用。比如 被调用者的参数类: CallerInfo 定义在被调用者的插件中, 调用者的参数类: CallerInfo 定义在调用者的插件中。就必须配合 @Supplier.Method("")、@Caller.Method("") 注解使用, 否则会导致NotFoundClass 异常。
如果调用者没有使用注解 @Caller.Method("") 则默认使用方法和参数类型来调用。
4 对于3步骤中问题的建议
可以将被调用者和调用者的公用参数和返回值定义在主程序中、或者单独提出一个api maven包, 然后两者都依赖该包。
5 案例位置
basic-example:
com.basic.example.plugin1.service.SupplierService
com.basic.example.plugin2.service.CallerService
com.basic.example.plugin2.rest.ProxyController
插件业务扩展功能
场景描述
在某些情况下有一些业务需要进行动态扩展, 比如扩展定时任务, 这时就需要使用该功能进行业务扩展,
给每一个扩展绑定一个唯一的业务坐标, 就可以快速命中扩展的实现, 进行快速调用
在主程序定义一个业务接口
- 比如:
public interface ExtractExample {
void exe();
void exe(String name);
void exe(Info info);
Info exeInfo(Info info);
class Info{
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Info{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
在任意插件中实现该接口定义的业务扩展
- 实现第一步骤定义的接口
- 新增扩展注解
@Extract(bus = "业务标识", scene="场景标识", useCase="用例标识")
该注解标识唯一扩展的描述, 其中有三个值:
bus
: 业务标识, 必填scene
: 场景标识, 可选useCase
: 用例标识, 可选order
: 优先级别(不同插件存在同一业务时, 用于指定优先级别. 数字越大, 优先级别越高), 可选
例如:
@Extract(bus = "PluginExtract1")
public class PluginExtract implements ExtractExample {
@Override
public void exe() {
System.out.println(PluginExtract.class.getName());
}
@Override
public void exe(String name) {
System.out.println(PluginExtract.class.getName() + ": name");
}
@Override
public void exe(Info info) {
System.out.println(PluginExtract.class.getName() + ": " + info);
}
@Override
public Info exeInfo(Info info) {
info.setName("Plugin1-PluginExtract1");
info.setAge(0);
return info;
}
}
主程序定义扩展实现
- 在主程序定义扩展的实现的时候, 必须使用 注解
@Extract
和@Component
组合定义
例如:
@Extract(bus = "main", scene = "1", useCase = "2")
@Component
public class MainExtractExample implements ExtractExample{
@Override
public void exe() {
System.out.println("Main exe");
}
@Override
public void exe(String name) {
System.out.println("Main exe, name=" + name);
}
@Override
public void exe(Info info) {
System.out.println("Main exe, info=" + info);
}
@Override
public Info exeInfo(Info info) {
info.setName(info.getName() + "-main");
return info;
}
}
调用扩展
- 在主程序或者任意插件中注入扩展工厂com.gitee.starblues.factory.process.pipe.extract.ExtractFactory
- 使用扩展工厂提供的如下接口进行调用扩展:
/**
* 通过坐标得到扩展
* @param coordinate 扩展的坐标
* @param <T> 扩展的泛型
* @return 扩展实例, 如果不存在则抛出 RuntimeException 异常
*/
public <T> T getExtractByCoordinate(ExtractCoordinate coordinate)
/**
* 根据插件id和坐标得到扩展
* @param pluginId 插件id
* @param coordinate 扩展的坐标
* @param <T> 扩展的泛型
* @return 扩展实例, 如果不存在则抛出 RuntimeException 异常
*/
public <T> T getExtractByCoordinate(String pluginId, ExtractCoordinate coordinate)
/**
* 根据坐标得到主程序的扩展
* 主程序扩展必须使用 @Extract+@Component 进行定义
* @param coordinate 扩展的坐标
* @param <T> 扩展的泛型
* @return 扩展实例, 如果不存在则抛出 RuntimeException 异常
*/
public <T> T getExtractByCoordinateOfMain(ExtractCoordinate coordinate)
/**
* 根据接口类型获取扩展
* @param interfaceClass 接口类类型
* @param <T> 接口类型泛型
* @return 扩展实现集合
*/
public <T> List<T> getExtractByInterClass(Class<T> interfaceClass)
/**
* 根据插件id和接口类型获取扩展
* @param pluginId 插件id
* @param interfaceClass 接口类类型
* @param <T> 接口类型泛型
* @return 扩展实现集合
*/
public <T> List<T> getExtractByInterClass(String pluginId, Class<T> interfaceClass)
/**
* 根据接口类型获取主程序的扩展
* 主程序扩展必须使用 @Extract+@Component 进行定义
* @param interfaceClass 接口类类型
* @param <T> 接口类型泛型
* @return 扩展实现集合
*/
public <T> List<T> getExtractByInterClassOfMain(Class<T> interfaceClass)
/**
* 得到所有的扩展坐标
* @return 扩展坐标集合, key 为插件id, 值为所有扩展坐标集合
*/
public Map<String, Set<ExtractCoordinate>> getExtractCoordinates()
例如:
@RestController
@RequestMapping("/extract")
public class ExtractController {
private final ExtractFactory extractFactory = ExtractFactory.getInstant();
@GetMapping("getExtractCoordinates")
public Map<String, Set<ExtractCoordinate>> getExtractCoordinates(){
return extractFactory.getExtractCoordinates();
}
@GetMapping("getExtractByInterClass")
public List<String> getExtractByInterClass(){
List<ExtractExample> extractByInterClass = extractFactory.getExtractByInterClass(ExtractExample.class);
return extractByInterClass.stream()
.map(extractExample -> extractExample.getClass().getName())
.collect(Collectors.toList());
}
@GetMapping("{name}/exeR")
public ExtractExample.Info exeInfoR(@PathVariable("name") String name){
ExtractExample extractExample = extractFactory.getExtractByCoordinate(ExtractCoordinate.build(name));
ExtractExample.Info info = new ExtractExample.Info();
return extractExample.exeInfo(info);
}
}
插件监听器
插件中定义的监听器
1 实现 com.gitee.starblues.realize.OneselfListener
接口即可。无需注册
例如:
public class Plugin1Listener implements OneselfListener {
private final Logger logger = LoggerFactory.getLogger(Plugin1Listener.class);
private final Plugin1Mapper plugin1Mapper;
public Plugin1Listener(Plugin1Mapper plugin1Mapper) {
this.plugin1Mapper = plugin1Mapper;
}
@Override
public OrderPriority order() {
// 定义监听器执行顺序。用于多个监听器
return OrderPriority.getMiddlePriority();
}
@Override
public void startEvent(BasePlugin basePlugin) {
// 启动事件
logger.info("Plugin1Listener {} start Event", basePlugin.getWrapper().getPluginId());
logger.info("plugin1Mapper getList : {}", plugin1Mapper.getList());
}
@Override
public void stopEvent(BasePlugin basePlugin) {
// 停止事件
logger.info("Plugin1Listener {} stop Event", basePlugin.getWrapper().getPluginId());
logger.info("plugin1Mapper getList : {}", plugin1Mapper.getList());
}
}
2 一个插件模块的监听器可定义多个。执行顺序由 OrderPriority 控制。
3 案例
参考: example/integration-mybatis 模块中定义的监听器。
插件Web拦截器
2.4.1-RELEASE 版本以上支持
1 实现接口org.springframework.web.servlet.HandlerInterceptor
或者 org.springframework.web.context.request.WebRequestInterceptor
@Component
public class PluginInterceptor1 implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("进入插件1拦截器");
}
}
2 注册拦截器
- 实现接口
com.gitee.starblues.factory.process.pipe.interceptor.PluginInterceptorRegister
进行注册
@Component
public class PluginInterceptorR implements PluginInterceptorRegister {
@Resource
private PluginInterceptor1 pluginInterceptor1;
@Override
public void registry(PluginInterceptorRegistry registry) {
registry.addInterceptor(pluginInterceptor1, PluginInterceptorRegistry.Type.PLUGIN)
.addPathPatterns("plugin1/**");
}
}
- 如果为全局拦截器, 则注册拦截器时, 类型为: PluginInterceptorRegistry.Type.GLOBAL
- 如果为当前插件级别的拦截器, 则注册拦截器时, 类型为: PluginInterceptorRegistry.Type.PLUGIN
WebSocket 集成
2.4.2-RELEASE 版本以上支持
集成步骤
1 主程序中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2 主程序中定义ServerEndpointExporter
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3 主程序中启用WebSocket
配置
将配置enableWebSocket
设置为true
插件中使用
@ServerEndpoint(value = "/test/no_path_param")
public class WebSocket1 {
private static final Logger log = LoggerFactory.getLogger(WebSocket1.class);
private static AtomicInteger onlineCount = new AtomicInteger(0);
@OnOpen
public void onOpen(Session session) {
onlineCount.incrementAndGet(); // 在线数加1
log.info("有新连接加入:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
@OnClose
public void onClose(Session session) {
onlineCount.decrementAndGet(); // 在线数减1
log.info("有一连接关闭:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
this.sendMessage("Hello, " + message, session);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
private void sendMessage(String message, Session toSession) {
try {
log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);
toSession.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("服务端发送消息给客户端失败", e);
}
}
}
自定义卸载预检查
说明
比如要卸载某个插件,但是该插件是系统核心功能,则不能卸载,则需要在插件模块中新增插件卸载检查机制
实现
1 实现接口com.gitee.starblues.realize.UnRegistryValidator
/**
* 校验是否可卸载
* @return 校验结果
* @throws Exception 校验异常. 校验异常后, 该插件默认不可卸载
*/
Result verify() throws Exception;
2 在verify
方法中实现检查机制,如果检查不能卸载在 Result
中设置为不可卸载
class Result{
/**
* 是否可卸载
*/
private boolean verify;
/**
* 不可卸载时的提示内容
*/
private String message;
public Result(boolean verify) {
this.verify = verify;
}
public Result(boolean verify, String message) {
this.verify = verify;
this.message = message;
}
public boolean isVerify() {
return verify;
}
public String getMessage() {
return message;
}
}
例如
@Component
public class UnRegistryValidatorImpl implements UnRegistryValidator {
@Override
public Result verify() throws Exception {
return new Result(false, "不可卸载");
}
}
插件集成自动装载的Bean
2.4.3-RELEASE 版本以上支持
当前文档说的配置文件都为: 插件SpringBoot配置文件
功能说明
如果您想在插件中集成 spring-boot
中扩展出的自动装配的实现, 您就可以使用该功能。
比如您想集成Quartz
框架到插件中, 可按照如下Quartz集成案例
步骤进行。
注意: 该功能的使用, 可能需要您具备了解所要集成的**xx-starter**
的源码, 然后根据自己的需求和查看源码来选择装载哪些, 一般选用 **xxxAutoConfiguration**
进行装载
集成说明
在插件配置文件中新增要装载的class
全包名
例如:
plugin:
auto-config-class:
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration
- org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
- org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration
集成案例
插件集成Quartz
1 新增依赖到主程序
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2 新增QuartzAutoConfiguration
自动装配类到配置文件
plugin:
auto-config-class:
- org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration
3 代码编写
- 外置配置文件对应的
Bean
@Component
@ConfigurationProperties(prefix = "job")
@Data
public class QuartzJobProp {
@Value("${databaseInsertJobCron:*/20 * * * * ?}")
private String databaseInsertJobCron;
@Value("${databaseInsertJobDelaySec:0}")
private Long databaseInsertJobDelaySec;
}
- 编写
Job
public class DatabaseInsertJob implements Job {
private static final Logger LOG = LoggerFactory.getLogger(DatabaseInsertJob.class);
public static final String APPLICATION_CONTEXT = "applicationContext";
private ApplicationContext applicationContext;
private Plugin1UserService plugin1UserService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap mergedJobDataMap = jobExecutionContext.getMergedJobDataMap();
applicationContext = (ApplicationContext) mergedJobDataMap.get(APPLICATION_CONTEXT);
LOG.info("定时任务开启" + new Data());
}
}
- 配置定时
Bean
@Configuration
public class QuartzJobConfig {
@Autowired
private QuartzJobProp quartzJobProp;
@Bean
public JobDetail databaseInsertJob(){
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(DatabaseInsertJob.APPLICATION_CONTEXT, applicationContext);
return JobBuilder.newJob(DatabaseInsertJob.class)
.storeDurably()
.setJobData(jobDataMap)
.build();
}
@Bean
public Trigger trigger1(){
CronScheduleBuilder cronScheduleBuilder =
CronScheduleBuilder.cronSchedule(quartzJobProp.getDatabaseInsertJobCron());
Long databaseInsertJobDelaySec = quartzJobProp.getDatabaseInsertJobDelaySec();
return TriggerBuilder.newTrigger()
.forJob(databaseInsertJob())
.withSchedule(cronScheduleBuilder)
.startAt(new Date(System.currentTimeMillis() + (databaseInsertJobDelaySec * 1000)))
.build();
}
}
插件集成jpa
1 新增依赖到主程序
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
</dependencies>
2 新增自动装配类到配置文件
plugin:
auto-config-class:
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration
- org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
3 配置Database
- 配置文件
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/plugin-test-example?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
initial-size: 5
max-active: 100
min-idle: 1
max-wait: 60000
- 自定义
Database
@Configuration
public class PluginJpaConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource dataSource(){
return new DruidDataSource();
}
}
- 注意: 本案例是使用
Druid
作为连接池. 如果您需要其他连接池,
一定要手动使用代码定义DataSource
, 否则会无法加载jpa
PluginUtils工具类说明
2.4.0-RELEASE 版本以上支持
介绍
- 可在插件中获取主程序中Spring容器中的Bean
- 可获取当前插件的信息
- 只能作用于当前插件
接口说明
/**
* 获取主程序的 ApplicationContext
* @return ApplicationContext
*/
public ApplicationContext getMainApplicationContext();
/**
* 获取当前插件的 ApplicationContext
* @return ApplicationContext
*/
public ApplicationContext getPluginApplicationContext();
/**
* 获取当前插件的描述信息
* @return PluginDescriptor
*/
public PluginDescriptor getPluginDescriptor();
/**
* 通过bean名称得到主程序中的bean
* @param name bean 名称
* @param <T> bean 类型
* @return bean
*/
public <T> T getMainBean(String name)();
/**
* 通过bean类型得到主程序中的bean
* @param aClass bean 类型
* @param <T> bean 类型
* @return bean
*/
public <T> T getMainBean(Class<T> aClass) ();
/**
* 通过接口或者抽象类类型得到主程序中的多个实现对象
* @param aClass bean 类型
* @param <T> bean 类型
* @return bean
*/
public <T> List<T> getMainBeans(Class<T> aClass)();
使用方式
直接在插件中注入即可
例如通过注解注入:
@Autowired
private PluginUtils pluginUtils;
ConfigDefinitionTip工具类说明
2.4.2-RELEASE 版本以上支持
介绍
针对插件配置文件存在如下问题
- 无法进行注入主程序Bean
- 无法获取当前插件其他配置Bean
因此为了弥补以上问题, 引入ConfigDefinitionTip
.
使用
- 在当前插件配置类中定义出
ConfigDefinitionTip
属性 - 使用
ConfigDefinitionTip
提供的方法
ConfigDefinitionTip只能作用于注解: @ConfigDefinition 定义的类中
/**
* 得到当前插件信息
* @return 插件信息
*/
public PluginInfo getCurrentPluginInfo();
/**
* 得到主程序的 ApplicationContext
* @return ApplicationContext
*/
public ApplicationContext getMainApplication();
/**
* 获取当前插件的其他的ConfigDefinition
* @param configDefinitionClass ConfigDefinition的类类型
* @param <T> 类类型
* @return T
*/
public <T> T getOtherConfigDefinition(Class<T> configDefinitionClass)
/**
* 将Springboot类型的配置文件中的值映射为bean
* 注意: 只针对插件扩展的 springboot 配置文件生效
* @param prefix 配置文件中的前置, 比如: plugin.config
* @param type 配置文件中结构对应的类类型, 比如: plugin.config 下定义的键值对和type类类型一致
* @param <T> 类类型
* @return T
*/
public <T> T getConfigOfBean(String prefix, Class<T> type)
/**
* 将Springboot类型的配置文件中的值映射为 List
* 注意: 只针对插件扩展的 springboot 配置文件生效
* @param prefix 配置文件中的前置, 比如: plugin.config
* @param type List元素的类类型
* @param <T> List中定义的类类型
* @return List
*/
public <T> List<T> getConfigOfList(String prefix, Class<T> type)
/**
* 将Springboot类型的配置文件中的值映射为 Set
* 注意: 只针对插件扩展的 springboot 配置文件生效
* @param prefix 配置文件中的前置, 比如: plugin.config
* @param type Set元素的类类型
* @param <T> 类类型
* @return Set
*/
public <T> Set<T> getConfigOfSet(String prefix, Class<T> type)
/**
* 将Springboot类型的配置文件中的值映射为 Map
* 注意: 只针对插件扩展的 springboot 配置文件生效
* @param prefix 配置文件中的前置, 比如: plugin.config
* @param keyType map的key元素类型
* @param valueType map的值元素类型
* @param <K> map key 元素的类类型
* @param <V> map value 元素的类类型
* @return Map
*/
public <K, V> Map<K, V> getConfigOfSet(String prefix, Class<K> keyType, Class<V> valueType)
/**
* 返回当前插件的ConfigurableEnvironment
* @return ConfigurableEnvironment
*/
public ConfigurableEnvironment getPluginEnvironment()
/**
* 返回当前插件的Binder
* @return Binder
*/
public Binder getPluginBinder()
- 使用
getMainApplication
即可拿到主程序的ApplicationContext
, 使用它可拿到主程序的任何Bean
- 使用
getOtherConfigDefinition
即可拿到当前插件其他的配置Bean
. 但无法拿到非配置Bean
例子
@ConfigDefinition
public class MybatisConfig implements SpringBootMybatisConfig {
private ConfigDefinitionTip configDefinitionTip;
@Override
public Set<String> entityPackage() {
// 测试 configDefinitionTip 的使用
PluginInfo currentPluginInfo = configDefinitionTip.getCurrentPluginInfo();
System.out.println("MybatisConfig=" + currentPluginInfo);
// 获取当前插件其他配置-Plugin1Config
Plugin1Config plugin1Config = configDefinitionTip.getOtherConfigDefinition(Plugin1Config.class);
System.out.println("plugin1Config=" + plugin1Config);
// 获取主程序配置
ApplicationContext mainApplication = configDefinitionTip.getMainApplication();
PluginBeanConfig bean = mainApplication.getBean(PluginBeanConfig.class);
System.out.println("PluginBeanConfig" + bean);
Set<String> typeAliasesPackage = new HashSet<>();
typeAliasesPackage.add("com.mybatis.plugin1.entity");
return typeAliasesPackage;
}
@Override
public Set<String> xmlLocationsMatch() {
Set<String> xmlLocationsMatch = new HashSet<>();
xmlLocationsMatch.add("classpath:mapper/*Mapper.xml");
return xmlLocationsMatch;
}
}
插件扩展配置
集成静态资源扩展
当前文档说的配置文件都为: 插件SpringBoot配置文件
包含内容
- 可集成插件静态资源访问
- 可集成
Thymeleaf
maven 仓库地址
主程序集成步骤
引入依赖
<dependency>
<groupId>com.gitee.starblues</groupId>
<artifactId>springboot-plugin-framework-extension-resources</artifactId>
<version>2.4.5-RELEASE</version>
</dependency>
配置扩展
定义PluginApplication bean时, 新增该扩展。
@Bean
public PluginApplication pluginApplication(){
PluginApplication pluginApplication = new AutoPluginApplication();
// 新增静态资源扩展
StaticResourceExtension staticResourceExtension = new StaticResourceExtension();
// 如果需要集成Thymeleaf, 则如下
// StaticResourceExtension staticResourceExtension = new StaticResourceExtension(StaticResourceExtension.Include.THYMELEAF);
// 插件静态资源Http访问前缀. 默认为: static-plugin
staticResourceExtension.setPathPrefix("static");
// 设置静态资源缓存策略
staticResourceExtension.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
// 添加扩展
pluginApplication.addExtension(staticResourceExtension);
}
插件程序集成步骤
以provided
方式引入主程序依赖, 例如:
<dependency>
<groupId>your-groupId-name</groupId>
<artifactId>your-artifact-name</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
集成静态文件
在插件配置文件中新增如下配置
plugin:
# 静态资源配置
static:
# 静态资源位置
locations:
- classpath:static
- file:D://static
插件的http资源url访问规则为: http://ip:port/pathPrefix(上述说明配置的值)/插件id/具体插件的资源路径
配置集成Thymeleaf
1 首先主程序
中需要引入如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
注意: 新增扩展时, 需要进行Thymeleaf
设置, 如下:
StaticResourceExtension staticResourceExtension =
new StaticResourceExtension(StaticResourceExtension.Include.THYMELEAF);
2 在插件配置文件中新增如下配置
plugin:
# 插件thymeleaf配置
thymeleaf:
prefix: plugin1-template
suffix: .html
mode: html
encoding: utf-8
thymeleaf
下所有配置说明:
- prefix: 模板引擎目录
- suffix: 模板引擎后缀
- mode: 模板引擎模式. 比如: html、xml、text、css 等
- encoding: 编码
- cache: 是否启用模板引擎的缓存
注意事项
插件中**resources**
中存放的资源文件目录一定不能和主程序相同, 否则就会加载到主程序的资源
- 例如: 主程序在
resources
中定义了web
文件夹. 插件中的resources
中不能再定义web
文件夹来存放静态资源
集成Log日志
当前文档说的配置文件都为: 插件SpringBoot配置文件
包含内容
- 可集成
logback
日志 - 可集成
log4j2
日志
maven 仓库地址
主程序集成步骤
引入依赖
<dependency>
<groupId>com.gitee.starblues</groupId>
<artifactId>springboot-plugin-framework-extension-resources</artifactId>
<version>2.4.5-RELEASE</version>
</dependency>
配置扩展
定义PluginApplication bean时, 新增该扩展。
@Bean
public PluginApplication pluginApplication(){
PluginApplication pluginApplication = new AutoPluginApplication();
// 添加插件logback扩展
autoPluginApplication.addExtension(new SpringBootLogExtension(SpringBootLogExtension.Type.LOGBACK));
// 添加插件log4j2扩展
// autoPluginApplication.addExtension(new SpringBootLogExtension(SpringBootLogExtension.Type.LOG4J));
}
插件的http资源url访问规则为: http://ip:port/pathPrefix(上述说明配置的值)/插件id/具体插件的资源路径
插件程序集成步骤
以provided
方式引入主程序依赖, 例如:
<dependency>
<groupId>your-groupId-name</groupId>
<artifactId>your-artifact-name</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
进行配置
1 在插件配置文件中新增日志配置文件路径
例如:
plugin:
log-config-location: classpath:log.xml
# log-config-location: file:D://log.xml
# 生产环境如果要使用相当路径, 建议使用如下配置(~表示当前jar包所在目录):
# log-config-location: file:~/plugin-configs/log.xml
2 配置log.xml
<?xml version="1.0" encoding="UTF-8"?>
<log>
<!-- ~:表示当前插件运行环境目录 -->
<rootDir>~\logs\</rootDir>
<fileName>current</fileName>
<level>INFO</level>
<maxFileSize>10MB</maxFileSize>
<totalFileSize>10GB</totalFileSize>
<maxHistory>30</maxHistory>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5p --- [%t] %-40.40logger{39} : %m%n</pattern>
</log>
配置文件字段说明
- rootDir: 日志存放目录, ~符号在开发环境表示当前插件模块目录, 在生产环境表示插件jar包存放目录
- fileName: 日志文件名称, 后缀固定为
.log
- level: 日志级别
- maxFileSize: 日志文件最大存储容量
- totalFileSize: 总日志文件存储最大容量
- maxHistory: 日志最大存储天数
- pattern: 日志格式
注意事项
由于插件日志是由**appender+filter**
机制去实现的, 所以所有插件模块的包名都不能一致, 要有所区分, 否则插件日志打印会混乱
集成Mybatis/Mybatis-Plus/Tk-Mybatis
包含集成
- 可集成
Mybatis
- 可集成
Mybatis-Plus
- 可集成
Tk-Mybatis
maven 仓库地址
主程序集成步骤
引入依赖
<dependency>
<groupId>com.gitee.starblues</groupId>
<artifactId>springboot-plugin-framework-extension-mybatis</artifactId>
<version>2.4.5-RELEASE</version>
</dependency>
<!-- 如果使用mybatis, 则自行引入如下依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
<!-- 如果使用mybatis-plus, 则自行引入如下依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${version}</version>
</dependency>
<!-- 如果使用tk-mybatis, 则自行引入如下依赖 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
配置扩展
定义PluginApplication bean时, 新增该扩展。
@Bean
public PluginApplication pluginApplication(){
PluginApplication pluginApplication = new AutoPluginApplication();
// 根据当前环境所集成的框架来选择类型
pluginApplication.addExtension(new SpringBootMybatisExtension(
SpringBootMybatisExtension.Type.MYBATIS));
return defaultPluginApplication;
}
注意:根据当前环境所集成的框架来选择类型
- Mybatis类型为:
SpringBootMybatisExtension.Type.MYBATIS
- Mybatis-Plus类型为:
SpringBootMybatisExtension.Type.MYBATIS_PLUS
- Tk-Mybatis类型为:
SpringBootMybatisExtension.Type.TK_MYBATIS
插件集成步骤
以provided方式引入主程序依赖, 例如:
<dependency>
<groupId>your-groupId-name</groupId>
<artifactId>your-artifact-name</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
进行配置
- 如果集成
Mybatis
, 则实现接口:com.gitee.starblues.extension.mybatis.SpringBootMybatisConfig
- 如果集成
Mybatis-Plus
, 则实现接口:com.gitee.starblues.extension.mybatis.SpringBootMybatisPlusConfig
- 如果集成
TkMybatis
, 则实现接口:com.gitee.starblues.extension.mybatis.SpringBootTkMybatisConfig
以上实现类添加注解@ConfigDefinition
例如集成Mybatis-Plus
:
@ConfigDefinition
public class MybatisConfig implements SpringBootMybatisConfig {
@Override
public Set<String> entityPackage() {
Set<String> typeAliasesPackage = new HashSet<>();
typeAliasesPackage.add("com.mybatis.plugin1.entity");
return typeAliasesPackage;
}
@Override
public Set<String> xmlLocationsMatch() {
Set<String> xmlLocationsMatch = new HashSet<>();
xmlLocationsMatch.add("classpath:mapper/*Mapper.xml");
return xmlLocationsMatch;
}
}
该步骤主要定义插件中的Mapper xml的位置。该位置的定义规则如下:
- 注意: 插件中的xml路径不能和主程序中的xml路径在
resources
相对一致, 比如文件名都为mapper
, 建议使用不同名称区分开
xmlLocationsMatch:
? 匹配一个字符
* 匹配零个或多个字符
** 匹配路径中的零或多个目录
例如:
文件路径-> file: D://xml/*PluginMapper.xml
classpath路径-> classpath: xml/mapper/*PluginMapper.xml
包路径-> package: com.plugin.xml.mapper.*PluginMapper.xml
定义的Mapper 接口需要加上注解 @Mapper
注解位置: org.apache.ibatis.annotations.Mapper
例如:
import com.gitee.starblues.extension.mybatis.annotation.PluginMapper;
import Plugin1;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface Plugin1Mapper {
/**
* 得到角色列表
* @return List
*/
List<Plugin1> getList();
/**
* 通过id获取数据
* @param id id
* @return Plugin2
*/
Plugin1 getById(@Param("id") String id);
}
如果插件不想使用主程序的配置或者数据源, 插件可自定义配置, 配置说明如下:
- 实现
enableOneselfConfig
方法, 并设置返回值为true - 实现
oneselfConfig(xx)
方法进行独立配置
- Mybatis独立配置:
/**
* 插件自主配置Mybatis的 SqlSessionFactoryBean
* SqlSessionFactoryBean 具体配置说明参考 Mybatis 官网
*/
@Override
public void oneselfConfig(SqlSessionFactoryBean sqlSessionFactoryBean) {
MysqlDataSource mysqlDataSource = new MysqlDataSource();
mysqlDataSource.setURL("jdbc:mysql://127.0.0.1:3306/xxx?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
mysqlDataSource.setUser("root");
mysqlDataSource.setPassword("root");
sqlSessionFactoryBean.setDataSource(mysqlDataSource);
}
- Mybatis-Plus独立配置:
/**
* 插件自主配置Mybatis的 SqlSessionFactoryBean
* MybatisSqlSessionFactoryBean 具体配置说明参考 Mybatis-plus 官网
*/
@Override
public void oneselfConfig(MybatisSqlSessionFactoryBean sqlSessionFactoryBean) {
...
}
- TkMybatis独立配置:
/**
* 插件自主配置Mybatis的 SqlSessionFactoryBean
* SqlSessionFactoryBean 具体配置说明参考 Mybatis 官网
*/
@Override
public void oneselfConfig(SqlSessionFactoryBean sqlSessionFactoryBean) {
...
}
/**
* 插件自主配置tk的 Config
* Config 具体配置说明参考 https://gitee.com/free/Mapper/wikis/1.1-java?sort_id=208196
* @param config Config
*/
public void oneselfConfig(Config config){
}
重新主程序配置
- 实现各个配置的
reSetMainConfig
方法进行重写 - 重写后不影响主程序的配置, 只在当前插件中起作用
集成Mybatis-Plus说明
- 集成mybatis-plus后,在插件中无法使用
LambdaQueryWrapper
条件构造器
自定义扩展
扩展 PluginApplication
1 扩展说明
- 方法一:自主实现
PluginApplication
接口。 - 方法二:继承
DefaultPluginApplication
或者AutoPluginApplication
, 重写自定义方法。
2 主程序初始化阶段,直接使用自定义的 PluginApplication
子类即可。
扩展PluginUser和PluginOperator
- 继承
PluginUser
或者PluginOperator
- 重写或者扩展自定义功能
- 继承
DefaultPluginApplication
或者AutoPluginApplication
, 重写如下方法(返回1步骤中继承的子类):
/**
* 创建插件使用者。子类可扩展
* @param applicationContext Spring ApplicationContext
* @param pluginManager 插件管理器
* @return PluginUser
*/
protected PluginUser createPluginUser(ApplicationContext applicationContext,
PluginManager pluginManager){
return new DefaultPluginUser(applicationContext, pluginManager);
}
/**
* 创建插件操作者。子类可扩展
* @param applicationContext Spring ApplicationContext
* @param pluginManager 插件管理器
* @param configuration 当前集成的配置
* @return PluginOperator
*/
protected PluginOperator createPluginOperator(ApplicationContext applicationContext,
PluginManager pluginManager,
IntegrationConfiguration configuration){
return new DefaultPluginOperator(
applicationContext,
configuration,
pluginManager,
this.listenerFactory
);
}
4 在项目初始化阶段,直接使用3步骤子类初始化即可。
针对扩展功能的实现说明
功能说明
该功能可扩展框架的额外功能,例如mybatis
和resources
两个扩展都是基于该功能开发。
实现步骤
1 新建maven模块
2 新增依赖。scope
以provided
方式引入
<dependency>
<groupId>com.gitee.starblues</groupId>
<artifactId>springboot-plugin-framework</artifactId>
<version>${springboot-plugin-framework.version}</version>
<scope>provided</scope>
</dependency>
3 继承com.gitee.starblues.extension.AbstractExtension
抽象类。
该抽象类有如下方法可扩展,扩展方式重写具体方法即可。具体方法说明如下:
/**
* 扩展唯一的key
* @return String
*/
public abstract String key();
/**
* 该扩展初始化的操作
* 主要是在插件初始化阶段被调用
* @param mainApplicationContext 主程序ApplicationContext
* @throws Exception 初始化异常
*/
public void initialize(ApplicationContext mainApplicationContext) throws Exception{
}
/**
* 返回插件的资源加载者。
* 主要是加载插件中的某些资源,比如文件、图片等。
* @return List PluginResourceLoader
*/
public List<PluginResourceLoader> getPluginResourceLoader(){
return null;
}
/**
* 返回扩展的插件中的类分组器。
* 该扩展主要是对插件中的Class文件分组,然后供 PluginPipeProcessor、PluginPostProcessor 阶段使用。
* @param mainApplicationContext 主程序ApplicationContext
* @return List PluginPipeProcessorExtend
*/
public List<PluginClassGroupExtend> getPluginClassGroup(ApplicationContext mainApplicationContext){
return null;
}
/**
* 返回扩展的插件前置处理者。
* 该扩展主要是对每一个插件进行处理
* @param mainApplicationContext 主程序ApplicationContext
* @return List PluginPipeProcessorExtend
*/
public List<PluginPreProcessorExtend> getPluginPreProcessor(ApplicationContext mainApplicationContext){
return null;
}
/**
* 返回扩展的bean定义注册者扩展
* 该扩展主要是对每一个插件进行处理
* @param mainApplicationContext 主程序ApplicationContext
* @return List PluginPipeProcessorExtend
*/
public List<PluginBeanRegistrarExtend> getPluginBeanRegistrar(ApplicationContext mainApplicationContext){
return null;
}
/**
* 返回扩展的流插件处理者。
* 该扩展主要是对每一个插件进行处理
* @param mainApplicationContext 主程序ApplicationContext
* @return List PluginPipeProcessorExtend
*/
public List<PluginPipeProcessorExtend> getPluginPipeProcessor(ApplicationContext mainApplicationContext){
return null;
}
/**
* 返回扩展的插件后置处理者。
* 该扩展主要是对全部插件进行处理。
* @param mainApplicationContext 主程序ApplicationContext
* @return List PluginPostProcessorExtend
*/
public List<PluginPostProcessorExtend> getPluginPostProcessor(ApplicationContext mainApplicationContext){
return null;
}
4 使用
在被继承的主程序中依赖该模块,然后在初始化PluginApplication阶段,将该扩展set进去。
例如:
PluginApplication pluginApplication = new AutoPluginApplication();
pluginApplication.addExtension(扩展1对象);
pluginApplication.addExtension(扩展2对象);
其他
插件案例
完整案例
位置:springboot-plugin-framework-example
案例源码启动说明
和Spring boot 启动方式一样。直接启动主类。如果插件未加载, 请检查 application-dev.yml 的如下配置。重点检查 pluginPath
配置,
该配置主要是插件代码或者jar存放的上一级目录。
plugin:
runMode: dev
pluginPath: ./example/basic-example/plugins
pluginConfigFilePath:
案例常见报错
- 插件未编译
java.lang.ClassNotFoundException: com.basic.example.plugin1.DefinePlugin
java.lang.ClassNotFoundException: com.basic.example.plugin2.DefinePlugin
该类型报错是由于插件源码没有编译成 class 文件; 需要手动编译, 保证在插件目录出现 target 文件
常见问题
插件配置文件说明
- 在开发环境:配置文件必须放在resources目录下。并且@ConfigDefinition("plugin1.yml")中定义的文件名和resources下配置的文件名一致
- 在生产环境: 该文件存放在
pluginConfigFilePath
配置的目录下。生产环境下插件的配置文件必须外置, 不能使用jar包里面的配置文件。
插件依赖问题
- 打包插件时,必须将插件包与主程序相同的依赖排除掉,不要打入jar包中,否则在生产环境下,会报错。
其中最常见的就是 Logger 的依赖,也就是某些依赖包中依赖了Logger,因此必须将该依赖包中的Logger依赖排除掉。如果不排除该日志包依赖,则会与主程序依赖冲突,就会报如下错误:
java.lang.LinkageError: loader constraint violation: when resolving method "org.slf4j.impl.StaticLoggerBinder.getLoggerFactory()Lorg/slf4j/ILoggerFactory;" the class loader (instance of org/pf4j/PluginClassLoader) of the current class, org/slf4j/LoggerFactory, and the class loader (instance of org/springframework/boot/loader/LaunchedURLClassLoader) for the method's defining class, org/slf4j/impl/StaticLoggerBinder, have different Class objects for the type org/slf4j/ILoggerFactory used in the signature
从kafka依赖中排除slf4j依赖的例子:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.12</artifactId>
<version>${kafka_2.12.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
runMode 说明
- 如果 runMode 设置为 prod. 则加载的插件必须为 jar 文件。
- 如果 runMode 设置为 dev. 则加载的插件必须为 class 文件。也就是 target 下的文件。
常见错误
插件未编译错误
java.lang.ClassNotFoundException: com.basic.example.plugin1.DefinePlugin
java.lang.ClassNotFoundException: com.basic.example.plugin2.DefinePlugin
- 开发环境下报该错误:由于插件源码没有编译成 class 文件; 需要手动编译, 保证在插件目录出现 target 文件
- 生成环境下报该错误:必须将配置文件的 runMode 设置为 prod
无法加载插件
请检查配置文件中的 pluginPath
如果 pluginPath 配置为相当路径,请检查是否是相对于当前工作环境的目录。
如果 pluginPath 配置为绝对路径,请检查路径是否正确。
Spring包冲突
如果出现Spring包冲突。可以排除Spring包。
例如:
<dependency>
<groupId>com.gitee.starblues</groupId>
<artifactId>springboot-plugin-framework</artifactId>
<version>${springboot-plugin-framework.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
</exclusions>
</dependency>
自行选择排除冲突的包。
框架依赖的包详见 springboot-plugin-framework/pom.xml
插件的单独设置的配置文件无法加载
- 检查
@ConfigDefinition("plugin1.yml")
注解中的值是否和配置文件名称一致。 - 检查 主程序配置的
pluginConfigFilePath
插件配置文件路径是否正确。开发环境下请将所以插件的配置文件放在统一的目录下,然后将pluginConfigFilePath
配置该目录。开发环境下该值一般为空,程序会自动从resources
获取配置文件, 所以请确保编译后的target 下存在该配置文件。
mvn clean install 打包插件时,提示找不到主程序依赖
该问题是spring-boot打包插件打包主程序时,将源项目结构更改,因此需要配置Spring-boot 打包插件如下:
Springboot高版本 2.1.x 以上的配置如下
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<classifier>exec</classifier>
</configuration>
</execution>
</executions>
</plugin>
Springboot低版本高版本2.1.0 以下的配置如下
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<classifier>exec</classifier>
</configuration>
</execution>
</executions>
</plugin>
Springboot 热部署问题说明
- 如果依赖并使用了SpringBoot热部署功能,会导致该框架的功能无法正常运行。例如会出现
主程序无法获取插件实现的Bean
、注入错误
、类的类型转换错误
等不可预知的问题。 - 如果存在 SpringBoot 热部署依赖,请自动移除。
Springboot 热部署依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
插件卸载时出现文件被占用问题
- 该问题是Springboot 高版本打包插件的问题,建议换成
2.1.7.RELEASE
版本,如下所示:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.1.7.RELEASE</version>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<classifier>exec</classifier>
</configuration>
</execution>
</executions>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
-------------------------------------------
个性签名:独学而无友,则孤陋而寡闻。做一个灵魂有趣的人!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
万水千山总是情,打赏一分行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!