乐于分享
好东西不私藏

插件模式(热插拔插件架构)

插件模式(热插拔插件架构)

插件模式(热插拔插件架构)

整体思路

  1. 1. 主项目只定义 插件接口
  2. 2. 插件打成独立 Jar,放到指定目录
  3. 3. 主项目 动态加载 Jar,扫描实现类
  4. 4. 运行时可 新增、替换、删除插件,不用重启
  5. 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

image-20260408170234175

3、打包

然后打包成 xxx.jar,放到主项目的 ./plugins 目录。

三、运行效果

  1. 1. 启动主服务
  2. 2. 访问:/plugin/reload
  3. 3. 自动加载 plugins 目录下所有 Jar
  4. 4. 访问:/plugin/exec/xxx
  5. 5. 控制台输出插件执行日志

四、新增插件

  • • 直接丢  Jar 到 plugins
  • • 调用 /plugin/reload
  • • 立即生效,不重启服务

五、这个模式为什么是顶级解耦?

  1. 1. 主项目完全不依赖插件代码
  2. 2. 插件可独立开发、独立发版
  3. 3. 运行时增删替换,零停机
  4. 4. 业务扩展 = 加插件 Jar,主代码永远不动
  5. 5. 不属于 23 种 GOF 模式,是 架构级实战模式