Solona address : vkHYqcvxocKjzL8F9t3G6p83YzuapXFEXV6YPXa8hNp
🚕🚕🚕EXP (Extension Plugin) 扩展点插件系统
相关文章🎯🎯🎯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>ExpAppContextexpAppContext = ExpAppContextSpiFactory.getFirst();
publicStringrun(StringtenantId) {
List<UserService> userServices = expAppContext.get(UserService.class);
// first 第一个就是这个租户优先级最高的.Optional<UserService> optional = userServices.stream().findFirst();
if (optional.isPresent()) {
optional.get().createUserExt();
} else {
return"not found";
}
}
publicStringinstall(Stringpath, StringtenantId) throwsThrowable {
Pluginplugin = expAppContext.load(newFile(path));
returnplugin.getPluginId();
}
publicStringunInstall(StringpluginId) throwsException {
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 依赖
publicinterfaceExpAppContext {
/** * 获取当前所有的插件 id */List<String> getAllPluginId();
/** * 预加载, 只读取元信息和 load boot class 和配置, 不做 bean 加载. */PluginpreLoad(Filefile);
/** * 加载插件 */Pluginload(Filefile) throwsThrowable;
/** * 卸载插件 */voidunload(StringpluginId) throwsException;
/** * 获取多个扩展点的插件实例 */
<P> List<P> get(StringextCode);
/** * 获取多个扩展点的插件实例 */
<P> List<P> get(Class<P> pClass);
/** * 获取单个插件实例. */
<P> Optional<P> get(StringextCode, StringpluginId);
}可在获取实例过程中过滤扩展点实现
publicinterfacePluginFilter {
<T> List<FModel<T>> filter(List<FModel<T>> list);
@DataclassFModel<T> {
Tt;
StringpluginId;
publicFModel(Tt, StringpluginId) {
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 维度, 方便基线管理各个插件的配置
publicinterfacePluginConfig {
StringgetProperty(StringpluginId, Stringkey, StringdefaultValue);
}插件获取配置示例代码:
publicclassBootextendsAbstractBoot {
// 定义配置, key name 和 Default value;publicstaticConfigSupportconfigSupport = newConfigSupport("bv2", null);
}
publicStringhello() {
returnconfigSupport.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 classplugin.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
