Spring Bean生命周期全解析:从创建到销毁的十三道工序与实战避坑
Spring 的 Bean 生命周期是每个 Java 开发者在进阶路上绕不过去的坎。很多人用 Spring 用了好几年Autowired 和 Component 用得贼溜但一遇到为什么构造器里拿不到注入的属性为什么 PostConstruct 里调事务方法不生效为什么销毁方法执行了两次这种问题就卡壳。说白了就是没搞懂 Spring 在背后把一个普通对象从创建到销毁的完整链路里到底安排了多少道工序。这篇文章不打算给你背源码我会按我实际排查问题的思路把 Spring Bean 从出生到报废的全过程拆开揉碎讲清楚每一道工序存在的理由、执行的顺序以及那些教科书里不会写、但你在生产环境大概率会踩的坑。无论你是刚接触 Spring 的初学者还是写了几年业务代码想补原理的老手这篇都值得花二十分钟认真读一遍。1. 先建立整体认知Spring 把一个普通 new 扩展成了十三道工序1.1 为什么需要这么长的链路先想一个最基础的问题我们自己 new 一个对象就是分配内存、调用构造器、然后对象就能用了三步走完。为什么 Spring 不这样做非要搞出一大堆回调接口、后置处理器、初始化方法答案是控制反转的代价。当你把对象的创建权交给容器之后容器必须提供足够的钩子让使用方能在对象创建的不同阶段插手干预。比如你可能想在 Bean 创建完之后给它注入一些额外配置想在某个 Bean 初始化完成后启动一个线程池想在容器关闭前释放数据库连接。如果没有这些阶段性的扩展点Spring 就只能是一个能 new 对象的高级工厂根本撑不起后面整个生态。游戏测试里有个概念叫Bug 生命周期从发现、提交、修复、验证到关闭每个阶段都有明确的状态和负责的人。Spring Bean 的生命周期本质上也是一样的逻辑从 BeanDefinition 注册开始到实例化、属性填充、初始化、使用、销毁每个阶段都有明确的触发时机和对应的扩展接口。理解了这种阶段化的设计思想后面所有的细节都只是往这个框架里填内容。1.2 一张图看懂完整时序下面这个表格是我自己整理的完整时序包含了单例、非懒加载的普通 Bean 从注册到销毁的全部关键节点。建议先通读一遍脑子里有个总体的顺序后面每个部分我会拆开细讲。阶段触发时机关键逻辑扩展点/说明1. BeanDefinition 注册容器启动、扫描/解析配置把类的元信息变成 BeanDefinitionComponent、Bean、XML 都会变成 BeanDefinition2. 实例化前置真正创建对象前调用 InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation如果返回了非 null 对象后续流程会被短路常用于 AOP 提前生成代理3. 构造器实例化选择合适的构造器推断构造器反射创建实例默认无参构造Autowired 标注的构造器优先4. 合并 BeanDefinition 后置处理实例化完成后MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinitionAutowiredAnnotationBeanPostProcessor 在这里缓存 Autowired、Value 等元数据5. 属性填充实例化完成后依赖注入真正发生的位置setter 注入、字段注入、Autowired 都在这步执行6. Aware 回调属性填充之后注入 Bean 名称、类加载器、BeanFactoryBeanNameAware、BeanClassLoaderAware、BeanFactoryAware7. 初始化前置第 6 步之后BeanPostProcessor.postProcessBeforeInitializationPostConstruct 实际上是在这一步被 CommonAnnotationBeanPostProcessor 执行的8. 执行初始化方法第 7 步之后InitializingBean.afterPropertiesSet然后是 init-methodBean(initMethod...) 或 XML 配置9. 初始化后置初始化之后BeanPostProcessor.postProcessAfterInitializationAOP 代理就是在这里创建的非常重要10. 使用中容器正常运行业务方法被调用此时 Bean 可能是一个代理对象而非原始对象11. 全部单例初始化完成所有非懒加载单例创建完毕后SmartInitializingSingleton.afterSingletonsInstantiated框架内部大量使用比如 MVC 的 HandlerMethod 初始化12. 销毁前置容器关闭时DestructionAwareBeanPostProcessor.postProcessBeforeDestructionPreDestroy 具体执行位置13. 销毁容器关闭时DisposableBean.destroy、destroy-method接口回调先执行自定义销毁方法后执行1.3 读源码最该盯住的两个类理解了整体结构之后如果你想去源码里印证不要漫无目的地翻。我个人的经验是盯住两个类就够AbstractAutowireCapableBeanFactory这是 Bean 创建的真正执行者。doCreateBean 方法统领了实例化、属性填充、初始化三个阶段你可以在里面看到整个生命周期的主干逻辑。DefaultSingletonBeanRegistry这是单例 Bean 注册和缓存的管理者。三级缓存的代码就在里面也就是 getSingleton 那一系列方法。记住一个笨办法在 doCreateBean 方法上打断点然后启动一个最简单的 Spring Boot 项目跟着调用栈走一遍比你看十篇博客都管用。你会发现所谓的生命周期根本不是一个方法搞定的而是四五个方法嵌套调用、中间穿插着各种回调的结果。2. 构造器实例化与属性填充依赖注入真正发生的位置2.1 实例化阶段的择优录取很多初学者以为 Spring 创建对象就是调用无参构造器实际上 Spring 在实例化之前要做一件事推断构造器。虽然默认情况下确实优先使用无参构造器但一旦你的类里存在 Autowired 标注的构造器或者只定义了一个有参构造器Spring 就会尝试从这里择优录取。推断构造器的核心逻辑是如果类只有一个构造器且是有参的Spring 会用这个构造器参数从容器里按类型找。如果有多个构造器无参构造器优先如果都没有无参构造器Spring 会找参数最多的那个但如果参数在容器里找不到对应的 Bean就会抛异常。Autowired(required false) 标注的构造器在多个构造器场景下有特殊地位Spring 会优先选择它但允许参数缺失。这个设计的意义在于Spring 需要支持构造器注入这种不可变的依赖注入方式。构造器注入的好处是依赖不可变、不会出现部分初始化的问题所以 Spring 官方在很多时候都推荐它而不是 Autowired 字段注入。但代价就是 Spring 必须承担决定用哪个构造器的复杂度。2.2 属性填充真正的注入发生地实例化完成之后对象已经存在于堆内存里但内部所有字段要么是默认值要么是 null。接下来进入的属性填充阶段才是 Autowired、Resource、Value 这些注解真正生效的地方。这里有一个很重要的细节AutowiredAnnotationBeanPostProcessor在实例化阶段就会通过postProcessMergedBeanDefinition提前扫描当前 Bean 的类元数据把哪些字段、哪些 setter 方法上标注了 Autowired、Value、Inject 全部记录下来。然后等到属性填充阶段它才按照记录下来的元数据一个个去容器里查找依赖反射注入。为什么要分成两步因为反射获取信息的成本比反射赋值的成本高得多而且提前缓存元数据可以避免属性填充时再花时间去扫描整个类。这也是 Spring 性能优化里面一个不起眼但很重要的设计。属性填充的执行顺序也很有意思通过名称自动注入autowireByName通过类型自动注入autowireByType应用 InstantiationAwareBeanPostProcessor.postProcessPropertiesAutowired 在这里执行也就是说如果你在一个 Bean 里混用了 XML 的 autowire 配置和 Autowired 注解Autowired 的执行顺序排在按名称、按类型注入之后这在实际项目中可能导致依赖被覆盖的诡异问题。虽然现在几乎没人混用了但排查老项目时你可能会遇到。2.3 在构造器里使用依赖新手最常见的 NPE这个坑我见过太多次了而且几乎每个团队都有新人踩过。代码如下Component public class OrderService { Autowired private UserService userService; public OrderService() { // 这里会报 NullPointerException String name userService.getUserName(); } }很多新人的第一反应是Autowired 标注了呀为什么构造器里 userService 还是 null原因其实很简单构造器执行的时候对象才刚刚被 new 出来属性填充还没有发生。Autowired 注入的执行点在构造器之后、属性填充阶段。在构造器执行的那一刻userService 还只是默认值 null。正确做法是把依赖作为构造器参数传入构造器注入或者在 PostConstruct 或 InitializingBean.afterPropertiesSet 里做初始化逻辑Component public class OrderService { private final UserService userService; public OrderService(UserService userService) { this.userService userService; } PostConstruct public void init() { String name userService.getUserName(); } }记住一个原则构造器里只做对象的构建不要访问任何被注入的依赖。所有依赖注入完成之后的初始化工作统一放到初始化阶段去处理。这个原则在你理解了下一个小节的初始化阶段之后会刻进骨子里。3. 初始化阶段Aware 回调、PostConstruct、InitializingBean 和 init-method 的先后秩序3.1 Aware 接口让 Bean 知道自己叫什么、从哪里来在属性填充完成之后、初始化方法调用之前Spring 会先做一件事回调 Aware 接口。所谓 Aware 接口翻译过来就是感知的意思——让 Bean 感知到自己所处的容器环境。public interface BeanNameAware { void setBeanName(String name); } public interface BeanFactoryAware { void setBeanFactory(BeanFactory beanFactory) throws BeansException; }这里分为两类处理方式细节值得注意BeanNameAware、BeanClassLoaderAware、BeanFactoryAware这三个是硬编码在initializeBean方法里的直接调用不需要经过 BeanPostProcessor。其他 Aware 接口比如 ApplicationContextAware、EnvironmentAware、ApplicationEventPublisherAware则是通过一个叫ApplicationContextAwareProcessor的 BeanPostProcessor 统一处理的。为什么要分两类我的理解是BeanNameAware 这种属于最基础的容器级回调必须确保在最早的时机被调用所以框架直接写死在初始化流程里而 ApplicationContextAware 这类属于 ApplicationContext 体系的高级感知通过后置处理器处理更灵活也方便在不使用 ApplicationContext比如只用 BeanFactory的情况下自动跳过。实际开发中你应该减少对 Aware 接口的直接依赖因为这意味着你的 Bean 被耦合到了 Spring 容器上。但如果需要做框架层面的扩展比如编写一个能感知容器的组件Aware 接口就是标准做法。3.2 初始化方法的三重奏谁先谁后这是面试最喜欢问的点也是实际开发中决定你代码行为的关键顺序。在 Bean 已经完成属性填充和 Aware 回调之后Spring 会依次执行三个层次的初始化逻辑// AbstractAutowireCapableBeanFactory 中 initializeBean 方法的简化逻辑 Object wrappedBean bean; // 1. 执行 BeanPostProcessor.postProcessBeforeInitialization - PostConstruct 在这里被调用 wrappedBean applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); // 2. 执行初始化方法 invokeInitMethods(beanName, wrappedBean, mbd); // - 先执行 InitializingBean.afterPropertiesSet() // - 再执行 init-methodBean(initMethod ...) 或 XML 配置 // 3. 执行 BeanPostProcessor.postProcessAfterInitialization - AOP 代理在这里创建 wrappedBean applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);具体的执行顺序是PostConstruct标注的方法InitializingBean.afterPropertiesSet()方法init-methodBean(initMethod ...) 或 XML init-method 配置很多人会疑惑PostConstruct 不是初始化方法吗为什么排在 beforeInitialization 阶段执行答案前面提过PostConstruct 是 javax.annotation 包下的标准注解由 CommonAnnotationBeanPostProcessor 这个 BeanPostProcessor 在 postProcessBeforeInitialization 回调中负责扫描和调用。所以从框架角度看它只是初始化前置处理器所触发的业务回调而不是框架自身的初始化方法。这个认知上的差异会在你调试代码时提供极大帮助。如果你需要记忆可以用一句话构造器创建对象PostConstruct 做依赖注入后的准备InitializingBean 做接口回调型初始化init-method 做配置文件型初始化。3.3 为什么在 PostConstruct 里调 Transactional 方法不生效这是一个非常经典的生产环境问题很多团队在项目启动阶段初始化数据时遇到。场景是这样的Component public class DataInitializer { PostConstruct public void init() { // 这个方法上标了 Transactional但事务根本不生效 doInit(); } Transactional public void doInit() { // 数据库操作 } }原因要牵扯到 AOP 代理的创建时机。Spring 的事务功能是基于 AOP 代理实现的。一个 Bean 的代理对象是在postProcessAfterInitialization阶段创建的而PostConstruct方法是在postProcessBeforeInitialization阶段执行的。也就是说PostConstruct 执行的时候当前 Bean 还是一个原始对象不是代理对象所以 Transactional 注解根本不会被解析。正确做法是把需要事务的初始化逻辑放到一个单独的 Service 里注入进来再调用让代理生效或者实现ApplicationRunner、ApplicationListenerContextRefreshedEvent在容器完全启动后再执行初始化逻辑。我个人的习惯是使用 ApplicationRunner因为它在所有 Bean 初始化完成、代理创建完毕之后才执行事务、AOP 全部可用。4. BeanPostProcessor 与 AOP 代理生命周期中真正偷梁换柱的扩展点4.1 一个 BPP 就是一道流水线工序如果你理解了前面所有阶段你会发现一个规律Spring 生命周期的很多阶段本质都是通过 BeanPostProcessor后置处理器来扩展的。BeanPostProcessor 是整个生命周期里最核心、最强大的扩展点它提供两个方法postProcessBeforeInitialization在初始化之前执行postProcessAfterInitialization在初始化之后执行框架内置了一堆后置处理器它们在普通 Bean 创建之前就已经被优先实例化并注册。常见的有后置处理器职责AutowiredAnnotationBeanPostProcessor处理 Autowired、Value、Inject 的注入CommonAnnotationBeanPostProcessor处理 PostConstruct、PreDestroy、ResourceAnnotationAwareAspectJAutoProxyCreator创建 AOP 代理对象ApplicationContextAwareProcessor处理 ApplicationContextAware 等感知接口理解 BPP 的执行时机是理解整个生命周期的钥匙。比如 Autowired 的注入发生在属性填充阶段但它的元数据扫描在实例化后就开始了PostConstruct 能生效是因为有一个专门的 BPP 在 beforeInitialization 阶段调用了它AOP 代理能生效是因为有一个专门的 BPP 在 afterInitialization 阶段创建了代理对象替换掉了原始 Bean。4.2 AOP 代理对象诞生的那一刻AOP 代理的创建时机值得单独拿出来讲因为它解释了开发中大量为什么这样写不生效的问题。当 Spring 检测到某个 Bean 需要被增强比如类上或方法上有 Transactional、Async、自定义切面AnnotationAwareAspectJAutoProxyCreator会在postProcessAfterInitialization阶段调用wrapIfNecessary方法通过 JDK 动态代理或 CGLIB 生成一个代理对象并把它返回给容器。这里有个关键点从这一步开始容器里那个 Bean 的身份被李代桃僵了。你从容器里 getBean 拿到的不再是原始对象而是代理对象。代理对象持有原始对象的引用方法调用会先经过代理的拦截器链然后才是原始逻辑。这带来一个经典陷阱如果你在测试代码里用assertSame比较从容器里获取的 Bean 和某个内部持有的原始引用会发现它们不是同一个对象。很多排查了一晚上的AOP 不生效问题最后查出来是自己在代码里手动new了一个原始对象绕过了代理。4.3 自己写 BPP 时的三个注意事项如果你需要自定义 BeanPostProcessor 做扩展有几点经验值得记住第一BPP 本身的执行时机比你想象的要早。它会在所有普通 Bean 实例化之前被创建和注册所以你的 BPP 里不要依赖任何业务 Bean否则很容易引发循环依赖或提前实例化。第二不要随便调用 beanFactory.getBean()。在 BPP 的回调方法里当前 Bean 可能还在创建过程中你主动去获取别的 Bean可能引发连锁的提前实例化问题甚至导致循环依赖意外出现。如果需要获取其他 Bean优先考虑延迟获取的方式或通过依赖注入获取。第三注意 postProcessAfterInitialization 返回值的含义。如果你在这个方法里返回了一个非原始对象的新对象这个新对象会被当作最终的 Bean 存入容器原始对象的生命周期回调可能就断了。很多框架就是利用这一点偷梁换柱但如果理解不深很容易帮 Spring 换了一个自己都不知道的 Bean。Component public class MyBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // 可以在 bean 初始化前做标记、包装、校验 return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // 可以对 bean 做代理、装饰等操作 return bean; } }5. 循环依赖与提前暴露三级缓存背后的时序代价5.1 三级缓存到底在解决什么问题讨论 Bean 生命周期不可能绕开循环依赖。所谓循环依赖就是 A 依赖 BB 又依赖 A。如果按照标准的生命周期顺序走——A 实例化、属性填充时需要 B就去创建 BB 实例化、属性填充时需要 A又回来创建 A——这就成了死循环最终栈溢出。Spring 解决这个问题的核心是提前暴露。在 A 实例化完成、还没有做属性填充的时候Spring 就会把一个 ObjectFactory 放进三级缓存singletonFactories里。当 B 在属性填充时需要 A 的时候Spring 发现一级缓存里没有 A 的成品但三级缓存里有 A 的工厂就执行getEarlyBeanReference方法拿到一个 A 的早期引用半成品然后把这个引用注入给 B。整个流程用表格可以看得更清楚缓存级别名称存放内容一级缓存singletonObjects完整初始化完成的单例 Bean二级缓存earlySingletonObjects提前暴露的半成品 Bean可能已经被代理三级缓存singletonFactoriesObjectFactory用于生成早期引用二级缓存的意义在于防止同一个 Bean 在循环依赖过程中被创建多次早期引用。三级缓存的意义在于把生成早期引用这件事延迟到真正需要的时候因为不是所有 Bean 都会遇到循环依赖提前生成代理是一种浪费。5.2 提前暴露对生命周期顺序的影响这是理解循环依赖最关键的一点也是很多人忽略的参与循环依赖的 Bean它的生命周期不是严格按照标准顺序走完的。正常的 A 流程是实例化 - 属性填充 - 初始化 - 代理创建。如果 A 依赖 BB 又依赖 A实际流程变成A 实例化完成被暴露到三级缓存。A 开始属性填充发现需要 B于是去创建 B。B 实例化完成也被暴露到三级缓存。B 开始属性填充发现需要 A于是从三级缓存拿到 A 的早期引用注入给自己。B 完成初始化如果 B 需要代理就在此时创建代理然后 B 作为一个完整的 Bean 放入一级缓存。A 继续它的属性填充把 B 的完整引用注入给自己。A 完成初始化此时会有个关键判断如果 A 在循环依赖中已经被提前获取过早期引用那么postProcessAfterInitialization阶段不会再次创建代理否则 A 的代理就会变成两个不同的对象。看到问题了吗A 的代理不是在标准的 afterInitialization 阶段创建的而是在第 4 步提前暴露时通过 getEarlyBeanReference 创建的。这个时序上的差异会导致某些情况下 AOP 增强失效或产生意外行为。5.3 哪些循环依赖注定解决不了理解了三级缓存原理你就能推理出它为什么只能解决一部分循环依赖构造器注入的循环依赖无解。因为构造器注入要求依赖在实例化阶段就必须存在而此时目标对象还没被 new 出来根本无处提前暴露。Spring 5 之后官方推荐的构造器注入方式天然规避了循环依赖问题这也是一个理由。原型作用域的循环依赖无解。三级缓存缓存的是单例对象原型 Bean 每次 getBean 都新建无法实现早期引用的共享。Async 等基于 AOP 的循环依赖存在风险。虽然 Spring 做了很多处理但涉及提前代理和最终代理不一致时仍可能出现奇怪问题。网上大量案例表明Async 参与循环依赖时偶尔报错或增强失效本质就是生命周期时序被打乱后暴露出的边界情况。如果你在设计阶段能避免循环依赖尽量避免。它虽然被 Spring 支持但这是以其时序被特异化为代价的。循环依赖的代价不是看不到的它就藏在生命周期偏离标准顺序的那一瞬。6. 销毁流程与尾声钩子单例 Bean 的最后一公里6.1 销毁三件套的执行顺序前面讲的都是生现在讲死。容器关闭时Spring 会依次销毁所有的单例 Bean。销毁阶段同样有一组固定的顺序PreDestroy标注的方法DisposableBean.destroy()接口方法destroy-method配置的销毁方法Bean(destroyMethod ...) 或 XML注意这里与初始化阶段不太一样PreDestroy 虽然是注解但它是由 DestructionAwareBeanPostProcessor 处理的所以排在第一位接口方法是第二位XML/注解配置的 destroy-method 是第三位。实际开发中用 PreDestroy 或 Bean(destroyMethod ...) 都行但建议同一个 Bean 只选一种方式避免出现同一份清理逻辑执行两遍的错觉。之前有同事在实现 DisposableBean 接口的同时又在 Bean 上配置了 destroyMethod 指向同一个方法容器关闭时那段逻辑跑了两次排查了很久才发现是重复配置了。6.2 Bean 的 destroyMethod 默认推断一个容易忽略的行为Spring 5 开始Bean 注解的 destroyMethod 有一个默认值(inferred)表示 Spring 会自动推断容器关闭时要调用的销毁方法。推断规则是如果目标类里有名为 close 或 shutdown 的无参方法就会把它当作销毁方法。这个设计初衷是方便整合数据源、连接池等资源类它们的关闭方法通常就叫 close。但隐患随之而来如果你在自己的业务类里写了一个名为 close 的普通业务方法Spring 会在容器关闭时鬼使神差地调用它而你根本没在 Bean 里显式声明过。解决办法是显式声明Bean(destroyMethod ) public MyService myService() { return new MyService(); }把 destroyMethod 置为空字符串表示不自动推断销毁方法。网上很多关于Spring 容器关闭时莫名其妙调用了一个方法的案例八九成都是这个默认推断行为导致的。6.3 SmartInitializingSingleton所有单例初始化完成之后的钩子讲完销毁再补充一个容易被忽略的尾声钩子SmartInitializingSingleton。public interface SmartInitializingSingleton { void afterSingletonsInstantiated(); }它和 PostConstruct 最大的区别在于触发时机PostConstruct 是在单个 Bean初始化完成后触发而afterSingletonsInstantiated是在所有非懒加载的单例 Bean 都创建完成之后才触发。前者是个人赛后者是团体赛终场哨。这个接口在 Spring 框架内部使用非常多。比如 Spring MVC 的RequestMappingHandlerAdapter就是在afterSingletonsInstantiated里初始化所有接口方法的参数解析器EventListenerMethodProcessor也是在这个阶段把所有 EventListener 方法注册成监听器的。为什么需要这样一个接口因为在单个 Bean 初始化时你依赖的其他 Bean 可能还没创建完无法保证全局状态就绪。很多项目里用ApplicationListenerContextRefreshedEvent做启动后的初始化逻辑但如果你更关注所有 Bean 都准备好了这个节点SmartInitializingSingleton是更早、更精确的选择。区别在于ContextRefreshedEvent 是在容器刷新完成、整个上下文可用后触发而 afterSingletonsInstantiated 是在单例 Bean 全部创建完成、但容器还没完全刷新的间隙触发。两者偶尔会有微妙的顺序差需要根据你的场景选择。7. 实战复盘这些年我踩过和见过的生命周期翻车现场7.1 翻车一在构造器里使用注入的依赖这个前面提过。我见过最离谱的版本是有人在构造器里启动一个定时任务任务里用了 Autowired 注入的 Service结果每次启动都 NPE然后他把注入方式改成 static 字段又引发了静态变量和 Spring 生命周期错乱的新问题。最后的解法很简单把启动定时任务的逻辑移到 PostConstruct 里。构造器只做一件事——构建对象本身这应该成为肌肉记忆。7.2 翻车二PostConstruct 里跑异步任务结果事务和 AOP 全部失效之前做一个数据迁移功能同事在 PostConstruct 方法里直接调用了一个 Async 标注的方法做数据同步。结果发现异步线程根本没有被 Spring 管理事务也不生效。原因还是那个PostConstruct 执行时当前 Bean 连代理都还没生成Async 的拦截器自然无从谈起。而哪怕 PostConstruct 执行完了它调用的异步方法所在的 Bean 如果是获取当前类自身的原始引用同样不是代理对象。正确姿势是注入一个独立的 AsyncService在 PostConstruct 里调用这个 Service 的异步方法让代理链完整地走一遍。别在一个 Bean 的内部方法之间互相调用带 AOP 注解的方法这是 Spring 使用的基本共识。7.3 翻车三BeanPostProcessor 里主动 getBean 导致的连锁爆炸有一次我在一个自定义 BPP 的postProcessAfterInitialization方法里写了applicationContext.getBean(SomeService.class)本意是拿到另一个 Bean 做初始化联动。结果启动时项目直接报循环依赖而且报错信息极其迷惑指向了一个完全没有循环关系的类。排查后才发现BPP 在执行时SomeService 可能还没开始创建我的 getBean 强制提前触发了它的创建而 SomeService 又可能依赖了当时正在处理的 Bean形成了看不到的循环。从那以后我给自己定了一条规矩在 BPP 里绝对不主动 getBean如果有获取需求要么通过构造器注入要么用 ObjectProvider 延迟解析确保不会打乱容器自己的创建顺序。7.4 翻车四容器关闭时资源被提前释放导致优雅停机失败这个问题更隐蔽。我们的一个定时任务服务在收到停机信号后总是报连接池已关闭的异常。排查发现某个消息队列的监听器 Bean 实现了 DisposableBean在 destroy 里关闭了连接但另一个定时任务 Bean 的 PreDestroy 方法里还会往队列里发一条下线通知。由于销毁顺序不可控先销毁了连接后执行发送自然就失败了。解决思路有两个一是把有依赖关系的两个 Bean 的销毁逻辑合并到同一个生命周期阶段控制先后二是不要依赖销毁顺序用 ApplicationListener 监听 ContextClosedEvent在事件里统一处理优雅停机逻辑因为事件监听器的顺序是可以显式控制的。这也是我在实际项目里踩过最深的一个坑分享出来希望大家别再为销毁顺序纠结。整条链路走到这里Spring Bean 从出生到销毁的每一道工序基本都过了一遍。我最后想说的是生命周期不是背出来的而是用出来的。你每次遇到一个诡异的启动问题、一次莫名其妙的代理失效、一回容器关闭时的异常本质上都是在和生命周期打交道。把它理解成一条流水线知道每个阶段哪里可以插手、哪里不可变更、哪里藏着陷阱很多问题根本不用查资料你自己就能推出答案。