徹底解耦!Spring Boot 實現外部 Jar 動態加載與熱插拔式擴展架構
在大型系統中,業務快速變化、功能可插拔、模塊可裁剪成為主流趨勢。為此,我們可以基于 Spring Boot 實現一種插件式架構,支持動態加載外部 Jar 并熱插拔模塊功能,進一步實現架構解耦、運行期擴展、服務熱更新等目標。
本文將帶你深入實現一套完整的插件機制,加載目錄 /usr/local/java/plugins
中的 Jar 包,并將其中的 Spring Bean 自動注入主工程上下文中。
架構目標與實現思路
核心目標可以拆解為兩個步驟:
- 將 Jar 加載到 JVM
- 讓 Spring 識別并注冊其中的 Bean
Spring Boot 本身對類加載器有一定擴展能力,再結合自定義加載器和 spring.factories
,便可輕松實現這一機制。
項目結構概覽
plugin-loader-demo/
├── plugin-host/ # 主工程,負責加載插件
│ ├── src/main/java/com/icoderoad/host/
│ │ └── PluginHostApplication.java
│ │ └── loader/
│ │ ├── PluginClassLoader.java
│ │ ├── PluginScanner.java
│ │ └── PluginRegistrar.java
├── plugin-user-center/ # 插件模塊:用戶中心功能
│ ├── src/main/java/com/icoderoad/plugins/user/
│ │ ├── UserService.java
│ │ └── UserAutoConfiguration.java
│ └── resources/META-INF/spring.factories
主工程 plugin-host 的 pom.xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.icoderoad</groupId>
<artifactId>plugin-host</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>ZIP</layout>
</configuration>
</plugin>
</plugins>
</build>
</project>
插件模塊 plugin-user-center 的 pom.xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.icoderoad.plugins</groupId>
<artifactId>plugin-user-center</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
</project>
插件加載器實現
PluginClassLoader:從目錄中加載外部 Jar
package com.icoderoad.host.loader;
public class PluginClassLoader extends URLClassLoader {
public PluginClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public static PluginClassLoader fromDirectory(String dirPath) throws IOException {
File[] jars = new File(dirPath).listFiles(f -> f.getName().endsWith(".jar"));
if (jars == null) return new PluginClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
URL[] urls = Arrays.stream(jars)
.map(file -> {
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
return new PluginClassLoader(urls, Thread.currentThread().getContextClassLoader());
}
}
PluginScanner:讀取插件 Jar 中的 spring.factories
package com.icoderoad.host.loader;
public class PluginScanner {
public static List<Class<?>> scanAutoConfigClasses(ClassLoader pluginClassLoader) throws IOException, ClassNotFoundException {
List<Class<?>> result = new ArrayList<>();
Enumeration<URL> resources = pluginClassLoader.getResources("META-INF/spring.factories");
while (resources.hasMoreElements()) {
try (InputStream input = resources.nextElement().openStream()) {
Properties props = new Properties();
props.load(input);
String classList = props.getProperty(EnableAutoConfiguration.class.getName());
if (classList != null) {
for (String className : classList.split(",")) {
result.add(Class.forName(className.trim(), true, pluginClassLoader));
}
}
}
}
return result;
}
}
PluginRegistrar:注冊配置類到 Spring 容器
package com.icoderoad.host.loader;
public class PluginRegistrar {
public static void registerPlugins(ConfigurableApplicationContext context, String pluginDir) throws Exception {
PluginClassLoader loader = PluginClassLoader.fromDirectory(pluginDir);
List<Class<?>> configClasses = PluginScanner.scanAutoConfigClasses(loader);
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) context.getBeanFactory();
AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(registry);
reader.setBeanClassLoader(loader);
for (Class<?> clazz : configClasses) {
reader.register(clazz);
}
}
}
主程序入口整合插件加載邏輯
package com.icoderoad.host;
@SpringBootApplication
public class PluginHostApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(PluginHostApplication.class, args);
try {
PluginRegistrar.registerPlugins(context, "/usr/local/java/plugins");
} catch (Exception e) {
e.printStackTrace();
}
}
}
插件模塊:UserService + 自動配置類
// com.icoderoad.plugins.user.UserService
public class UserService {
public String getUsername() {
return "Plugin User";
}
}
// com.icoderoad.plugins.user.UserAutoConfiguration
@Configuration
public class UserAutoConfiguration {
@Bean
public UserService userService() {
return new UserService();
}
}
插件 spring.factories 配置
路徑:src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.icoderoad.plugins.user.UserAutoConfiguration
插件測試驗證代碼
在主應用中通過注入判斷插件是否成功加載:
@Autowired(required = false)
private UserService userService;
@PostConstruct
public void init() {
if (userService != null) {
System.out.println("插件加載成功:" + userService.getUsername());
} else {
System.out.println("插件未加載!");
}
}
總結
通過動態構建類加載器、讀取 spring.factories
并手動注冊 Bean,我們成功在 Spring Boot 中實現了外部 Jar 的插件化加載。
優點:
- 插件 Jar 可獨立開發部署
- 插件可運行期動態加載
- 支持 Spring 自動配置機制