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
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

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

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

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

欢迎 star 交流。

EXP 一款 Java 插件化热插拔框架的更多相关文章

  1. Winform开发框架之插件化应用框架实现

    支持插件化应用的开发框架能给程序带来无穷的生命力,也是目前很多系统.程序追求的重要方向之一,插件化的模块,在遵循一定的接口标准的基础上,可以实现快速集成,也就是所谓的热插拔操作,可以无限对已经开发好系 ...

  2. android插件化-apkplug框架启动-02

    本文章基于apkplug v1.6.7 版本号编写,最新方式以官网最新消息为准 一 apkplug框架所须要的库文件(宿主) 可从http://git.oschina.net/plug/apkplug ...

  3. 分享非常漂亮的WPF界面框架源码及插件化实现原理

      在上文<分享一个非常漂亮的WPF界面框架>中我简单的介绍了一个界面框架,有朋友已经指出了,这个界面框架是基于ModernUI来实现的,在该文我将分享所有的源码,并详细描述如何基于Mod ...

  4. Adroid动态加载Apk-插件化技术框架(动态代理方案)

    技术:Android + java +动态加载+插件化   概述 为什么要使用插件化?在开发中,一个项目只会越做越大.初始版本可能是单一功能,后续可能加上各种风马牛不相及的功能.所以我认为插件化可以使 ...

  5. 有关Android插件化思考

    最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优越性,令人目不暇接.随着公司业务快速发展,项目增多,开发资源却有限,如何能在有限资源内 ...

  6. 包建强的培训课程(10):Android插件化从入门到精通

    @import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/c ...

  7. WCF开发框架之插件化应用模式升级

    自从在<Winform开发框架之插件化应用框架实现>一文中,介绍并总结了Winform开发框架插件化应用框架的实现后,赢得了很多同行和客户的支持,于是把我的WCF开发框架.混合式开发框架都 ...

  8. android 插件化框架speed-tools

    项目介绍: speed-tools 是一款基于代理模式的动态部署apk热更新框架.插件化开发框架: speed-tools这个名字主要指的快速迭代开发工具集的意思. 功能与特性: 1.支持Androi ...

  9. Android的Proxy/Delegate Application框架 (主要介绍插件化开发)

    1. 插件化的原理 是 Java ClassLoader 的原理:Java ClassLoader基础 常用的其他解决方法还包括:Google Multidex,用 H5 代替部分逻辑,删无用代码,买 ...

  10. Android Small插件化框架解读——Activity注册和生命周期

    通过对嵌入式企鹅圈原创团队成员degao之前发表的<Android Small插件化框架源码分析>的学习,对Android使用的插件化技术有了初步的了解,但还是有很多需要认真学习的地方,特 ...

随机推荐

  1. 2022-12-28:有n个黑白棋子,它们的一面是黑色,一面是白色, 它们被排成一行,位置0~n-1上。一开始所有的棋子都是黑色向上, 一共有q次操作,每次操作将位置标号在区间[L,R]内的所有棋子翻

    2022-12-28:有n个黑白棋子,它们的一面是黑色,一面是白色, 它们被排成一行,位置0~n-1上.一开始所有的棋子都是黑色向上, 一共有q次操作,每次操作将位置标号在区间[L,R]内的所有棋子翻 ...

  2. 2022-10-15:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。 你可以按 任意顺序 返回答案。 要求时间复杂度O(N)。 输入: nums = [1,1,1

    2022-10-15:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素. 你可以按 任意顺序 返回答案. 要求时间复杂度O(N). 输入: nums = [1,1,1 ...

  3. Vue跨域详解

    碰到这种问题,其实你的接口已经通了,但是在页面上就是访问不通过. 你可以把API请求地址单独拎出来新开个网站打开看请求是否成功,成功,但是你的项目不通. 有那么几个可能吧: 1.请求头设置错误 hea ...

  4. GPT大语言模型Alpaca-lora本地化部署实践【大语言模型实践一】

    模型介绍 Alpaca模型是斯坦福大学研发的LLM(Large Language Model,大语言)开源模型,是一个在52K指令上从LLaMA 7B(Meta公司开源的7B)模型微调而来,具有70亿 ...

  5. 原来.NET写的Linux桌面这么好看?

    如何使用Blazor在Linux平台下运行Desktop程序 本文将讲解如何使用Blazor运行跨平台应用,应用到的技术有以下几点 Blazor Masa Blazor Photino.Blazor ...

  6. 【汇编】DOS系统功能调用(INT 21H)

    前言 最近又听了听汇编的课程,发现代码里的MOV xxxxx INT 21H,老师都是一句话带过,而不讲讲其中的原因(也可能前面讲了我没有听QAQ). 顺便夸一下老师,老师懒省事录的视频画质已经成功从 ...

  7. python mitmproxy抓包库

    一.简介 mitmproxy是一款用Python编写的支持HTTP(S)的中间人代理工具.它可以拦截.查看.修改.重放和保存HTTP/HTTPS流量 ,支持命令行界面和图形界面,可用于安全测试.网络调 ...

  8. 癌症中克隆种群结构统计推断分析软件PyClone安装小记

    由于微信不允许外部链接,你需要点击文章尾部左下角的 "阅读原文",才能访问文中链接. PyClone 是一种用于推断癌症中克隆种群结构的统计模型. 它是一种贝叶斯聚类方法,用于将深 ...

  9. Python3.9安装

    一.安装python3.9 链接:https://pan.baidu.com/s/1mDkgKt2KSoMrKVxesb76Pg?pwd=ma4n 提取码:ma4n --来自百度网盘超级会员V4的分享 ...

  10. SRE 的工作介绍

    哈喽大家好,我是咸鱼 今天看到了一篇很不错的文章,作者是一名 SRE 工程师,在 Shopee 工作,base 新加坡 分享出来给大家看看 作者:卡瓦邦噶 原文链接:https://www.kawab ...