SpringBean的生命周期这个问题,我从刚学Spring就被面试官问过,工作几年后又因为线上启动变慢、AOP代理不生效这些怪问题,回头认真把生命周期重新盘了一遍。说句大实话,它绝对不只是用来应付八股问答的知识,而是把IoC容器的实例化、属性填充、Aware回调、代理生成、初始化、销毁这些环节全部串起来的主线。只要把这条主线看明白了,后面看Spring源码、排查启动慢、处理循环依赖、自定义Starter都会顺手很多。这篇内容适合正在啃Spring基础的同学,也适合写了好几年@Service却对Bean到底怎么被创建出来感觉模糊的Java工程师。我尽量把原理、代码、坑都放在一起,方便你直接对照着验证。
1. 为什么SpringBean的生命周期值得从头盘一遍
1.1 面试题背后考察的是框架的底层整合能力
很多同学背SpringBean生命周期,会背出“实例化、属性填充、初始化、销毁”这几个字,但面试官一旦追问“AOP代理是在哪一步生成的”“为什么构造器注入的循环依赖会报错”“@PostConstruct和init-method到底谁先执行”,能答上来的人就少了很多。原因很简单:生命周期不是一个孤立的知识点,它是整个Spring容器运行机制的承重墙。
比如你在项目里用了事务注解或切面,底层其实就是一个BeanPostProcessor在Bean初始化之后帮你生成了代理对象。你在@PostConstruct里写初始化逻辑,本质上也是容器在初始化阶段通过内置的BeanPostProcessor回调了你的方法。理解了这套机制,就不再是“为了背而背”,而是能把Spring的IoC、AOP、事务、事件监听、自动配置全部串到一条线上去理解。
1.2 生命周期贯穿IoC、AOP、事务三层
从使用者的视角来看,Spring就像一个大工厂,你只负责声明“我要一个什么Bean”,剩下创建、装配、增强、销毁都由容器完成。但这个工厂不是黑盒子,它对外提供了很多扩展点。BeanPostProcessor、BeanFactoryPostProcessor、InitializingBean、Aware接口族,这些都是生命周期在不同阶段暴露出来的钩子。
事务、Redis、MQ等组件的Starter,本质上都是靠这些钩子把核心逻辑注入到容器里。比如Spring事务管理就是在Bean初始化之后判断是否有@Transactional注解,有就生成一个事务增强代理。如果你不了解生命周期,遇到“事务不生效”这种问题只会查配置,很难想到可能是Bean提前被代理或者循环依赖导致早期暴露造成的。所以我觉得,只要还在用Spring写Java,生命周期这道坎早晚都得过,越早过越划算。
2. 完整生命周期流程拆解:Bean从创建到销毁的每一步
2.1 起点:BeanDefinition的注册与合并
很多人以为Bean生命周期是从构造方法开始的,实际上容器里第一件事不是创建对象,而是准备“对象的图纸”,也就是BeanDefinition。无论是@Component扫描到的类,还是@Bean方法返回的类型,容器都会把类的元信息、作用域、懒加载标记、初始化方法名、销毁方法名等打包进一个BeanDefinition对象里。
这一步非常关键,因为后续所有创建步骤都要参考这份图纸。比如@Scope("prototype")、@Lazy、@DependsOn这些注解,都会被读取并设置到BeanDefinition中。如果你的配置类里有父类或者抽象配置,Spring还会在getMergedLocalBeanDefinition这里做BeanDefinition的合并,把子定义的属性补齐。我排查过一些奇怪的Bean属性丢失问题,最后定位到就是BeanDefinition合并阶段没有生效,说明这一步直接决定了后面Bean长什么样。
2.2 创建实例:从实例化前拦截到真正new出来
有了图纸之后,容器开始创建Bean实例。这里有一个很容易被忽略的扩展点:InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation。这个方法的返回值可以是一个代理对象,如果返回了非空对象,Spring会直接跳过后面的默认实例化过程,连构造方法都不会走。很多框架的AOP代理、远程调用代理,就是在这里提前返回自定义对象。
如果这个方法没有返回对象,Spring就会走构造器反射来创建实例。对于单例Bean,构造器默认会选择无参构造器,如果是有参构造或构造器注入,则需要从容器里解析依赖参数。这个地方是循环依赖最容易爆雷的位置:singleton对象还在创建中,就又要去获取另一个尚未创建完成的Bean,如果对方需要构造器注入,那基本就无解了。
2.3 属性填充:依赖注入发生的地方
实例创建完成之后,Spring开始给这个Bean“喂水喂饭”,也就是属性填充。这里处理的是@Autowired、@Resource、@Value、XML里的property配置。Spring会通过BeanWrapper把依赖注入到Bean中。字段注入和setter注入都在这个阶段完成。
属性填充不是无条件执行的,它的前置条件是InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation返回true。如果这个方法返回false,容器会直接跳过属性填充阶段。这在某些特殊场景下可以避免对代理对象或半成品对象做不必要的注入,不过我平时用得不多,但在阅读Spring源码时会看到很多内置处理器在利用这个钩子控制注入行为。
2.4 初始化前:Aware回调与BeanPostProcessor前置处理
属性填充完成之后,Bean终于有了一个相对完整的状态,但还不能直接交给业务使用。这里Spring会先做Aware回调,比如BeanNameAware.setBeanName传入Bean名字,BeanFactoryAware.setBeanFactory传入BeanFactory,让你能在Bean内部感知到容器的存在。
接下来是初始化前的BeanPostProcessor.postProcessBeforeInitialization。这个阶段是Spring扩展点最密集的地方之一。AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、ApplicationContextAwareProcessor等一大票内置处理器都挂在上面。你会在这里看到@PostConstruct被调用,也会看到ApplicationContextAware被回调。很多人困惑“@PostConstruct到底算生命周期哪一步”,严格来说它是在初始化前的BeanPostProcessor里执行的,并不是一个单独的初始化内核阶段。
2.5 初始化:InitializingBean、init-method与初始化后置处理
初始化阶段本身按顺序包含三件事:先执行InitializingBean.afterPropertiesSet(),再执行自定义的init-method(也就是XML里的init-method或@Bean(initMethod=...))。这里有个容易搞混的点:因为@PostConstruct是在前面的BeanPostProcessor里被回调的,所以实际顺序其实是@PostConstruct在afterPropertiesSet之前。
初始化完成后是最后一个强扩展点:BeanPostProcessor.postProcessAfterInitialization。AOP代理的默认生成时机就在这里。AnnotationAwareAspectJAutoProxyCreator会在这个阶段判断Bean是否匹配切面,如果匹配就创建代理对象并返回。所以你在Spring里拿到的很多Bean,其实已经不是原始实例,而是经过这个阶段包装后的代理对象。如果没有循环依赖,这个时机基本是固定的。
2.6 销毁:@PreDestroy、DisposableBean和destroy-method
单例Bean在容器关闭的时候会执行销毁逻辑。这一段相对简单,但顺序也有讲究:先回调@PreDestroy注解方法,再执行DisposableBean.destroy(),最后执行自定义的destroy-method。和初始化阶段一样,@PreDestroy也是CommonAnnotationBeanPostProcessor在销毁阶段通过BeanPostProcessor回调触发的。
这里需要注意,只有容器管理的单例Bean会保证执行销毁流程。原型Bean在交付给你之后,容器就不再跟踪它了,销毁方法默认不会被调用。这也是“原型Bean只有生命周期前半段”这句话的由来。如果业务里确实需要释放原型Bean的资源,要么自己手动拿到DisposableBean适配器来调用,要么换一种Scope设计,否则很容易出现资源泄漏。
2.7 初始化与销毁顺序速查表
| 阶段 | 触发机制 | 执行顺序 |
|---|---|---|
| 构造实例 | 构造器反射 | 无依赖时先执行无参构造器 |
| 属性填充 | BeanWrapper注入 | @Autowired、@Resource、@Value |
| Aware回调 | BeanNameAware等 | 在属性填充后、初始化前 |
| 初始化前 | BeanPostProcessor.before | 内置BPP优先,@PostConstruct在此阶段 |
| 初始化 | InitializingBean | afterPropertiesSet |
| 初始化方法 | init-method | @Bean(initMethod)配置 |
| 初始化后 | BeanPostProcessor.after | AOP代理默认在此生成 |
| 销毁前 | @PreDestroy | 在destroy方法之前 |
| 销毁 | DisposableBean | destroy() |
| 销毁方法 | destroy-method | @Bean(destroyMethod)配置 |
3. 实操:写个Demo把生命周期完整打印出来
3.1 工程准备与依赖选择
为了直观看到每个生命周期阶段,我建议直接建一个Spring Boot工程,不用引入额外的Web组件,只需spring-boot-starter基础依赖就行。我用的是Spring Boot 3.x,因此@PostConstruct和@PreDestroy需要从jakarta.annotation包导入;如果你还在用Spring Boot 2.x,用javax.annotation即可。这个差异在实际项目中经常遇到,尤其是老项目升级SpringBoot3的时候,包名一变,很多人找不到注解在哪。
创建一个普通Java类,起名LifecycleLogBean,让它实现BeanNameAware、BeanFactoryAware、InitializingBean、DisposableBean这几个接口,然后加上@PostConstruct、@PreDestroy、自定义initMethod和destroyMethod,再把每个阶段打印出来。为了验证属性填充,我加了一个setName方法并用@Value注入。这样你运行起来后,日志会把每个环节的顺序完完整整地列出来。
3.2 实现可打印生命周期的生命周期Bean
public class LifecycleLogBean implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { private String name; public LifecycleLogBean() { System.out.println("1. 构造方法执行"); } @Value("${demo.name:default}") public void setName(String name) { this.name = name; System.out.println("2. 属性填充 setName(" + name + ")"); } @Override public void setBeanName(String name) { System.out.println("3. BeanNameAware.setBeanName -> " + name); } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("4. BeanFactoryAware.setBeanFactory"); } @PostConstruct public void postConstruct() { System.out.println("5. @PostConstruct"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("6. InitializingBean.afterPropertiesSet"); } public void initMethod() { System.out.println("7. init-method"); } @PreDestroy public void preDestroy() { System.out.println("9. @PreDestroy"); } @Override public void destroy() throws Exception { System.out.println("10. DisposableBean.destroy"); } public void destroyMethod() { System.out.println("11. destroy-method"); } }配置类里用@Bean注册这个Bean,并在注解上指定initMethod和destroyMethod:
@Configuration public class LifecycleConfig { @Bean(initMethod = "initMethod", destroyMethod = "destroyMethod") public LifecycleLogBean lifecycleLogBean() { return new LifecycleLogBean(); } }3.3 自定义BeanPostProcessor输出前后置日志
只是Bean自己打印还不够,我还加了一个自定义的BeanPostProcessor,专门针对LifecycleLogBean打印初始化前后的日志:
@Component public class LifecycleBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof LifecycleLogBean) { System.out.println("5.5 BeanPostProcessor.postProcessBeforeInitialization"); } return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof LifecycleLogBean) { System.out.println("8. BeanPostProcessor.postProcessAfterInitialization"); } return bean; } }运行后我的控制台输出顺序如下:
1. 构造方法执行 2. 属性填充 setName(default) 3. BeanNameAware.setBeanName -> lifecycleLogBean 4. BeanFactoryAware.setBeanFactory 5. @PostConstruct 5.5 BeanPostProcessor.postProcessBeforeInitialization 6. InitializingBean.afterPropertiesSet 7. init-method 8. BeanPostProcessor.postProcessAfterInitialization ... 9. @PreDestroy 10. DisposableBean.destroy 11. destroy-method需要注意的是,第5和第5.5的顺序并不完全固定,取决于容器里BeanPostProcessor的注册顺序。Spring内置的CommonAnnotationBeanPostProcessor通常在用户自定义BPP之前注册,所以@PostConstruct会先打印。如果在项目里把自定义BPP的注册顺序调整到内置之前,输出顺序就会反过来。这也是生命周期流程里容易产生幻觉的地方,建议对照源码去理解,而不是死记某一个输出序列。
4. 核心扩展点拆解:每个钩子背后的设计意图
4.1 BeanPostProcessor:扩展点之王
BeanPostProcessor是Spring生命周期里最强大的扩展点,它就像给每个Bean在初始化前后都装上了拦截器。你可以在postProcessBeforeInitialization里修改Bean的属性,也可以在postProcessAfterInitialization里替换整个Bean对象。AOP代理、注解驱动注入、ApplicationContext感知等功能,本质上都是靠不同类型的BeanPostProcessor实现的。
有一个容易踩的坑:BeanPostProcessor自身也是由Spring容器管理的Bean,而且它必须在其他普通Bean创建之前被实例化并注册,否则无法拦截后续创建过程。所以在项目里如果看到自定义BeanPostProcessor里的依赖注入一直不生效,或者启动顺序异常,可以优先检查是不是这个处理器被创建得太晚,或者构造器里用了需要代理的Bean。记住,BeanPostProcessor不是普通业务Bean,创建它的时候要尽量轻量,不要在它的构造器里做太多依赖操作。
4.2 Aware接口族:让Bean主动感知容器
Aware接口是Spring提供给Bean“认识容器”的一扇门。常见的有BeanNameAware、BeanFactoryAware、ApplicationContextAware、EnvironmentAware、ResourceLoaderAware等。它们的执行时机基本都在属性填充之后、初始化之前,但具体实现方式略有区别。
比如BeanNameAware和BeanFactoryAware是容器在AbstractAutowireCapableBeanFactory.invokeAwareMethods里直接回调的,而ApplicationContextAware则是通过ApplicationContextAwareProcessor这个BeanPostProcessor,在postProcessBeforeInitialization里被调用。这导致一个有意思的现象:如果容器是纯粹的BeanFactory而不是ApplicationContext,ApplicationContextAware不会生效,因为缺少对应的处理器。理解了这个差异,以后遇到“为什么Bean拿不到ApplicationContext”这类问题,就知道是容器类型或者处理器没注册导致的。
4.3 三种初始化方式怎么选
@PostConstruct、InitializingBean、init-method这三种初始化方式,功能上重叠,但细节不同:
| 初始化方式 | 执行依据 | 适用场景 |
|---|---|---|
| @PostConstruct | JSR250规范,注解驱动 | 业务代码中最为常见,直观 |
| InitializingBean | Spring接口,重写afterPropertiesSet | 框架内部、组件封装时使用 |
| init-method | XML或@Bean(initMethod)配置 | 不想让业务类实现Spring接口,或者配置来自外部 |
我的习惯是:业务项目里优先用@PostConstruct,因为它语义清晰,而且不用实现Spring接口,和对象自身逻辑解耦。如果是自己封装Starter或者写框架组件,会考虑InitializingBean,因为组件内部不适合依赖注解扫描,接口更直接。init-method主要用在配置类、老XML配置或第三方Bean不希望代码侵入时。销毁方法的选择逻辑也类似,优先@PreDestroy,其次destroy-method,DisposableBean只在特殊场景下使用。
5. 高频问题与排查经验
5.1 循环依赖为什么能成立,三级缓存干了什么
Spring Bean生命周期中最经典的问题就是循环依赖。A依赖B,B依赖A,对于单例且使用的是setter注入或字段注入,Spring可以通过三级缓存让循环依赖成立。三级缓存分别是:singletonObjects(成品缓存)、earlySingletonObjects(提前暴露的半成品缓存)、singletonFactories(单例工厂缓存)。
流程大致是这样的:A创建后,在属性填充阶段发现自己需要B,于是去创建B;B创建时又会依赖A,此时容器发现A还没完成初始化,但已经有一个singletonFactory放在三级缓存里,于是通过这个工厂拿到A的早期引用,把早期引用注入给B;B创建完成后,A再把B注入进来,最后完成A的初始化。早期引用本质上改变了生命周期中的暴露时机:本来A要到init之后才完全可用,但因为循环依赖,A在属性填充阶段就被提前暴露了。
这里最大的坑是构造器循环依赖。因为构造器执行发生在属性填充之前,A在构造时就需要B,但A此时还没有实例被放入三级缓存,Spring根本没有办法提前暴露,所以构造器循环依赖会直接报错。解决办法一般是把构造器注入改成setter注入,或者给其中一个依赖加@Lazy延迟获取。实际排查时先看异常信息和日志,明确是构造器循环依赖还是setter循环依赖,再决定怎么改。
5.2 AOP代理对象到底在哪一步生成
正常情况下,Spring AOP的代理对象是在Bean初始化之后,通过AbstractAutoProxyCreator这个BeanPostProcessor的postProcessAfterInitialization生成的。也就是说,你的@Transactional、@Async切面,默认都是在Bean已经完成属性填充和初始化之后才包装上一层代理。
但存在一个例外:如果Bean参与了循环依赖,那么代理可能会提前生成。因为三级缓存中的singletonFactory里保存的是getEarlyBeanReference方法,这个早期引用查询会给SmartInstantiationAwareBeanPostProcessor一个机会,在Bean还没有完成初始化的阶段就生成代理对象。这么设计是为了保证循环依赖中的多个Bean拿到的是同一个代理,而不是原始对象和代理对象互相交错。
我调过一个AOP不生效的问题,最后发现是用户自己写了个InstantiationAwareBeanPostProcessor,在postProcessBeforeInstantiation里直接返回了原始对象,绕过了正常代理流程。如果你碰到类似“明明有切面,但Bean方法没有被增强”的情况,可以先检查是不是有自定义处理器提前拦截了实例化过程。整体来说,理解AOP代理生成的默认时机和例外情况,比死记“代理在初始化后生成”更有用。
5.3 原型Bean的生命周期并不完整
默认的@Scope("singleton")下,单例Bean从创建到销毁都由容器管理。原型Bean则不同,每次getBean都会创建新实例,完整的创建流程依然会走:实例化、属性填充、Aware、初始化前、初始化、初始化后都会执行,但容器不会保存这个Bean,所以销毁阶段不会自动触发。
这就意味着,原型Bean用完后如果你不去处理资源释放,是有泄漏风险的。我有一次排查数据库连接池连接数持续增长,最后定位到某个原型Bean在初始化时创建了一个文件句柄,但销毁方法从来没执行过,句柄一直没释放。解决办法要么在业务调用处手动调用清理方法,要么把这种需要资源管理的对象设计成单例,不要图方便全部用原型。如果你确实希望原型Bean生命周期也能被容器接管,可以考虑自定义Scope,在get和remove阶段做更多控制,但成本不低,一般不建议轻易引入。
5.4 启动变慢与Bean初始化顺序问题的排查思路
启动变慢经常和Bean生命周期里的初始化逻辑直接相关。尤其是单例Bean默认预实例化,Spring容器启动时会把所有非懒加载的单例Bean都创建并初始化一遍。如果里面有大量初始化网络连接、预热缓存、批量查询数据库的操作,启动时间就会肉眼可见地变长。
排查时可以用@Lazy把不需要启动时初始化的Bean懒加载,也可以用@DependsOn显式指定Bean的创建顺序。遇到更复杂的情况建议打开Spring的debug日志,或者用Actuator的Beans端点查看Bean创建顺序。平时开发时我也习惯在启动类上临时加一个ApplicationRunner,把容器里关键Bean的状态打印出来,确认再判断是Bean初始化顺序不对,还是某个初始化逻辑阻塞了主线程。
6. Spring Boot和现代Spring中的生命周期变化
6.1 自动配置、条件注解与生命周期入口
Spring Boot出现后,Bean生命周期本身并没有改变,但BeanDefinition的注册方式变得更“智能”了。自动配置类里大量使用@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,决定哪些BeanDefinition会被注册。这些判断发生在BeanDefinition加载阶段,早于生命周期创建阶段。
所以你会看到很多自动配置虽然写了一大堆@Bean方法,但最终容器里只创建了满足条件的Bean。这种设计对生命周期的影响主要体现在“启动顺序”和“Bean覆盖”上:一个自动配置Bean可能在业务Bean之前被创建,也可能被用户自定义的同类型Bean覆盖。排查时先看条件注解是否命中,再看是否存在同名Bean导致的覆盖,不要一上来就怀疑生命周期顺序。
6.2 SmartLifecycle和ApplicationRunner的调用边界
Spring容器在refresh完成后,会调用SmartLifecycle的start方法。这里看起来和Bean生命周期有点交叉,但实际上是独立的容器生命周期阶段。普通Bean的初始化发生在finishBeanFactoryInitialization阶段,SmartLifecycle.start则是在finishRefresh阶段。
因此,如果需要在容器完全启动后再执行某些任务,请用ApplicationRunner、CommandLineRunner或SmartLifecycle,而不是在一个普通Bean的@PostConstruct里去做。在@PostConstruct里启动线程或连接外部服务,如果遇到上下文还没完全刷新、依赖Bean尚未全部就绪,很容易出现各种“神奇”的空指针。这个我踩过好多次,后来统一规范:业务初始化放在ApplicationRunner里,组件内部资源准备放在@PostConstruct但尽量不依赖其他业务Bean。
6.3 工程落地建议:生命周期知识怎么用到实际项目
理解生命周期后,我觉得最有价值的落地方式是检查项目里的初始化逻辑是否放在了正确的位置。不要急着把所有逻辑堆到构造器里,也不要让业务初始化依赖“恰好某个Bean先创建”。我一般在团队里定的规范是:
- 构造器里只做必填参数校验和基础赋值;
- @PostConstruct里做需要依赖注入完成的初始化;
- 需要上下文完全就绪后执行的任务放到ApplicationRunner;
- 需要感知容器信息的Bean用Aware接口,但避免在Aware回调里做重操作;
- 不在BeanPostProcessor里创建业务Bean,以免引入额外的依赖顺序。
这些规范看起来很普通,但在实际项目中能省掉很多莫名其妙的启动问题。特别是团队大了以后,大家对生命周期理解深度不一致,代码里很容易出现“时间上碰巧能跑通”的隐性依赖,一旦调整启动顺序或者扩容,就会瞬间暴露。
我个人的经验是,SpringBean生命周期不能只当成面试题去背,最好自己动手写一个带日志的Demo,在工程里跑一遍,再把常用的AOP、事务、事件监听插进去观察顺序变化。踩过几次坑之后你会发现,很多看源码时觉得抽象的概念,真正落到日志里就变得非常直观了。最后再分享一个排查技巧:遇到Bean异常时,先打开调试日志,观察Bean创建到哪一步失败的,是构造器、属性填充还是初始化前处理器,基本就能快速缩小问题范围。