中间件多版本冲突的4种解决方案和我们的选择

news2024/12/23 9:12:17

背景

在小小的公司里面,挖呀挖呀挖。最近又挖到坑里去了。一个稳定运行多年的应用,需要在里面支持多个版本的中间件客户端;而多个版本的客户端在一个应用里运行时会有同名类冲突的矛盾。在经过询问chatGPT,百度,google,github,和各位大佬的文章后,进行了总结。大概有以下几种解决方案

业界方案1----更改类路径

低版本客户端,更改类路径;然后重新打包编译客户端;这样不同版本客户端,使用的类名就不同了

此解决方案
优点

1、适合简单的小项目:无需编写新代码

2、可能是最快的一种方式;只需要把代码下载下来;更改clients下的类路径,重新编译即可;属于不怎么费脑力,但有点费体力的方式。

缺点

1、遇到版本升级,需要把步骤2的过程重新人肉再来一遍。这个比较的那啥。。。。。。

2、不同版本客户端,可能也会依赖不同版本的三方jar包。这个也是蛮棘手的

此种方式适合小项目或者外包等一锤子买卖

解决方案2 -----自定义ClassLoader

使用ClassLoader进行类的隔离;不同版本客户端和依赖三方包,用不同classLoader进行加载和隔离;完全杜绝版本问题

此解决方案

1、属于自研;对ClassLoader的类加载机制需要有一定的了解

解决方案3------业界开源方案 sofa-ark

sofa-ark是动态热部署和类隔离框架,由蚂蚁集团开源贡献。主要提供应用模块的动态热部署和类隔离能力

提供功能:

1、包冲突的解决(能解决现有项目遇到的多版本问题)

2、合并部署:多个项目分工程开发,但可以合并部署;还支持静态合并部署和动态合并部署

资料传送门:

https://github.com/sofastack/sofa-ark

https://www.sofastack.tech/projects/sofa-boot/sofa-ark-readme/

此解决方案:
优点

1、功能强大不仅能执行类的隔离;还能静动态的热部署;还能进行插件的热插拔;能解决现有工程遇到的问题;文档也挺齐全的

2、性能,稳定性,易用性,应该是所有保障的,毕竟蚂蚁内部也在使用;

缺点

1、虽号称是轻量级,但那是和OSGI这种重型框架相比,在sofa-ark里,也还有蛮多概念比如:Ark Container,ark包,插件包,biz包;如何打ark包,如何打插件包等等;有一定的学习成本,好在文档齐全能降低一定的入门门槛

2、需要对现有工程进行改造,以符合sofa-ark的规范;打包和部署上还需要遵循其规范。对于小公司主打的就是一个“自由”这种状态来说;有一点点束缚和被迫学习了,因为我们都比较的“懒”

此种方式适合大型项目;大型项目开发人员和开发应用众多;而sofa-ark制定了相应的biz包和插件包的开发规范;在代码复杂性,模块化开发,扩展性,项目维护,应用运行期等都进行了综合考虑。

解决方案4------OSGI

属于比较重型解决方案

OSGI 作为业内最出名的类隔离框架,自然是可以被用于解决上述包冲突问题,但是 OSGI 框架太过臃肿,功能繁杂。为了解决包冲突问题,引入 OSGI 框架,有牛刀杀鸡之嫌,反而使工程变得更加复杂,不利于开发。

我们的选择

解决方案这么多,我们该选择哪个方案了?

我们选择方案2。为什么了?

先说说为什么不选择其它方案

方案1: 此种方式不一定是最快的方式;因为后续考虑到每增加一个版本,或者社区有更新,都要去做一次,更名,打包等。还是挺麻烦,不太自动化,属于不费脑子费体力。

方案3: 功能强大:在模块化开发,类隔离,热部署等功能性上无比优异;性能和稳定性也有大公司在背书;但我们目前是个小公司,遇到的业务场景和技术场景,没有那么复杂,强大的功能给我们,我们不一定能用的上,用的不好可能还会被反噬;由于公司技术人员对该框架缺少实际使用经验;并且对该框架的实现原理也没人懂属于需要现学;使用后万一后续出现什么问题,问题定位和维护也挺麻烦;对小公司来说还是“太重”了;并且还需要对现有工程进行改造

方案4: 就不用在详说了,比方案3还重的解决方案

为什么方案2适合我们?

实现难度上: 对我们小公司来说虽然要自己写代码实现;但经过评估大概几百行代码就能搞定,技术上不是那么的高不可攀;

技术熟悉度: 团队内大家对ClassLoader的机制还蛮熟悉;

使用经验上: 有多个同事曾经用classLoader进行过这种隔离机制的实现,但业务场景不同;

复用性上: 写一次代码;后续在遇到多个版本冲突;经过简单的配置即可,不需要修改代码;更不需要修改三方依赖源码,比方案1好

在性能和稳定性上: 性能上不影响运行期,只会影响到代码加载期,所以性能这块还好;而在稳定性上,可通过测试环境长期稳定运行和一定的业务压测进行验证;而恰好我们有这样的测试环境和线上引流进行压测验证工具

设计图

类的加载机制.png

ClassLoader的原理: 从上图可看出,ClassLoader会影响JVM加载类的路径类的加载顺序
类的加载路径
bootStrap ClassLoader: 加载%JRE_HOME%\lib 下的jar,比如rt.jar等
Extendtion ClassLoader: 加载%JRE_HOME%\lib\ext 目录下的jar
AppClass ClassLoader: 加载应用classpath下的所有类,即工程里依赖三方jar和工程的class
加载顺序 :双亲委托机制;加载类时,先让父ClasserLoader进行加载,父Classloader找不到,才让子ClassLoader进行查找和加载。

主要想法: 自定义ClassLoader继承URLClassLoader(即图里的AppClassClassLoader);基础类的加载 还是用双亲委托机制,由父类去加载,自定义类实现findClass方法,在该方法里加载指定目录class和jar。

具体工具类-----应该可以拿来即用

该实现类主要参考了Cyber365大佬的文章;然后做了一些改动(简化类,抽取工具类,去除多重if嵌套)
主要由两个类进行实现

  • MiddlewareClassLoader 类 主要做了两件事
    1:读取Class文件内容;根据传入类名,从指定url中查找并读取到对应class文件内容
    2:生产Class对象: 传入class文件内容,调用底层defineClass方法生产

  • UrlUtils 类: 主要做了一件事
    1:根据指定的url,计算出该url和对应子目录下jar的url。

MiddlewareClassLoader 类

public class MiddlewareClassLoader extends URLClassLoader {
    private URL[] allUrl;

    public MiddlewareClassLoader(String[] paths){
        this(UrlUtils.getURLs(paths));
    }
    public MiddlewareClassLoader(URL[] urls) {
        this(urls, MiddlewareClassLoader.class.getClassLoader());
    }
    public MiddlewareClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
        this.allUrl = urls;
    }

    protected Class<?> findClass(final String name)
            throws ClassNotFoundException{
        return loadExtendClass(name);
    }

//    /**
//     * 不建议重新定义loadClass 方法,打破双亲委派机制,采用逆向双亲委派
//     *
//     * @param className 加载的类名
//     * @return java.lang.Class<?>
//     * @author Cyber
//     * <p> Created by 2022/11/22
//     */
//    @Override
//    public Class<?> loadClass(String className) throws ClassNotFoundException {
//        Class extClazz = loadExtendClass(className);
//        if(null != extClazz){
//            return extClazz;
//        }
//     return super.loadClass(className);
//    }

    public Class<?> loadExtendClass(String className) throws ClassNotFoundException {
        if(null == allUrl){
            return null;
        }
        String classPath = className.replace(".", "/");
        classPath = classPath.concat(".class");
        for (URL url : allUrl) {
            byte[] data = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            InputStream is = null;
            try {
                File file = new File(url.toURI());
                if (!file.exists()) {
                    continue;
                }
                JarFile jarFile = new JarFile(file);
                if (jarFile == null) {
                    continue;
                }
                JarEntry jarEntry = jarFile.getJarEntry(classPath);
                if (jarEntry == null) {
                    continue;
                }
                is = jarFile.getInputStream(jarEntry);
                byte[] buffer = new byte[1024 * 10];
                int length = -1;
                while ((length = is.read(buffer)) > 0) {
                    baos.write(buffer, 0, length);
                }
                data = baos.toByteArray();
                System.out.println("********找到classPath=" + classPath + "的jar=" + url.toURI().getPath() + "*******");
                Class clazz =  this.defineClass(className, data, 0, data.length);
                return clazz;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

工具类UrlUtils

public class UrlUtils {

    /**
     * description 通过文件目录获取目录下所有的jar全路径信息
     *
     * @param paths 文件路径
     * @return java.net.URL[]
     * @author Cyber
     * <p> Created by 2022/11/22
     */
    public static URL[] getURLs(String[] paths) {
        if (null == paths || 0 == paths.length) {
            throw new RuntimeException("jar包路径不能为空.");
        }
        List<String> dirs = new ArrayList<String>();
        for (String path : paths) {
            dirs.add(path);
            collectDirs(path, dirs);
        }
        List<URL> urls = new ArrayList<URL>();
        for (String path : dirs) {
            urls.addAll(doGetURLs(path));
        }
        URL[] threadLocalurls = urls.toArray(new URL[0]);

        return threadLocalurls;
    }

    /**
     * description 递归获取文件目录下的根目录
     *
     * @param path      文件路径
     * @param collector 根目录
     * @return void
     * @author Cyber
     * <p> Created by 2022/11/22
     */
    private static void collectDirs(String path, List<String> collector) {
        if (null == path || "".equalsIgnoreCase(path)) {
            return;
        }
        File current = new File(path);
        if (!current.exists() || !current.isDirectory()) {
            return;
        }
        for (File child : current.listFiles()) {
            if (!child.isDirectory()) {
                continue;
            }
            collector.add(child.getAbsolutePath());
            collectDirs(child.getAbsolutePath(), collector);
        }
    }

    private static List<URL> doGetURLs(final String path) {
        if (null == path || "".equalsIgnoreCase(path)) {
            throw new RuntimeException("jar包路径不能为空.");
        }
        File jarPath = new File(path);
        if (!jarPath.exists() || !jarPath.isDirectory()) {
            throw new RuntimeException("jar包路径必须存在且为目录.");
        }

        FileFilter jarFilter = new FileFilter() {
            /**
             * description  判断是否是jar文件
             * @param pathname jar 全路径文件
             * @return boolean
             * @author Cyber
             * <p> Created by 2022/11/22
             */
            @Override
            public boolean accept(File pathname) {
                return pathname.getName().endsWith(".jar");
            }
        };
        File[] allJars = new File(path).listFiles(jarFilter);
        List<URL> jarURLs = new ArrayList<URL>(allJars.length);
        for (int i = 0; i < allJars.length; i++) {
            try {
                jarURLs.add(allJars[i].toURI().toURL());
            } catch (Exception e) {
                throw new RuntimeException("系统加载jar包出错", e);
            }
        }
        return jarURLs;
    }
}

kafka发送基础类

@Slf4j
public abstract class AbstractKafkaProducer {

    private String kafkaClassName = "org.apache.kafka.clients.producer.KafkaProducer";
    private String stringSerializerClassName = "org.apache.kafka.common.serialization.StringSerializer";
    private String serializerClassName = "org.apache.kafka.common.serialization.Serializer";
    private String producerRecordClassName = "org.apache.kafka.clients.producer.ProducerRecord";

    private MiddlewareClassLoader middlewareClassLoader;
    private Object producerObject = null;
    private Method sendMethod = null;
    private Constructor producerRecordConstructor = null;

   //类的加载和初始化
    public void init(String jarPath){
        middlewareClassLoader = new MiddlewareClassLoader(new String[]{jarPath});
        try {
            ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(middlewareClassLoader);
            Class kafkaProduceClazz = middlewareClassLoader.loadClass(kafkaClassName);
            Class kafkaStringSerializerClazz = middlewareClassLoader.loadClass(stringSerializerClassName);
            Class kafkaSerializerClazz = middlewareClassLoader.loadClass(serializerClassName);
            //加载KafkaProducer类
            Class ProducerRecordClazz = middlewareClassLoader.loadClass(producerRecordClassName);

            Constructor producerConstructor = kafkaProduceClazz.getConstructor(Map.class, kafkaSerializerClazz, kafkaSerializerClazz);

            Map<String,Object> produceConfigMap = new HashMap<String,Object>();
            produceConfigMap.put("retries",3);
            produceConfigMap.put("retry.backoff.ms",10000);
            produceConfigMap.put("acks","all");
            //回调方法,让自来可更改生产端配置
            addExtendConfig(produceConfigMap);

            producerObject = producerConstructor.newInstance(produceConfigMap,kafkaStringSerializerClazz.newInstance(),kafkaStringSerializerClazz.newInstance());
            sendMethod = kafkaProduceClazz.getMethod("send",new Class[]{ProducerRecordClazz});
            producerRecordConstructor =  ProducerRecordClazz.getConstructor(String.class,Object.class);
            Thread.currentThread().setContextClassLoader(threadClassLoader);
            System.out.println("========end======");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected abstract void addExtendConfig(Map<String, Object> produceConfigMap);

   //发送消息
    public void send(String topic,String msg){
        try {
            sendMethod.invoke(producerObject,producerRecordConstructor.newInstance(topic,msg));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Kafka09Producer

@Data
public class Kafka09Producer extends AbstractKafkaProducer {

    private String bootstrapServers;
    @Override
    protected void addExtendConfig(Map<String, Object> produceConfigMap) {
        produceConfigMap.put("bootstrap.servers",bootstrapServers);
    }

}

Kafka32Producer

@Data
public class Kafka32Producer extends AbstractKafkaProducer {

    private String bootstrapServers;
    @Override
    protected void addExtendConfig(Map<String, Object> produceConfigMap) {
        produceConfigMap.put("bootstrap.servers",bootstrapServers);
    }

}

核心测试代码

@Slf4j
public class MiddlewareClassLoaderTest {

    public static void main(String[] args) throws InterruptedException {
        //kafka 0.9的测试
        Kafka09Producer kafka09Producer = new Kafka09Producer();
        kafka09Producer.setBootstrapServers(KafkaConfig.getInstance().getProperty("bootstrap.servers"));
        kafka09Producer.init("/data/app/product-kafka/ext-lib/kafka090");
        kafka09Producer.send("topic090","msg090:" + System.currentTimeMillis());

        //kafka 3.2的测试
        Kafka32Producer kafka32Producer = new Kafka32Producer();
        kafka32Producer.setBootstrapServers(KafkaConfig.getInstance().getProperty("bootstrap.servers"));
        kafka32Producer.init("/data/app/product-kafka/ext-lib/kafka32");
        kafka32Producer.send("topic320","msg320:" + System.currentTimeMillis());
        Thread.sleep(60 * 1000);
    }
}

测试结果

kafka09类的加载
image.png

kafka32类的加载
image.png

总结

ClassLoader除了能加载指定版本jar包外;还可以做热部署和热更新;如果要再次加载同一个类达到热更新;可 new一个classLoader然后loadClass,再用该Class去实例化对象即可。
还有一个困扰新手较久的注意点:Class的加载和Object实例化需要分开去看待,ClassLoader只影响类的加载;类的实例化是另外一个问题。

原创不易,请点赞,留言,关注,收藏 4暴击 ^^

参考资料:

https://blog.csdn.net/briblue/article/details/54973413 ClassLoader类加载机制,类加载顺序

https://juejin.cn/post/7168678691839410213 Cyber365大佬的文章

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/850511.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度补全算法-CompletionFormer-已开源效果最好

《CompletionFormer: Depth Completion with Convolutions and Vision Transformers 》 摘要 给定稀疏深度和相应的 RGB 图像&#xff0c;深度补全旨在整个图像中空间传播稀疏测量值&#xff0c;以获得密集的深度预测。尽管基于深度学习的深度补全方法取得了巨大进步&#xff0…

NAND Flash 失效之 Data Rentention | 闪存数据保持力 | 数据放几年就坏掉了?

依公知及经验整理,原创保护,禁止转载。专栏 《深入理解Flash:闪存特性与实践》 图1: Data Retention 对 Vt 电压分布影响 图片来源: 知乎 [2] 全文 1900 字, 内容摘要 Data Retention 产生 Data Retention 的影响因素  如何规避 Data Rention 问题 发生Data Retent…

Vue + VSCode + 浏览器显示乱码

浏览器乱码&#xff1a; 将GBK改为UTF-8: 欧克:

领航优配:沪指震荡涨0.47%,保险、券商板块强势,互联金融概念活跃

4日早盘&#xff0c;两市股指高开高走&#xff0c;沪指一度涨逾1%打破3300点&#xff0c;随后涨幅有所收窄&#xff1b;两市半日成交超6000亿元&#xff0c;北向资金小幅净流入。 截至午间收盘&#xff0c;沪指涨0.47%报3295.91点&#xff0c;深成指涨0.67%&#xff0c;创业板指…

CrossOver是什么软件 CrossOver软件好用吗

CrossOver是一款由CodeWeavers公司开发的软件&#xff0c;它可以在Mac和Linux等操作系统上运行Windows软件&#xff0c;而无需在计算机上安装Windows操作系统。这款软件的核心技术是Wine&#xff0c;它是一种在Linux和macOS等操作系统上运行Windows应用程序的开源软件。本文将会…

github pages 用法详解 发布自己的网站

github pages 基础用法 URL 规则 假设你的 github 帐号为 mygithub&#xff0c;需要发布的仓库名为 myrepo&#xff0c;那么 pages 的 URL 为&#xff1a; https://mygithub.github.io/myrepo 添加内容 用任意编辑器写好&#xff08;或者生成&#xff09;标准的网页内容&a…

【QT调用ST-link-使用QT编写程序-调用ST-LINK_CLI.exe-烧写STM32F4xxx-基础样例】

【QT结合ST-link&#xff0c;使用QT编写程序&#xff0c;调用ST-LINK_CLI.exe,烧写STM32F4xxx-基础样例】 1、前言2、实验环境3、先前了解-自我总结4、实验过程&#xff08;0&#xff09;硬件连接与供电&#xff08;1&#xff09;安装&使用STM32 ST-LINK Utility&#xff0…

骑士牛(BFS)

题面 john用他的一头母牛和Don先生交换了一头“骑士牛”。这头牛有一个独特的能力——在牧场中能像中国象棋中的马一样跑跳&#xff08;会中国象棋吗&#xff1f;不会&#xff1f;注意&#xff1a;本题不考虑马被“蹩脚”的情况&#xff09;。 当然&#xff0c;这头牛不能跳到岩…

Flutter:文件读取—— video_player、chewie、image_picker、file_picker

前言 简单学习一下几个比较好用的文件读取库 video_player 简介 用于视频播放 官方文档 https://pub-web.flutter-io.cn/packages/video_player 安装 flutter pub add video_player加载网络视频 class _MyHomePageState extends State<MyHomePage> {// 控制器late…

uniapp 将标题背景更换背景图片 完美解决(附加源码+实现效果图)

问题描述 今天拿到小程序的设计效果图后&#xff0c;标题部分背景需要加背景图片&#xff0c;以往我做的都是标题背景更换颜色等&#xff0c;加背景图片还是第一次遇到&#xff0c;大家可以先看下我的效果图是否与你遇到的问题一致&#xff01; 首页标题的背景是个背景图片。 …

MachineLearningWu_13/P60-P64_Tensorflow

P60-P64的学习目录如下&#xff0c; x.1 TF网络模型实现 以一个简单的TF的分类网络为例&#xff0c;将模型翻译成框架下的语义&#xff0c;即如右侧所表达的。 当然上面对于分类网络的解释是一个简洁的解释&#xff0c;我们来进行更加具象的了解一下。左边是机器学习的三步骤&…

RS485实验

RS485实验 介绍 RS485采用差分信号进行传输&#xff0c;半双工通信。RS485是一个总线&#xff0c;在同一总线上最多可以挂接32个节点。通信流程简单理解为默认为接收状态&#xff0c;发送数据时切换为发送状态&#xff0c;数据发送完毕后切换为接收状态。发送和接收分别由一个…

01_什么是ansible、基本架构、ansible工作机制、Ansible安装、配置主机清单、设置SSH无密码登录等

1.什么是ansible 1.1.基本介绍 1.2.基本架构 1.3.基本特征 1.4.优点 1.5.ansible工作机制 2.Ansible安装 2.1.机器准备 2.2.安装ansible 2.2.1.安装epel源 2.2.2.安装ansible 2.2.3.查看ansible版本 2.2.4.树状结构展示文件夹 2.2.4.1.其中ansible.cfg的内容如下 2.2.4.2.host的…

SQL 相关子查询 和 不相关子查询、Exists 、Not Exists、 多表连接(包含自连接)

不相关子查询 子查询的查询条件不依赖于父查询&#xff0c;称不相关子查询。子查询可以单独运行的 select stu_id,sex,age from student t where sex(select sexfrom studentwhere stu_id10023 )相关子查询 关联子查询 子查询的查询条件依赖于父查询&#xff0c;称为 相关子…

C++笔记之两个类的实例之间传递参数——通过构造函数传递类对象的方法详细探究

C笔记之两个类的实例之间传递参数——通过构造函数传递类对象的方法详细探究 code review! 文章目录 C笔记之两个类的实例之间传递参数——通过构造函数传递类对象的方法详细探究1.传递对象的const引用——ClassB的实例只能访问ClassA的实例&#xff0c;但不会修改ClassA的实…

Python小白学习:超级详细的字典介绍(字典的定义、存储、修改、遍历元素和嵌套)

目录 一、字典简介1.1 创建字典1.2 访问字典中的值1.3 添加键值对1.4 修改字典中的值实例 1.5 删除键值对1.6 由多个类似对象组成的字典1.7 使用get()访问值1.8 练习题 二、遍历字典2.1 遍历所有键值对实例 2.2 遍历字典中的所有键2.3 按照特定顺序遍历字典中的所有键2.4 遍历字…

10_Pulsar跨机房复制、Pulsar跨机房复制

2.2.4.Pulsar跨机房复制 2.2.5.Pulsar跨机房复制 如何配置 2.2.4.Pulsar跨机房复制 在大型的分布式系统中&#xff0c;都会涉及到跨多个数据中心的需求&#xff0c;通常会使用跨地域复制机制提供额外的冗余防止服务无法正常运作。Apache Pulsar 的跨地域多机房互备特性&#…

Android 13 Hotseat定制化修改——001 hotseat布局方向

目录 一.背景 二.hotseat布局方向 一.背景 由于需求是需要自定义修改Hotseat,所以此篇文章是记录如何自定义修改hotseat的,应该可以覆盖大部分场景,修改点有修改hotseat布局方向,hotseat图标数量,hotseat图标大小,hotseat布局位置,hotseat图标禁止形成文件夹,hotseat图…

接口测试——python接口开发(二)

目录 1. python接口开发框架Flask简介与安装 2. 使用Flask开发一个Get接口 3. 使用Flask开发一个Post接口 4. Flask结合PyMySQL接口与数据库的交互 1. python接口开发框架Flask简介与安装 Flask接口测试框架的简介与安装Flask是轻量级的web开发框架相比于其他框架&#xff…

Could not resolve host: mirrorlist.centos.org; Unknown error解决方法

今天服务器安装完CentOS系统后&#xff0c;安装网络的时候&#xff0c;出现无法联网yum yum -y install net-tools 以上代码无法运行并报错&#xff0c;这里我要提醒大家&#xff0c;如果在初始安装的时候选中安装网络工具模块就不用在安装net-tools了&#xff0c;因为我选中…