Spring Boot 如何熱載入jar實現動態外掛?

zlt2000發表於2021-10-18

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

相關文章