You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot运行时动态注册卸载Bean、Rest Controller及模块热加载方案

实现结论

你的需求完全可以基于Spring原生能力实现,不需要修改Spring核心源码,核心设计原则如下:

  • 必须为每个动态加载的模块创建独立的子ApplicationContext,将主应用的ApplicationContext设为子上下文的父容器:既可以让模块内的Bean正常注入主应用暴露的公共组件,也能实现模块间Bean隔离,卸载时不会污染主容器
  • 不要尝试把模块内的Bean直接注册到主ApplicationContext:后续卸载清理极易出现引用残留、内存泄漏、类冲突问题
  • 模块加载时自动扫描内部组件注册到专属子上下文,同步将Web层接口注册到主应用的请求映射体系;模块卸载时直接关闭子上下文、清理请求映射、释放模块专属ClassLoader,即可彻底移除所有模块相关的功能
  • 你提到的PF4J和Spring集成的实现逻辑也是上述父子上下文的思路,你自研框架完全可以参考这套逻辑落地
具体落地步骤

模块加载流程

  • 按你现有逻辑为模块创建独立URLClassLoader,将主应用的ClassLoader设为该类加载器的父级,保证Spring、公共工具类等通用依赖由主加载器加载,避免重复类定义导致的类型转换异常
  • 基于模块ClassLoader创建AnnotationConfigApplicationContext实例,绑定父上下文为Spring Boot主应用上下文
  • 配置子上下文的扫描路径,自动扫描模块内标注了@Component、@Service、@RestController等注解的类,完成依赖注入
  • 子上下文刷新完成后,提取所有Controller实例,注册到主应用的RequestMappingHandlerMapping中,模块提供的接口即可正常对外访问

模块卸载流程

  • 遍历当前模块持有的所有Controller实例,从主应用的RequestMappingHandlerMapping中移除对应的请求映射,保证接口无法再被访问
  • 调用模块对应子上下文的close()方法,触发所有Bean的销毁回调,清空子上下文持有的所有Bean引用
  • 关闭模块专属ClassLoader,释放模块所有类的资源引用
  • 从你自研的模块管理容器中移除该模块的所有记录,避免残留引用导致GC无法回收
核心代码示例
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;

public class PluginManager {
    private final ApplicationContext mainContext;
    private final RequestMappingHandlerMapping handlerMapping;

    public PluginManager(ApplicationContext mainContext) {
        this.mainContext = mainContext;
        this.handlerMapping = mainContext.getBean(RequestMappingHandlerMapping.class);
    }

    // 加载模块
    public PluginInstance loadModule(File moduleJar) throws Exception {
        // 创建模块专属类加载器
        URLClassLoader moduleClassLoader = new URLClassLoader(
                new URL[]{moduleJar.toURI().toURL()},
                mainContext.getClassLoader()
        );

        // 创建模块专属子Spring上下文
        AnnotationConfigApplicationContext moduleContext = new AnnotationConfigApplicationContext();
        moduleContext.setClassLoader(moduleClassLoader);
        moduleContext.setParent(mainContext);
        // 按约定扫描模块包路径,可根据模块自身配置调整
        moduleContext.scan("com.plugin.module.*");
        moduleContext.refresh();

        // 注册模块内所有Controller到主应用请求映射
        Set<String> controllerBeanNames = new HashSet<>();
        Map<String, Object> controllerBeans = moduleContext.getBeansWithAnnotation(Controller.class);
        for (Map.Entry<String, Object> entry : controllerBeans.entrySet()) {
            controllerBeanNames.add(entry.getKey());
            handlerMapping.registerHandler(entry.getValue());
        }

        return new PluginInstance(moduleClassLoader, moduleContext, controllerBeanNames);
    }

    // 卸载模块
    public void unloadModule(PluginInstance pluginInstance) throws Exception {
        // 移除所有接口映射
        for (String beanName : pluginInstance.getControllerBeanNames()) {
            Object controller = pluginInstance.getContext().getBean(beanName);
            handlerMapping.unregisterHandler(controller);
        }
        // 关闭模块上下文,销毁所有Bean
        pluginInstance.getContext().close();
        // 关闭类加载器,释放类资源
        pluginInstance.getClassLoader().close();
    }

    // 模块实例持有类
    public static class PluginInstance {
        private final URLClassLoader classLoader;
        private final AnnotationConfigApplicationContext context;
        private final Set<String> controllerBeanNames;

        public PluginInstance(URLClassLoader classLoader, AnnotationConfigApplicationContext context, Set<String> controllerBeanNames) {
            this.classLoader = classLoader;
            this.context = context;
            this.controllerBeanNames = controllerBeanNames;
        }

        public URLClassLoader getClassLoader() { return classLoader; }
        public AnnotationConfigApplicationContext getContext() { return context; }
        public Set<String> getControllerBeanNames() { return controllerBeanNames; }
    }
}
落地注意事项
  • 每个模块的包名需要做独立前缀隔离,不要和主应用、其他模块的包路径重叠,避免组件扫描冲突
  • 模块JAR打包时不要包含Spring、通用工具包等主应用已经引入的依赖,避免类加载重复导致的异常
  • 如果模块需要用到AOP、事务、配置绑定等Spring能力,只需要在创建子上下文时注册对应的处理器即可,用法和普通Spring上下文完全一致
  • 卸载模块时需要确保主应用、其他模块没有持有该模块内类、Bean的引用,否则会出现ClassLoader无法回收的内存泄漏问题

内容的提问来源于stack exchange,提问作者Dominic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 06:21:19