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

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

时间:2023-03-11 23:04:19 科技观察

1。背景动态插件编程是一件很酷的事情。可以实现业务功能的“解耦”,便于维护,也可以提高“可扩展性”。可以在不停止服务器的情况下随时扩展功能,也有很好的“开放性”。除了自己的研发人员可以开发功能外,也可以接受第三方开发者按照规范开发的插件。动态插件常见的实现方式有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:/SpringBootPluginTest/pluginTest/plugin-SNAPSHOT.jar";privatefinalStringpluginClass="com.plugin.impl.@luginBeanDefinitionsPublicsOverridempl";(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){ClassLoaderclassLoader=ClassLoaderUtil.getClassLoader(classUrluz.claz);BeanDefinitionBuilderbuilder=BeanDefinitionBuilder.genericBeanDefinition(clazz);BeanDefinitionbeanDefinition=builder.getBeanDefinition();registry.registerBeanDefinition(clazz.getName(),beanDefinition);}}实现,示例代码如下:@GetMapping("/reload")publicObjectreload()抛出类sNotFoundException{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);}}4.通过《总结本文介绍的插件实现思路》共享ClassLoader”和“动态注册bean”的方式打通了插件与主程序之间的类加载器和Spring容器,非常方便的实现了插件与插件之间的“类交互”插件与主程序之间,例如在插件中注入主程序的Redis、DataSource,调用远程Dubbo接口等,但是由于之间的ClassLoader之间没有“隔离”插件,可能存在类冲突、版本冲突等问题;并且因为ClassLoader中的Class对象是不能被销毁的,所以除非修改了类名或者类路径,否则插件中加载到ClassLoader中的类是不能动态修改的。因此,该方案更适用于插件数据量不多,开发规范良好,插件测试后才能上线或发布的场景。5.完整demohttps://github.com/zlt2000/springs-boot-plugin-test