Note 03. Bean 的生命周期:从创建到销毁的完整旅程
摘要:在前两章中,我们学习了如何将对象注册为 Bean,以及 Spring 如何创建这些对象。但故事并没有结束——Bean 被创建出来之后,还要经历属性注入、初始化回调、使用、销毁回调等一系列阶段。理解这个完整的生命周期,能帮助我们在正确的时机执行正确的操作,避免很多诡异的 Bug。本章将通过一个真实的 Bug 案例引入,带你彻底搞懂 Bean 的生命周期。
本章学习路径
- 问题驱动:从一个 “@Autowired 字段为 null” 的 Bug 出发,理解为什么要学习生命周期。
- 全景认知:掌握 Bean 生命周期的四个阶段,建立完整的心智模型。
- 作用域理解:搞清 singleton 和 prototype 的区别,以及 “单例注入多例” 的经典陷阱。
- 回调机制:学会使用 @PostConstruct 和 @PreDestroy 在正确的时机执行初始化和清理逻辑。
- 原理窥探:了解 BeanPostProcessor 的作用,理解 AOP 等高级功能的实现基础。
3.1. 为什么要理解 Bean 生命周期
“Bean 生命周期” 听起来像是一个很理论的话题。但实际上,不理解它会让你在开发中踩很多坑。让我们从一个真实的 Bug 开始。
3.1.1. 一个真实的 Bug:@Autowired 的字段为什么是 null
场景描述
小明写了一个 OrderService,想在构造函数中调用 UserService 的方法来初始化一些数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Service public class OrderService {
@Autowired private UserService userService;
private List<String> vipUserNames;
public OrderService() { System.out.println("OrderService 构造函数执行"); System.out.println("userService = " + userService);
this.vipUserNames = userService.getVipUserNames(); } }
|
1 2 3 4 5 6 7
| @Service public class UserService {
public List<String> getVipUserNames() { return List.of("张三", "李四", "王五"); } }
|
运行结果
1 2 3
| OrderService 构造函数执行 userService = null java.lang.NullPointerException: Cannot invoke method on null object
|
小明的困惑
“我明明加了 @Autowired 注解啊,为什么 userService 是 null?Spring 不是应该自动注入吗?”
问题的根源:生命周期顺序
这个 Bug 的根源在于:构造函数执行时,依赖注入还没有发生。
让我们看看 Spring 创建 OrderService 的真实顺序:

正确的写法
如果需要在 Bean 创建后执行初始化逻辑,应该使用 @PostConstruct 注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Service public class OrderService {
@Autowired private UserService userService;
private List<String> vipUserNames;
public OrderService() { System.out.println("OrderService 构造函数执行"); }
@PostConstruct public void init() { System.out.println("@PostConstruct 方法执行"); System.out.println("userService = " + userService); this.vipUserNames = userService.getVipUserNames(); } }
|
运行结果
1 2 3
| OrderService 构造函数执行 @PostConstruct 方法执行 userService = com.example.demo.service.UserService@1a2b3c4d
|
或者使用构造器注入(更推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Service public class OrderService {
private final UserService userService; private final List<String> vipUserNames;
public OrderService(UserService userService) { this.userService = userService; System.out.println("userService = " + userService); this.vipUserNames = userService.getVipUserNames(); } }
|
这个 Bug 告诉我们什么?
理解 Bean 的生命周期,就是理解 Spring 在什么时机做什么事情。只有知道了这个顺序,才能在正确的地方写正确的代码。
3.1.2. 生命周期的全景图:四个阶段一张图说清
Spring Bean 的生命周期可以分为 四个主要阶段:


四个阶段详解
| 阶段 | 做什么 | 关键时机 |
|---|
| 实例化 | 调用构造函数,创建对象 | 此时对象是 “空壳”,@Autowired 字段还是 null |
| 属性填充 | 注入依赖(@Autowired、@Value 等) | 此时依赖已经可用 |
| 初始化 | 执行初始化回调(@PostConstruct 等) | 适合执行 “依赖已就绪后” 的初始化逻辑 |
| 销毁 | 执行销毁回调(@PreDestroy 等) | 适合释放资源、关闭连接等清理工作 |
一个完整的生命周期演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Service public class LifecycleDemoService {
@Autowired private UserService userService;
public LifecycleDemoService() { System.out.println("1️⃣ [实例化] 构造函数执行"); System.out.println(" 此时 userService = " + userService); }
@PostConstruct public void init() { System.out.println("3️⃣ [初始化] @PostConstruct 方法执行"); System.out.println(" 此时 userService = " + userService); }
public void doSomething() { System.out.println("🔄 [使用中] 业务方法被调用"); }
@PreDestroy public void cleanup() { System.out.println("4️⃣ [销毁] @PreDestroy 方法执行"); System.out.println(" 执行清理工作..."); } }
|
启动并关闭应用后的输出
1 2 3 4 5 6 7 8 9
| 1️⃣ [实例化] 构造函数执行 此时 userService = null 3️⃣ [初始化] @PostConstruct 方法执行 此时 userService = com.example.demo.service.UserService@1a2b3c4d
... 应用运行中 ...
4️⃣ [销毁] @PreDestroy 方法执行 执行清理工作...
|
3.1.3. 生命周期与作用域的关系
在深入学习生命周期的各个阶段之前,我们需要先理解一个相关的概念:作用域(Scope)。
作用域决定了:
- 创建几个实例:是所有地方共享一个,还是每次都创建新的
- 生命周期有多长:是跟随容器存活,还是用完就销毁
作用域会影响生命周期的行为,我们将在下一节详细讨论。
3.2. 作用域:决定 Bean “活多久、有几个”
作用域是 Spring 管理 Bean 的一个重要维度。不同的作用域决定了 Bean 实例的创建策略和生命周期长度。
3.2.1. singleton:默认的单例模式
什么是单例作用域
singleton 是 Spring 的 默认作用域。在这种模式下:
- 整个 Spring 容器中,只有一个实例
- 所有注入这个 Bean 的地方,拿到的都是 同一个对象
- Bean 在 容器启动时创建,在 容器关闭时销毁
代码验证
1 2 3 4 5 6 7
| @Service public class SingletonService {
public SingletonService() { System.out.println("SingletonService 被创建,hashCode: " + this.hashCode()); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @RestController @RequestMapping("/test") public class TestController {
@Autowired private SingletonService service1;
@Autowired private SingletonService service2;
@GetMapping("/singleton") public String testSingleton() { return String.format( "service1.hashCode = %d\nservice2.hashCode = %d\n是否同一个对象: %s", service1.hashCode(), service2.hashCode(), service1 == service2 ); } }
|
访问 /test/singleton 的输出
1 2 3
| service1.hashCode = 123456789 service2.hashCode = 123456789 是否同一个对象: true
|
单例的优点
- 节省资源:只创建一个实例,减少内存占用和创建开销
- 状态共享:可以在 Bean 中维护共享状态(但要注意线程安全)
- 生命周期可控:Spring 负责创建和销毁,可以使用 @PostConstruct 和 @PreDestroy
单例的注意事项:线程安全
因为单例 Bean 会被多个线程共享,所以 不要在单例 Bean 中存储可变的状态:
1 2 3 4 5 6 7 8 9 10
| @Service public class CounterService {
private int count = 0;
public int increment() { return ++count; } }
|
如果确实需要在单例中维护状态,要使用线程安全的方式:
1 2 3 4 5 6 7 8 9 10
| @Service public class CounterService {
private final AtomicInteger count = new AtomicInteger(0);
public int increment() { return count.incrementAndGet(); } }
|
最佳实践:单例 Bean 应该是 无状态的,只包含方法逻辑,不存储请求相关的数据。
3.2.2. prototype:每次都是新实例
什么是多例作用域
prototype(原型)作用域与 singleton 相反:
- 每次获取都创建新实例
- 每个注入点拿到的都是 不同的对象
- Spring 只负责创建,不负责销毁
如何指定 prototype 作用域
1 2 3 4 5 6 7 8
| @Service @Scope("prototype") public class PrototypeService {
public PrototypeService() { System.out.println("PrototypeService 被创建,hashCode: " + this.hashCode()); } }
|
代码验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @RestController @RequestMapping("/test") public class TestController {
@Autowired private ApplicationContext context;
@GetMapping("/prototype") public String testPrototype() { PrototypeService service1 = context.getBean(PrototypeService.class); PrototypeService service2 = context.getBean(PrototypeService.class);
return String.format( "service1.hashCode = %d\nservice2.hashCode = %d\n是否同一个对象: %s", service1.hashCode(), service2.hashCode(), service1 == service2 ); } }
|
访问 /test/prototype 的输出
1 2 3 4 5
| PrototypeService 被创建,hashCode: 111111111 PrototypeService 被创建,hashCode: 222222222 service1.hashCode = 111111111 service2.hashCode = 222222222 是否同一个对象: false
|
prototype 的使用场景
| 场景 | 说明 |
|---|
| 有状态的 Bean | 每个使用者需要独立的状态 |
| 非线程安全的对象 | 如 SimpleDateFormat、StringBuilder |
| 需要每次都是新实例的场景 | 如生成唯一 ID 的对象 |
prototype 的重要特性:Spring 不管理销毁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Service @Scope("prototype") public class PrototypeService {
@PostConstruct public void init() { System.out.println("✅ @PostConstruct 会执行"); }
@PreDestroy public void cleanup() { System.out.println("❌ @PreDestroy 不会执行!"); } }
|
这意味着:如果 prototype Bean 持有需要释放的资源(如数据库连接),你需要 自己负责清理。
3.2.3. 单例中注入多例的陷阱与解决方案
这是一个 经典的面试题,也是实际开发中容易踩的坑。
问题场景
假设我们有一个 prototype 的 RequestContext(用于存储当前请求的上下文信息),想在 singleton 的 OrderService 中使用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Component @Scope("prototype") public class RequestContext {
private String requestId;
public RequestContext() { this.requestId = UUID.randomUUID().toString(); System.out.println("创建 RequestContext: " + requestId); }
public String getRequestId() { return requestId; } }
|
1 2 3 4 5 6 7 8 9 10
| @Service public class OrderService {
@Autowired private RequestContext requestContext;
public String createOrder() { return "订单创建成功,请求ID: " + requestContext.getRequestId(); } }
|
测试代码
1 2 3 4 5 6 7 8 9 10 11 12
| @RestController @RequestMapping("/order") public class OrderController {
@Autowired private OrderService orderService;
@GetMapping("/create") public String create() { return orderService.createOrder(); } }
|
多次访问 /order/create 的输出
1 2 3
| 第一次访问: 订单创建成功,请求ID: abc-123 第二次访问: 订单创建成功,请求ID: abc-123 // 还是同一个! 第三次访问: 订单创建成功,请求ID: abc-123 // 还是同一个!
|
问题分析
虽然 RequestContext 是 prototype 作用域,但它被注入到 singleton 的 OrderService 中。OrderService 只会被创建一次,所以它的 requestContext 字段也只会被注入一次。之后每次调用 createOrder(),用的都是同一个 RequestContext 实例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| graph TD A["Spring 容器启动"] --> B["创建 OrderService(singleton)"] B --> C["注入 RequestContext(此时创建一个 prototype 实例)"] C --> D["OrderService 持有这个 RequestContext 引用"]
E["第一次请求"] --> F["调用 orderService.createOrder()"] F --> G["使用的是启动时注入的那个 RequestContext"]
H["第二次请求"] --> I["调用 orderService.createOrder()"] I --> J["还是使用同一个 RequestContext!"]
style G fill:#ffcdd2 style J fill:#ffcdd2
|
解决方案一:使用 @Lookup 注解(推荐)
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public abstract class OrderService {
@Lookup protected abstract RequestContext getRequestContext();
public String createOrder() { RequestContext context = getRequestContext(); return "订单创建成功,请求ID: " + context.getRequestId(); } }
|
解决方案二:使用 ObjectFactory 或 ObjectProvider
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class OrderService {
@Autowired private ObjectProvider<RequestContext> requestContextProvider;
public String createOrder() { RequestContext context = requestContextProvider.getObject(); return "订单创建成功,请求ID: " + context.getRequestId(); } }
|
解决方案三:直接从 ApplicationContext 获取
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class OrderService {
@Autowired private ApplicationContext applicationContext;
public String createOrder() { RequestContext context = applicationContext.getBean(RequestContext.class); return "订单创建成功,请求ID: " + context.getRequestId(); } }
|
三种方案对比
| 方案 | 优点 | 缺点 |
|---|
@Lookup | 代码简洁,语义清晰 | 需要抽象类或接口 |
ObjectProvider | 不需要改类结构,支持可选依赖 | 代码稍显繁琐 |
ApplicationContext | 最灵活 | 与 Spring 容器耦合,不推荐 |
推荐:优先使用 ObjectProvider,它是最通用的方案。
3.2.4. Web 作用域:request、session 的实际应用
在 Web 应用中,Spring 还提供了几种特殊的作用域,它们的生命周期与 HTTP 请求或会话绑定。
Web 作用域一览
| 作用域 | 生命周期 | 典型用途 |
|---|
request | 一次 HTTP 请求 | 存储当前请求的上下文信息 |
session | 一个用户会话 | 存储用户登录状态、购物车等 |
application | 整个 Web 应用 | 类似 singleton,但语义更明确 |
request 作用域示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Component @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) public class RequestScopedContext {
private final String requestId = UUID.randomUUID().toString(); private final long startTime = System.currentTimeMillis();
public String getRequestId() { return requestId; }
public long getElapsedTime() { return System.currentTimeMillis() - startTime; } }
|
注意 proxyMode 参数
Web 作用域的 Bean 注入到 singleton Bean 时,必须指定 proxyMode。这是因为:
- singleton Bean 在容器启动时就创建了
- 但 request 作用域的 Bean 只有在 HTTP 请求到来时才存在
- Spring 通过代理来解决这个 “时间差” 问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Service public class OrderService {
@Autowired private RequestScopedContext requestContext;
public String createOrder() { return String.format( "请求ID: %s, 已耗时: %dms", requestContext.getRequestId(), requestContext.getElapsedTime() ); } }
|
session 作用域示例:购物车
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Component @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) public class ShoppingCart {
private final List<String> items = new ArrayList<>();
public void addItem(String item) { items.add(item); }
public List<String> getItems() { return new ArrayList<>(items); }
public void clear() { items.clear(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @RestController @RequestMapping("/cart") public class CartController {
@Autowired private ShoppingCart cart;
@PostMapping("/add") public String addItem(@RequestParam String item) { cart.addItem(item); return "已添加: " + item + ",当前购物车: " + cart.getItems(); }
@GetMapping public List<String> getCart() { return cart.getItems(); }
@DeleteMapping public String clearCart() { cart.clear(); return "购物车已清空"; } }
|
Web 作用域的使用建议
| 场景 | 推荐作用域 |
|---|
| 请求级别的追踪信息(如 traceId) | request |
| 用户登录状态 | session(或使用 Spring Security) |
| 购物车 | session(简单场景)或 Redis(分布式场景) |
| 全局配置 | singleton 或 application |
3.3. 初始化回调:Bean 创建后的 “装修”
Bean 被创建并注入依赖后,往往还需要执行一些初始化逻辑,比如:
- 验证配置是否正确
- 建立数据库连接
- 预加载缓存数据
- 启动后台线程
Spring 提供了三种方式来实现初始化回调。
3.3.1. @PostConstruct:最推荐的初始化方式
@PostConstruct 是 JSR-250 规范定义的注解,Spring 对它提供了支持。它标记的方法会在 依赖注入完成后 自动调用。
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Service public class CacheService {
@Autowired private ProductRepository productRepository;
private Map<Long, Product> productCache;
@PostConstruct public void initCache() { System.out.println("正在初始化产品缓存...");
List<Product> products = productRepository.findAll(); this.productCache = products.stream() .collect(Collectors.toMap(Product::getId, Function.identity()));
System.out.println("缓存初始化完成,共加载 " + productCache.size() + " 个产品"); }
public Product getProduct(Long id) { return productCache.get(id); } }
|
@PostConstruct 的规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Service public class ExampleService {
@PostConstruct public void init() { System.out.println("初始化"); }
@PostConstruct public void initWithParam(String param) { }
@PostConstruct public String initWithReturn() { return "这个返回值会被忽略"; }
@PostConstruct public static void staticInit() { } }
|
@PostConstruct 的优点
- 标准注解:JSR-250 规范,不依赖 Spring 特定 API
- 简洁直观:一个注解搞定,不需要实现接口
- IDE 友好:大多数 IDE 都能识别并提供跳转支持
3.3.2. InitializingBean 接口:框架级别的选择
InitializingBean 是 Spring 提供的接口,实现它的 afterPropertiesSet() 方法可以达到同样的效果。
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Service public class DatabaseConnectionService implements InitializingBean {
@Value("${database.url}") private String databaseUrl;
@Value("${database.username}") private String username;
private Connection connection;
@Override public void afterPropertiesSet() throws Exception { System.out.println("InitializingBean.afterPropertiesSet() 被调用"); System.out.println("正在建立数据库连接: " + databaseUrl);
this.connection = DriverManager.getConnection(databaseUrl, username, "password");
System.out.println("数据库连接建立成功"); }
public Connection getConnection() { return connection; } }
|
InitializingBean vs @PostConstruct
| 对比维度 | @PostConstruct | InitializingBean |
|---|
| 来源 | JSR-250 标准注解 | Spring 特有接口 |
| 侵入性 | 低(只是一个注解) | 高(需要实现接口) |
| 方法名 | 自定义 | 固定为 afterPropertiesSet |
| 适用场景 | 业务代码 | 框架/基础设施代码 |
| 推荐程度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
什么时候用 InitializingBean?
一般来说,优先使用 @PostConstruct。但在以下场景中,InitializingBean 可能更合适:
- 编写框架或基础组件:让使用者明确知道这是 Spring 管理的组件
- 需要抛出受检异常:
afterPropertiesSet() 声明了 throws Exception - 团队规范要求:某些团队可能有统一的规范
3.3.3. @Bean(initMethod):配置第三方库时使用
当我们使用 @Bean 方法配置第三方库的类时,无法在类上加 @PostConstruct 注解(因为我们无法修改第三方库的源码)。这时可以使用 @Bean 的 initMethod 属性。
场景:配置一个连接池
假设我们使用一个第三方的连接池库,它有一个 init() 方法需要在创建后调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class ThirdPartyConnectionPool {
private String url; private int maxConnections;
public void setUrl(String url) { this.url = url; }
public void setMaxConnections(int maxConnections) { this.maxConnections = maxConnections; }
public void init() { System.out.println("ThirdPartyConnectionPool 正在初始化..."); System.out.println("连接地址: " + url); System.out.println("最大连接数: " + maxConnections); }
public void shutdown() { System.out.println("ThirdPartyConnectionPool 正在关闭..."); } }
|
使用 @Bean 的 initMethod 属性
1 2 3 4 5 6 7 8 9 10 11 12
| @Configuration public class ConnectionPoolConfig {
@Bean(initMethod = "init", destroyMethod = "shutdown") public ThirdPartyConnectionPool connectionPool() { ThirdPartyConnectionPool pool = new ThirdPartyConnectionPool(); pool.setUrl("jdbc:mysql://localhost:3306/mydb"); pool.setMaxConnections(10); return pool; } }
|
启动应用后的输出
1 2 3
| ThirdPartyConnectionPool 正在初始化... 连接地址: jdbc:mysql://localhost:3306/mydb 最大连接数: 10
|
initMethod 的规则
- 指定的方法必须是 无参数 的
- 方法可以是 public、protected 或 package-private
- 方法名以字符串形式指定,拼写错误只会在运行时发现
3.3.4. 三种方式的执行顺序与选择建议
如果一个 Bean 同时使用了三种初始化方式,它们的执行顺序是固定的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Service public class MultiInitService implements InitializingBean {
@PostConstruct public void postConstruct() { System.out.println("1️⃣ @PostConstruct"); }
@Override public void afterPropertiesSet() { System.out.println("2️⃣ InitializingBean.afterPropertiesSet()"); }
public void customInit() { System.out.println("3️⃣ @Bean(initMethod)"); } }
|
执行顺序
1 2 3
| 1️⃣ @PostConstruct 2️⃣ InitializingBean.afterPropertiesSet() 3️⃣ @Bean(initMethod)
|
选择建议

总结
| 方式 | 适用场景 | 推荐程度 |
|---|
@PostConstruct | 自己写的业务类 | ⭐⭐⭐⭐⭐ 首选 |
InitializingBean | 框架/基础设施代码 | ⭐⭐⭐ 特定场景 |
@Bean(initMethod) | 第三方库的类 | ⭐⭐⭐⭐ 配置第三方库时使用 |
3.4. 销毁回调:Bean 退场前的 “清理”
与初始化回调对应,Spring 也提供了销毁回调机制,让我们在 Bean 被销毁前执行清理工作,比如:
- 关闭数据库连接
- 释放文件句柄
- 停止后台线程
- 保存状态到持久化存储
3.4.1. @PreDestroy:优雅关闭资源
@PreDestroy 是 JSR-250 规范定义的注解,与 @PostConstruct 对应。它标记的方法会在 Bean 被销毁前 自动调用。
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| @Service public class FileProcessingService {
private ExecutorService executorService; private FileWriter logWriter;
@PostConstruct public void init() throws IOException { System.out.println("初始化资源..."); this.executorService = Executors.newFixedThreadPool(4); this.logWriter = new FileWriter("processing.log", true); }
public void processFile(String filename) { executorService.submit(() -> { System.out.println("处理文件: " + filename); }); }
@PreDestroy public void cleanup() { System.out.println("清理资源...");
if (executorService != null) { executorService.shutdown(); try { if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { executorService.shutdownNow(); } System.out.println("线程池已关闭"); } catch (InterruptedException e) { executorService.shutdownNow(); Thread.currentThread().interrupt(); } }
if (logWriter != null) { try { logWriter.close(); System.out.println("日志文件已关闭"); } catch (IOException e) { System.err.println("关闭日志文件失败: " + e.getMessage()); } } } }
|
应用关闭时的输出
@PreDestroy 的触发时机
@PreDestroy 方法会在以下情况下被调用:
- Spring Boot 应用正常关闭(如调用
SpringApplication.exit() 或收到 SIGTERM 信号) - ApplicationContext 被关闭(调用
context.close()) - Web 应用被卸载(如 Tomcat 停止)
注意:如果应用被强制杀死(如 kill -9),@PreDestroy 不会被调用。
3.4.2. DisposableBean 接口与 destroyMethod
与初始化回调类似,销毁回调也有三种方式:
方式一:@PreDestroy(推荐)
1 2 3 4 5 6 7 8
| @Service public class MyService {
@PreDestroy public void cleanup() { System.out.println("@PreDestroy 清理"); } }
|
方式二:DisposableBean 接口
1 2 3 4 5 6 7 8
| @Service public class MyService implements DisposableBean {
@Override public void destroy() throws Exception { System.out.println("DisposableBean.destroy() 清理"); } }
|
方式三:@Bean(destroyMethod)
1 2 3 4 5 6 7 8
| @Configuration public class AppConfig {
@Bean(destroyMethod = "shutdown") public ThirdPartyConnectionPool connectionPool() { return new ThirdPartyConnectionPool(); } }
|
三种方式的执行顺序
如果同时使用了三种方式,执行顺序是:
1 2 3
| 1️⃣ @PreDestroy 2️⃣ DisposableBean.destroy() 3️⃣ @Bean(destroyMethod)
|
destroyMethod 的默认推断
Spring 有一个智能特性:如果 Bean 的类中有名为 close 或 shutdown 的公共无参方法,Spring 会 自动将其作为销毁方法,即使你没有显式指定 destroyMethod。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class MyResource {
public void close() { System.out.println("资源已关闭"); } }
@Configuration public class AppConfig {
@Bean public MyResource myResource() { return new MyResource(); } }
|
如果你不想让 Spring 自动推断,可以显式设置为空字符串:
1 2 3 4
| @Bean(destroyMethod = "") public MyResource myResource() { return new MyResource(); }
|
3.5. 生命周期的扩展点:BeanPostProcessor 简介
在前面的章节中,我们学习了 Bean 生命周期的四个阶段。但 Spring 的强大之处在于,它允许我们在这些阶段之间 插入自定义逻辑。这就是 BeanPostProcessor 的作用。
3.5.1. BeanPostProcessor 是什么:AOP 的幕后功臣
BeanPostProcessor(Bean 后置处理器)是 Spring 提供的一个扩展接口,它允许我们在 Bean 初始化前后插入自定义逻辑。
接口定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public interface BeanPostProcessor {
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; }
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }
|
BeanPostProcessor 在生命周期中的位置

为什么说它是 AOP 的幕后功臣?
Spring AOP 的实现原理就是通过 BeanPostProcessor 在 Bean 初始化后,将原始对象替换为代理对象:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class AopBeanPostProcessor implements BeanPostProcessor {
@Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (needsProxy(bean)) { return createProxy(bean); } return bean; } }
|
这就是为什么当你使用 @Transactional、@Async、@Cacheable 等注解时,注入的 Bean 实际上是一个代理对象。
3.5.2. 一个简单的例子:记录 Bean 创建耗时
让我们写一个简单的 BeanPostProcessor,记录每个 Bean 的创建耗时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Component public class BeanCreationTimingPostProcessor implements BeanPostProcessor {
private final Map<String, Long> startTimeMap = new ConcurrentHashMap<>();
@Override public Object postProcessBeforeInitialization(Object bean, String beanName) { startTimeMap.put(beanName, System.currentTimeMillis()); return bean; }
@Override public Object postProcessAfterInitialization(Object bean, String beanName) { Long startTime = startTimeMap.remove(beanName); if (startTime != null) { long duration = System.currentTimeMillis() - startTime; if (duration > 10) { System.out.printf("⏱️ Bean [%s] 初始化耗时: %dms%n", beanName, duration); } } return bean; } }
|
启动应用后的输出示例
1 2 3
| ⏱️ Bean [dataSource] 初始化耗时: 156ms ⏱️ Bean [entityManagerFactory] 初始化耗时: 892ms ⏱️ Bean [cacheManager] 初始化耗时: 45ms
|
这个简单的例子可以帮助我们发现启动慢的 Bean,进行针对性优化。
3.5.3. 为什么一般不需要自己写 BeanPostProcessor
虽然 BeanPostProcessor 很强大,但在日常业务开发中,几乎不需要自己实现它。原因如下:
原因一:Spring 已经提供了丰富的内置实现
| BeanPostProcessor | 作用 |
|---|
AutowiredAnnotationBeanPostProcessor | 处理 @Autowired、@Value 注入 |
CommonAnnotationBeanPostProcessor | 处理 @PostConstruct、@PreDestroy、@Resource |
AsyncAnnotationBeanPostProcessor | 处理 @Async,创建异步代理 |
ScheduledAnnotationBeanPostProcessor | 处理 @Scheduled,注册定时任务 |
PersistenceAnnotationBeanPostProcessor | 处理 @PersistenceContext(JPA) |
这些内置的 BeanPostProcessor 已经覆盖了绝大多数场景。
原因二:AOP 是更好的选择
如果你想在方法执行前后添加逻辑(如日志、权限检查、事务),应该使用 AOP(@Aspect),而不是 BeanPostProcessor。
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Aspect @Component public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))") public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("方法开始: " + joinPoint.getSignature()); Object result = joinPoint.proceed(); System.out.println("方法结束: " + joinPoint.getSignature()); return result; } }
|
原因三:BeanPostProcessor 的坑比较多
- 执行时机早:
BeanPostProcessor 本身也是 Bean,但它会在其他 Bean 之前被创建。如果它依赖其他 Bean,可能会导致问题。 - 影响所有 Bean:一个
BeanPostProcessor 会对容器中的所有 Bean 生效,稍有不慎就会影响整个应用。 - 调试困难:出问题时很难定位是哪个
BeanPostProcessor 导致的。
什么时候需要自己写 BeanPostProcessor?
只有在以下场景才需要考虑:
- 编写框架或基础组件:如自定义的 RPC 框架、自定义注解处理器
- 需要在 Bean 创建过程中做全局处理:如上面的耗时统计例子
- 需要替换或包装 Bean:如创建自定义代理
3.6. 本章总结与生命周期问题排查指南
摘要回顾
本章我们从一个 “@Autowired 字段为 null” 的 Bug 出发,深入学习了 Spring Bean 的完整生命周期。
首先,我们建立了生命周期的 全景认知:实例化 → 属性填充 → 初始化 → 使用 → 销毁。理解这个顺序是避免很多 Bug 的关键。
接着,我们学习了 作用域 的概念:
singleton:默认作用域,整个容器一个实例,要注意线程安全prototype:每次获取都是新实例,Spring 不管理其销毁- Web 作用域:
request、session,生命周期与 HTTP 请求/会话绑定 - 特别讨论了 “单例注入多例” 的经典陷阱及解决方案
然后,我们学习了 初始化回调 的三种方式:
@PostConstruct:首选,标准注解,简洁直观InitializingBean:框架级别使用@Bean(initMethod):配置第三方库时使用
以及 销毁回调 的对应方式:
@PreDestroy:首选DisposableBean:框架级别使用@Bean(destroyMethod):配置第三方库时使用- 特别强调了 prototype Bean 的销毁回调不会被 Spring 调用
最后,我们简要介绍了 BeanPostProcessor,理解了它是 AOP 等高级功能的实现基础,但日常开发中一般不需要自己实现。
核心概念速查表
| 概念 | 说明 | 关键要点 |
|---|
| 生命周期四阶段 | 实例化 → 属性填充 → 初始化 → 销毁 | 构造函数中不能访问 @Autowired 字段 |
| singleton | 单例作用域,默认 | 整个容器一个实例,注意线程安全 |
| prototype | 多例作用域 | 每次获取新实例,Spring 不管理销毁 |
| @PostConstruct | 初始化回调 | 在依赖注入完成后调用,首选方式 |
| @PreDestroy | 销毁回调 | 在 Bean 销毁前调用,用于资源清理 |
| BeanPostProcessor | 生命周期扩展点 | AOP 的实现基础,一般不需要自己写 |
本章核心心智模型
生命周期就是 “时间线”
理解 Bean 生命周期的### 本章核心心智模型
生命周期就是 “时间线”
理解 Bean 生命周期的关键,是把它想象成一条 时间线。在这条时间线上,不同的事件按固定顺序发生:
1 2 3 4 5 6 7 8 9 10
| 时间 ──────────────────────────────────────────────────────────────────►
│ │ │ │ │ ▼ ▼ ▼ ▼ ▼ 构造函数 @Autowired @PostConstruct 业务方法 @PreDestroy │ 字段注入 执行 调用 执行 │ │ │ │ │ │ │ │ │ │ 字段=null 字段有值了 可以安全使用 正常工作 清理资源 所有依赖
|
在正确的时间做正确的事
| 时间点 | 可以做什么 | 不能做什么 |
|---|
| 构造函数中 | 简单的字段初始化、参数校验 | 访问 @Autowired 字段、调用依赖的方法 |
| @PostConstruct 中 | 使用所有依赖、初始化缓存、建立连接 | - |
| 业务方法中 | 正常的业务逻辑 | - |
| @PreDestroy 中 | 释放资源、关闭连接、保存状态 | 依赖其他可能已销毁的 Bean |
核心认知升级
读完本章,你应该建立起以下认知:
- 构造函数 ≠ 初始化完成:构造函数执行时,依赖注入还没发生,不要在构造函数中使用 @Autowired 字段
- @PostConstruct 是初始化逻辑的正确位置:此时所有依赖都已就绪,可以安全使用
- singleton 是默认且最常用的作用域:但要注意线程安全问题
- prototype Bean 的销毁需要自己负责:Spring 只管创建,不管销毁
- 单例注入多例是个经典陷阱:使用 ObjectProvider 或 @Lookup 解决
- BeanPostProcessor 是高级扩展点:理解原理即可,日常开发不需要自己写