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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章