一、背景
动态插件化编程是一件很酷的事情,能实现业务功能的 「解耦」 便于维护,另外也可以提升 「可扩展性」 随时可以在不停服务器的情况下扩展功能,也具有非常好的 「开放性」 除了自己的研发人员可以开发功能之外,也能接纳第三方开发商按照规范开发的插件。
常见的动态插件的实现方式有 SPI、OSGI 等方案,由于脱离了 Spring IOC 的管理在插件中无法注入主程序的 Bean 对象,例如主程序中已经集成了 Redis 但是在插件中无法使用。
本文主要介绍在 Spring Boot 工程中热加载 jar 包并注册成为 Bean 对象的一种实现思路,在动态扩展功能的同时支持在插件中注入主程序的 Bean 实现功能更强大的插件。
二、热加载 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);
- returnnull;
- }
- }
- }
其中在创建 URLClassLoader 时,指定当前系统的 ClassLoader 为父类加载器 ClassLoader.getSystemClassLoader() 这步比较关键,用于打通主程序与插件之间的 ClassLoader ,解决把插件注册进 IOC 时的各种 ClassNotFoundException 问题。
三、动态注册 Bean
将插件 jar 中加载的实现类注册到 Spring 的 IOC 中,同时也会将 IOC 中已有的 Bean 注入进插件中;分别在程序启动时和运行时两种场景下的实现方式。
3.1. 启动时注册
使用 ImportBeanDefinitionRegistrar 实现在 Spring Boot 启动时动态注册插件的 Bean,样例代码如下:「PluginImportBeanDefinitionRegistrar 类」
- publicclassPluginImportBeanDefinitionRegistrarimplementsImportBeanDefinitionRegistrar{
- privatefinalStringtargetUrl="file:/D:/SpringBootPluginTest/plugins/plugin-impl-0.0.1-SNAPSHOT.jar";
- privatefinalStringpluginClass="com.plugin.impl.PluginImpl";
- @SneakyThrows
- @Override
- publicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){
- ClassLoaderclassLoader=ClassLoaderUtil.getClassLoader(targetUrl);
- Class>clazz=classLoader.loadClass(pluginClass);
- BeanDefinitionBuilderbuilder=BeanDefinitionBuilder.genericBeanDefinition(clazz);
- BeanDefinitionbeanDefinition=builder.getBeanDefinition();
- registry.registerBeanDefinition(clazz.getName(),beanDefinition);
- }
- }
3.2. 运行时注册
程序运行时动态注册插件的 Bean 通过使用 ApplicationContext 对象来实现,样例代码如下:
- @GetMapping("/reload")
- publicObjectreload()throwsClassNotFoundException{
- ClassLoaderclassLoader=ClassLoaderUtil.getClassLoader(targetUrl);
- Class>clazz=classLoader.loadClass(pluginClass);
- springUtil.registerBean(clazz.getName(),clazz);
- PluginInterfaceplugin=(PluginInterface)springUtil.getBean(clazz.getName());
- returnplugin.sayHello("testreload");
- }
「SpringUtil 类」
- @Component
- publicclassSpringUtilimplementsApplicationContextAware{
- privateDefaultListableBeanFactorydefaultListableBeanFactory;
- privateApplicationContextapplicationContext;
- @Override
- publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{
- this.applicationContext=applicationContext;
- ConfigurableApplicationContextconfigurableApplicationContext=(ConfigurableApplicationContext)applicationContext;
- this.defaultListableBeanFactory=(DefaultListableBeanFactory)configurableApplicationContext.getBeanFactory();
- }
- publicvoidregisterBean(StringbeanName,Class>clazz){
- BeanDefinitionBuilderbeanDefinitionBuilder=BeanDefinitionBuilder.genericBeanDefinition(clazz);
- defaultListableBeanFactory.registerBeanDefinition(beanName,beanDefinitionBuilder.getRawBeanDefinition());
- }
- publicObjectgetBean(Stringname){
- returnapplicationContext.getBean(name);
- }
- }
四、总结
本文介绍的插件化实现思路通过 「共用 ClassLoader」 和 「动态注册 Bean」 的方式,打通了插件与主程序之间的类加载器和 Spring 容器,使得可以非常方便的实现插件与插件之间和插件与主程序之间的 「类交互」,例如在插件中注入主程序的 Redis、DataSource、调用远程 Dubbo 接口等等。
但是由于没有对插件之间的 ClassLoader 进行 「隔离」 也可能会存在如类冲突、版本冲突等问题;并且由于 ClassLoader 中的 Class 对象无法销毁,所以除非修改类名或者类路径,不然插件中已加载到 ClassLoader 的类是没办法动态修改的。
所以本方案比较适合插件数据量不会太多、具有较好的开发规范、插件经过测试后才能上线或发布的场景。
五、完整 demo
https://github.com/zlt2000/springs-boot-plugin-test
原文链接:https://mp.weixin.qq.com/s/Fg-jsoFon5LwsPAaBbeiew