springboot插件式开发 springboot-plugin-framework-v2.4.5使用文档

功能介绍

简介

文章备份自:https://www.yuque.com/starblues

介绍

此框架可在SpringBoot项目上开发出用于扩展项目的插件,可在插件模块中单独定义接口、静态文件、mybatis-xml等扩展功能。

核心功能

  • 插件配置式插拔于springboot项目。
  • 在springboot上可以进行插件式开发, 扩展性极强, 可以针对不同项目开发不同插件, 进行不同插件jar包的部署。
  • 可通过配置文件指定要启用或者禁用插件。
  • 支持上传插件和插件配置文件到服务器, 并且无需重启主程序, 动态部署插件、更新插件。
  • 支持查看插件运行状态, 查看插件安装位置。
  • 无需重启主程序, 动态的安装插件、卸载插件、启用插件、停止插件、备份插件、删除插件。
  • 在插件应用模块上可以使用Spring注解定义组件, 进行依赖注入。
  • 支持在插件中开发Rest接口。
  • 支持在插件中单独定义持久层访问等需求。
  • 可以遵循主程序提供的插件接口开发任意扩展功能。
  • 支持注解进行任意业务场景扩展, 并使用定义的坐标进行场景命中。
  • 插件可以根据生产和开发环境自定义独立的配置文件。目前只支持yml文件。
  • 支持自定义扩展开发接口, 使用者可以在预留接口上扩展额外功能。
  • 支持插件之间的通信。
  • 支持插件接口文档: SwaggerSpringDoc
  • 插件支持拦截器的定制开发。

快速入门

新建项目

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";
    }

}

项目启动

  1. 手动将插件模块进行编译, 确保target目录下存在编译的class文件
  2. 启动主程序, 观察日志出现如下信息, 说明插件加载启动成功

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

结构说明:

  1. main.jar 为主程序。
  2. main.yml 为主程序配置文件。
  3. plugins 为插件存放的目录。plugin1.jar、plugin2.jar 分别为两个插件。
  4. plugin-configs 插件的配置文件存放位置。plugin1.yml、plugin2.yml 分别为 plugin1.jar、plugin2.jar 的配置。
  5. 不一定每个插件都需要配置文件,可根据需求来。如果代码中定义了配置文件,则启动时需要将配置文件存放到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

结构说明:

  1. pom.xml 代表maven的pom.xml
  2. plugin.properties 为开发环境下, 插件的元信息配置文件, 配置内容详见下文。
  3. example 为项目的总Maven目录。
  4. example-runner 在运行环境下启动的模块。主要依赖example-main模块和插件中使用到的依赖包, 并且解决开发环境下无法找到插件依赖包的问题。
  5. example-main 该模块为项目的主程序模块。
  6. example-plugin-parent 该模块为插件的父级maven pom 模块, 主要定义插件中公共用到的依赖, 以及插件的打包配置。
  7. plugins 该文件夹下主要存储插件模块。上述模块中主要包括example-plugin1、example-plugin2 两个插件。
  8. 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

  1. 自行引入Swagger依赖
  2. 配置启用Swagger接口刷新机制
    将配置项enableSwaggerRefresh设置为true (框架已经默认为启用了)
  3. 配置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>

集成插件步骤

集成步骤

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.ymlplugin1-prod.yml文件,分别对应开发环境和生产环境下的配置文件。

开发环境配置文件读取说明

开发环境的配置文件将会从resources下读取,因此一定记着将配置文件存放在当前插件的resources目录下,而且保证运行时target->classes 目录下存在该配置文件,否则启动阶段会报找不到配置文件的异常。

读取的文件为: plugin1-dev.yml

生产环境下配置文件读取说明

在生产环境下配置文件会有如下读取顺序:

  1. 从当前配置的插件jar目录下加载配置文件。加载不到走2步骤。
  2. pluginConfigFilePath 配置目录下加载配置文件。加载不到走3步骤。
  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());
    }

}

插件之间方法调用

原理

插件之间的数据交互功能, 是在同一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 +
                    '}';
        }
    }

}

在任意插件中实现该接口定义的业务扩展

  1. 实现第一步骤定义的接口
  2. 新增扩展注解 @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;
    }
}

调用扩展

  1. 在主程序或者任意插件中注入扩展工厂com.gitee.starblues.factory.process.pipe.extract.ExtractFactory
  2. 使用扩展工厂提供的如下接口进行调用扩展:
/**
 * 通过坐标得到扩展
 * @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 版本以上支持

介绍

针对插件配置文件存在如下问题

  1. 无法进行注入主程序Bean
  2. 无法获取当前插件其他配置Bean

因此为了弥补以上问题, 引入ConfigDefinitionTip.

使用

  1. 在当前插件配置类中定义出ConfigDefinitionTip属性
  2. 使用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 仓库地址

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 仓库地址

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 仓库地址

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);

}

如果插件不想使用主程序的配置或者数据源, 插件可自定义配置, 配置说明如下:

  1. 实现enableOneselfConfig方法, 并设置返回值为true
  2. 实现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){
}

重新主程序配置

  1. 实现各个配置的 reSetMainConfig 方法进行重写
  2. 重写后不影响主程序的配置, 只在当前插件中起作用

集成Mybatis-Plus说明

  • 集成mybatis-plus后,在插件中无法使用 LambdaQueryWrapper 条件构造器

自定义扩展

扩展 PluginApplication

1 扩展说明

  • 方法一:自主实现 PluginApplication 接口。
  • 方法二:继承 DefaultPluginApplication 或者 AutoPluginApplication, 重写自定义方法。

2 主程序初始化阶段,直接使用自定义的 PluginApplication 子类即可。

扩展PluginUser和PluginOperator

  1. 继承 PluginUser 或者 PluginOperator
  2. 重写或者扩展自定义功能
  3. 继承 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步骤子类初始化即可。

针对扩展功能的实现说明

功能说明

该功能可扩展框架的额外功能,例如mybatisresources 两个扩展都是基于该功能开发。

实现步骤

1 新建maven模块

2 新增依赖。scopeprovided方式引入

<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:

案例常见报错

  1. 插件未编译
    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包里面的配置文件。

插件依赖问题

  1. 打包插件时,必须将插件包与主程序相同的依赖排除掉,不要打入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 说明

  1. 如果 runMode 设置为 prod. 则加载的插件必须为 jar 文件。
  2. 如果 runMode 设置为 dev. 则加载的插件必须为 class 文件。也就是 target 下的文件。

常见错误

插件未编译错误

java.lang.ClassNotFoundException: com.basic.example.plugin1.DefinePlugin
java.lang.ClassNotFoundException: com.basic.example.plugin2.DefinePlugin

  1. 开发环境下报该错误:由于插件源码没有编译成 class 文件; 需要手动编译, 保证在插件目录出现 target 文件
  2. 生成环境下报该错误:必须将配置文件的 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>
posted @ 2023-02-28 17:42  你樊不樊  阅读(1873)  评论(0编辑  收藏  举报