文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Spring Boot 如何热加载Jar实现动态插件?

2024-12-02 19:28

关注

一、背景

动态插件化编程是一件很酷的事情,能实现业务功能的 「解耦」 便于维护,另外也可以提升 「可扩展性」 随时可以在不停服务器的情况下扩展功能,也具有非常好的 「开放性」 除了自己的研发人员可以开发功能之外,也能接纳第三方开发商按照规范开发的插件。

常见的动态插件的实现方式有 SPI、OSGI 等方案,由于脱离了 Spring IOC 的管理在插件中无法注入主程序的 Bean 对象,例如主程序中已经集成了 Redis 但是在插件中无法使用。

本文主要介绍在 Spring Boot 工程中热加载 jar 包并注册成为 Bean 对象的一种实现思路,在动态扩展功能的同时支持在插件中注入主程序的 Bean 实现功能更强大的插件。

二、热加载 jar 包

通过指定的链接或者路径动态加载 jar 包,可以使用 URLClassLoader 的 addURL 方法来实现,样例代码如下:

「ClassLoaderUtil 类」

  1. public class ClassLoaderUtil { 
  2.     public static ClassLoader getClassLoader(String url) { 
  3.         try { 
  4.             Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); 
  5.             if (!method.isAccessible()) { 
  6.                 method.setAccessible(true); 
  7.             } 
  8.             URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoader.getSystemClassLoader()); 
  9.             method.invoke(classLoader, new URL(url)); 
  10.             return classLoader; 
  11.         } catch (Exception e) { 
  12.             log.error("getClassLoader-error", e); 
  13.             return null
  14.         } 
  15.     } 

其中在创建 URLClassLoader 时,指定当前系统的 ClassLoader 为父类加载器 ClassLoader.getSystemClassLoader() 这步比较关键,用于打通主程序与插件之间的 ClassLoader ,解决把插件注册进 IOC 时的各种 ClassNotFoundException 问题。

三、动态注册 Bean

将插件 jar 中加载的实现类注册到 Spring 的 IOC 中,同时也会将 IOC 中已有的 Bean 注入进插件中;分别在程序启动时和运行时两种场景下的实现方式。

3.1. 启动时注册

使用 ImportBeanDefinitionRegistrar 实现在 Spring Boot 启动时动态注册插件的 Bean,样例代码如下:「PluginImportBeanDefinitionRegistrar 类」

  1. public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { 
  2.     private final String targetUrl = "file:/D:/SpringBootPluginTest/plugins/plugin-impl-0.0.1-SNAPSHOT.jar"
  3.     private final String pluginClass = "com.plugin.impl.PluginImpl"
  4.  
  5.     @SneakyThrows 
  6.     @Override 
  7.     public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { 
  8.         ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl); 
  9.         Class clazz = classLoader.loadClass(pluginClass); 
  10.         BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz); 
  11.         BeanDefinition beanDefinition = builder.getBeanDefinition(); 
  12.         registry.registerBeanDefinition(clazz.getName(), beanDefinition); 
  13.     } 

3.2. 运行时注册

程序运行时动态注册插件的 Bean 通过使用 ApplicationContext 对象来实现,样例代码如下:

  1. @GetMapping("/reload"
  2. public Object reload() throws ClassNotFoundException { 
  3.   ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl); 
  4.   Class clazz = classLoader.loadClass(pluginClass); 
  5.   springUtil.registerBean(clazz.getName(), clazz); 
  6.   PluginInterface plugin = (PluginInterface)springUtil.getBean(clazz.getName()); 
  7.   return plugin.sayHello("test reload"); 

「SpringUtil 类」

  1. @Component 
  2. public class SpringUtil implements ApplicationContextAware { 
  3.     private DefaultListableBeanFactory defaultListableBeanFactory; 
  4.     private ApplicationContext applicationContext; 
  5.  
  6.     @Override 
  7.     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 
  8.         this.applicationContext = applicationContext; 
  9.         ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext; 
  10.         this.defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); 
  11.     } 
  12.  
  13.     public void registerBean(String beanName, Class clazz) { 
  14.         BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz); 
  15.         defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition()); 
  16.     } 
  17.  
  18.     public Object getBean(String name) { 
  19.         return applicationContext.getBean(name); 
  20.     } 

四、总结

本文介绍的插件化实现思路通过 「共用 ClassLoader」 和 「动态注册 Bean」 的方式,打通了插件与主程序之间的类加载器和 Spring 容器,使得可以非常方便的实现插件与插件之间和插件与主程序之间的 「类交互」,例如在插件中注入主程序的 Redis、DataSource、调用远程 Dubbo 接口等等。

但是由于没有对插件之间的 ClassLoader 进行 「隔离」 也可能会存在如类冲突、版本冲突等问题;并且由于 ClassLoader 中的 Class 对象无法销毁,所以除非修改类名或者类路径,不然插件中已加载到 ClassLoader 的类是没办法动态修改的。

所以本方案比较适合插件数据量不会太多、具有较好的开发规范、插件经过测试后才能上线或发布的场景。

五、完整 demo

 

https://github.com/zlt2000/springs-boot-plugin-test

 

来源: 陶陶技术笔记内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯