Spring BeanCreationNotAllowedException:容器销毁阶段Bean创建异常解析与解决方案
1. 问题引入:当Spring容器说“不”
如果你在用Spring Boot开发项目,尤其是在处理一些复杂的应用上下文生命周期,或者尝试在单元测试中做一些“骚操作”时,很可能在控制台见过下面这个让人心头一紧的异常:
org.springframework.beans.factory.BeanCreationNotAllowedException: Error creating bean with name ‘xxxService’: Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)这个异常信息量不小,但核心意思很明确:Spring容器正在关闭(或者处于销毁阶段),此时它拒绝为你创建新的单例Bean。这就像你去一家正在打烊的餐厅,厨师已经下班了,你非要他再给你炒个菜,餐厅经理(Spring容器)自然会拒绝你。
这个异常看似简单,但背后牵扯到Spring容器的核心生命周期管理、Bean的作用域、以及我们编码时容易忽略的线程安全问题。很多开发者第一次遇到时,会感到困惑:我的代码明明在正常调用getBean()或者依赖注入,为什么容器会“翻脸不认人”?尤其是在集成测试、热部署重启,或者应用优雅关闭(Graceful Shutdown)的场景下,这个问题出现的频率不低。
今天,我们就来彻底拆解这个BeanCreationNotAllowedException。我会结合源码和实际踩坑案例,不仅告诉你它“是什么”和“为什么”,更会分享一套从快速定位到根治解决的实战思路。你会发现,理解了这个异常,你对Spring Bean生命周期的掌握会上一个台阶。
2. 异常深度解析:Spring容器的“状态机”
要理解这个异常,我们必须先跳出单行代码的局限,把Spring IoC容器想象成一个有明确状态的状态机。它并非随时都准备好为你服务。
2.1 异常信息的字面解读
我们再把异常信息拆开看:
Singleton bean creation not allowed: 单例Bean的创建不被允许。这是结果。while singletons of this factory are in destruction:关键原因,当前工厂(即ApplicationContext)中的所有单例正处于销毁过程中。Do not request a bean from a BeanFactory in a destroy method implementation!: 一个非常具体的警告——不要在某个Bean的销毁方法(如@PreDestroy标注的方法、DisposableBean接口的destroy方法)内部,再去通过BeanFactory请求(获取或创建)其他Bean。
最后这句警告是理解很多复杂案例的钥匙。它暗示了一种典型的错误模式:在A Bean的销毁逻辑里,试图去获取B Bean,而B Bean可能尚未创建,触发创建流程,但此时容器整体已进入销毁状态,于是被无情拒绝。
2.2 Spring容器的生命周期阶段
Spring的AbstractApplicationContext定义了容器的核心生命周期,其中与我们的异常密切相关的状态是:
ACTIVE(活跃状态): 容器已刷新(refresh()完成),所有单例Bean已实例化、属性填充、初始化完毕。这是应用正常运行的状态,可以任意获取Bean。STOPPED(停止状态): 调用context.stop()后进入。此时生命周期处理器(LifecycleProcessor)会触发所有实现了Lifecycle接口的Bean的stop()方法。但Bean实例本身还未被销毁。CLOSED(关闭状态): 调用context.close()后进入。这是销毁阶段的开始。容器会:- 将状态标记为不活跃。
- 发布
ContextClosedEvent事件。 - 调用所有单例Bean的销毁方法(
@PreDestroy,DisposableBean.destroy())。 - 销毁所有缓存的单例Bean实例。
- 关闭底层的Bean工厂(
BeanFactory)。
我们的异常,就精准地发生在从CLOSED状态开始,到所有单例Bean销毁完毕这个时间窗口内。在此期间,容器设置了一个“破坏中”的标志,任何试图创建新单例Bean的请求都会被BeanCreationNotAllowedException拦截。
2.3 源码一瞥:异常抛出的地方
我们跟踪一下Spring源码(以Spring Framework 5.3.x为例),看看这个异常是如何被抛出的。核心逻辑在DefaultSingletonBeanRegistry这个类中,它负责单例Bean的注册与获取。
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { // 标志当前是否正在销毁单例Bean private boolean singletonsCurrentlyInDestruction = false; @Override public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { // ... 省略部分代码 ... synchronized (this.singletonObjects) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { // ★★★ 关键检查点 ★★★ if (this.singletonsCurrentlyInDestruction) { throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction " + "(Do not request a bean from a BeanFactory in a destroy method implementation!)"); } // ... 后续创建Bean的逻辑 ... } return singletonObject; } } // 在销毁单例时,会调用此方法 public void destroySingletons() { // ... 省略 ... this.singletonsCurrentlyInDestruction = true; // 设置标志位为true // ... 遍历并销毁所有单例Bean ... this.singletonsCurrentlyInDestruction = false; // 销毁完成后重置 } }逻辑非常清晰:当开始销毁单例(destroySingletons())时,标志位singletonsCurrentlyInDestruction被置为true。在此之后,任何通过getSingleton方法(这是创建和获取单例Bean的核心入口)试图创建一个尚未存在于缓存中的新Bean时,都会触发检查,如果标志位为true,则立即抛出BeanCreationNotAllowedException。
注意:这里检查的是“创建”,而不是“获取”。如果Bean已经被创建并缓存在
singletonObjects中,即使在销毁阶段,你依然可以获取到它的引用(虽然这可能不是个好主意)。异常针对的是“需要触发初始化流程”的那种Bean获取。
3. 典型场景与根因分析:你的代码踩了哪些雷?
理解了原理,我们来看看在什么情况下你会成为那个“在餐厅打烊时点菜的顾客”。以下场景根据我多年的排查经验,按出现频率排序。
3.1 场景一:在@PreDestroy或DisposableBean.destroy()方法中依赖注入或查找Bean
这是最经典、最符合异常警告信息的场景。举个例子,你有一个CacheManagerBean,在它销毁时,你想优雅地通知一个ClusterEventPublisherBean,将缓存失效事件广播到集群。
@Component public class MyCacheManager implements DisposableBean { @Autowired // 或者通过 @Resource, @Inject private ClusterEventPublisher publisher; // 方式一:字段注入 // 方式二:Setter注入 private ClusterEventPublisher publisher; @Autowired public void setPublisher(ClusterEventPublisher publisher) { this.publisher = publisher; } // 方式三:最危险的做法:在销毁方法中通过 ApplicationContext 获取 @Autowired private ApplicationContext applicationContext; @Override public void destroy() { // 尝试在销毁时发布一个事件 // 错误示范:如果 publisher 是延迟加载的,或者之前没被用过,这里会触发它的创建! publisher.publishEvent(new CacheDestructionEvent(this)); // 更错误的示范: // ClusterEventPublisher publisher = applicationContext.getBean(ClusterEventPublisher.class); // publisher.publishEvent(...); } }为什么这会出问题?Spring销毁Bean的顺序是不确定的(虽然可以通过@DependsOn影响,但非绝对)。假设ClusterEventPublisher本身也是一个单例Bean,并且它还没有被其他任何Bean依赖过(比如它是懒加载的,或者你的MyCacheManager是它唯一的依赖者),那么它在容器启动后可能从未被实例化。
当容器开始关闭,执行到MyCacheManager.destroy()时,它第一次尝试访问publisher字段。Spring会尝试解决这个依赖,于是触发ClusterEventPublisherBean的创建流程。但此时容器销毁标志singletonsCurrentlyInDestruction已经是true,于是创建请求被拒绝,抛出异常。
根因:在销毁阶段执行了可能触发新Bean初始化的代码。字段注入的Bean在第一次被访问时才真正解析(对于非构造器注入),这埋下了隐患。
3.2 场景二:异步任务、定时任务或线程池任务在容器关闭后仍在运行
这是生产环境更常见、也更隐蔽的问题。你的应用可能使用了@Async、@Scheduled,或者自己创建了线程池来执行后台任务。
@Service public class DataSyncService { @Async // 或使用 @Scheduled public void syncData() { // 这是一个长时间运行的任务 while (syncCondition) { // 任务中会调用其他Spring Bean someRepository.update(data); someRemoteClient.call(); // 如果容器在此时关闭,而此循环还在运行... } } } @Component public class MyThreadPoolComponent implements DisposableBean { private ExecutorService executor = Executors.newFixedThreadPool(5); public void submitTask() { executor.submit(() -> { // 任务内部 someService.process(); // 这里someService可能是个代理,内部会获取目标Bean }); } @Override public void destroy() { executor.shutdown(); // 尝试优雅关闭 // 但shutdown()不会等待所有任务完成,如果任务没完成,容器已关,任务内再调Bean就炸了 } }当通过/actuator/shutdown端点、发送SIGTERM信号等方式触发Spring Boot的优雅关闭时,容器会开始销毁流程。但如果你的异步/定时任务没有正确感知到容器关闭事件,或者关闭等待时间(spring.lifecycle.timeout-per-shutdown-phase)设置太短,任务线程可能还在活跃状态。
此时,任务代码中任何对Spring Bean的调用(尤其是通过AOP代理的Bean,其方法调用会触发从容器获取目标对象),都可能因为目标Bean尚未创建(比如是懒加载的),或者代理本身需要容器环境来解析,而触发BeanCreationNotAllowedException。
根因:容器生命周期与线程生命周期不同步。销毁是单线程顺序进行的,但异步任务是并发的。容器不会、也无法自动等待所有用户线程结束。
3.3 场景三:集成测试中的上下文管理混乱
在写Spring Boot Test时,尤其是结合@DirtiesContext、@TestExecutionListeners或者手动管理ApplicationContext时,容易掉进这个坑。
@SpringBootTest class MyServiceTest { @Autowired private MyService myService; @Autowired private ApplicationContext context; @Test void testSomething() { // 测试1 } @Test @DirtiesContext // 这个注解会标记测试方法执行后销毁并重建上下文 void testThatDirtiesContext() { // 测试2 // 在这个方法执行后,上下文被标记为“脏”,即将关闭 } // 假设有另一个测试类或方法,在上下文关闭后尝试访问Bean @Test void testAfterDirty() { // 错误!如果上下文已经被上一个测试关闭,这里autowired的myService可能处于一个无效状态。 // 或者,如果你在这里执行 context.getBean(...),很可能触发异常。 myService.doSomething(); } }@DirtiesContext的工作原理是在测试方法执行后,关闭当前的ApplicationContext。如果测试框架(如JUnit)的测试实例缓存、或者你的测试设计有问题,导致在上下文关闭后还有测试代码试图使用它,就会触发异常。
根因:对测试框架的生命周期钩子(@BeforeAll,@AfterAll,@BeforeEach,@AfterEach)与Spring测试上下文生命周期之间的交互理解不深。特别是当使用@TestInstance(TestInstance.Lifecycle.PER_CLASS)时,所有测试方法共享同一个测试类实例,其@Autowired字段在整个类生命周期内只注入一次。如果上下文在某个测试方法后被销毁,后续测试方法再使用这些字段就是访问一个“已死”的上下文中的Bean引用,风险极高。
3.4 场景四:自定义Scope或BeanPostProcessor中的不当操作
如果你扩展了Spring,注册了自定义的Scope(例如一个“会话”Scope),或者在BeanPostProcessor中进行了复杂的Bean交互,也可能在容器关闭时遇到问题。
@Component public class MyCustomScope implements Scope { private final Map<String, Object> scopedObjects = new ConcurrentHashMap<>(); @Override public Object get(String name, ObjectFactory<?> objectFactory) { return scopedObjects.computeIfAbsent(name, k -> objectFactory.getObject()); } @Override public void registerDestructionCallback(String name, Runnable callback) { // 注册销毁回调 } @Override public Object remove(String name) { return scopedObjects.remove(name); } // ... 其他方法 } @Component public class MyBeanPostProcessor implements BeanPostProcessor { @Autowired private LateInitBean lateInitBean; // 依赖一个可能晚初始化的Bean @Override public Object postProcessAfterInitialization(Object bean, String beanName) { // 如果在容器关闭阶段,某个Bean的初始化后处理触发了对lateInitBean的访问... // 而lateInitBean还未创建,就可能出问题。 if (bean instanceof SomeType) { lateInitBean.doSomething(); // 危险操作! } return bean; } }在容器关闭时,自定义Scope需要清理其存储的对象。如果清理逻辑(registerDestructionCallback中注册的)又试图获取其他Spring Bean,就可能触发异常。同样,如果BeanPostProcessor本身依赖其他Bean,并且在容器销毁阶段的某个后期回调中被触发,也可能遇到依赖的Bean无法创建的情况。
根因:扩展组件没有充分考虑容器生命周期的所有阶段,在销毁阶段执行了依赖于活跃容器状态的操作。
4. 实战排查指南:从日志到根因
当异常发生时,光看栈顶信息是不够的。你需要像一个侦探一样,收集线索,还原案发现场。
4.1 第一步:分析完整的异常堆栈
不要只看第一行。异常堆栈会告诉你:
- 是谁在请求创建Bean?找到栈帧中你写的类和方法。通常是某个
@PreDestroy方法、一个Runnable的run()方法、一个@Scheduled方法,或者测试方法。 - 请求创建的Bean是什么?异常信息里包含了Bean的名字(如
‘xxxService’)。记下它。 - 请求发生的路径是什么?查看从你的代码到
DefaultSingletonBeanRegistry.getSingleton()的调用链。这能帮你理解是哪种依赖注入或查找方式触发了创建。
4.2 第二步:审查相关Bean的定义与依赖
找到试图被创建的Bean(比如xxxService)和触发创建的Bean(比如MyCacheManager)。
- 检查
xxxService是否是懒加载(@Lazy)?如果是,那么它很可能在销毁阶段才第一次被真正初始化。 - 检查触发创建的Bean,它的销毁方法(
@PreDestroy,destroy())里做了什么?是否直接或间接地引用了xxxService? - 检查它们之间是否存在循环依赖?Spring解决循环依赖时可能会提前暴露未初始化完成的Bean引用,这在销毁阶段可能带来复杂情况。
4.3 第三步:检查应用的生命周期配置
- 优雅关闭配置:检查
application.yml中关于优雅关闭的配置。
如果超时时间太短,异步任务可能来不及完成。server: shutdown: graceful # 启用优雅关闭 spring: lifecycle: timeout-per-shutdown-phase: 30s # 关闭阶段超时时间 - Actuator端点:如果使用了
/actuator/shutdown,确认关闭请求的来源和时机。 - 日志搜索:在应用日志中搜索
Closing org.springframework.context.ApplicationContext或Destroying singletons等关键字,确定容器关闭的确切时间点。然后对比异常发生的时间点,看是否在关闭之后。
4.4 第四步:审查并发与异步代码
这是排查的难点。你需要:
- 找出所有使用
@Async、@Scheduled、ExecutorService、ThreadPoolTaskExecutor、@EventListener(异步模式)的地方。 - 判断这些异步任务是否有可能在收到关闭信号后继续运行。任务里是否有长循环、等待I/O、睡眠等操作?
- 检查是否有持有Spring Bean引用的线程局部变量(ThreadLocal)。容器关闭后,这些引用就失效了,使用它们会导致不可预知的行为,有时也会间接引发创建异常。
4.5 第五步:在测试环境中复现
对于难以在线上调试的问题,尝试在本地或测试环境复现是关键。
- 模拟关闭:在集成测试中,手动调用
applicationContext.close(),然后观察后续是否还有业务逻辑在执行。 - 使用调试器:在
DefaultSingletonBeanRegistry.getSingleton方法中抛出异常的那一行设置条件断点(条件:singletonsCurrentlyInDestruction == true)。然后触发应用关闭,当断点命中时,查看完整的调用栈,一目了然。
5. 解决方案与最佳实践
针对不同的场景,有不同的解决策略。原则是:让代码感知并尊重容器的生命周期。
5.1 针对场景一(销毁方法中依赖Bean)
方案1:提前注入,避免延迟解析确保在销毁方法中使用的依赖,在Bean的初始化阶段就已经被完全解析。最安全的方式是使用构造器注入。
@Component public class MyCacheManager implements DisposableBean { private final ClusterEventPublisher publisher; // final 字段 // 构造器注入:Spring在创建MyCacheManager时,必须首先准备好ClusterEventPublisher public MyCacheManager(ClusterEventPublisher publisher) { this.publisher = publisher; } @Override public void destroy() { // 此时publisher肯定已经存在,不会触发创建 publisher.publishEvent(new CacheDestructionEvent(this)); } }如果必须用字段或Setter注入,确保这个依赖Bean不是懒加载的,并且在本Bean的初始化方法(@PostConstruct)中至少访问一次,强制其初始化。
方案2:将销毁逻辑与Bean销毁解耦销毁时不要直接调用其他Bean,而是发布一个事件。让事件监听器(也是Spring Bean)在容器还活跃的时候去处理。
@Component public class MyCacheManager { @Autowired private ApplicationEventPublisher eventPublisher; @PreDestroy public void onDestroy() { // 只是发布事件,不直接依赖其他Bean eventPublisher.publishEvent(new CacheDestructionEvent(this)); } } @Component public class CacheDestructionEventListener { // 这个监听器会在容器还正常的时候同步处理事件 @EventListener public void handleCacheDestruction(CacheDestructionEvent event) { // ... 处理逻辑,这里可以安全使用其他Bean ... } }注意:确保事件监听器是同步的(默认就是),这样事件处理会在容器关闭流程中、发布事件的Bean销毁之前完成。
方案3:防御性编码,进行空值或状态检查如果无法避免在销毁方法中使用可能为空的依赖,至少进行保护。
@PreDestroy public void destroy() { if (this.publisher != null) { // 但注意,如果publisher是代理,这可能不够 try { publisher.publishEvent(...); } catch (Exception e) { // 记录日志,但不要抛出异常,以免影响其他Bean的销毁 log.warn("Failed to publish event during destruction", e); } } }5.2 针对场景二(异步任务在关闭后运行)
方案1:实现生命周期感知接口让执行异步任务的组件实现SmartLifecycle或Lifecycle接口。在stop()方法中,主动停止任务执行。
@Component public class MyAsyncService implements SmartLifecycle { private volatile boolean running = false; private Future<?> future; @Override public void start() { this.running = true; this.future = executor.submit(this::longRunningTask); } @Override public void stop() { this.running = false; // 让任务循环检测这个标志 if (this.future != null) { this.future.cancel(true); // 尝试中断任务 } } @Override public boolean isRunning() { return running; } private void longRunningTask() { while (running && !Thread.currentThread().isInterrupted()) { // 业务逻辑 try { someService.process(); } catch (Exception e) { if (running) { // 只有还在运行状态才处理业务异常 log.error("Task error", e); } // 如果是因为中断或停止导致的异常,则忽略或记录为INFO } } log.info("Long running task stopped gracefully."); } }方案2:使用Spring管理的TaskExecutor,并配置优雅关闭对于@Async和@Scheduled,Spring底层使用TaskExecutor。你可以配置一个自定义的ThreadPoolTaskExecutor,并设置setWaitForTasksToCompleteOnShutdown(true)和setAwaitTerminationSeconds(60)。
@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(25); executor.setThreadNamePrefix("MyAsync-"); // ★★★ 关键配置 ★★★ executor.setWaitForTasksToCompleteOnShutdown(true); // 等待任务完成 executor.setAwaitTerminationSeconds(60); // 最多等待60秒 executor.initialize(); return executor; } }这样,当容器关闭时,Spring会先调用这个Executor的shutdown(),并等待指定时间,让正在运行的任务完成,从而避免任务在执行中途因容器关闭而访问不到Bean。
方案3:在任务代码中增加容器状态检查这是一种更细粒度的控制。你可以注入ApplicationContext,在任务的关键点检查上下文是否还活跃。
@Component public class DataSyncService { @Autowired private ApplicationContext context; @Async public void syncData() { while (syncCondition) { // 每次循环前检查容器是否活跃 if (!((AbstractApplicationContext) context).isActive()) { log.warn("ApplicationContext is no longer active, stopping sync task."); break; } try { someRepository.update(data); // 这里调用Bean } catch (IllegalStateException e) { // 可能会捕获到与容器状态相关的异常 if (e.getMessage().contains("BeanFactory not initialized")) { break; } throw e; } } } }5.3 针对场景三(测试上下文问题)
方案1:合理使用@DirtiesContext
- 尽量将
@DirtiesContext注解范围缩小。使用@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)而不是类级别。 - 考虑使用
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS),并配合@TestInstance(TestInstance.Lifecycle.PER_CLASS),确保一个测试类只破坏一次上下文。 - 最佳实践:如果测试需要修改大量全局状态(如嵌入式数据库、静态变量),考虑将其放入一个独立的测试类,并在这个类上使用
@DirtiesContext,避免污染其他测试。
方案2:使用@TestExecutionListeners手动控制上下文对于极其复杂的测试场景,可以自定义测试执行监听器,更精细地控制上下文的创建和销毁。
@SpringBootTest @TestExecutionListeners( listeners = MyCustomTestExecutionListener.class, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS ) class MyIntegrationTest { // ... } public class MyCustomTestExecutionListener extends AbstractTestExecutionListener { @Override public void afterTestClass(TestContext testContext) throws Exception { // 在测试类结束后执行自定义清理,而不是依赖@DirtiesContext // 例如,清理静态数据,但保持上下文存活供其他类使用 } }方案3:避免在测试字段中持有易失效的引用如果测试必须跨方法共享状态,考虑使用静态字段或TestContext缓存,而不是依赖@Autowired字段,因为后者与特定的ApplicationContext实例绑定。
5.4 通用防御性设计原则
- 优先使用构造器注入:它能强制依赖在Bean创建时就被满足,避免了在生命周期后期(如销毁时)才解析依赖的风险,同时也使Bean的不可变性更好。
- 明确Bean的作用域:如果不是必须,不要轻易使用
@Scope("prototype”)或自定义Scope。单例Bean的生命周期最清晰,也最容易管理。 - 谨慎使用
@Lazy:明确知道为什么需要懒加载。如果只是为了解决循环依赖,应该优先考虑重构代码。懒加载会将Bean的初始化推迟到第一次使用时,这无形中扩大了可能触发初始化的时间窗口,包括危险的销毁阶段。 - 关闭阶段避免复杂逻辑:Bean的销毁方法应该只做最必要的资源释放(如关闭文件句柄、网络连接)。不要在其中执行业务逻辑,尤其是可能触发其他Bean初始化的逻辑。
- 监控与日志:在销毁方法和重要的异步任务循环中,增加详细的日志记录。这样当问题发生时,你能清晰地看到事件发生的顺序,这对于排查
BeanCreationNotAllowedException这类与时机强相关的问题至关重要。
6. 总结与核心要点
org.springframework.beans.factory.BeanCreationNotAllowedException不是一个简单的Bug,它是Spring容器对你代码架构的一种“抗议”,抗议你的代码没有尊重它设定的生命周期规则。处理这个异常的过程,本质上是一次对应用生命周期管理、并发编程以及依赖注入最佳实践的审视。
记住几个核心点:
- 根本原因:在Spring容器已经开始销毁单例Bean的阶段(
singletonsCurrentlyInDestruction = true),试图创建新的单例Bean。 - 高频雷区:
- 在
@PreDestroy/destroy()方法中,通过注入的字段(非构造器注入)第一次访问一个懒加载的Bean。 - 容器关闭后,未被妥善管理的异步线程仍在运行并尝试调用Spring Bean。
- 集成测试中,上下文被标记为
@DirtiesContext后,后续操作仍使用了旧的、已关闭的上下文引用。
- 在
- 解决之道:
- 销毁方法:使用构造器注入;将逻辑转为同步事件;保持方法简单。
- 异步任务:实现
Lifecycle接口感知停止;配置TaskExecutor等待任务结束;在任务代码中检查容器状态。 - 测试:审慎使用
@DirtiesContext;理解测试实例生命周期。
最后,分享一个我个人的调试习惯:遇到任何Spring生命周期相关的诡异问题,不妨在AbstractApplicationContext的close()方法和DefaultSingletonBeanRegistry的destroySingletons()方法入口打上断点。观察关闭流程的调用栈,看看是谁、在什么时候、为什么触发了销毁,以及后续又是谁不识趣地跑来创建Bean。这幅“动态时序图”往往比静态代码分析更能直击问题本质。