直接说结论:Bean生命周期这种题目,网上搜出来大多是一张图加一段代码,看着很全,真到了排查问题的时候一点忙都帮不上。我最早接触Spring的时候也被那套“实例化→属性填充→初始化→销毁”的口诀糊弄过,直到某次线上环境出现初始化顺序异常,才被迫把整个流程从头到尾捋了一遍。这篇文章把我实际踩过的坑、调试时验证过的执行顺序、以及扩展点的底层逻辑全部整理出来,希望能帮你把散落的知识点串成一条线。
适合谁看?刚看完Spring入门教程但想深入理解容器运作机制的人,面试前想系统梳理生命周期知识点的求职者,以及天天和Spring Boot打交道但遇到诡异Bean问题只能重启解决的业务开发。我会尽量把每个阶段为什么存在、在什么场景下发挥作用讲清楚,而不是只扔给你一张执行顺序表。
1. 先搞清楚Bean生命周期到底在讲什么
1.1 为什么面试和实战都绕不开它
如果你只是用Spring做CRUD,可能永远不需要手动干预Bean的创建。但只要你开始写框架类组件、封装公共模块、或者需要操控第三方库的初始化逻辑,生命周期就是你绕不过去的门槛。
举个最直观的例子:你想在Spring容器启动完成后,自动执行一段数据预热逻辑。放进Bean的构造方法里不合适,因为那时候依赖还没注入;放在普通方法里又不知道什么时候被调用。这时候你就需要理解Spring提供的那一堆“钩子方法”——它们散落在不同阶段,各有各的触发时机和限制。
另一个现实场景是排查问题。你有没有遇到过这种诡异现象:某个静态变量在运行时是null,但代码里明明在启动时赋值了?或者自定义的拦截器没有生效,但配置看起来完全正确?这些问题十有八九都和Bean的初始化时机有关,尤其是当你用了静态工具类、静态字段、或者外部配置注入的时候。不懂得生命周期,你连从哪里下手指去排查都不知道。
我再强调一下:生命周期描述的不是“Bean对象本身”的生命周期,而是“Spring容器管理这个Bean的全过程”。换句话说,是容器在替你完成“创建对象、装配依赖、执行初始化、注册销毁回调”这一整套动作。开发者能做的,就是在这些动作中插入自己的逻辑。
1.2 先记住这条时间线:从定义到消亡
在深入细节之前,我们先把主干时间线拉出来,后面所有知识都会挂在这条线上:
- 容器读取配置,生成BeanDefinition(Bean的定义信息,包括类名、作用域、依赖、初始化方法名等)。
- 容器根据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专有API;InitializingBean是Spring原生的,适合框架内部人员;init-method则能让你在不修改类源码的情况下,通过配置指定初始化逻辑。
在实际执行时,Spring的initializeBean方法会依次调用这三者。如果同一个Bean中同时定义了三种初始化方法,它们都会按上述顺序执行。这一点很多文章都讲,但很少有人告诉你:如果三种方法都声明了,其实是一种代码坏味道。绝大多数场景只需要选择一种。我个人的习惯是能用@PostConstruct就用它,简洁也标准;只有在需要读取配置值后才能设置资源时,才在init-method里做。
3.3 BeanPostProcessor:Spring中最强大的扩展点
如果说初始化方法只是给单个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对应@PostConstruct,DisposableBean对应InitializingBean,destroy-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。 postProcessAfterInitialization(AOP代理通常在此阶段生成)。- Bean进入就绪状态,可以直接使用。
- 容器关闭,触发
@PreDestroy。 DisposableBean.destroy()。- 自定义
destroy-method。
这份清单里最容易被忽略的是第6步和第10步。很多开发者只关心自己写的@PostConstruct,却不知道在它前后还有两个系统级算子。当你发现某个Bean的属性在@PostConstruct阶段已经是个代理对象,那就是postProcessAfterInitialization的手笔——很可能是某个框架在最后修改了Bean。
5.2 循环依赖与三级缓存
聊生命周期,必然绕不开循环依赖。A依赖B,B依赖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-type=none,同时没有主动关闭上下文,应用结束后不会触发销毁流程。还有一个常见坑:在@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完全初始化之后,再对容器中的某些对象做二次处理,可以借助ApplicationListener<ApplicationReadyEvent>——这不是Bean生命周期的标准环节,但在Spring Boot中它就是“容器完全启动”的最佳信号。很多时候,它比在@PostConstruct中瞎折腾要安全得多。
以上这些内容,是我踩坑和读源码的总结。如果你照着这份顺序清单去打断点验证一遍,大概率会对Spring产生一种“原来如此”的清晰感。希望这篇长文对你有用。