EXP 一款 Java 插件化热插拔框架

EXP 一款 Java 插件化热插拔框架

前言

多年以来,ToB 的应用程序都面临定制化需求应该怎么搞的问题。

举例,大部分本地化软件厂家,都有一个标准程序,这个程序支持大部分企业的功能需求,但面对世界 500 强等大客户时,他们的特殊需求,厂家通常是无法拒绝的(通常因为订单大,给的多,可背书)。比如使用非标准数据库,业务流程里加入一些安全检查等,回调里加入一些定制字段等;

由此而来的需求,一般有几种解决方案;

  1. 将这个需求做进标准产品里。让这个功能有个配置开关,也可以被其他的客户使用,通常这类需求可能比较"通用"
  2. 由于客户工期时间紧,虽然功能"通用",但奈何时间不足,无法排进标准产品里,只能使用 git 拉一个标准的客户分支来进行开发,后期可能将其 merge 到标准产品里;
  3. 这个功能太不常见了,无法放到到标准产品里,那就直接拉新的 git 分支开发;

分支开发,他的好处是效率非常快,噼里啪啦一顿改,定制需求就完成了。但是,这种方式会带来一个致命的问题:后期程序升级成本非常巨大。

本地化程序和 saas 服务不同,本地化的程序通常是需要手动升级的,使用分支开发,升级的方式无非就是 merge 分支,解决冲突。

如果改动很小,merge 倒也问题不大。 但是如果改动很大,merge 的方式,会带来很大的问题。因为如果举例定制开发的时间久了,当时拉分支改的代码和后面的标准产品迭代代码早就不兼容了,此时,merge 升级就非常困难。

由此,我们思考,到底什么样的方式才能解决这种场景;

目前来看,使用插件机制扩展客户需求,是其中一种方式。其本质用简单一句话概括:主程序预留扩展接口,定制客户实现接口逻辑。

你可以将其理解为一种更复杂的策略模式或者 SPI;只是,插件通常是 classloader 类隔离的。

大概如下图:

本文我们假设插件系统是当前解决定制需求痛点的方案之一,那我们今天就来设计一下这个插件系统。

设计

首先分析需求,插件系统需要哪些功能:

  1. 通常主程序定义接口,插件实现逻辑;即这个功能在主程序里是空的。在客户侧是安装的。那么,我们可能需要一个可插拔的功能,即需要的时候,我们安装,不需要的时候,不安装或者卸载。
  2. 需要热插拔吗?我想不是必须的,但如果每次都需要重启才能调整插件,用户体验会很不好。那我们就加上热插拔吧。
  3. 插件里可以写 servlet or spring rest api 吗?我想是需要的。插件里可以对外新增接口,为定制客户提供新的服务能力。对了,插件还得支持事务,Mybatis ,AOP,RPC 等。
  4. 一个扩展点可以有多个实现,那这个扩展点可以同时存在多个插件吗?我想是需要的,比方说对接短信服务商,a 客户走 s1 厂商,b 客户走 s2 厂商。另外,一个客户可能对同一个扩展点有多个实现,此时可能需要更复杂的路由策略,那么,这个时候,我们可以提供一种机制,支持这种策略。
  5. 插件里的配置怎么办?插件配置通常是可以热更新的,且通常是在一个单独的插件系统里配置的。此时,我们需要提供一个区分于 spring application.yml 的配置策略,即几个基于插件维度的配置 API。
  6. 出于安全考虑,插件包类型不仅仅支持 jar 包,还需要支持 zip 包。
  7. 插件的技术问题,要支持类隔离,否则,如果插件开发者引入了一个有问题的 lib 或版本不兼容的 lib,将会导致灾难。另外,无法保证各个插件之间的包名完全不同。

需要 7788 差不多了,我们来设计一下编程界面。

  1. 入口 API
java 复制代码
public interface ExpAppContext {

    /**
     * 加载插件
     */
    Plugin load(File file) throws Throwable;

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

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

    /**
     * 简化操作, code 就是全路径类名
     */
    <P> List<P> get(Class<P> pClass);

    /**
     * 获取单个插件实例.
     */
    <P> P get(String extCode, String pluginId);
}

ExpAppContext 接口,作为核心模型,提供以下能力

  1. 安装一个 file 插件,并在 jvm 里生效,返回插件信息,每个插件都有一个 id
  2. 可以根据 id 从 jvm spring 里卸载插件。
  3. 可以根据扩展点 code 获取多个实现,这个返回的实现是一个集合
  4. 可以根据扩展点 code + 插件 id 指定获取多单个实现,这个返回的实现是一个对象。

这几个 API 可以实现插件的基本功能。

我们再添加关于租户的 API

java 复制代码
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 有了,我们的编程界面就出来了,他应该是这样的:

java 复制代码
 public static void main(String[] args) throws Throwable {
        Class<UserService> 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<UserService> first = expAppContext.get(extensionClass).stream().findFirst();
        first.ifPresent(userService -> {
            System.out.println(userService.getClass());
            System.out.println(userService.getClass().getClassLoader());
            userService.createUserExt();
        });
    }
  1. 我们的扩展点介绍名是UserService,方法名是 createUserExt
  2. 我们使用 Bootstrap 配置工作目录和插件目录,并启动,启动过程中包含调用 load 方法,然后返回一个核心领域对象。
  3. 可以使用 Context 配置租户策略;
  4. 最后我们使用 expAppContext.get().findFirst() 方法,返回一个这个扩展点优先级最高的实现。

读取插件配置 API:

java 复制代码
public interface PluginConfig {
    String getProperty(String pluginId, String key, String defaultValue);
}

注意这个 API 和正常的 config api 不同,他新增了 pluginId 维度,使插件配置之间是互相隔离的。具体的 PluginConfig 还可以根据租户再进行配置隔离。

表面的 API 已经差不多了,内部的实现,需要开始了,比如

  1. 类加载机制,包含 zip jar 的类隔离加载。
  2. 容器注入,需要将插件里代码注入到 spring 里。
  3. 插件的热插拔,怎么 unload,怎么 load,怎么从 spring 里 remove,怎么卸载等等。

开发

具体细节本文不再展开,因为代码都在 github stateis0/exp 项目里,这个项目包含实现代码,example 代码,api 使用,适配 springboot starter,最佳实践等。

项目代码结构依赖:

总结

EXP 全称: Extension Plugin 扩展点插件系统;

希望本项目可以帮助你解决本地化软件的定制需求问题。同时,也欢迎为本项目提 issue,pr 等。

项目地址 EXP 扩展点插件系统 for Github

欢迎 star 交流。

相关推荐
千叶寻-1 小时前
正则表达式
前端·javascript·后端·架构·正则表达式·node.js
小咕聊编程2 小时前
【含文档+源码】基于SpringBoot的过滤协同算法之网上服装商城设计与实现
java·spring boot·后端
追逐时光者8 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_8 小时前
敏捷开发流程-精简版
前端·后端
苏打水com9 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧10 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧10 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧10 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧10 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧10 小时前
Spring Cloud Gateway详解与应用实战
后端