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

本文转载自微信公众号「陶陶技术笔记」,作者zlt2000。转载本文请联系陶陶技术笔记公众号。

创新互联建站坚持“要么做到,要么别承诺”的工作理念,服务领域包括:做网站、成都网站设计、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的丰都网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!

一、背景

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

常见的动态插件的实现方式有 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.     }
  16. }

其中在创建 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.     @SneakyThrows
  5.     @Override
  6.     public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
  7.         ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl);
  8.         Class clazz = classLoader.loadClass(pluginClass);
  9.         BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
  10.         BeanDefinition beanDefinition = builder.getBeanDefinition();
  11.         registry.registerBeanDefinition(clazz.getName(), beanDefinition);
  12.     }
  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");
  8. }

「SpringUtil 类」

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

四、总结

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

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

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

五、完整 demo

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

本文标题:SpringBoot如何热加载Jar实现动态插件?
新闻来源:http://www.36103.cn/qtweb/news38/7588.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联