Spring Bean的生命周期

85

Spring Bean生命周期:从诞生到消亡的那些事儿

两年前做电商项目时,我曾栽过一个特别"隐蔽"的坑:订单服务里的支付回调处理器,偶尔会出现数据库连接为空的情况。排查了三天,日志翻了无数遍,最后才发现是Bean的初始化顺序出了问题——数据源Bean还没创建完成,支付回调处理器的初始化方法就已经执行了,导致依赖注入失败。那是我第一次真切感受到:不搞懂Spring Bean的生命周期,写Spring项目就像在"盲人摸象"。

很多程序员刚学Spring时,都觉得"把类加个@Service或@Component注解,Spring就会自动管理Bean",这其实只看到了表面。Bean从被Spring"发现",到初始化、完成依赖注入,再到最终销毁,整个过程藏着一套严密的流程。今天就结合我的开发经历,把Bean生命周期掰开揉碎了讲,再聊聊那些年踩过的生命周期相关的坑。

一、先搞懂核心:Bean生命周期的"十二步心法"

不少资料把Bean生命周期讲得太复杂,动辄十几二十步,反而让人抓不住重点。其实站在开发视角,核心流程就十二步,我画了个简化版的流程图,先有个整体认知:

为Bean分配内存

注入依赖

告知Bean的ID

注入BeanFactory

注入ApplicationContext

初始化前增强

自定义初始化逻辑

官方初始化接口

配置文件指定的初始化

初始化后增强

容器关闭时

1.实例化Bean

2.设置Bean的属性值

3.调用BeanNameAware的setBeanName方法

4.调用BeanFactoryAware的setBeanFactory方法

5.调用ApplicationContextAware的setApplicationContext方法

6.调用BeanPostProcessor的postProcessBeforeInitialization方法

7.调用@PostConstruct注解的方法

8.调用InitializingBean的afterPropertiesSet方法

9.调用自定义的init-method方法

10.调用BeanPostProcessor的postProcessAfterInitialization方法

11.Bean就绪使用

12.销毁阶段(@PreDestroy、DisposableBean、destroy-method)

这十二步里,有几个关键节点是开发中最常接触的,也是最容易出问题的。下面结合我做过的用户服务案例,逐个拆解核心步骤。

关键节点1:实例化与属性设置——Bean的"诞生第一步"

实例化(第一步)和属性设置(第二步)是Bean生命周期的起点,对应我们写Java代码时"new对象"和"给对象的属性赋值"两个操作。但Spring的实例化有个特殊点:它是通过反射实现的,而且实例化时只分配内存,此时Bean的属性还是默认值(比如String为null,int为0)。

我之前在用户服务里定义过一个UserService,代码大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service public class UserService { // 依赖的Dao层Bean @Autowired private UserDao userDao; // 自定义属性 private String serviceName; // setter方法,用于Spring设置属性 public void setServiceName(String serviceName) { this.serviceName = serviceName; } }

Spring首先会通过反射调用UserService的无参构造器,创建一个UserService实例(实例化),此时userDao是null,serviceName也是null。然后Spring会通过setter方法把配置文件里的serviceName值传进去,同时把UserDao的实例注入到userDao属性中(属性设置)。这里有个小细节:如果UserService没有无参构造器,且没有显式定义有参构造器,Spring实例化时会直接报错——这个坑我在初学的时候踩过,当时为了传参加了个有参构造器,结果忘了保留无参的,启动项目直接报"No default constructor found"。

关键节点2:初始化阶段——Bean的"成长修炼"

初始化阶段是Bean生命周期的核心,从第三步到第九步都属于这个阶段,主要做三件事:实现Aware接口获取Spring容器信息、通过BeanPostProcessor做增强、执行自定义初始化逻辑。这部分也是开发中最常自定义的地方。

比如我曾在项目中需要获取当前Bean的名称,就实现了BeanNameAware接口:

1
2
3
4
5
6
7
8
9
10
11
@Service public class UserService implements BeanNameAware { private String beanName; // 实现BeanNameAware接口的方法,Spring会自动传入Bean的名称 @Override public void setBeanName(String name) { this.beanName = name; System.out.println("当前Bean的名称:" + beanName); } }

启动项目后,控制台会输出"当前Bean的名称:userService",这就是Spring在第三步自动调用setBeanName方法的效果。同理,如果需要获取Spring容器对象,就实现ApplicationContextAware接口,Spring会在第五步注入ApplicationContext。

而初始化逻辑的执行顺序,我曾做过一次实测:在同一个Bean里同时用@PostConstruct注解、实现InitializingBean接口和配置init-method,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service // 配置init-method @Bean(initMethod = "customInit") public class UserService implements InitializingBean { // @PostConstruct注解的方法 @PostConstruct public void postConstructInit() { System.out.println("1.@PostConstruct注解的方法执行"); } // 实现InitializingBean接口的方法 @Override public void afterPropertiesSet() throws Exception { System.out.println("2.InitializingBean的afterPropertiesSet方法执行"); } // 自定义的init-method方法 public void customInit() { System.out.println("3.init-method指定的方法执行"); } }

启动后控制台的输出顺序和代码里标注的一致,这说明Spring执行初始化逻辑的优先级是:@PostConstruct > InitializingBean > init-method。这个顺序很重要,比如我们需要在初始化时用到注入的属性,就可以把逻辑写在@PostConstruct里,确保属性已经设置完成。

关键节点3:销毁阶段——Bean的"优雅落幕"

当Spring容器关闭时,Bean就会进入销毁阶段,对应第十二步。销毁阶段主要用于释放资源,比如关闭数据库连接、释放文件流等。和初始化类似,销毁逻辑也有三种实现方式:@PreDestroy注解、实现DisposableBean接口和配置destroy-method,执行优先级是@PreDestroy > DisposableBean > destroy-method。

我在之前的文件处理服务里,就用@PreDestroy注解释放过文件流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service public class FileService { private FileInputStream fis; // 初始化时打开文件流 @PostConstruct public void initFileStream() throws FileNotFoundException { fis = new FileInputStream("data.txt"); System.out.println("文件流打开成功"); } // 销毁时关闭文件流 @PreDestroy public void closeFileStream() throws IOException { if (fis != null) { fis.close(); System.out.println("文件流关闭成功"); } } }

当容器正常关闭时,会自动执行@PreDestroy注解的方法,确保文件流被正确关闭,避免资源泄露。如果是粗暴关闭容器(比如kill进程),销毁方法可能不会执行,这点在生产环境要特别注意。

二、踩坑复盘:那些栽在生命周期上的坑

讲完了理论,再聊聊我这些年踩过的生命周期相关的坑,每个坑都对应一个真实的生产问题,希望能帮大家避坑。

坑1:Bean初始化顺序混乱,依赖注入失败

这就是我开头提到的订单服务问题:支付回调处理器(PayCallbackService)依赖数据源(DataSource),但PayCallbackService的初始化方法先执行了,导致获取数据源时为空。代码大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 数据源Bean,通过@Configuration配置 @Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { // 初始化数据源,耗时较长 return new DruidDataSource(...); } } // 支付回调处理器 @Service public class PayCallbackService { @Autowired private DataSource dataSource; @PostConstruct public void init() { // 初始化时使用数据源,偶尔会报空指针 Connection conn = dataSource.getConnection(); } }

排查后发现,Spring默认是按Bean的名称字母顺序初始化的,PayCallbackService的首字母是P,DataSource的首字母是D,按理说DataSource会先初始化。但因为DataSource的初始化耗时较长,偶尔会出现PayCallbackService先完成实例化并执行@PostConstruct方法的情况。

解决方案有两个:一是用@DependsOn注解明确指定依赖关系,强制Spring先初始化DataSource;二是把初始化逻辑里使用数据源的代码,放到实现InitializingBean接口的afterPropertiesSet方法里,虽然执行时机和@PostConstruct差不多,但稳定性更高。我当时用了@DependsOn注解,问题就解决了:

1
2
3
4
5
6
// 明确指定依赖dataSource Bean @Service @DependsOn("dataSource") public class PayCallbackService { // 省略其他代码... }

坑2:单例Bean的初始化只执行一次,动态属性不生效

Spring默认的Bean作用域是单例,也就是说Bean只会被实例化和初始化一次。我曾在项目中犯过一个错:在单例的UserService里,把用户的登录状态存到了成员变量里,结果导致后续登录的用户拿到了上一个用户的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service // 单例Bean(默认) public class UserService { // 错误:把用户状态存到单例Bean的成员变量里 private UserLoginVO loginUser; public void setLoginUser(UserLoginVO user) { this.loginUser = user; } public UserLoginVO getLoginUser() { return loginUser; } }

这就是因为单例Bean的生命周期和容器一致,成员变量会被所有请求共享。解决方案很简单:如果需要存储线程相关的变量,用ThreadLocal;如果需要每次请求都创建新的Bean,就把作用域改成prototype:

1
2
3
4
5
6
7
@Service // 原型作用域,每次请求都会创建新的Bean @Scope("prototype") public class UserService { private UserLoginVO loginUser; // 省略其他代码... }

坑3:BeanPostProcessor使用不当,导致所有Bean初始化失败

BeanPostProcessor是Spring的"增强器",能对所有Bean的初始化前后进行处理。但我曾因为在BeanPostProcessor里抛出了异常,导致整个容器的Bean都初始化失败。当时的代码是这样的:

1
2
3
4
5
6
7
8
9
@Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // 错误:没有判断Bean类型,直接强转 UserService userService = (UserService) bean; return bean; } }

因为Spring会对所有Bean执行这个方法,当遇到非UserService类型的Bean时,强转就会抛出ClassCastException,导致容器启动失败。正确的做法是先判断Bean的类型,再进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
@Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // 先判断Bean类型 if (bean instanceof UserService) { UserService userService = (UserService) bean; // 执行增强逻辑 } return bean; } }

三、总结:理解生命周期的核心价值

看到这里,可能有同学会问:搞这么清楚Bean的生命周期,到底有什么用?其实答案很简单:当你需要自定义Bean的创建逻辑、解决依赖注入问题、排查Bean相关的异常时,生命周期就是你的"解题钥匙"。

比如你想在Bean初始化前统一给属性加解密,就用BeanPostProcessor;想在Bean销毁时释放资源,就用@PreDestroy;想控制Bean的创建顺序,就用@DependsOn。这些实际开发中常用的技巧,都建立在对生命周期的理解之上。

最后给大家一个小建议:如果对某个生命周期节点的执行时机不确定,最好的方式就是写个测试类实测一下,就像我之前测试初始化逻辑顺序那样。亲手验证过的知识,比死记硬背要牢固得多。

Bean的生命周期看似复杂,但只要抓住"实例化-初始化-销毁"这三大阶段,再结合实际开发中的案例和坑点去理解,很快就能掌握。希望这篇带着我个人踩坑经历的分享,能帮大家真正搞懂Bean的生命周期,让你在写Spring项目时更有底气。

(注:文档部分内容可能由 AI 生成)

目录