Spring Bean生命周期详解:从实例化到销毁的完整执行顺序与扩展点

发布时间:2026/10/18 8:42:06
Spring Bean生命周期详解:从实例化到销毁的完整执行顺序与扩展点 直接说结论Bean生命周期这种题目网上搜出来大多是一张图加一段代码看着很全真到了排查问题的时候一点忙都帮不上。我最早接触Spring的时候也被那套“实例化→属性填充→初始化→销毁”的口诀糊弄过直到某次线上环境出现初始化顺序异常才被迫把整个流程从头到尾捋了一遍。这篇文章把我实际踩过的坑、调试时验证过的执行顺序、以及扩展点的底层逻辑全部整理出来希望能帮你把散落的知识点串成一条线。适合谁看刚看完Spring入门教程但想深入理解容器运作机制的人面试前想系统梳理生命周期知识点的求职者以及天天和Spring Boot打交道但遇到诡异Bean问题只能重启解决的业务开发。我会尽量把每个阶段为什么存在、在什么场景下发挥作用讲清楚而不是只扔给你一张执行顺序表。1. 先搞清楚Bean生命周期到底在讲什么1.1 为什么面试和实战都绕不开它如果你只是用Spring做CRUD可能永远不需要手动干预Bean的创建。但只要你开始写框架类组件、封装公共模块、或者需要操控第三方库的初始化逻辑生命周期就是你绕不过去的门槛。举个最直观的例子你想在Spring容器启动完成后自动执行一段数据预热逻辑。放进Bean的构造方法里不合适因为那时候依赖还没注入放在普通方法里又不知道什么时候被调用。这时候你就需要理解Spring提供的那一堆“钩子方法”——它们散落在不同阶段各有各的触发时机和限制。另一个现实场景是排查问题。你有没有遇到过这种诡异现象某个静态变量在运行时是null但代码里明明在启动时赋值了或者自定义的拦截器没有生效但配置看起来完全正确这些问题十有八九都和Bean的初始化时机有关尤其是当你用了静态工具类、静态字段、或者外部配置注入的时候。不懂得生命周期你连从哪里下手指去排查都不知道。我再强调一下生命周期描述的不是“Bean对象本身”的生命周期而是“Spring容器管理这个Bean的全过程”。换句话说是容器在替你完成“创建对象、装配依赖、执行初始化、注册销毁回调”这一整套动作。开发者能做的就是在这些动作中插入自己的逻辑。1.2 先记住这条时间线从定义到消亡在深入细节之前我们先把主干时间线拉出来后面所有知识都会挂在这条线上容器读取配置生成BeanDefinitionBean的定义信息包括类名、作用域、依赖、初始化方法名等。容器根据BeanDefinition进行实例化相当于new一个对象此时属性还是默认值。容器进行属性填充把配置文件里的值、自动注入的依赖对象挨个设置到Bean实例上。容器执行各种Aware回调让Bean感知自己的名称、类加载器、容器上下文等。容器执行BeanPostProcessor的前置处理方法postProcessBeforeInitialization。容器执行初始化逻辑PostConstruct、InitializingBean接口、自定义init-method。容器执行BeanPostProcessor的后置处理方法postProcessAfterInitialization。Bean进入“就绪”状态可以被应用程序使用了。容器关闭时执行销毁逻辑PreDestroy、DisposableBean接口、自定义destroy-method。这条时间线看着简单但每一个步骤背后都有对应的源码实现和扩展接口。接下来的每一节我会按这条线拆开细讲不会跳过任何容易混淆的地方。2. 从“定义”到“就绪”Bean的诞生过程2.1 第一站BeanDefinition的解析与注册很多人一上来就讲“实例化”但忽略了Bean生命周期真正意义上的起点其实更早。在对象实例化发生之前Spring会先做大量准备工作其中最核心的就是生成BeanDefinition。BeanDefinition在Spring中就是一份“蓝图”或“说明书”它明确记录了Bean的类名、构造器参数、属性值、是否懒加载、作用域单例还是原型、初始化方法名、销毁方法名等元信息。这份BluePrint是从哪里来的可能来自XML配置文件的bean标签可能来自ComponentScan扫描到的Component注解也可能来自Bean注解的方法。不同来源最终都会转成BeanDefinition对象注册到Spring容器的BeanDefinitionRegistry中。这一步和“类是否被加载”是两回事。Spring在启动时并不会立即new所有Bean而是先把每个Bean的定义信息读取并注册好等到真正需要创建该Bean实例时再依据这份定义来执行实例化。这也是“懒加载”能成立的前提如果BeanDefinition没有提前注册懒加载也就无从谈起。这里有一个容易忽略的细节BeanFactoryPostProcessor可以在此时对BeanDefinition进行修改。例如PropertySourcesPlaceholderConfigurer会解析BeanDefinition中的${...}占位符CustomScopeConfigurer可以注册自定义作用域。这个接口的执行时机非常早早于任何Bean的实例化所以它能“篡改”Bean的规划而BeanPostProcessor则只能在对象实例化后“干预”对象本身。2.2 第二站实例化——new对象只是开始当Spring决定创建某个Bean的实例时默认单例Bean在容器启动阶段就创建除非设置了懒加载它会走实例化流程。这一步的实质就是创建一个Java对象但Spring不会简简单单地反射调用构造函数它会根据情况选择不同的策略。默认情况下Spring会使用无参构造函数来实例化。如果类定义了多个构造器或者用了Autowired标注某个构造器Spring会按照一定规则选择一个构造器来执行。这时候也引出了构造器注入的原理Spring会先把构造器所需的依赖准备齐全然后通过构造器把依赖传进去从而创建出一个“依赖已就位”的对象。实例化阶段还有一个重要的扩展点InstantiationAwareBeanPostProcessor实际归属于BeanPostProcessor家族。它提供了postProcessBeforeInstantiation方法允许你在Spring真正开始反射创建对象之前返回一个自定义的代理对象从而完全跳过Spring默认的实例化逻辑。这在某些AOP场景和代理对象创建中非常常用。这里要注意区分“实例化”之后对象已经存在了但它还是一个“没组装好”的半成品。它的所有属性都是默认值依赖也还没注入。如果你的Bean在构造方法中调用了被依赖对象的方法此时一定会得到null或者空指针因为依赖还没填充。2.3 第三站属性填充与依赖注入实例化完成后Spring会开始填充Bean的属性对应源码中的populateBean方法。这一步做的事情包括根据BeanDefinition中的property标签设置XML属性根据Autowired、Resource、Value等注解注入依赖和配置值。属性填充的顺序很关键Spring会先处理“注解驱动”的注入再处理“XML或显式配置”的属性设置实际上源码中并没有严格的固定顺序而是在多个步骤中处理不同类型的属性。但对开发者而言你不需要纠结内部细节只需要知道一个原则依赖注入包括字段注入、setter注入都在初始化回调如PostConstruct之前完成。所以你在PostConstruct方法中可以安全地使用已经Autowired进来的依赖但不能反过来用。踩坑提示不要在字段初始化处直接使用依赖对象。例如Service public class OrderService { Autowired private UserService userService; private String userName userService.getName(); // 这里会报错或得到null }原因是字段初始化的时机早于属性填充也就是在执行到userName的赋值语句时userService还没有被注入此时还是null。正确做法是在PostConstruct或者afterPropertiesSet中赋值确保依赖已就位。属性填充完成后Bean的各个依赖就已经组装好了但还没有执行任何“自定义初始化”步骤。接下来进入的就是整个生命周期中最富戏剧性的部分。3. 初始化前中后那些容易被忽略的扩展点3.1 Aware接口家族让Bean感知容器在初始化开始之前Spring会给一部分实现了特定接口的Bean“开小灶”让它们感知到容器的存在。这一批接口统称为Aware接口常见的有BeanNameAware让Bean获取自己在Spring容器中的名称即Bean的id或name。BeanFactoryAware让Bean获取当前所在的BeanFactory。ApplicationContextAware让Bean获取当前的ApplicationContext。ClassLoaderAware让Bean获取类加载器。EnvironmentAware让Bean获取当前环境信息profile、属性源等。ResourceLoaderAware让Bean获取资源加载器。这些Aware回调的执行时机都发生在属性填充之后、初始化逻辑之前。设计它们的初衷是让Bean能够在需要的时候直接访问Spring容器提供的资源。比如你想在一个普通工具类中获取当前ApplicationContext就可以让这个类实现ApplicationContextAware在回调方法中把context保存到静态字段。源码中都有一个invokeAwareMethods的方法它会先检查Bean是否实现了BeanNameAware、BeanClassLoaderAware、BeanFactoryAware如果实现了就依次调用对应的setter方法。而ApplicationContextAware是通过ApplicationContextAwareProcessor一个内置的BeanPostProcessor来调用的时机稍微靠后一点但这几个接口都在“初始化”之前生效。使用Aware接口要注意一点如果你只是想在业务代码中获取容器或者环境信息优先考虑直接注入Environment、ApplicationContext而不是实现Aware接口。因为Aware接口和Spring容器强耦合会让你的类很难脱离Spring框架单独使用。如果确实需要封装框架代码也建议先缓存到静态变量再对外提供访问方法。3.2 初始化三步曲PostConstruct、InitializingBean、init-method这一节几乎是面试必考也是实际开发最常见的“扩展点”战场。Spring初始化逻辑分三道关卡按执行顺序分别是PostConstruct注解标志的方法。它来自JSR-250规范Spring在标准Java环境中自动识别。这个方法必须在依赖注入完成后执行并且同一个Bean中只能有一个方法标注PostConstruct如果标注了多个启动时会直接报错。InitializingBean接口的afterPropertiesSet()方法。这是一个Spring特有的接口方法名直译就是“属性设置之后”。Bean实现了该接口容器在填充完属性后就会调用afterPropertiesSet。这个方法不能返回参数也不允许抛受检异常只能抛运行时异常或特定异常。XML配置中init-method指定的方法或者Bean(initMethod xxx)指定的方法。这个方法可以直接在Spring管理的普通类中使用不需要实现任何接口或添加任何注解非常灵活。为什么Spring要提供三种初始化方式最主要的原因是为了兼容不同的使用场景PostConstruct来自标准注解不依赖Spring专有APIInitializingBean是Spring原生的适合框架内部人员init-method则能让你在不修改类源码的情况下通过配置指定初始化逻辑。在实际执行时Spring的initializeBean方法会依次调用这三者。如果同一个Bean中同时定义了三种初始化方法它们都会按上述顺序执行。这一点很多文章都讲但很少有人告诉你如果三种方法都声明了其实是一种代码坏味道。绝大多数场景只需要选择一种。我个人的习惯是能用PostConstruct就用它简洁也标准只有在需要读取配置值后才能设置资源时才在init-method里做。3.3 BeanPostProcessorSpring中最强大的扩展点如果说初始化方法只是给单个Bean插入逻辑那BeanPostProcessor就是给容器中所有Bean的创建过程插入逻辑。它定义了“初始化前”和“初始化后”两个钩子方法public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; }但凡容器中有一个Bean实现了BeanPostProcessor接口那么容器中每一个其他Bean在初始化前后都会被它“过一遍”。这个接口是Spring AOP、事务管理、自定义注入等强大功能的底层基础。比如AutowiredAnnotationBeanPostProcessor就是借助它完成Autowired注入的AbstractAutoProxyCreator借助它创建AOP代理对象。很重要的是postProcessBeforeInitialization的调用时机在Aware接口之后、初始化方法之前postProcessAfterInitialization则在初始化方法之后此时Bean已经“准备就绪”。如果postProcessAfterInitialization返回的对象和传入的对象不是同一个Spring会用新对象替换掉原来的Bean实例这也是AOP代理最标准的实现方法。这里面的坑如果某个BeanPostProcessor本身也需要依赖其他Bean它的实例化时机早于普通Bean导致它内部的依赖无法通过自动注入完成。Spring针对这个问题做了特殊处理——可以直接让BeanPostProcessor实现ApplicationContextAware在回调中去获取依赖而不是在字段中注入。但更常见的做法是避免在BeanPostProcessor中用Autowired而是通过构造器或延迟加载来绕过。4. 销毁阶段不是new完就完事了4.1 DisposableBean、PreDestroy、destroy-method初始化讲究“三步曲”销毁同样有一套相似但又不完全相同的机制。Spring容器在关闭时会调用Bean的销毁逻辑主要包含三个入口实现DisposableBean接口重写destroy()方法。使用PreDestroy注解标注方法。在配置中声明destroy-method或Bean(destroyMethod xxx)。执行顺序恰好与初始化相反先执行PreDestroy然后执行DisposableBean.destroy()最后执行destroy-method指定的方法。如果记不住顺序有个简单的类比PreDestroy对应PostConstructDisposableBean对应InitializingBeandestroy-method对应init-method。比如你想在容器关闭前释放线程池、关闭网络连接、清理临时文件就可以把这些操作放在销毁方法中。但这里要留意销毁方法只对“单例Bean”有意义原型作用域的Bean通常不会由Spring管理销毁因为Spring容器不持有原型Bean的引用它只知道如何创建它不知道什么时候该销毁它。真正的销毁过程在AbstractApplicationContext的close()方法中触发。容器会先调用所有已注册的DisposableBeanAdapter这些适配器会负责执行PreDestroy和destroy-method然后才将容器标记为关闭状态。如果你用的是Spring Boot在应用关闭时可以观察到这些销毁方法的日志输出顺序。4.2 单例Bean的销毁时机与注意陷阱单例Bean的销毁时机是“容器关闭”时不是JVM退出时。如果你只是执行了ApplicationContext.getBean()而没有调用context.close()那么销毁方法根本不会执行。在Spring Boot中ApplicationContext会在应用优雅停机时自动关闭所以正常情况下不需要手动调用。但有几个陷阱值得你注意。第一如果你在PreDestroy中执行完逻辑后又手动调用context.close()可能造成方法执行两次。因为Spring Boot已经注册了关闭钩子你再调用close()就是重复销毁。正确的做法是不自己调用除非你在非Spring Boot的裸容器中测试。第二原型作用域的Bean默认不会被销毁。如果你在原型Bean中注册了资源需要自己手动调用销毁逻辑。常见的做法是让原型Bean实现AutoCloseable拿到引用后主动调用close()或者在创建原型Bean后手动注册一个DisposableBeanAdapter到容器中但这种操作非常罕见。第三销毁逻辑中不要再去调用其他可能已被销毁的Bean。比如A依赖B容器关闭时销毁顺序是不确定的你无法保证在A的销毁方法中B还活着。所以销毁方法应该只做“清理自身资源”的事情不要做跨Bean的协作。5. 实战中的执行顺序与依赖问题5.1 完整的执行顺序清单值得收藏我把一个Bean从启动到关闭的完整执行顺序整理成了一份清单。你可以直接拿去当自查表也可以用这段描述设计一个小测试来验证。BeanDefinition解析与注册启动阶段完成。构造器实例化如果是构造器注入此时参数已被Spring装配好。属性填充Autowired、Value、XML属性等注入完成。setXxxAware回调BeanNameAware、BeanFactoryAware等。ApplicationContextAware回调如果有。postProcessBeforeInitialization所有注册的BeanPostProcessor依次执行。PostConstruct方法。InitializingBean.afterPropertiesSet()。自定义init-method。postProcessAfterInitializationAOP代理通常在此阶段生成。Bean进入就绪状态可以直接使用。容器关闭触发PreDestroy。DisposableBean.destroy()。自定义destroy-method。这份清单里最容易被忽略的是第6步和第10步。很多开发者只关心自己写的PostConstruct却不知道在它前后还有两个系统级算子。当你发现某个Bean的属性在PostConstruct阶段已经是个代理对象那就是postProcessAfterInitialization的手笔——很可能是某个框架在最后修改了Bean。5.2 循环依赖与三级缓存聊生命周期必然绕不开循环依赖。A依赖BB依赖A这是最常见的构造器注入循环依赖。如果两个Bean都用构造器注入Spring直接报错因为实例化A时需要B而实例化B又需要A形成了死锁。如果是字段注入或setter注入Spring就能通过“提前暴露半成品对象”来破解。对应的机制是Spring容器的三级缓存第一级缓存singletonObjects存放完全创建好的单例Bean。第二级缓存earlySingletonObjects存放“提前暴露”的早期单例Bean还没有填充属性和初始化。第三级缓存singletonFactories存放创建早期单例Bean的对象工厂。为什么需要三级而不是两级核心在于BeanPostProcessor的能力。比如AOP代理需要在目标对象创建完成后才能生成但如果循环依赖导致A在创建过程中就需要注入B而A此时还需要被代理Spring用第三级缓存保存了一个“对象工厂”在有人需要A的早期引用时调用工厂生成一个可能被代理过的早期对象而不是直接暴露原始对象。这样既解决了循环依赖又保证了代理逻辑不会乱套。这个知识点容易被面试题引向一个坑Async注解的Bean出现循环依赖时会抛异常。因为Async的代理创建时机在postProcessAfterInitialization而在循环依赖中早期引用提前暴露此时代理还没生成后续找到的原始对象和最终代理对象不一致Spring只能放弃。解决方法是让其中一个Bean不使用字段注入而用Lazy注入延迟代理的创建。6. 常见问题与排查技巧实录6.1 问题一初始化方法执行了两次现象日志中自定义初始化逻辑打印了两次或者某个计数器被累加了两次。排查思路先看初始化方法是不是被不同机制同时触发。比如你在类上同时加了PostConstruct又实现了InitializingBean接口还在配置里写了init-method——那这个方法在逻辑上可能执行了多次如果它们各自调用了同一个私有方法。另外如果Bean被定义了两次比如XML里声明了一个组件扫描又扫到了Component也会导致两个不同的Bean实例分别执行初始化。检查你的配置中是否有重复定义。还有一种隐蔽情况当前类被CGLIB代理后代理对象和目标对象可能都执行了初始化逻辑尤其是在自定义BeanPostProcessor中又触发了对目标类的再次实例化。遇到这类问题建议先打印日志确认执行初始化的Bean的类名、hashCode和Bean名称。6.2 问题二PostConstruct不生效现象方法没有被调用或方法里的代码没生效。排查思路最常见的原因是方法所在的类没有被Spring管理。比如你在普通工具类上用PostConstruct但类上没有加Component或其他注解Spring根本不会扫描它。另一个原因是方法被声明为static。PostConstruct要求方法是非静态的实例方法如果被设置为静态Spring不会调用。第三方法不能有参数必须是无参方法否则Spring会忽略或抛异常。还有一个规则一个类中如果同时有PostConstruct方法它会先于afterPropertiesSet执行但如果你在某些自定义的BeanPostProcessor中提前调用了初始化还可能会绕过注解识别。这种场景比较少见但排查时可以关注一下ApplicationContext的启动日志看有没有关于该Bean的初始化记录。6.3 问题三销毁方法没被调用现象应用关闭时清理资源的日志没打印或者数据库连接没有释放。排查思路先确认你是不是用了原型作用域。原型Bean默认不被Spring管理销毁逻辑需要自己负责释放。如果确认是单例Bean再看容器是否真正执行了关闭流程。在非Spring Boot环境下如果你用的是AnnotationConfigApplicationContext必须手动调用close()或registerShutdownHook()。如果只调用了refresh()而没有关闭销毁方法永远不会执行。在Spring Boot中如果配置了spring.main.web-application-typenone同时没有主动关闭上下文应用结束后不会触发销毁流程。还有一个常见坑在PreDestroy方法里使用了依赖Bean而这个依赖Bean在销毁时已经被提前销毁导致抛出异常并吞掉了原销毁逻辑。6.4 排查工具与调试技巧除了阅读源码我推荐几个非常实用的排查手段。第一在BeanPostProcessor中打印日志。你可以在自己的postProcessAfterInitialization中临时记录所有Bean的名称和处理结果这样能看到整个容器创建Bean的顺序和耗时。第二利用Spring Boot的/actuator/beans端点。访问这个端点可以列出所有Bean的类型、作用域、依赖关系甚至还能看到Bean的实际类名。如果发现某个Bean的类型和你预期的不一致比如变成了$$EnhancerBySpringCGLIB那一定是被代理了。第三断点调试AbstractBeanFactory.doGetBean和AbstractAutowireCapableBeanFactory.createBean。在你关心的Bean创建时打上条件断点逐步跟进populateBean、initializeBean等关键方法可以看到每个阶段到底发生了什么。这个方法的门槛稍高但对理解生命周期有奇效。7. 我的一些实操建议和收尾体会写了这么多最后分享几点我在实际开发中总结的经验希望能帮你绕开那些书本上不会写的坑。第一优先使用PostConstruct和PreDestroy作为初始化和销毁的标准入口而不是实现InitializingBean和DisposableBean接口。原因是接口方式和Spring耦合太深换到其他框架时连代码都要重写。当然如果你在编写公共框架、需要保证所有模块强制实现某个方法用接口反而更合适。第二在PostConstruct中不要做耗时操作。PostConstruct在Spring Boot的启动阶段执行如果这里去做远程调用或大计算会拖慢应用启动而且容易遇到依赖尚未初始化完成的问题。如果需要异步执行建议配合Async使用但要注意Async的代理时机问题。第三学会利用生命周期写优雅的代码。例如在封装线程池时可以把线程池创建放在PostConstruct把优雅停机放在PreDestroy。再比如动态数据源路由可以在afterPropertiesSet中初始化路由表避免首次访问时性能抖动。生命周期不是面试题它是你控制组件行为的最直接工具。第四如果你在阅读框架源码时看到各种BeanPostProcessor不要被它们吓住。你只需要明白在Bean被创建到他真正可用之间任何东西都可以被拦截、替换、代理。理解了这一点你对Spring的掌控力会上一个台阶。最后再补一个小技巧在极少数情况下你想在某个Bean完全初始化之后再对容器中的某些对象做二次处理可以借助ApplicationListenerApplicationReadyEvent——这不是Bean生命周期的标准环节但在Spring Boot中它就是“容器完全启动”的最佳信号。很多时候它比在PostConstruct中瞎折腾要安全得多。以上这些内容是我踩坑和读源码的总结。如果你照着这份顺序清单去打断点验证一遍大概率会对Spring产生一种“原来如此”的清晰感。希望这篇长文对你有用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询