Spring Bean生命周期全流程:从BeanDefinition到销毁
1. 从一个让你困惑的现象说起
假设你刚接手一个Spring Boot项目,代码里有一个配置类:
java
@Configuration
public class MyConfig {
@Bean(initMethod = "init", destroyMethod = "cleanup")
public MyService myService() {
return new MyService();
}
}
你直觉认为当Spring容器启动后,MyService对象就会被创建。但是,当你写了一个单元测试,只加载这个配置类,却发现MyService的构造器根本没有执行,除非你去调用它。
再看另一个常见场景:你写了两个Bean互相依赖,比如A需要B,B也需要A,你觉得Spring一定会报错,可实际Spring容器却成功启动,只是打印了一行奇怪的日志"正在创建Bean 'a'"。
这些现象背后,隐藏的就是Spring Bean的生命周期管理。如果你不理解它,就会在排查"为什么我的Bean没初始化""为什么销毁方法没被调用"等问题上浪费大量时间。本文将以实例为引导,带你一步步搞清楚从BeanDefinition到销毁的完整流程。
但在深入之前,先记住一个最简模型:Spring容器的核心工作就是把一个"配方"(BeanDefinition)变成一个"成品"(Bean实例),并在容器关闭时负责"回收"(销毁回调)。
2. 整体框架:一次请求"走过"的完整路径
Spring容器内部有两大部分:BeanFactory (工厂)和ApplicationContext(应用上下文,它是BeanFactory的增强版,增加了国际化、事件发布等能力)。我们通常用的是ApplicationContext,但底层对Bean的管理都委托给BeanFactory。
管理一个Bean分三个阶段:注册阶段 、创建阶段 、销毁阶段。先看下面的ASCII图,它展示了整个流程的骨架和关键角色:
text
+----------------+ 1. 解析配置 +----------------+ 2. 封装成 +----------------+
| | --------------> | | -------------> | |
| XML/注解/Java类 | | BeanDefinition | | BeanFactory |
| | | 注册表 | | 容器中 |
+----------------+ +----------------+ +----------------+
|
| 3. 创建Bean时
v
+----------------+ 6. 初始化 +----------------+ 5. 属性填充 +----------------+
| 对象可用 | 完成初始化 | | 实例化 |
+----------------+ +----------------+ +----------------+
|
| 4. 解析依赖
v
+----------------+
| 依赖的Bean |
| (可能递归) |
+----------------+
销毁阶段:容器关闭 -> 调用@PreDestroy或destroyMethod
这个流程中,BeanFactory是核心引擎,它驱动着一系列BeanPostProcessor(后置处理器)在Bean创建的各个时机插入自定义逻辑。先记住几个重要时机:
- 实例化前 (beforeInstantiation)和实例化后(afterInstantiation)。
- 初始化前 (postProcessBeforeInitialization)和初始化后(postProcessAfterInitialization)。
这些后处理器是Spring实现AOP、自动注入、@Autowired等功能的钩子。理解它们,你就能在合适的阶段扩展自定义逻辑。
3. 注册阶段:BeanDefinition从哪里来
3.1 什么是BeanDefinition?
BeanDefinition是Bean的"元信息",它不包含对象本身,而是描述如何创建这个Bean:类名、作用域、构造参数、属性值、初始化方法名、销毁方法名等。可以把BeanDefinition想象成烹饪食谱,而Bean是最终做出来的菜。
3.2 配置解析示例
我们来运行一个最简例子。先看一个XML配置:
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="com.example.UserService">
<property name="name" value="张三"/>
</bean>
</beans>
示例1:从XML加载Bean并查看BeanDefinition
下面是一个完整的可运行类,用于加载上面的XML,并在创建Bean前打印BeanDefinition的内容。
java
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;
public class BeanDefinitionDemo {
public static void main(String[] args) {
// 创建一个BeanFactory(Spring容器最基础的实现)
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 用XML读取器解析XML配置
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new ClassPathResource("beans.xml"));
// 注意:此时只是注册了BeanDefinition,还没创建UserService实例
BeanDefinition bd = factory.getBeanDefinition("userService");
System.out.println("Bean类: " + bd.getBeanClassName());
System.out.println("作用域: " + bd.getScope());
System.out.println("属性值: " + bd.getPropertyValues());
// 输出初始化方法名,默认null
System.out.println("初始化方法: " + bd.getInitMethodName());
// 现在真正触发创建(如果调用getBean)
UserService service = factory.getBean(UserService.class);
System.out.println(service.getName());
}
}
// UserService类
public class UserService {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
前置环境:将上述XML放在resources目录,并将UserService和示例类放在任意包下,编译运行即可。
关键步骤与结果 :运行后你会看到控制台先打印Bean的类名和属性,最后打印"张三"。注意getBean之前,Spring并没有执行构造器。这证明了BeanDefinition是惰性加载的基础 。什么时候才真正创建?通常是在首次getBean()时,除非使用lazy-init="true",否则Spring在启动时就会预先创建所有单例Bean。
适用场景:排查"为什么我的Bean没有被创建"时,先检查BeanDefinition是否被注册,再检查触发创建的时机。
常见错误 :很多人以为@Configuration类中的@Bean方法在Spring启动时就会被调用,实际上Spring会解析配置类,将@Bean方法的信息转为BeanDefinition,但Bean的实例还是在容器启动阶段(除非是@Lazy)一次性创建。
3.3 注册方式对比
下表总结了三种常见的BeanDefinition注册方式:
| 方式 | 典型配置 | 特点 |
|---|---|---|
| XML | <bean> |
早年间标准,配置冗长 |
| 注解 | @Component, @Service, @Bean |
基于类路径扫描或@Configuration,是当前主流 |
| 编程式 | BeanDefinitionRegistry.registerBeanDefinition() |
代码动态注册,适合框架集成、条件判断场景 |
4. 创建阶段:实例化前后的处理
4.1 实例化从哪里开始?
当容器需要某个Bean时(通常是启动阶段所有单例Bean),会进入AbstractAutowireCapableBeanFactory.createBean()。在这里,Spring先会询问每个BeanPostProcessor:"你是否要在这个Bean创建前返回一个代理对象?"这就是实例化前的时机。
请记住:实例化在这里不是调用构造器,而是通过createBeanInstance选择构造器并创建对象。
4.2 实例化前后的扩展点示例
示例2:实例化前返回代理对象
我们自定义一个BeanPostProcessor,干扰Bean创建。
java
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
public class ProxyPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
// 只对MyService应用代理
if ("myService".equals(beanName)) {
System.out.println("实例化前:为myService创建代理");
// 返回一个代理对象替代真实实例,后面真正的实例化过程将会被跳过
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(beanClass);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("代理拦截调用:" + method.getName());
return proxy.invokeSuper(obj, args);
});
return enhancer.create();
}
return null;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
注册这个BeanPostProcessor(通过@Configuration或XML),然后加载一个包含myService的配置,看控制台输出。
前置环境:需要一个MyService类(可以是空类),并注册上面的PostProcessor。
运行效果 :控制台会打印"实例化前:为myService创建代理"。当调用myService的方法时,会先打印代理拦截调用。注意:由于在postProcessBeforeInstantiation返回了非null对象,Spring会完全跳过后续的实例化和属性填充等步骤,直接进入postProcessAfterInitialization(实际上它会在返回代理后直接返回)。
关键解析 :这个机制被Spring AOP使用。当开启@EnableAspectJAutoProxy时,AspectJAutoProxyCreator就是这个后处理器。它会在实例化前判断是否有切面适用,如果适用,就返回代理。
边界与常见错误 :如果后处理器在postProcessBeforeInstantiation返回了非null代理,那么Bean还是会被创建登记到容器里,但它的生命周期其余回调(比如InitializingBean)将不会执行。因此如果你在代理类中依赖初始化逻辑,可能失效。
4.3 完整创建过程的时间线
我们再看看一个Bean从实例化到初始化完成的详细时间线。postProcessAfterInstantiation只是属性填充后,postProcessBeforeInitialization之前的一个时机,通常很少使用。下面用ASCII时间线展示:
text
时间轴 →
步骤1: 实例化 (调用构造器) -> new Object()
步骤2: postProcessAfterInstantiation (如果返回false则跳过属性填充,默认返回true)
步骤3: 属性填充 (populateBean) -> @Autowired,@Resource,@Value 在这里注入
步骤4: postProcessBeforeInitialization <- 在这里可以做before初始化的逻辑
步骤5: 初始化 (调用afterPropertiesSet或init-method)
步骤6: postProcessAfterInitialization <- AOP代理通常在这里
步骤7: 生成了现成的Bean,放入单例池
5. 初始化过程:Bean准备好被使用
5.1 初始化方法有几种?
Spring提供了三种指定初始化回调的方式,各自的优先级在注释中体现。下表比较:
| 方式 | 机制 | 执行顺序 | 典型应用 |
|---|---|---|---|
@PostConstruct |
JSR-250注解,由CommonAnnotationBeanPostProcessor处理 | 最先执行 | 需要依赖注入后才做初始化,且不想和Spring耦合 |
InitializingBean接口 |
Spring直接调用afterPropertiesSet() |
在@PostConstruct之后 | 需要明确实现Spring接口 |
@Bean(initMethod) + init-method XML |
反射调用指定方法名 | 最后执行 | 主要用于配置类中的@Bean方法,或第三方Bean |
5.2 一个综合初始化的例子
我们用三种方式混用,观察执行顺序。你可能会遇到很多源码提问"顺序是什么?",这个例子给出了实证。
示例3:三种初始化方式执行顺序验证
定义一个包含三种初始化的Bean。
java
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component(initMethod = "customInit") // 注意@Bean的initMethod不能和@Component并存,这里为演示改用@Bean方式
public class LifecycleBean implements InitializingBean {
@Value("${app.name}")
private String name;
public LifecycleBean() {
System.out.println("构造器执行");
}
@PostConstruct
public void postConstruct() {
System.out.println("1 @PostConstruct");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("2 InitializingBean#afterPropertiesSet");
}
public void customInit() {
System.out.println("3 @Bean(initMethod)");
}
}
然后编写一个@Configuration类注册它:
java
@Configuration
public class LifecycleConfig {
@Bean(initMethod = "customInit")
public LifecycleBean lifecycleBean() {
return new LifecycleBean();
}
}
启动一个简单Spring应用:
java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class LifecycleDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LifecycleConfig.class);
context.close();
}
}
运行结果:
text
构造器执行
1 @PostConstruct
2 InitializingBean#afterPropertiesSet
3 @Bean(initMethod)
关键解析 :顺序完全符合预期。@PostConstruct在执行时,属性已经被填充。如果你依赖属性注入,这些属性已经可用。
边界提醒 :@Component不能直接设置initMethod,所以必须使用@Bean(initMethod)。如果你的Bean是由@Component注册的,里面只有@PostConstruct和实现InitializingBean这两种方式可用。
5.3 为什么需要这么多初始化方式?
这些方式的历史背景不同:InitializingBean是Spring老接口;@PostConstruct是JDK的注解,只是Spring借用它,好处是不依赖Spring;init-method非常适合第三方Bean,因为你无法给那个类增加注解。
最容易被误解的是,@PostConstruct实际上在postProcessBeforeInitialization阶段被调用,而InitializingBean在afterPropertiesSet中被调用,后者发生在postProcessAfterInitialization里。不要混淆这个先后顺序:@PostConstruct一定会优先于InitializingBean,因为CommonAnnotationBeanPostProcessor的postProcessBeforeInitialization先注册了。
6. 销毁回调:优雅地清理资源
6.1 销毁的时机
当容器关闭时(比如context.close()),Spring会调用每个单例Bean的销毁回调。但要注意,只有单例Bean在容器关闭时才会被销毁;原型作用域的Bean由调用方负责清理,容器不管理。
6.2 销毁方法的三种方式
和初始化类似,有三种对应方式。记住一个关键点:销毁方法只会调用一次,且如果同时定义了@PreDestroy和InitializingBean(这里用错误示例),实际顺序会符合预期。
下表对比销毁方式:
| 方式 | 调用时机 | 示例 |
|---|---|---|
@PreDestroy |
先于其他销毁回调执行 | 资源关闭、线程池关闭 |
DisposableBean#destroy() |
在@PostDestroy之后,但先于destroy-method | 需要实现Spring接口 |
destroyMethod / @Bean(destroyMethod) |
最后执行 | 第三方Bean,如连接池 |
6.3 一个带清理操作的完整示例
我们模拟一个占用资源的Bean。
示例4:带有三种销毁方法的Bean
java
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
@Component(destroyMethod = "cleanup")
public class ResourceBean implements DisposableBean {
public ResourceBean() {
System.out.println("ResourceBean构造");
}
@PreDestroy
public void preDestroy() {
System.out.println("1 @PreDestroy: 释放资源");
}
@Override
public void destroy() throws Exception {
System.out.println("2 DisposableBean#destroy");
}
public void cleanup() {
System.out.println("3 @Bean(destroyMethod)");
}
}
启动上下文后关闭:
java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DestroyDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example");
context.close();
}
}
输出:
1 @PreDestroy: 释放资源
2 DisposableBean#destroy
3 @Bean(destroyMethod)
关键解析 :注意@Component(destroyMethod = "cleanup")的写法可行,通过属性指定销毁方法。
适用场景:数据库连接、网络连接、文件句柄、线程池等需要释放的资源。不释放可能导致内存泄漏或连接泄漏。
常见错误 :很多人以为在@Bean上指定destroyMethod,Bean被getBean获取后手动close时会触发,其实不一定。@Bean(destroyMethod)只有在容器关闭时才回调。如果希望手动关闭这个Bean,要调用context.close()。
7. 循环依赖处理:一个巧妙的设计
7.1 什么是循环依赖?
循环依赖指Bean A依赖Bean B,而B又依赖A。最常见的形式是构造器注入(无法解决)和setter注入(可以解决)。
7.2 三级缓存机制
Spring解决setter注入的循环依赖依靠的是"提前暴露半成品"的三级缓存。看下面的ASCII图:
text
一级缓存: singletonObjects (完成品)
二级缓存: earlySingletonObjects (提前暴露的半成品,尚未属性填充)
三级缓存: singletonFactories (ObjectFactory,能生成代理或完整对象)
创建A时:
1. new A() 还没属填充
2. A放入三级缓存 (singletonFactories) 作为ObjectFactory
3. 填充A属性,发现需要B
4. 创建B,填充B属性,发现需要A
5. 从三级缓存得到A的ObjectFactory,并提前暴露给B (移到二级缓存)
6. B完成创建,放入一级缓存
7. A继续完成初始化,放入一级缓存
7.3 什么情况下会报错?
构造器注入无法解决循环依赖,因为new A时就需要B的实例,而此时B还没创建。还有一种情况是@Async等代理未提前暴露,也会报错。
常见误区是:只要出现循环就一定会报错。实际上如果你的代码使用setter注入(包括@Autowired字段注入,这是setter注入的变体),Spring能自动处理。
7.4 使用@Lazy解决顽固循环
当无法避免构造器循环时,可以在其中一个依赖上加@Lazy,Spring会创建一个代理注入,真正的依赖在第一次使用时才创建。
示例5:演示三级缓存能解决的问题
我们写一个简单的循环依赖例子。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AService {
private BService bService;
@Autowired
public void setBService(BService bService) {
System.out.println("注入BService到A");
this.bService = bService;
}
}
@Component
public class BService {
private AService aService;
@Autowired
public void setAService(AService aService) {
System.out.println("注入AService到B");
this.aService = aService;
}
}
启动上下文,观察日志顺序:
java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class CircularDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example");
context.close();
}
}
输出可能类似:
注入BService到A
注入AService到B
关键解析 :这个顺序表明,在填充A的bService属性时,Spring开始创建B,然后在B的属性填充中获得A的"早期引用"。
边界与常见错误 :如果你将注入改成构造器注入,容器启动会抛出BeanCurrentlyInCreationException。下面的示例展示了如何用@Lazy来解决。
示例6:使用@Lazy打破构造器循环
java
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
public class ServiceA {
private ServiceB serviceB;
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Component
public class ServiceB {
private ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
如果去掉@Lazy启动会报错。加上后正常。但如果运行时真的调用了serviceB的方法,代理会去获取真正的Bean,到时候可能再次产生创建问题?实际上因为有缓存,代理会触发创建,所以能成功。
关键解析 :@Lazy注入的是代理对象,Spring会为它生成一个代理,该代理在首次访问目标对象时才触发创建。这种方式能解决构造器循环,但代价是延迟了依赖的初始化,可能影响启动时检查。
8. 可见误区:这些情况你以为Bean会被创建和销毁
误区1:给prototype作用域的Bean写销毁方法,以为容器关闭时会调用。 原型Bean容器不管理其生命周期,除非你手动获得Bean并调用销毁方法。
误区2:使用了@PostConstruct就一定会在依赖注入后执行。 是的,但如果你在一个修改的Bean上(如从@PostConstruct返回了代理),后处理器可能跳过初始化,导致@PostConstruct不执行。
误区3:Bean的构造器会先于某些静态字段的初始化。 无关。Java类加载时静态块先于任何对象创建,但Bean的依赖注入发生在构造器之后。
9. 生产实践建议
经验1:尽量使用构造器注入,它能使依赖不可变,且能避免循环依赖,更容易测试。如果必须用字段或setter注入,确保没有构造器循环。
经验2:合理利用@PostConstruct和@PreDestroy,因为它们不依赖Spring接口,在其他容器(比如CDI)中也可复用。
经验3:在分布式场景或微服务中,谨慎使用通过Bean初始化的资源连接。 如果连接不可用,会导致Bean创建失败,容器无法启动。建议使用懒初始化或重试机制。
经验4:调试循环依赖时,开启DEBUG日志 ,可以看到Creating shared instance of singleton bean等信息。
10. 排障清单:常见的Bean生命周期异常
下表列出几个典型异常及其原因:
| 异常信息 | 可能原因 | 排查方向 |
|---|---|---|
BeanCreationException: Error creating bean with name 'xxx' |
构造器异常、依赖注入失败、初始化方法抛错 | 查看堆栈定位具体Bean和行 |
BeanCurrentlyInCreationException |
构造器循环依赖或不可解决的循环 | 检查是否用setter注入,或使用@Lazy |
DefaultListableBeanFactory 找不到Bean |
没有被扫描或没有注册 | 检查@ComponentScan路径和BeanDefinition是否注册 |
| 销毁方法没有被调用 | 作用域不是singleton;或Bean代理导致没走销毁回调 | 检查作用域和代理方式 |
11. 面试/复盘问题
- 请描述Spring Bean从定义到初始化的完整流程,包括各个扩展点。
@PostConstruct、InitializingBean、init-method三种方式的执行顺序是什么?为什么?- 为什么构造器注入无法解决循环依赖?Spring的三级缓存是如何工作的?
- 单例Bean的销毁回调一定会在容器关闭时执行吗?原型Bean呢?
- 如果我想在Bean实例化前直接返回代理对象,应该实现哪个接口?
12. 总结与全局框架图
现在,我们用一个简化的时序图来总结整个生命周期,它同时也是我们上面所有细节的浓缩:
text
BeanDefinition注册
|
v
实例化 (构造器)
|
v
属性填充 (依赖注入)
|
v
初始化前 *扩展点 (postProcessBeforeInitialization)
|
v
初始化 (@PostConstruct -> InitializingBean -> init-method)
|
v
初始化后 *扩展点 (postProcessAfterInitialization) AOP在此
|
v
Bean就绪 (放入单例池)
|
v
容器关闭 -> 销毁前 @PreDestroy -> DisposableBean.destroy -> destroy-method
把握这个主线,遇到任何Spring Bean初始化问题,你都可以按这个顺序去推断:配置文件是否正确?是否到了初始化时机?有没有后处理器干扰?销毁是否被容器管理?
13. 参考资料
- Spring Framework Reference Documentation
- Spring Bean生命周期源码:AbstractAutowireCapableBeanFactory
- JSR 250: Common Annotations
(本文示例基于Spring Framework 5.x,文中未特别说明的源码位置可查阅上述文档。)
附录:完整示例清单
本文包含可运行的完整示例6个:
BeanDefinitionDemo.java(示例1)ProxyPostProcessor.java(示例2,需配合配置注册)LifecycleDemo.java(示例3,初始化顺序)DestroyDemo.java(示例4,销毁顺序)CircularDemo.java(示例5,setter循环)- 示例6 (构造器循环+@Lazy,可独立运行)
运行方法:创建Maven项目,引入spring-context依赖,将代码复制至对应包,确保类路径正确,即可运行main方法。
注意:如果使用JDK9+,需要增加模块开放;使用Spring Boot则直接添加spring-boot-starter。
至此,从BeanDefinition到销毁的旅程全部结束,希望你能在遇到Bean相关问题时,回想起这篇文章的顺序。