EXP 一款 Java 插件化热插拔框架
前言
多年以来,ToB 的应用程序都面临定制化需求应该怎么搞的问题。
举例,大部分本地化软件厂家,都有一个标准程序,这个程序支持大部分企业的功能需求,但面对世界 500 强等大客户时,他们的特殊需求,厂家通常是无法拒绝的(通常因为订单大,给的多,可背书)。比如使用非标准数据库,业务流程里加入一些安全检查等,回调里加入一些定制字段等;
由此而来的需求,一般有几种解决方案;
分支开发,他的好处是效率非常快,噼里啪啦一顿改,定制需求就完成了。但是,这种方式会带来一个致命的问题:后期程序升级成本非常巨大。
本地化程序和 saas 服务不同,本地化的程序通常是需要手动升级的,使用分支开发,升级的方式无非就是 merge 分支,解决冲突。
如果改动很小,merge 倒也问题不大。
但是如果改动很大,merge 的方式,会带来很大的问题。因为如果举例定制开发的时间久了,当时拉分支改的代码和后面的标准产品迭代代码早就不兼容了,此时,merge 升级就非常困难。
由此,我们思考,到底什么样的方式才能解决这种场景;
目前来看,使用插件机制扩展客户需求,是其中一种方式。其本质用简单一句话概括:主程序预留扩展接口,定制客户实现接口逻辑。
你可以将其理解为一种更复杂的策略模式或者 SPI;只是,插件通常是 classloader 类隔离的。
大概如下图:
本文我们假设插件系统是当前解决定制需求痛点的方案之一,那我们今天就来设计一下这个插件系统。
设计
首先分析需求,插件系统需要哪些功能:
需要 7788 差不多了,我们来设计一下编程界面。
public interface ExpAppContext {
/**
* 加载插件
*/
Plugin load(File file) throws Throwable;
/**
* 卸载插件
*/
void unload(String id) throws Exception;
/**
* 获取多个扩展点的插件实例
*/
List
get(String extCode);
/**
* 简化操作, code 就是全路径类名
*/
List
get(Class
pClass);
/**
* 获取单个插件实例.
*/
P get(String extCode, String pluginId);
}
ExpAppContext 接口,作为核心模型,提供以下能力
这几个 API 可以实现插件的基本功能。
我们再添加关于租户的 API
public interface TenantService {
/**
* 获取 TenantCallback 扩展逻辑;
*/
default TenantCallback getTenantCallback() {
return TenantCallback.TenantCallbackMock.instance;
}
/**
* 设置 callback;
*/
default void setTenantCallback(TenantCallback callback) {
}
}
public interface TenantCallback {
/**
* 返回这个插件的序号, 默认 0;
* {@link cn.think.in.java.open.exp.client.ExpAppContext#get(java.lang.Class)} 函数返回的List 的第一位就是 sort 最高的.
*/
Integer getSort(String pluginId);
/**
* 这个插件是否属于当前租户, 默认是;
* 这个返回值, 会影响 {@link cn.think.in.java.open.exp.client.ExpAppContext#get(java.lang.Class)} 的结果
* 即进行过滤, 返回为 true 的 plugin 实现, 才会被返回.
*/
Boolean isOwnCurrentTenant(String pluginId);
}
在调用 ExpAppContext#get 时,需要过滤租户实现,还需要对单个租户的多个实现进行排序。用户可以实现自己的 getSort(pluginId) 和 isOwnCurrentTenant(pluginId) 逻辑。
API 有了,我们的编程界面就出来了,他应该是这样的:
public static void main(String[] args) throws Throwable {
Class extensionClass = UserService.class;
ExpAppContext expAppContext = Bootstrap.bootstrap("exp-plugins/", "workdir-simple-java-app");
expAppContext.setTenantCallback(new TenantCallback() {
@Override
public Integer getSort(String pluginId) {
return new Random().nextInt(10);
}
@Override
public Boolean isOwnCurrentTenant(String pluginId) {
return true;
}
});
Optional first = expAppContext.get(extensionClass).stream().findFirst();
first.ifPresent(userService -> {
System.out.println(userService.getClass());
System.out.println(userService.getClass().getClassLoader());
userService.createUserExt();
});
}
读取插件配置 API:
public interface PluginConfig {
String getProperty(String pluginId, String key, String defaultValue);
}
注意这个 API 和正常的 config api 不同,他新增了 pluginId 维度,使插件配置之间是互相隔离的。具体的 PluginConfig 还可以根据租户再进行配置隔离。
表面的 API 已经差不多了,内部的实现,需要开始了,比如
开发
具体细节本文不再展开,因为代码都在 github stateis0/exp 项目里,这个项目包含实现代码,example 代码,api 使用,适配 springboot starter,最佳实践等。
项目代码结构依赖:
总结
EXP 全称: Extension Plugin 扩展点插件系统;
希望本项目可以帮助你解决本地化软件的定制需求问题。同时,也欢迎为本项目提 issue,pr 等。
项目地址 EXP 扩展点插件系统 for Github
欢迎 star 交流。