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 MapString, 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和ScheduledSpring底层使用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。这幅“动态时序图”往往比静态代码分析更能直击问题本质。