EXP (Ex
tension P
lugin) 扩展点插件系统
名词定义:
- 主应用
- exp 需要运行在一个 jvm 之上, 通常, 这是一个 springboot, 这个 springboot 就是主应用;
- 扩展点
- 主应用定义的接口, 可被插件实现;
- 注意:插件是扩展点的具体实现集合,扩展点,仅仅是接口定义。一个插件里,可以有多个扩展点的实现,一个扩展点,可以有多个插件的实现。
- 插件
- 扩展功能使用插件的方式支持,你可以理解为 idea、eclipse 里的插件。
- 插件里的代码写法和 spring 一样(如果你的程序是在 spring 里运行)
- 热插拔
- 插件支持从 jvm 和 spring 容器里摘除.
- 支持运行时动态安装 jar 和 zip;
-
贵州茅台和五粮液都购买了你司的标准产品, 但是. 由于客户有定制需求. 需要开发新功能.
-
贵州茅台客户定制了 2 个插件;
-
五粮液客户定制了 3 个插件;
-
程序运行时, 会根据客户的租户 id 进行逻辑切换.
场景:
- B 端大客户对业务进行定制, 需要对主代码扩展.
- 传统做法是 git 拉取分支.
- 现在基于扩展点的方式进行定制, 可热插拔
- 需要多个程序可分可合, 支持将多个 springboot 应用合并部署, 或拆开部署.
- 扩展点类似 swagger 文档 doc, 用于类插件系统管理平台进行展示.
- 支持 热插拔 or 启动时加载(spring or 普通 jvm)
- 基于 classloader 双亲委派的类隔离机制
- 支持多租户场景下的单个扩展点有多实现, 业务支持租户过滤, 租户多个实现可自定义排序
- 支持 springboot2.x/1.x 依赖
- 支持插件内对外暴露 Spring Controller Rest, 可热插拔;
- 支持插件覆盖 spring 主程序 Controller.
- 支持插件获取独有的配置, 支持自定义设计插件配置热更新逻辑;
- 支持插件和主应用绑定事务.
- 提供流式 api,使主应用在接入扩展点时更干净。
环境准备:
- JDK 1.8
- 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>
ExpAppContext expAppContext = ExpAppContextSpiFactory.getFirst();
public String run(String tenantId) {
List<UserService> userServices = expAppContext.get(UserService.class, TenantCallback.DEFAULT);
// 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 依赖
public interface ExpAppContext {
/**
* 获取当前所有的插件 id
*/
List<String> getAllPluginId();
/**
* 加载插件
*/
Plugin load(File file) throws Throwable;
/**
* 卸载插件
*/
void unload(String pluginId) throws Exception;
/**
* 获取多个扩展点的插件实例
*/
<P> List<P> get(String extCode);
/**
* 简化操作, code 就是全路径类名
*/
<P> List<P> get(Class<P> pClass);
/**
* 获取单个插件实例.
*/
<P> Optional<P> get(String extCode, String pluginId);
}
public interface StreamAppContext {
/**
* 针对有返回值的 api, 需要支持流式调用
*/
<R, P> R listStream(Class<P> pClass, Ec<R, List<P>> ecs);
/**
* 针对有返回值的 api, 需要支持流式调用
*/
<R, P> R stream(Class<P> clazz, String pluginId, Ec<R, P> ec);
}
cn.think.in.java.open.exp.client.TenantCallback
public interface TenantCallback {
/**
* 返回这个插件的序号, 默认 0;
* {@link cn.think.in.java.open.exp.client.ExpAppContext#get(java.lang.Class)} 函数返回的List 的第一位就是 sort 最高的.
*/
int getSort(String pluginId);
/**
* 这个插件是否属于当前租户, 默认是;
* 这个返回值, 会影响 {@link cn.think.in.java.open.exp.client.ExpAppContext#get(java.lang.Class)} 的结果
* 即进行过滤, 返回为 true 的 plugin 实现, 才会被返回.
*/
boolean filter(String pluginId);
}
租户过滤示例代码:
TenantCallback registerCallback = new TenantCallback() {
@Override
public int getSort(String pluginId) {
// 获取这个插件的排序
return sortMap.get(pluginId);
}
@Override
public boolean filter(String pluginId) {
// 判断当前租户是不是这个匹配这个插件
return context.get().equals(pluginIdTenantIdMap.get(pluginId));
}
}
;
List<UserService> userServices = expAppContext.get(UserService.class, registerCallback);
// first 第一个就是这个租户优先级最高的.
Optional<UserService> optional = userServices.stream().findFirst();
插件获取配置示例代码:
public class Boot extends AbstractBoot {
private static String selfPluginId;
@Override
protected String getScanPath() {
return Boot.class.getPackage().getName();
}
@Override
public void setPluginId(String pluginId) {
// 系统自动注入自身的插件 id;
selfPluginId = pluginId;
}
public static String get(String key, String value) {
// 简化操作, 读取配置
return PluginConfig.getSpi().getProperty(selfPluginId, key, value);
}
}
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}