Spring Boot启动初始化的静态ApplicationContext变量后续访问为null问题排查
问题根因
你遇到的问题不是类卸载,本质是Spring Boot DevTools的双类加载器机制导致的静态变量隔离:
Spring Boot DevTools为了实现快速热重启,会启动两类类加载器:
- 基础类加载器(AppClassLoader):加载不会频繁改动的第三方依赖类
- 重启类加载器(RestartClassLoader):加载你编写的业务代码,每次代码变更后会销毁旧的重启类加载器,新建实例加载修改后的代码,实现快速重启
你的场景中,Spring托管的ApplicationContextListenerBean是由RestartClassLoader加载的,setApplicationContext方法赋值的是RestartClassLoader加载的ApplicationContextListener类的静态变量。而你的SPI(META-INF/services)逻辑是由AppClassLoader加载的ApplicationContextListener类,两个类加载器加载的同一个全类名类是完全独立的Class对象,静态变量绑定到Class对象本身,所以你访问到的AppClassLoader加载的类的静态变量从来没有被赋值,自然为null。
疑问解答
1. 为什么应用上下文中的有效Bean对应类会被卸载?
类卸载的触发条件非常苛刻,需要同时满足:类的所有实例被GC回收、加载该类的ClassLoader被GC回收、没有任何地方引用该类的Class对象。你的ApplicationContextListener实例是Spring容器托管的Bean,只要Spring上下文没有销毁,实例就不会被回收,类也不可能被卸载,你之前的类卸载猜测不成立。
2. 如何修复该问题?
可按照优先级选择以下方案:
- 方案1(最简便,无额外配置):移除spring-boot-devtools依赖。如果你不需要热重启功能,直接删除devtools依赖即可消除双类加载器机制,问题直接解决。
- 方案2(保留DevTools热重启能力):配置DevTools将指定类交给基础类加载器加载,保证全应用只有一份
ApplicationContextListener的Class对象,静态变量全局唯一。在application.properties中添加如下配置:# 替换为你自己的ApplicationContextListener的完整类路径 spring.devtools.restart.additional-exclude=com/xxx/xxx/ApplicationContextListener.class - 方案3(适配SPI类加载逻辑):如果需要保留双类加载器机制,修改静态获取Bean的代码,主动用线程上下文类加载器(即RestartClassLoader)加载
ApplicationContextListener类,再获取已赋值的静态变量:public static MyBean getMyBean() { try { // 替换为你自己的ApplicationContextListener的全类名 Class<?> listenerClazz = Thread.currentThread().getContextClassLoader().loadClass("com.xxx.xxx.ApplicationContextListener"); Field contextField = listenerClazz.getDeclaredField("CONTEXT"); contextField.setAccessible(true); ApplicationContext context = (ApplicationContext) contextField.get(null); return context.getBean(MyBean.class); } catch (Exception e) { throw new RuntimeException("获取Bean实例失败", e); } } - 方案4(架构最优方案):避免用静态变量持有Spring上下文,将你的SPI实现类也注册为Spring Bean,直接通过依赖注入的方式注入需要的
MyBean实例,从根源上规避类加载器和静态变量的问题。
内容的提问来源于stack exchange,提问作者Sergey Shcherbakov
相关产品推荐
相关产品推荐

