轻松解决定制化需求,这款 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
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> get(String extCode, String pluginId);
}

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

  1. 安装一个 file 插件,并在 jvm 里生效,返回插件信息,每个插件都有一个 id

  2. 可以根据 id 从 jvm spring 里卸载插件。

  3. 可以根据扩展点 code 获取多个实现,这个返回的实现是一个集合

  4. 可以根据扩展点 code + 插件 id 指定获取多单个实现,这个返回的实现是一个对象。

这几个 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<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:

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 扩展点插件系统;

希望本项目可以帮助你解决本地化软件的定制需求问题。

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

参考资料

[1]

EXP 扩展点插件系统 for Github: https://github.com/stateIs0/exp

-End-

精彩推荐  1、IDEA中这么强大的接口调试插件,开发调试一站式!2、SpringBoot 插件化开发模式,强烈推荐!
3、自己动手写 IDEA Mybatis 插件:大大提高效率4、Spring Event 业务解耦神器,大大提高可扩展性,好用到爆!
程序架构技术群

构建高质量的技术交流社群,欢迎从事编程开发、技术招聘HR进群,也欢迎大家分享自己公司的内推信息,相互帮助,一起进步!

文明发言,以交流技术职位内推行业探讨为主

广告人士勿入,切勿轻信私聊,防止被骗,加我好友,拉你进群 

感谢家人们、兄弟们的关注与支持 🙏分享成长之路,不忘初心,惠泽他人 🌟与时俱进,终身学习,点赞关注不迷路 ✨

相关推荐

  • 为什么kafka需要去实现高可用和为什么不支持读写分离
  • 腾讯员工爆料:公司内部到底有多少在考公了,最近老看到在会议室有刷题和准备面试的人
  • 整理了2024年最新顶会论文【附PDF】
  • 马斯克打算为特斯拉量身定制一个ChatGPT
  • ​有了LLM,所有程序员都将转变为架构师?
  • ICLR2024|生成式视角下的实体对齐
  • FlashAttention加速升级!又快了200%!
  • LangChain库
  • 【干货】史上最全的AIGC大模型工具集汇总(下)
  • 从B 树、B+ 树、B* 树谈到R 树
  • 如何使用Docker来渐进式提升开发及部署体验
  • 一个现代化轻量级的跨平台 Redis 桌面客户端,支持 Mac、Windows 和 Linux
  • 京东快递小程序分包优化实践
  • 前端最全的5种换肤方案总结
  • 今天面试了一个前端女生,当场想给她offer!
  • 实现 SpringBoot 程序加密,禁止 jadx 反编译
  • SpringBoot+ElasticSearch实现文档内容抽取、高亮分词、全文检索
  • Angular、React等前端框架掌门人的2024预测
  • 阿里核心业务互动频繁,业务上市计划均有变化;英伟达禁止其他硬件平台运行CUDA;微软将不支持Windows安卓子系统 |极客头条
  • 传MiniMax估值超25亿美元;淡马锡考虑投资OpenAI;谷歌搜索将过滤AI生成内容丨AIGC大事日报