Spring Boot 如何熱加載Jar實現動態插件?
本文轉載自微信公眾號「陶陶技術筆記」,作者zlt2000。轉載本文請聯系陶陶技術筆記公眾號。
一、背景
動態插件化編程是一件很酷的事情,能實現業務功能的 「解耦」 便于維護,另外也可以提升 「可擴展性」 隨時可以在不停服務器的情況下擴展功能,也具有非常好的 「開放性」 除了自己的研發人員可以開發功能之外,也能接納第三方開發商按照規范開發的插件。
常見的動態插件的實現方式有 SPI、OSGI 等方案,由于脫離了 Spring IOC 的管理在插件中無法注入主程序的 Bean 對象,例如主程序中已經集成了 Redis 但是在插件中無法使用。
本文主要介紹在 Spring Boot 工程中熱加載 jar 包并注冊成為 Bean 對象的一種實現思路,在動態擴展功能的同時支持在插件中注入主程序的 Bean 實現功能更強大的插件。
二、熱加載 jar 包
通過指定的鏈接或者路徑動態加載 jar 包,可以使用 URLClassLoader 的 addURL 方法來實現,樣例代碼如下:
「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. 啟動時注冊
使用 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 通過使用 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