Spring Boot 如何熱加載jar實現動態插件?


mark

一、背景

動態插件化編程是一件很酷的事情,能實現業務功能的 解耦 便於維護,另外也可以提升 可擴展性 隨時可以在不停服務器的情況下擴展功能,也具有非常好的 開放性 除了自己的研發人員可以開發功能之外,也能接納第三方開發商按照規范開發的插件。

常見的動態插件的實現方式有 SPIOSGI 等方案,由於脫離了 Spring IOC 的管理在插件中無法注入主程序的 Bean 對象,例如主程序中已經集成了 Redis 但是在插件中無法使用。

本文主要介紹在 Spring Boot 工程中熱加載 jar 包並注冊成為 Bean 對象的一種實現思路,在動態擴展功能的同時支持在插件中注入主程序的 Bean 實現功能更強大的插件。

 

二、熱加載 jar 包

通過指定的鏈接或者路徑動態加載 jar 包,可以使用 URLClassLoaderaddURL 方法來實現,樣例代碼如下:

ClassLoaderUtil 類

public class ClassLoaderUtil {
    public static ClassLoader getClassLoader(String url) {
        try {
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            if (!method.isAccessible()) {
                method.setAccessible(true);
            }
            URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoader.getSystemClassLoader());
            method.invoke(classLoader, new URL(url));
            return classLoader;
        } catch (Exception e) {
            log.error("getClassLoader-error", e);
            return null;
        }
    }
}

其中在創建 URLClassLoader 時,指定當前系統的 ClassLoader 為父類加載器 ClassLoader.getSystemClassLoader() 這步比較關鍵,用於打通主程序與插件之間的 ClassLoader ,解決把插件注冊進 IOC 時的各種 ClassNotFoundException 問題。

 

三、動態注冊 Bean

將插件 jar 中加載的實現類注冊到 Spring 的 IOC 中,同時也會將 IOC 中已有的 Bean 注入進插件中;分別在程序啟動時和運行時兩種場景下的實現方式。

3.1. 啟動時注冊 Bean

使用 ImportBeanDefinitionRegistrar 實現在 Spring Boot 啟動時動態注冊插件的 Bean,樣例代碼如下:

PluginImportBeanDefinitionRegistrar 類

public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    private final String targetUrl = "file:/D:/SpringBootPluginTest/plugins/plugin-impl-0.0.1-SNAPSHOT.jar";
    private final String pluginClass = "com.plugin.impl.PluginImpl";

    @SneakyThrows
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl);
        Class<?> clazz = classLoader.loadClass(pluginClass);
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        BeanDefinition beanDefinition = builder.getBeanDefinition();
        registry.registerBeanDefinition(clazz.getName(), beanDefinition);
    }
}

 

3.2. 運行時注冊 Bean

程序運行時動態注冊插件的 Bean 通過使用 ApplicationContext 對象來實現,樣例代碼如下:

@GetMapping("/reload")
public Object reload() throws ClassNotFoundException {
		ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl);
		Class<?> clazz = classLoader.loadClass(pluginClass);
		springUtil.registerBean(clazz.getName(), clazz);
		PluginInterface plugin = (PluginInterface)springUtil.getBean(clazz.getName());
		return plugin.sayHello("test reload");
}

SpringUtil 類

@Component
public class SpringUtil implements ApplicationContextAware {
    private DefaultListableBeanFactory defaultListableBeanFactory;
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
        this.defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
    }

    public void registerBean(String beanName, Class<?> clazz) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
    }

    public Object getBean(String name) {
        return applicationContext.getBean(name);
    }
}

 

四、總結

本文介紹的插件化實現思路通過 共用 ClassLoader動態注冊 Bean 的方式,打通了插件與主程序之間的類加載器和 Spring 容器,使得可以非常方便的實現插件與插件之間和插件與主程序之間的 類交互,例如在插件中注入主程序的 Redis、DataSource、調用遠程 Dubbo 接口等等。

但是由於沒有對插件之間的 ClassLoader 進行 隔離 也可能會存在如類沖突、版本沖突等問題;並且由於 ClassLoader 中的 Class 對象無法銷毀,所以除非修改類名或者類路徑,不然插件中已加載到 ClassLoader 的類是沒辦法動態修改的。

所以本方案比較適合插件數據量不會太多、具有較好的開發規范、插件經過測試后才能上線或發布的場景。

 

五、完整 demo

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

 

掃碼關注有驚喜!

file


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM