Skip to content
/ exp Public

Java extension plugin and hot swap plugin(Java 扩展点/插件系统,支持热插拔,旨在解决大部分软件的功能定制问题)

License

Notifications You must be signed in to change notification settings

stateIs0/exp

Repository files navigation

⭐️⭐️⭐️Introduction

🚕🚕🚕EXP (Extension Plugin) 扩展点插件系统

相关文章🎯🎯🎯EXP 一款 Java 插件化热插拔框架

名词定义:

  1. 🏅主应用
    • exp 需要运行在一个 jvm 之上, 通常, 这是一个 springboot, 这个 springboot 就是主应用;
  2. 🎖扩展点
    • 主应用定义的接口, 可被插件实现;
    • 注意:插件是扩展点的具体实现集合,扩展点,仅仅是接口定义。一个插件里,可以有多个扩展点的实现,一个扩展点,可以有多个插件的实现。
  3. 🥇插件
    • 扩展功能使用插件的方式支持,你可以理解为 idea、eclipse 里的插件。
    • 插件里的代码写法和 spring 一样(如果你的程序是在 spring 里运行)
    • 插件是类隔离的,支持 parent-first
  4. 🥈热插拔
    • 插件支持从 jvm 和 spring 容器里摘除.
    • 支持运行时动态安装 jar 和 zip;

🎧Example

  • 比亚迪和华为、魅族都购买了你司的标准产品, 但是,由于客户有定制需求. 需要开发新功能.

  • 比亚迪客户定制了 2 个插件;

  • 华为客户定制了 3 个插件;

  • 魅族客户定制了 3 个插件;

  • 程序运行时, 会根据客户的租户 id 进行逻辑切换.

场景:

  1. B 端大客户对业务进行定制, 需要对主代码扩展.
    • 传统做法是 git 拉取分支.
    • 现在基于扩展点的方式进行定制, 可热插拔
  2. 需要多个程序可分可合, 支持将多个 springboot 应用合并部署, 或拆开部署.
  3. 扩展点类似 swagger 文档 doc, 用于类插件系统管理平台进行展示.

Feature

  1. 支持 热插拔 or 启动时加载(Spring or 普通 jvm)
  2. 支持 Classloader Parent First 的类隔离机制(通过配置指定模式)
  3. 支持多租户场景下的单个扩展点有多实现, 业务支持租户过滤, 租户多个实现可自定义排序
  4. 支持 Springboot3.x/2.x/1.x 依赖
  5. 支持插件内对外暴露 Spring Controller Rest, 可热插拔;
  6. 支持插件覆盖 Spring 主程序 Controller.
  7. 支持插件获取独有的配置, 支持自定义设计插件配置热更新逻辑;
  8. 支持插件和主应用绑定事务.
  9. 提供流式 api,使主应用在接入扩展点时更干净。
  10. 支持插件 log 逻辑隔离, 支持 slf4j MDC, 支持修改插件 logger 名称
  11. 提供类似 swagger 的文档注释, 可暴露 JSON 格式的扩展点文档
  12. 提供 maven 辅助插件,可在打包时自动热部署代码,秒级部署新代码;

USE

环境准备:

  1. JDK 1.8
  2. Maven
git clone [email protected]:stateIs0/exp.git
cd all-package
mvn clean package

主程序依赖(springboot starter)

<dependency>
    <groupId>cn.think.in.java</groupId>
    <!-- 这里是 springboot 2 例子, 如果是普通应用或者 springboot 1 应用, 请进行 artifactId 更换  -->
    <artifactId>open-exp-adapter-springboot2-starter</artifactId>
</dependency>

插件依赖

<dependency>
   <groupId>cn.think.in.java</groupId>
   <artifactId>open-exp-plugin-depend</artifactId>
</dependency>

编程界面 API 使用

ExpAppContext expAppContext = ExpAppContextSpiFactory.getFirst();

public String run(String tenantId) {
   List<UserService> userServices = expAppContext.get(UserService.class);
   // first 第一个就是这个租户优先级最高的.
   Optional<UserService> optional = userServices.stream().findFirst();
   if (optional.isPresent()) {
       optional.get().createUserExt();
   } else {
       return "not found";
   }
}


public String install(String path, String tenantId) throws Throwable {
  Plugin plugin = expAppContext.load(new File(path));
  return plugin.getPluginId();
}

public String unInstall(String pluginId) throws Exception {
  log.info("plugin id {}", pluginId);
  expAppContext.unload(pluginId);
  return "ok";
}

模块

  1. all-package 打包模块
  2. bom-manager pom 管理, 自身管理和三方依赖管理
  3. open-exp-code exp 核心代码
  4. example exp 使用示例代码
  5. spring-adapter springboot starter, exp 适配 spring boot

模块依赖

核心 API

public interface ExpAppContext {
   /**
    * 获取当前所有的插件 id
    */
   List<String> getAllPluginId();
   
   /**
    * 预加载, 只读取元信息和 load boot class 和配置, 不做 bean 加载.
    */
   Plugin preLoad(File file);
   
   /**
    * 加载插件
    */
   Plugin load(File file) throws Throwable;

   /**
    * 卸载插件
    */
   void unload(String pluginId) throws Exception;

   /**
    * 获取多个扩展点的插件实例
    */
   <P> List<P> get(String extCode);

   /**
    * 获取多个扩展点的插件实例
    */
   <P> List<P> get(Class<P> pClass);
   
   /**
    * 获取单个插件实例.
    */
   <P> Optional<P> get(String extCode, String pluginId);
}

SPI 扩展

cn.think.in.java.open.exp.client.PluginFilter

可在获取实例过程中过滤扩展点实现

public interface PluginFilter {

   <T> List<FModel<T>> filter(List<FModel<T>> list);

   @Data
   class FModel<T> {
      T t;
      String pluginId;

      public FModel(T t, String pluginId) {
         this.t = t;
         this.pluginId = pluginId;
      }
   }
}
// 假如实现了 PluginFilter SPI 接口, 可进行自定义过滤
List<UserService> userServices = expAppContext.get(UserService.class);
// first 第一个就是这个租户优先级最高的.
Optional<UserService> optional = userServices.stream().findFirst();

cn.think.in.java.open.exp.client.PluginConfig

插件配置 SPI, 相较于普通的 config api, 会多出一个 pluginId 维度, 方便基线管理各个插件的配置

public interface PluginConfig {
    String getProperty(String pluginId, String key, String defaultValue);
}

插件获取配置示例代码:

public class Boot extends AbstractBoot {
    // 定义配置, key name 和 Default value;
   public static ConfigSupport configSupport = new ConfigSupport("bv2", null);
}
public String hello() {
   return configSupport.getProperty();
}

插件核心其他配置

springboot 配置项(-D 或 application.yml 都支持):

plugins_path={springboot 启动时, exp主动加载的插件目录}
plugins_work_dir={exp 的工作目录, 其会将代码解压达成这个目录里,子目录名为插件 id}
plugins_auto_delete_enable={是否自动删除已经存在的 plugin 目录}
plugins_spring_url_replace_enable={插件是否可以覆盖主程序 url, 注意, 目前无法支持多租户级别的覆盖}
exp_object_field_config_json={插件动态增加字段json, json 结构定义见: cn.think.in.java.open.exp.object.field.ext.ExtMetaBean}

插件配置

  1. pluginMeta.properties
# 插件 boot class
plugin.boot.class=cn.think.in.java.open.exp.example.empty.Boot
# code 名 不能为空
plugin.code=example.plugin.empty
# 描述
plugin.desc=this a plugin a empty demo
# 版本
plugin.version=1.0.0
  1. extension.properties

扩展点映射

cn.think.in.java.open.exp.adapter.springboot2.example.UserService=\
  cn.think.in.java.open.exp.example.b.UserPlugin

License

Apache 2.0 License.

Stargazers over time

Stargazers over time

About

Java extension plugin and hot swap plugin(Java 扩展点/插件系统,支持热插拔,旨在解决大部分软件的功能定制问题)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages