Solona address : vkHYqcvxocKjzL8F9t3G6p83YzuapXFEXV6YPXa8hNp
🚕🚕🚕EXP (Ex
tension P
lugin) 扩展点插件系统
相关文章🎯🎯🎯EXP 一款 Java 插件化热插拔框架
名词定义:
- 🏅主应用
- exp 需要运行在一个 jvm 之上, 通常, 这是一个 springboot, 这个 springboot 就是主应用;
- 🎖扩展点
- 主应用定义的接口, 可被插件实现;
- 注意:插件是扩展点的具体实现集合,扩展点,仅仅是接口定义。一个插件里,可以有多个扩展点的实现,一个扩展点,可以有多个插件的实现。
- 🥇插件
- 扩展功能使用插件的方式支持,你可以理解为 idea、eclipse 里的插件。
- 插件里的代码写法和 spring 一样(如果你的程序是在 spring 里运行)
- 插件是类隔离的,支持 parent-first
- 🥈热插拔
- 插件支持从 jvm 和 spring 容器里摘除.
- 支持运行时动态安装 jar 和 zip;
-
比亚迪和华为、魅族都购买了你司的标准产品, 但是,由于客户有定制需求. 需要开发新功能.
-
比亚迪客户定制了 2 个插件;
-
华为客户定制了 3 个插件;
-
魅族客户定制了 3 个插件;
-
程序运行时, 会根据客户的租户 id 进行逻辑切换.
场景:
- B 端大客户对业务进行定制, 需要对主代码扩展.
- 传统做法是 git 拉取分支.
- 现在基于扩展点的方式进行定制, 可热插拔
- 需要多个程序可分可合, 支持将多个 springboot 应用合并部署, 或拆开部署.
- 扩展点类似 swagger 文档 doc, 用于类插件系统管理平台进行展示.
- 支持 热插拔 or 启动时加载(Spring or 普通 jvm)
- 支持 Classloader Parent First 的类隔离机制(通过配置指定模式)
- 支持多租户场景下的单个扩展点有多实现, 业务支持租户过滤, 租户多个实现可自定义排序
- 支持 Springboot3.x/2.x/1.x 依赖
- 支持插件内对外暴露 Spring Controller Rest, 可热插拔;
- 支持插件覆盖 Spring 主程序 Controller.
- 支持插件获取独有的配置, 支持自定义设计插件配置热更新逻辑;
- 支持插件和主应用绑定事务.
- 提供流式 api,使主应用在接入扩展点时更干净。
- 支持插件 log 逻辑隔离, 支持 slf4j MDC, 支持修改插件 logger 名称
- 提供类似 swagger 的文档注释, 可暴露 JSON 格式的扩展点文档
- 提供 maven 辅助插件,可在打包时自动热部署代码,秒级部署新代码;
环境准备:
- JDK 1.8
- Maven
git clone git@github.com: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>
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";
}
- all-package 打包模块
- bom-manager pom 管理, 自身管理和三方依赖管理
- exp-one-bom 自身包管理
- exp-third-bom 三方包管理
- open-exp-code exp 核心代码
- open-exp-classloader-container classloader 隔离 API
- open-exp-classloader-container-impl classloader 隔离 API 具体实现
- open-exp-client-api 核心 api 模块
- open-exp-core-impl 核心 api 实现; 内部 shade cglib 动态代理, 可不以来 spring 实现;
- open-exp-document-api 扩展点文档 api
- open-exp-document-core-impl 扩展点文档导出实现
- open-exp-object-field-extend 字节码动态扩展字段模块
- open-exp-plugin-depend exp 插件依赖
- example exp 使用示例代码
- example-extension-define 示例扩展点定义
- example-plugin1 示例插件实现 1
- example-plugin2 示例插件实现 2
- example-springboot1 示例 springboot 1.x 例子
- example-springboot2 示例 springboot 2.x 例子; 使用 spring cglib 动态代理
- spring-adapter springboot starter, exp 适配 spring boot
- open-exp-adapter-springboot2 springboot2 依赖
- open-exp-adapter-springboot1-starter springboot1 依赖
- feature/springboot3 springboot3 依赖
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);
}
可在获取实例过程中过滤扩展点实现
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();
插件配置 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}
# 插件 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
扩展点映射
cn.think.in.java.open.exp.adapter.springboot2.example.UserService=\
cn.think.in.java.open.exp.example.b.UserPlugin