插件模式(热插拔插件架构)
整体思路
1. 主项目只定义 插件接口 2. 插件打成独立 Jar,放到指定目录 3. 主项目 动态加载 Jar,扫描实现类 4. 运行时可 新增、替换、删除插件,不用重启 5. 完全解耦,主代码永远不动
一、主项目结构
1、插件接口(必须单独包,让插件依赖)
package cn.com.yunkeyo.plugin;import cn.com.yunkeyo.context.PluginContext;/** * 定义插件接口,所有业务插件都必须实现它 */public interface BizPlugin { // 插件唯一标识 String pluginId(); // 是否内置插件,内置插件不允许卸载 Boolean isBuiltIn(); // 插件执行逻辑 void execute(PluginContext context);}2、动态类加载器(核心)
package cn.com.yunkeyo.loader;import java.net.URL;import java.net.URLClassLoader;public class PluginClassLoader extends URLClassLoader { public PluginClassLoader(URL[] urls) { super(urls, ClassLoader.getSystemClassLoader()); } @Override public void close() { try { super.close(); } catch (Exception e) { e.printStackTrace(); } }}3、插件管理器(加载、卸载、执行)
package cn.com.yunkeyo.plugin;import cn.com.yunkeyo.context.PluginContext;import cn.com.yunkeyo.loader.PluginClassLoader;import org.springframework.stereotype.Component;import java.io.File;import java.net.URL;import java.util.Map;import java.util.ServiceLoader;import java.util.concurrent.ConcurrentHashMap;/** * 插件管理器(核心:注册 + 发现 + 执行) */@Componentpublic class PluginManager { /** * 插件注册表:热插拔核心 */ private static final Map<String, BizPlugin> PLUGIN_MAP = new ConcurrentHashMap<>(); // 类加载器持有,用于卸载 private final Map<String, PluginClassLoader> loaderMap = new ConcurrentHashMap<>(); // 插件目录 private static final String PLUGIN_DIR = "./plugins"; /** * 注册插件(可动态调用) * * @param plugin 插件 */ public void register(BizPlugin plugin) { PLUGIN_MAP.put(plugin.pluginId(), plugin); } public void loadOutsidePlugins() { File dir = new File(PLUGIN_DIR); if (!dir.exists()) dir.mkdirs(); File[] jars = dir.listFiles((f) -> f.getName().endsWith(".jar")); if (jars == null) return; for (File jar : jars) { loadPlugin(jar); } } // 加载单个 Jar 插件 public void loadPlugin(File jarFile) { try { URL url = jarFile.toURI().toURL(); PluginClassLoader loader = new PluginClassLoader(new URL[]{url}); // 扫描 IPlugin 实现类 ServiceLoader<BizPlugin> serviceLoader = ServiceLoader.load(BizPlugin.class, loader); for (BizPlugin plugin : serviceLoader) { if (plugin == null) { continue; } if (plugin.isBuiltIn()) { continue; } String id = plugin.pluginId(); PLUGIN_MAP.put(id, plugin); loaderMap.put(id, loader); System.out.println("外部插件加载成功:" + id + " → " + jarFile.getName()); } } catch (Exception e) { e.printStackTrace(); } } /** * 卸载插件 * * @param pluginId 插件ID */ public void unregister(String pluginId) { BizPlugin bizPlugin = PLUGIN_MAP.get(pluginId); if (bizPlugin != null && !bizPlugin.isBuiltIn()) { throw new RuntimeException("内置插件不能卸载:" + pluginId); } BizPlugin plugin = PLUGIN_MAP.remove(pluginId); if (plugin == null) return; PluginClassLoader loader = loaderMap.remove(pluginId); if (loader != null) loader.close(); System.out.println("外部插件已卸载:" + pluginId); } /** * 执行某个插件 * * @param pluginId 插件ID * @param context 上下文 */ public void executePlugin(String pluginId, PluginContext context) { BizPlugin plugin = PLUGIN_MAP.get(pluginId); if (plugin == null) { throw new RuntimeException("插件不存在:" + pluginId); } plugin.execute(context); } /** * 执行所有插件 * * @param context 上下文 */ public void executeAll(PluginContext context) { for (BizPlugin plugin : PLUGIN_MAP.values()) { plugin.execute(context); } } /** * 获取注册的插件 * * @return 插件 */ public Map<String, BizPlugin> getRegisteredPlugins() { return PLUGIN_MAP; } /** * 判断某个插件是否已注册 * * @param pluginId 插件ID * @return 是否已注册 */ public Boolean isRegistered(String pluginId) { return PLUGIN_MAP.containsKey(pluginId); }}4、提供一个 HTTP 接口手动触发热加载
package cn.com.yunkeyo.controller;import cn.com.yunkeyo.context.PluginContext;import cn.com.yunkeyo.plugin.PluginManager;import jakarta.annotation.Resource;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class PluginController { @Resource private PluginManager pluginManager; /** * 热加载所有外部插件 * @return 提示信息 */ @GetMapping("/plugin/reload") public String reload() { pluginManager.loadOutsidePlugins(); return "热加载外部插件"; } // 卸载插件 @GetMapping("/plugin/unload/{id}") public String unload(@PathVariable String id) { pluginManager.unregister(id); return "卸载插件"; } // 执行插件 @GetMapping("/plugin/exec/{id}") public String exec(@PathVariable String id) { PluginContext context = new PluginContext(); context.setOrderNo("ORDER_123"); pluginManager.executePlugin(id, context); return "exec success"; } // 查看插件列表 @GetMapping("/plugin/list") public Object list() { return pluginManager.getRegisteredPlugins(); }}二、插件项目(单独 Maven 模块)
1、只依赖主项目的 plugin-api,实现 BizPlugin 接口
package cn.com.yunkeyo.plugin;import cn.com.yunkeyo.context.PluginContext;import cn.com.yunkeyo.plugin.BizPlugin;import org.springframework.stereotype.Component;@Componentpublic class PointPlugin implements BizPlugin { @Override public String pluginId() { return "point"; } @Override public Boolean isBuiltIn() { return false; } @Override public void execute(PluginContext context) { System.out.println("插件执行:积分 +100"); context.getData().put("point", 100); }}2、SPI 配置
新建 /META-INF/services/cn.com.yunkeyo.plugin.BizPlugin 配置,添加 cn.com.yunkeyo.plugin.PointPlugin

3、打包
然后打包成 xxx.jar,放到主项目的 ./plugins 目录。
三、运行效果
1. 启动主服务 2. 访问: /plugin/reload3. 自动加载 plugins目录下所有Jar4. 访问: /plugin/exec/xxx5. 控制台输出插件执行日志
四、新增插件
• 直接丢 Jar到plugins• 调用 /plugin/reload• 立即生效,不重启服务
五、这个模式为什么是顶级解耦?
1. 主项目完全不依赖插件代码 2. 插件可独立开发、独立发版 3. 运行时增删替换,零停机 4. 业务扩展 = 加插件 Jar,主代码永远不动5. 不属于 23 种 GOF模式,是 架构级实战模式
夜雨聆风