当前位置: 首页 > 科技观察

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

时间:2023-03-17 21:59:35 科技观察

本文转载自微信公众号《淘淘技术笔记》,作者zlt2000。转载本文请联系淘淘技术笔记公众号。一、后台动态插件编程是个很酷的东西。可以实现业务功能的“解耦”,便于维护,也可以提高“可扩展性”。随时可以在不停止服务器的情况下进行功能扩展,而且它还具有非常好的“开放性”,除了自己的研发人员可以开发功能外,还可以接受第三方开发者按照规范开发的插件.动态插件常见的实现方式有SPI、OSGI等方案。由于SpringIOC的管理是分离的,所以主程序的Bean对象无法注入到插件中。比如Redis已经集成在主程序中,但不能在插件中使用。本文主要介绍一种在SpringBoot项目中热加载jar包并注册为Bean对象的实现思路。在动态扩展功能的同时,支持在插件中向主程序注入bean,实现更强大的插件。2、热加载jar包通过指定的链接或路径动态加载jar包,可以使用URLClassLoader的addURL方法实现。示例代码如下:“ClassLoaderUtil类”publicclassClassLoaderUtil{publicstaticClassLoadergetClassLoader(Stringurl){try{Methodmethod=URLClassLoader.class.getDeclaredMethod("addURL",URL.class);if(!method.isAccessible()){method.setAccessible(true);}URLClassLoaderclassLoader=newURLClassLoader(newURL[]{},ClassLoader.getSystemClassLoader());method.invoke(classLoader,newURL(url));returnclassLoader;}catch(Exceptione){log.error("getClassLoader-error",e);returnull;}}}创建URLClassLoader时指定当前系统的ClassLoader为父类加载器ClassLoader.getSystemClassLoader()这一步比较关键。用于打通主程序和插件之间的ClassLoader,解决插件注册到IOC时的各种ClassNotFoundException问题。3、bean的动态注册将插件jar中加载的实现类注册到Spring的IOC中,同时将IOC中已有的bean注入到插件中;分别在程序启动和运行时两种场景下的实现方法。3.1.启动时注册使用ImportBeanDefinitionRegistrar在SpringBoot启动时动态注册插件bean。示例代码如下:《PluginImportBeanDefinitionRegistrar类》publicclassPluginImportBeanDefinitionRegistrariimplementsImportBeanDefinitionRegistrar{privatefinalStringtargetUrl="file:/D:/SpringBootPlugin-pluginTest/plugin0-SNAPSHOT.jar";privatefinalStringpluginClass="com.plugin.impl.DefunitionThis@pluginBeanOverrides@pluginneregisterOverride";(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){ClassLoaderclassLoader=ClassLoaderUtil.getClassLoader(classUrluz.claz);BeanDefinitionBuilderbuilder=BeanDefinitionBuilder.genericBeanDefinition(clazz);BeanDefinitionbeanDefinition=builder.getBeanDefinition();registry.registerBeanDefinition(clazz.getName(),beanDefinition);}}实现,示例代码如下:@GetMapping("/reload")publicObjectreload()扔sClassNotFoundException{ClassLoaderclassLoader=ClassLoaderUtil.getClassLoader(targetUrl);Classclazz=classLoader.loadClass(pluginClass);springUtil.registerBean(clazz.getName(),clazz);PluginInterfaceplugin=(PluginInterface)springUtil.getBean(clazz.getName());returnplugin.sayHello("testreload");}「SpringUtil类」@ComponentpublicclassSpringUtilimplementsApplicationContextAware{privateDefaultListableBeanFactorydefaultListableBeanFactory;privateApplicationContextapplicationContext;@OverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{this.applicationContext=applicationContext;ConfigurableApplicationContextconfigurableApplicationContext=(ConfigurableApplicationContext)applicationContext;this.defaultListableBeanFactory=(DefaultListableBeanFactory)configurableApplicationContext.getBeanFactory();}publicvoidregisterBean(StringbeanName,Classclazz){BeanDefinitionBuilderbeanDefinitionBuilder=BeanDefinitionBuilder.genericBeanDefinition(clazz);defaultListableBeanFactory.registerBeanDefinition(beanName,beanDefinitionBuilder.getRawBeanDefinition());}publicObjectgetBean(Stringname){returnapplicationContext.getBean(name);}}四、总结本文介绍的插件实现思路通过“共享ClassLoader”和“Bean动态注册”打通了插件与主程序之间的类加载器和Spring容器,非常方便的实现了插件与插件之间的“类交互”在插件和主程序之间。比如在插件中注入主程序的Redis和DataSource,调用远程Dubbo接口等。但是由于插件之间的ClassLoader不是“孤立”;并且由于ClassLoaderObjects中的Class无法被销毁,所以除非修改类名或类路径,否则无法动态修改插件中加载到ClassLoader中的类。因此,该方案更适用于插件数据量不多,开发规范良好,插件测试后才能上线或发布的场景。5.完整demohttps://github.com/zlt2000/springs-boot-plugin-test