SpringBoot通过加装外部JAR包中的类实现业务插件功能

news2024/11/16 1:37:45

综合记录一下关于ClassLoader和Spring Bean的动态加载卸载功能

目录

  • 一、需要说明
  • 二、总体设计
  • 三、具体设计
    • 3.1 加载卸载Bean工具类
    • 3.2 创建卸载方法
    • 3.3 创建加载方法
    • 3.4 创建获取具体服务类方法
  • 四、总结

一、需要说明

  1. 有一个公共的发送通知的接口,这个接口需要做成单独jar,可以通过maven包引入该接口
public interface NotifyService {
    void notify(MyMessage myMessage); 
}
  1. 会有多个实现类
    com.pv1.notify.MailNotifyService 邮件通知
    com.pv1.notify.WxNotifyService 微信通知
    com.pv1.notify.DingNotifyService 钉钉通知

  2. 每一个实现类为一个插件, 插件打成一个jar包上传OSS文件服务中

  3. 从远程文件服务中下载到本地并且注入到Spring容器中
    在这里插入图片描述

  4. 支持不同版本的jar加载和卸载功能

二、总体设计

  1. 设计接口和公共参数对象,并且打成jar包 发布到maven仓库中

    public interface NotifyService {
    	Map<String, NotifyService > NOTIFY_SERVICE_MAP = new ConcurrentHashMap<>(32);
        void notify(MyMessage myMessage); 
    }
    
  2. 创建插件信息表 jarInfo,主要是用于存放插件的注册信息,核心字段包含如下:

字段说明
serverFlag服务标识
jarVersion件版本号
jarUrl插件下载地址
jarServerName插件注册Spring服务名
jarClassNameClassLoader 加载时传入的类名称
isController是否web接口服务 默认0, 1是 0否
isEnable状态 是否启用, 启用=已加载 默认0, 1是 0否
  1. 各个实现类工程从Maven仓库中引入接口包,并且实现自身业务逻辑

  2. 采用策略模式 创建一个Map存放各个实现类 供应用系统调用

     Map<String, NotifyService > NOTIFY_SERVICE_MAP = new ConcurrentHashMap<>(32);
    

    这里的key是 各个实现类的标识 serverFlag 字段 ,例如邮件服务的话 key为 “MAIL”
    NOTIFY_SERVICE_MAP 可以作为 接口 NotifyService 的成员常量

  3. 创建加载方法 void loadProtocol(JarInfo jarInfo);

  4. 创建卸载方法 void unloadProtocol(JarInfo jarInfo);

  5. 创建获取具体服务类方法 NotifyService getNotifyService(String serverFlag );

总的设置原则:

表 jarInfo 里面需保证: 相同 serverFlag 的多行记录中只能有一条处于加载状态中。

三、具体设计

3.1 加载卸载Bean工具类

这里使用了hutools工具,总体上方法差不多,如有你要注册Controller 则先注册为SpringBean然后在调用注册Controller方法。


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;
import java.util.List;

@Slf4j
public class ExtendBeanUtil {
    
    public static void registerBeanDefinition(String beanName, Class<?> targetClass) {
        registerBeanDefinition(beanName, targetClass, null, null, null);
    }

    public static void registerBeanDefinition(String beanName, Class<?> targetClass, List<Object> constructorArgs) {
        registerBeanDefinition(beanName, targetClass, constructorArgs, null, null);
    }

    public static void registerBeanDefinition(
            String beanName,
            Class<?> targetClass,
            String initMethodName) {
        registerBeanDefinition(beanName, targetClass, null, initMethodName, null);
    }

    public static void registerBeanDefinition(
            String beanName,
            Class<?> targetClass,
            String initMethodName,
            String destoryMethodName) {
        registerBeanDefinition(beanName, targetClass, null, initMethodName, destoryMethodName);
    }

    public static void registerBeanDefinition(
            String beanName,
            Class<?> targetClass,
            List<Object> constructorArgs,
            String initMethodName,
            String destoryMethodName) {
        ApplicationContext applicationContext = SpringUtil.getApplicationContext();
        //获取BeanFactory
        DefaultListableBeanFactory defaultListableBeanFactory =
                (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        //创建bean信息.
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(targetClass);
        // 如果有构造函数参数
        if (CollectionUtil.isNotEmpty(constructorArgs)) {
            for (Object arg : constructorArgs) {
                beanDefinitionBuilder.addConstructorArgValue(arg);
            }
        }
        // 设置 init方法
        if (StrUtil.isNotBlank(initMethodName)) {
            beanDefinitionBuilder.setInitMethodName(initMethodName);
        }
        // 设置 destory方法
        if (StrUtil.isNotBlank(destoryMethodName)) {
            beanDefinitionBuilder.setDestroyMethodName(destoryMethodName);
        }
        //动态注册bean.
        defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
    }

    public static void unRegisterBeanDefinition(String beanName) {
        ApplicationContext applicationContext = SpringUtil.getApplicationContext();
        if (!applicationContext.containsBean(beanName)) {
            return;
        }
        //获取BeanFactory
        DefaultListableBeanFactory defaultListableBeanFactory =
                (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        defaultListableBeanFactory.removeBeanDefinition(beanName);
    }

    public static void registerController(String controllerBeanName)
            throws Exception {
        final RequestMappingHandlerMapping requestMappingHandlerMapping =
                SpringUtil.getBean(RequestMappingHandlerMapping.class);
        if (requestMappingHandlerMapping != null) {
            ApplicationContext applicationContext = SpringUtil.getApplicationContext();
            if (!applicationContext.containsBean(controllerBeanName)) {
                log.warn("注册Controller {} 不成功,因为在BeanFactory未找到对应的Bean信息", controllerBeanName);
                return;
            }
            //注册Controller
            Method method = requestMappingHandlerMapping.getClass().getSuperclass().getSuperclass().
                    getDeclaredMethod("detectHandlerMethods", Object.class);
            //将private改为可使用
            method.setAccessible(true);
            method.invoke(requestMappingHandlerMapping, controllerBeanName);
        }
    }

    public static void unregisterController(String controllerBeanName) {
        final RequestMappingHandlerMapping requestMappingHandlerMapping
                = SpringUtil.getBean("requestMappingHandlerMapping");
        if (requestMappingHandlerMapping != null) {
            ApplicationContext applicationContext = SpringUtil.getApplicationContext();
            if (!applicationContext.containsBean(controllerBeanName)) {
                return;
            }
            Object controller = SpringUtil.getBean(controllerBeanName);
            if (controller == null) {
                log.warn("卸载Controller {} 取消执行,因为在BeanFactory未找到对应的Bean信息", controllerBeanName);
                return;
            }
            final Class<?> targetClass = controller.getClass();
            ReflectionUtils.doWithMethods(targetClass, method -> {
                Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
                try {
                    Method createMappingMethod = RequestMappingHandlerMapping.class.
                            getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
                    createMappingMethod.setAccessible(true);
                    RequestMappingInfo requestMappingInfo = (RequestMappingInfo)
                            createMappingMethod.invoke(requestMappingHandlerMapping, specificMethod, targetClass);
                    if (requestMappingInfo != null) {
                        requestMappingHandlerMapping.unregisterMapping(requestMappingInfo);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, ReflectionUtils.USER_DECLARED_METHODS);
        }
    }
}

3.2 创建卸载方法

    public void unloadProtocol(JarInfo jarInfo) {
        printLog(jarInfo, "开始卸载服务", "...");
        String jarServerName = jarInfo.getJarServerName();
        String isController = jarInfo.getIsController();
        if (YesOrNoEnum.YES.getCode().equals(isController)) {
            ExtendBeanUtil.unregisterController(jarServerName);
        }
        ExtendBeanUtil.unRegisterBeanDefinition(jarServerName);
		// 从全局策略模式 NOTIFY_SERVICE_MAP 剔除
        NotifyService.NOTIFY_SERVICE_MAP.remove(jarInfo.getServerFlag());
        printLog(jarInfo, "完成卸载服务", "");
    }

这里的 printLog 就是简单的日志打印方法根据自己业务自行实现具体逻辑, YesOrNoEnum是个简单枚举 “1” 和 “0” 。

说明:
一般ClassLoader 已经加载的Class 建议不要想从虚拟机中卸载,这样可能导致很多异常情况。我们的web服务为Spring容器服务,我们直接从容器中卸载该服务即可。

建议:
不同版本的实现类文件放在不同包下面
,例如:

com.pv1.notify.MailNotifyService
com.pv2.notify.MailNotifyService
com.pv3.notify.MailNotifyService

如果放同一包下面, 相同ClassLoader 不会重复加载相同类名 (包路径+文件名称一致) 的类文件。

3.3 创建加载方法

加载方法稍微复杂一些,可能需要从远程下载jar文件,
另外需要保证是同一个ClassLoader进行类的加载,且这个ClassLoader需实现双亲委派机制。

这里用了 hutools里面的工具类。

classLoader相关知识点参考不错的知乎文章: https://zhuanlan.zhihu.com/p/51374915

    public void loadProtocol(JarInfo protocol) {
        String jarClassName = protocol.getJarClassName();
        String jarServerName = protocol.getJarServerName();
        String isController = protocol.getIsController();
        printLog(protocol, "开始加载服务", "...");
        try {
            // 获得一个ClassLoader
            JarClassLoader jarClassLoader = getJarClassLoader(protocol);
            
            // 先卸载
            unloadProtocol(protocol);
            
            // 加载目标类
            Class<?> targetClass = jarClassLoader.loadClass(jarClassName);
            printLog(protocol, "classLoader完成", "...");
            
            // 注入到Spring容器中
            ExtendBeanUtil.registerBeanDefinition(jarServerName, targetClass);
            
            // 是否controller层接口
            if (YesOrNoEnum.YES.getCode().equals(isController)) {
                ExtendBeanUtil.registerController(jarServerName);
                printLog(protocol, "通知类服务加载controller层接口", "...");
            }
            
            // 设置到相应的业务 MAP中,构建策略模式
            afterLoadProtocol(protocol);
            printLog(protocol, "完成加载服务", "");
            
        } catch (Exception e) {
            printLog(protocol, "加载服务失败", e.getMessage());
        }
    }

获取对应的 JarClassLoader

      // 全局变量
    protected final Map<String, JarClassLoader> jarClassLoaderMap = new ConcurrentHashMap<>(16);

    protected JarClassLoader getJarClassLoader(JarInfo protocol) throws IOException {
        String jarUrl = protocol.getJarUrl();
        String baseLoaderPath = "notifyJarFiles";
        JarClassLoader jarClassLoader = jarClassLoaderMap.get(jarUrl);
        
        if (jarClassLoader == null) {
            String[] jarUrlItems = jarUrl.split("/");
            // 创建本地临时文件路径
            File file = CreateTmpFileUtil.createTmpFile(baseLoaderPath, jarUrlItems[jarUrlItems.length - 1]);
            // 本地文件不存在从远程下载
            if (!file.exists()) {
                OutputStream outputStream = Files.newOutputStream(file.toPath());
                // ossService 为文件服务下载工具类
                ossService.downloadFileToOutputStream(jarUrl, outputStream);
                printLog(protocol, "从远程服务器下载jar文件完成", "...");
            }
            printLog(protocol, "jar文件地址:" + file.getAbsolutePath(), "...");
            
            jarClassLoader = ClassLoaderUtil.getJarClassLoader(file);
            // 保存ClassLoader 下次再用
            jarClassLoaderMap.put(jarUrl, jarClassLoader);
        }
        return jarClassLoader;
    }

设置到相应的业务 MAP中,构建策略模式


    protected void afterLoadProtocol(JarInfo protocol) {
        String jarType = protocol.getJarType();
        String jarServerName = protocol.getJarServerName();
        NotifyService notifyService = SpringUtil.getBean(jarServerName, NotifyService.class);
		// 添加到 全局策略模式 NOTIFY_SERVICE_MAP 中
        NotifyService.NOTIFY_SERVICE_MAP.put(jarInfo.getServerFlag(), notifyService);
    }

3.4 创建获取具体服务类方法

    public NotifyService getNotifyService(String serverFlag ) {
        // 从最新缓存或数据库中加载jar信息,根据服务标识 serverFlag 
        JarInfo jarInfo = getEnableJarInfo(serverFlag);
        String jarClassName = jarInfo.getJarClassName();
        
        NotifyService service = NOTIFY_SERVICE_MAP.get(serverFlag);
        
        if (service != null) {
            // 检验是否和当前的协议转换层配置信息一致
            String existsClassName = service.getClass().getName();
            if (!existsClassName.equals(jarClassName)) {
                // 表中最新的jar信息和目前缓存的不一致
                // 重新加载
                loadProtocol(jarInfo);
            }
        } else {
            // 重新加载
            loadProtocol(jarInfo);
        }
        // 重新取一次
        return NotifyService.NOTIFY_SERVICE_MAP.get(serverFlag );
    }

重点说明:
getEnableJarInfo 方法是根据 serverFlag 取表jarInfo 中 enable=1的 唯一一条数据。

如果担心 getEnableJarInfo 每次都要重表里面取导致应用性能有问题,则建议先从缓存中取取不到从数据库中取,但确保缓存中和数据库中数据一致。

验证获取到的服务类的版本是否和表中一致 :
if (!existsClassName.equals(jarClassName)) 这行代码的理由是 一开始规定了 不同版本的Class文件对应的包路径也不一样,

因此jarClassName如果不相同则表示当前Spring容器中的服务类需要卸载然后重新加载。

四、总结

以上设计代码主要示意为主,真正用于生产环境还需进一步优化,总体上各功能都已经实现。
Spring应用启动时候需要根据表中的配置信息进行初始化操作。
建议实现 CommandLineRunner 接口,在 public void run(String… args) 方法中进行初始化:
查询所有启用中的jarInfo记录,然后调用 loadProtocol 方法

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

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

相关文章

CUDA配置正确,但是torch.cuda.is_available()却是False的解决方案

1.torch.cuda.is_available()返回为False 有时候我们想要使用GPU加速&#xff0c;但是发现CUDA、pytorch都安装好了&#xff0c;且版本也匹配&#xff0c;但是仍然无法使用GPU,显示信息如下&#xff1a; 这时候我们可以看看是不是我们的Pytorch的问题 2.输入下面命令查看pyto…

(学习日记)2023.04.26

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

cesium-native编译

我相信点进这个博客的都是一些cesium专业人才&#xff0c;这文章只起了一个抛砖引玉的作用&#xff0c;希望各位人才不惜赐教。 Github地址&#xff1a;CesiumGS/cesium-native (github.com) 编译需求&#xff1a;升级公司的3dtile的架构&#xff0c;提高性能 博客目的&…

(转载)基于混合粒子群算法的TSP问题求解(matlab实现)

1 理论基础 标准粒子群算法通过追随个体极值和群体极值完成极值寻优&#xff0c;虽然操作简单&#xff0c;且能够快速收敛&#xff0c;但是随着迭代次数的不断增加&#xff0c;在种群收敛集中的同时&#xff0c;各粒子也越来越相似&#xff0c;可能在局部最优解周边无法跳出。…

福利·分析

竞争使得生产者剩余和消费者剩余的和最大化 无谓损失指的是由于过量生产或生产不足造成的消费者剩余和生产者剩余的净损失。 税收与补贴的福利分析 从量税效果&#xff1a; 为简单期间&#xff0c;我们对某种商品征收从量税&#xff1a;对每一销售的单元&#xff0c;征收特定…

汽车电子设计之AUTOSAR中CanNM模块

目录 前言 正文 网络节点类型 仅本地唤醒 仅网络唤醒 本地网络唤醒 KL15电唤醒 NM状态机 Bus Sleep Mode Network Mode Prepare Bus-Sleep Mode Passive Mode 状态机时间参数总结 NM状态机切换 网络管理报文结构 NM报文总体结构解析 CBV详解 常用函数接口 前言…

matlab给变量名称

效果 做法&#xff1a; 构建table-> ‘VariableNames’,{‘y’,‘x’} adata; a(:,2)linspace(0.1,4.1,41); tbltable(a(:,1),a(:,2), VariableNames,{y,x});

Verilog学习(SPI协议的Flash驱动控制)

目录 一、SPI通信协议 1.1 SPI物理层 1.2 SPI协议层 二、实战 2.1 SPI控制FLASH实现全擦除代码编写 2.2 上板验证 一、SPI通信协议 1.1 SPI物理层 SPI通信模式为主-从模式 &#xff0c;分为一主一从、一主多从&#xff1a; 片选线CS用于主机选择对应的从机进行通信&…

html爱情表白神器,回忆纪念册(附源码)

文章目录 1.设计来源1.1 主界面1.2 相关界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/131022313 html爱情表白神器&#xff0c;回忆纪念册 html爱情表白神器&#x…

【JavaEE初阶】万字详解TCP/IP协议!!!(一)

文章目录 1. 应用层和传输层的联系2. UDP协议3. TCP协议3.1 TCP报头介绍3.2 TCP实现可靠传输的核心机制&#xff08;1&#xff09;确认应答&#xff08;2&#xff09;超时重传&#xff08;3&#xff09;连接管理建立连接&#xff08;三次握手&#xff09;断开连接(四次挥手) &a…

测试人挣破年入20万的束缚,从第一个python+selenium项目开始!

今天整理一下实战项目的代码共大家学习。&#xff08;注:项目是针对我们公司内部系统的测试&#xff0c;只能内部网络访问&#xff0c;外部网络无法访问&#xff09; 问&#xff1a; 1.外部网络无法访问&#xff0c;代码也无法运行&#xff0c;那还看这个项目有啥用 2.如何学…

English Learning - L3 作业打卡 Lesson4 Day28 2023.6.1 周四

English Learning - L3 作业打卡 Lesson4 Day28 2023.6.1 周四 引言&#x1f349;句1: Something may appear to be free of charge, but there may be a hidden cost.成分划分弱读连读爆破语调 &#x1f349;句2: When we fail to see problems at work, my supervisor tells …

一文了解0欧电阻的奥秘:它不是导线,也不是真的0欧

目录 一、0欧电阻的定义 二、 0欧电阻和导线的区别 三、0欧电阻的选型 四、0欧电阻长什么样 五、0欧姆电阻的用途 1.调试和兼容设计&#xff1a; 2.预留电阻位置&#xff1a; 3.方便布线&#xff1a; 4.方便测试电流&#xff1a; 5.噪声抑制&#xff1a; 6.信号隔离…

学习stm32f103c8t6,如何从正点原子官网下载资料及资料使用

学习stm32f103c8t6&#xff0c;如何从正点原子官网下载资料及资料使用 一、下载资料 用百度搜索“正点原子” 点击进入后找我们需要的芯片型号的资料&#xff0c;选择stm32f103-mini开发板的资料进行下载&#xff0c;其他的像stm32f103的精英版&#xff0c;战舰开发板啥的&am…

代码随想录二刷 day11 | 栈与队列 之 20. 有效的括号 1047. 删除字符串中的所有相邻重复项 150. 逆波兰表达式求值

day11 20. 有效的括号1047. 删除字符串中的所有相邻重复项150. 逆波兰表达式求值 20. 有效的括号 题目链接 解题思路&#xff1a; 有三种不匹配的情况&#xff1a; 第一种情况&#xff0c;字符串里左方向的括号多余了 。 第二种情况&#xff0c;括号没有多余&#xff0c;但是…

大专毕业,从6个月开发转入测试岗位的一些感悟——写在测试岗位3年之际

时光飞逝&#xff0c;我从前端开发岗位转入测试岗位已经三年了&#xff0c;这期间从迷茫到熟悉&#xff0c;到强化&#xff0c;到熟练&#xff0c;到总结&#xff0c;感受还是很深的&#xff01; 三年前的某一个晚上&#xff0c;我正准备下班回家&#xff0c;我们的项目经理把…

Selenium UI自动化测试入门

1.先下载Pycharm编辑器&#xff0c; 网站&#xff1a;下载地址 2.安装python环境包 地址&#xff1a;Download Python | Python.org 安装的时候记住勾选自动配置环境变量 3.在pycharm中配置python执行器路径 4.安装selenium库 pip install selenium4.1.1 查看当前selenium版…

Flutter架构——线程模型

Flutter的架构分为框架、引擎和嵌入器层(Embedder)&#xff0c;其中嵌入器层将Flutter嵌入各个平台。Flutter完整的架构图如下&#xff1a; Flutter中的隔离是通过引擎层的一个线程来实现的&#xff0c;但是Flutter引擎线程的创建与管理又是由嵌入器负责的&#xff0c;也就是说…

Qt个人项目——天气预报,内带QListWidget自定义组件,支持全球城市天气

个人项目&#xff0c;自己制作了一个天气预报&#xff0c;还有选择城市列表&#xff0c;列表分大洲区域选择&#xff0c;具体选择后再进行选择城市&#xff0c;可以更新城市数据&#xff0c;自定义了QListWidget&#xff0c;总体来说完成度比较高&#xff0c;难度也不大&#x…

【Linux】Linux环境基础开发工具使用(gcc/g++使用)

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;Linux ⭐代码仓库&#xff1a;Linux 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们的支持是我…