Bean生命周期概述
本章定位:Spring Core 第03章导学文档。理解 Bean 生命周期是掌握 Spring 容器运作机制的核心门槛——只有清楚知道一个对象从"出生"到"死亡"的完整历程,才能在正确的时机插入初始化逻辑、释放资源、甚至替换掉整个 Bean 实例。
定义与作用
Bean 生命周期(Bean Lifecycle) 指 Spring IoC 容器对一个 Bean 实例从创建到销毁所经历的完整阶段。容器并非简单地 new 一个对象就完事,而是在特定节点预留了扩展钩子,让开发者能够介入并定制行为。
掌握生命周期的意义在于:
- 初始化阶段:在 Bean 可用前完成资源准备(数据库连接池预热、缓存加载、配置校验)
- 运行阶段:确保 Bean 以正确状态参与业务逻辑
- 销毁阶段:在容器关闭时优雅释放资源(关闭连接、清理临时文件、注销服务),避免内存泄漏
完整生命周期流程
Spring 容器管理 Bean 的完整生命周期可分为八大阶段。以下流程基于 Spring 5.x / Spring Boot 2.x 的核心容器行为:
各阶段详解
| 阶段 | 核心动作 | 开发者可介入点 |
|---|---|---|
| 实例化 | 容器通过反射调用构造方法创建原始对象 | 构造器参数注入、工厂方法 |
| 属性填充 | 将配置值和依赖引用注入到 Bean 属性中 | 无直接钩子,依赖 @Autowired 等机制 |
| Aware 回调 | 向 Bean 注入容器级资源(名称、工厂、上下文) | 实现 BeanNameAware / ApplicationContextAware 等 |
| BPP 前处理 | 在初始化前对 Bean 实例进行加工 | 实现 BeanPostProcessor.postProcessBeforeInitialization() |
| 初始化 | 执行自定义初始化逻辑 | @PostConstruct / InitializingBean / init-method |
| BPP 后处理 | 在初始化后对 Bean 实例进行加工,AOP 代理在此生成 | 实现 BeanPostProcessor.postProcessAfterInitialization() |
| 使用 | Bean 处于就绪状态,响应业务调用 | 日常开发主要在此阶段工作 |
| 销毁 | 容器关闭时释放资源 | @PreDestroy / DisposableBean / destroy-method |
生命周期回调的三种方式
Spring 为初始化和销毁分别提供了三种回调机制,形成一条从侵入性强到解耦的演进路线:
| 方式 | 初始化 | 销毁 | 侵入性 | 推荐度 |
|---|---|---|---|---|
| JSR-250 注解 | @PostConstruct | @PreDestroy | 无,标准注解 | ⭐⭐⭐ 首选 |
| Spring 接口 | InitializingBean | DisposableBean | 强,耦合 Spring API | ⭐⭐ 不推荐 |
| 配置方法 | init-method | destroy-method | 无,纯配置 | ⭐⭐⭐ 灵活场景 |
团队规范建议:飞翔科技技术部在白歌架构师的推动下,统一使用
@PostConstruct和@PreDestroy作为生命周期回调标准。理由是与 Spring 解耦、代码语义清晰、且被 Jakarta EE 生态广泛支持。
Bean 作用域
Bean Scope(作用域) 定义了 Spring 创建 Bean 实例的数量和可见范围。生命周期与作用域紧密相关——不同作用域的 Bean,其"从生到死"的管辖权归属不同。
| 作用域 | 说明 | 生命周期管辖 | 适用场景 |
|---|---|---|---|
| singleton | 默认作用域,每个 Spring 容器只有一个实例 | 容器全程管理(创建 → 销毁) | 无状态服务、配置类、工具类 |
| prototype | 每次请求都创建新实例 | 容器仅负责创建,不管理销毁 | 有状态对象、多线程独立上下文 |
| request | 每个 HTTP 请求一个实例(Web 环境) | 请求开始创建,请求结束销毁 | 请求级数据封装 |
| session | 每个 HTTP Session 一个实例(Web 环境) | Session 开始创建,Session 失效销毁 | 用户登录状态 |
| application | 每个 ServletContext 一个实例(Web 环境) | 应用启动创建,应用关闭销毁 | 应用级全局配置 |
| websocket | 每个 WebSocket 会话一个实例 | 连接建立创建,连接关闭销毁 | 长连接会话状态 |
关键区别:
singleton和prototype在 Core Container 中直接可用;request、session、application、websocket需要在 Web-aware ApplicationContext 中使用(如 Spring MVC / Spring WebFlux 环境)。
singleton 与 prototype 的生命周期差异
完整示例:飞翔科技学生管理系统的数据源初始化
场景简述
飞翔科技(广州)为某 985 高校开发学生管理系统。CTO 大翔要求系统启动时自动初始化 HikariCP 连接池,并在应用关闭时优雅释放连接。后端开发小崔负责实现这一需求。
操作前:无生命周期管理的代码
在没有生命周期回调时,小崔只能在业务方法中手动管理连接池状态,导致首次请求延迟高,且容器关闭时连接泄漏:
@Service
public class StudentService {
private HikariDataSource dataSource;
// 问题1:每次调用都要检查连接池是否创建,首次请求慢
// 问题2:容器关闭时连接池无人关闭,造成连接泄漏
public List<Student> listAllStudents() {
if (dataSource == null) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db");
config.setUsername("root");
config.setPassword("secret");
dataSource = new HikariDataSource(config);
}
// ... 查询逻辑
return new ArrayList<>();
}
}
运行结果:系统启动后连接池未就绪,第一次查询耗时 800ms 以上;Tomcat 热部署时旧连接池的连接未被释放,MySQL 端出现大量 Sleep 状态连接。
操作后:使用生命周期回调的完整代码
小崔重构代码,将连接池封装为独立 Bean,利用 Spring 生命周期在正确时机执行初始化和销毁:
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
public class StudentDataSource {
private HikariDataSource dataSource;
/**
* 阶段五:初始化
* 依赖注入完成后,立即预热连接池
*/
@PostConstruct
public void init() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db");
config.setUsername("root");
config.setPassword("secret");
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
config.setConnectionTimeout(3000);
// 预热:启动时即创建最小空闲连接
config.setInitializationFailTimeout(5000);
this.dataSource = new HikariDataSource(config);
System.out.println("[StudentDataSource] 连接池初始化完成,最小空闲连接:5");
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 阶段八:销毁
* 容器关闭前,优雅关闭连接池
*/
@PreDestroy
public void cleanup() {
if (dataSource != null && !dataSource.isClosed()) {
System.out.println("[StudentDataSource] 正在关闭连接池,当前活跃连接:"
+ dataSource.getHikariPoolMXBean().getActiveConnections());
dataSource.close();
System.out.println("[StudentDataSource] 连接池已关闭");
}
}
}
Spring Boot 启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class StudentManagementApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(StudentManagementApplication.class, args);
// 模拟业务运行
StudentDataSource ds = context.getBean(StudentDataSource.class);
System.out.println("[Main] 获取到 DataSource Bean,准备执行业务...");
// 模拟系统关闭
context.close();
}
}
运行结果及分析:
[StudentDataSource] 连接池初始化完成,最小空闲连接:5
[Main] 获取到 DataSource Bean,准备执行业务...
[StudentDataSource] 正在关闭连接池,当前活跃连接:0
[StudentDataSource] 连接池已关闭
- 实例化 + 属性填充:Spring 容器通过无参构造器创建
StudentDataSource实例 - 初始化:
@PostConstruct标注的init()在依赖注入完成后立即执行,连接池在业务请求到达前已就绪 - 使用:
StudentService等业务 Bean 通过注入获取已就绪的连接池 - 销毁:
context.close()触发@PreDestroy标注的cleanup(),连接池在 JVM 退出前被显式关闭,无连接泄漏
生命周期扩展点全景图
以下大图展示了 Spring 容器中所有主要扩展接口在生命周期中的位置,帮助你在团队 Code Review 时快速判断一段初始化代码是否放在了正确的位置:
易错场景与面试考点
易错场景一:prototype 作用域 Bean 的销毁回调不执行
小崔曾误以为 @PreDestroy 对所有作用域都有效,将报表生成器配置为 prototype:
@Component
@Scope("prototype")
public class ReportGenerator {
@PreDestroy
public void cleanup() {
System.out.println("清理临时文件...");
}
}
问题:prototype Bean 创建后容器不再持有其引用,cleanup() 永远不会被调用。临时文件清理必须在使用方手动触发,或改用 DisposableBean 配合自定义作用域管理。
易错场景二:在构造器中调用依赖对象
@Service
public class StudentService {
private final StudentDao studentDao;
public StudentService(StudentDao studentDao) {
this.studentDao = studentDao;
// 错误!此时 studentDao 可能尚未完全初始化
this.studentDao.warmCache();
}
}
正确做法:将 warmCache() 移到 @PostConstruct 方法中,确保所有依赖已完成注入且可能的其他初始化回调已执行。
面试高频题
Q1:Spring Bean 的完整生命周期是怎样的?
实例化 → 属性填充 → Aware 接口回调 → BeanPostProcessor.beforeInit → 初始化(@PostConstruct / InitializingBean / init-method)→ BeanPostProcessor.afterInit(AOP 代理生成)→ 业务使用 → 销毁(@PreDestroy / DisposableBean / destroy-method)。
Q2:@PostConstruct 和 InitializingBean 有什么区别?
@PostConstruct是 JSR-250 标准注解,与 Spring 解耦,推荐优先使用;InitializingBean是 Spring 专有接口,侵入性强,代码直接依赖 Spring API。两者执行时机相同,都在依赖注入完成后、Bean 就绪前触发。
Q3:为什么同类内部方法调用 AOP 不生效?
因为
this.method()调用的是目标对象本身,而非代理对象。AOP 代理是在BeanPostProcessor.postProcessAfterInitialization()阶段生成的,内部调用绕过了代理层。解决方案:注入自身代理、使用AopContext.currentProxy()、或重构避免内部调用。
本章内容导航
本章后续文档将逐一深入讲解每个生命周期扩展点:
- @PostConstruct — JSR-250 初始化注解(推荐首选)
- @PreDestroy — JSR-250 销毁注解(推荐首选)
- InitializingBean — Spring 初始化接口(了解即可,不推荐新项目使用)
- DisposableBean — Spring 销毁接口(了解即可,不推荐新项目使用)
- BeanPostProcessor — Bean 级后置处理器(AOP 代理、日志、权限的核心机制)
- BeanFactoryPostProcessor — 容器级后置处理器(动态修改配置元数据)
引出下一章:AOP
当你理解了 BeanPostProcessor.postProcessAfterInitialization() 这个阶段,你就已经站在了 AOP 的门口——Spring AOP 正是通过 AbstractAutoProxyCreator(一个特殊的 BeanPostProcessor)在初始化后阶段为目标 Bean 生成代理对象,从而在不修改源码的情况下插入日志、事务、权限等横切逻辑。
下一章我们将以"切面"为手术刀,解剖 Spring AOP 的代理机制、五种 Advice 类型以及 AspectJ 注解开发模式。大翔已经在架构评审会上拍桌子了:"没有 AOP 的日志系统,我不要!"