Note 03. Bean 的高级管理:作用域、生命周期与实例化

Note 03. Bean 的生命周期:从创建到销毁的完整旅程

摘要:在前两章中,我们学习了如何将对象注册为 Bean,以及 Spring 如何创建这些对象。但故事并没有结束——Bean 被创建出来之后,还要经历属性注入、初始化回调、使用、销毁回调等一系列阶段。理解这个完整的生命周期,能帮助我们在正确的时机执行正确的操作,避免很多诡异的 Bug。本章将通过一个真实的 Bug 案例引入,带你彻底搞懂 Bean 的生命周期。

本章学习路径

  1. 问题驱动:从一个 “@Autowired 字段为 null” 的 Bug 出发,理解为什么要学习生命周期。
  2. 全景认知:掌握 Bean 生命周期的四个阶段,建立完整的心智模型。
  3. 作用域理解:搞清 singleton 和 prototype 的区别,以及 “单例注入多例” 的经典陷阱。
  4. 回调机制:学会使用 @PostConstruct 和 @PreDestroy 在正确的时机执行初始化和清理逻辑。
  5. 原理窥探:了解 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() {
// 在构造函数中使用 userService
System.out.println("OrderService 构造函数执行");
System.out.println("userService = " + userService); // 输出:null !

// 尝试调用 userService 的方法
this.vipUserNames = userService.getVipUserNames(); // 💥 NullPointerException!
}
}
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 的真实顺序:

mermaid-diagram-2026-01-03-171604

正确的写法

如果需要在 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() {
// 构造函数中不要访问 @Autowired 的字段
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 的生命周期可以分为 四个主要阶段

image-20260103172011628

image-20260103172028081

四个阶段详解

阶段做什么关键时机
实例化调用构造函数,创建对象此时对象是 “空壳”,@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;

// 阶段 1:实例化
public LifecycleDemoService() {
System.out.println("1️⃣ [实例化] 构造函数执行");
System.out.println(" 此时 userService = " + userService); // null
}

// 阶段 2:属性填充(由 Spring 自动完成,我们看不到具体过程)
// @Autowired 字段在这个阶段被赋值

// 阶段 3:初始化
@PostConstruct
public void init() {
System.out.println("3️⃣ [初始化] @PostConstruct 方法执行");
System.out.println(" 此时 userService = " + userService); // 有值了
}

// 正常使用阶段
public void doSomething() {
System.out.println("🔄 [使用中] 业务方法被调用");
}

// 阶段 4:销毁
@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)

作用域决定了:

  • 创建几个实例:是所有地方共享一个,还是每次都创建新的
  • 生命周期有多长:是跟随容器存活,还是用完就销毁

mermaid-diagram-2026-01-03-172212 作用域会影响生命周期的行为,我们将在下一节详细讨论。


3.2. 作用域:决定 Bean “活多久、有几个”

作用域是 Spring 管理 Bean 的一个重要维度。不同的作用域决定了 Bean 实例的创建策略和生命周期长度。

3.2.1. singleton:默认的单例模式

什么是单例作用域

singleton 是 Spring 的 默认作用域。在这种模式下:

  • 整个 Spring 容器中,只有一个实例
  • 所有注入这个 Bean 的地方,拿到的都是 同一个对象
  • Bean 在 容器启动时创建,在 容器关闭时销毁

代码验证

1
2
3
4
5
6
7
@Service  // 默认就是 singleton
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

单例的优点

  1. 节省资源:只创建一个实例,减少内存占用和创建开销
  2. 状态共享:可以在 Bean 中维护共享状态(但要注意线程安全)
  3. 生命周期可控: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 不会执行!");
// 因为 Spring 不管理 prototype Bean 的销毁
}
}

这意味着:如果 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  // 默认 singleton
public class OrderService {

@Autowired
private RequestContext requestContext; // 注入 prototype Bean

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 { // 注意:改成抽象类

// 每次调用这个方法,Spring 都会返回一个新的 prototype 实例
@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() {
// 每次调用 getObject() 都会获取新的 prototype 实例
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(分布式场景)
全局配置singletonapplication

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("正在初始化产品缓存...");

// 此时 productRepository 已经注入完成,可以安全使用
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 的优点

  1. 标准注解:JSR-250 规范,不依赖 Spring 特定 API
  2. 简洁直观:一个注解搞定,不需要实现接口
  3. 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);

// 此时 @Value 注入的值已经可用
this.connection = DriverManager.getConnection(databaseUrl, username, "password");

System.out.println("数据库连接建立成功");
}

public Connection getConnection() {
return connection;
}
}

InitializingBean vs @PostConstruct

对比维度@PostConstructInitializingBean
来源JSR-250 标准注解Spring 特有接口
侵入性低(只是一个注解)高(需要实现接口)
方法名自定义固定为 afterPropertiesSet
适用场景业务代码框架/基础设施代码
推荐程度⭐⭐⭐⭐⭐⭐⭐⭐

什么时候用 InitializingBean?

一般来说,优先使用 @PostConstruct。但在以下场景中,InitializingBean 可能更合适:

  1. 编写框架或基础组件:让使用者明确知道这是 Spring 管理的组件
  2. 需要抛出受检异常afterPropertiesSet() 声明了 throws Exception
  3. 团队规范要求:某些团队可能有统一的规范

3.3.3. @Bean(initMethod):配置第三方库时使用

当我们使用 @Bean 方法配置第三方库的类时,无法在类上加 @PostConstruct 注解(因为我们无法修改第三方库的源码)。这时可以使用 @BeaninitMethod 属性。

场景:配置一个连接池

假设我们使用一个第三方的连接池库,它有一个 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);
// 不需要手动调用 init(),Spring 会自动调用
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()");
}

// 假设通过 @Bean(initMethod = "customInit") 配置
public void customInit() {
System.out.println("3️⃣ @Bean(initMethod)");
}
}

执行顺序

1
2
3
1️⃣ @PostConstruct
2️⃣ InitializingBean.afterPropertiesSet()
3️⃣ @Bean(initMethod)

选择建议

mermaid-diagram-2026-01-03-173348

总结

方式适用场景推荐程度
@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());
}
}
}
}

应用关闭时的输出

1
2
3
清理资源...
线程池已关闭
日志文件已关闭

@PreDestroy 的触发时机

@PreDestroy 方法会在以下情况下被调用:

  1. Spring Boot 应用正常关闭(如调用 SpringApplication.exit() 或收到 SIGTERM 信号)
  2. ApplicationContext 被关闭(调用 context.close()
  3. 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 的类中有名为 closeshutdown 的公共无参方法,Spring 会 自动将其作为销毁方法,即使你没有显式指定 destroyMethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyResource {

// Spring 会自动检测并调用这个方法
public void close() {
System.out.println("资源已关闭");
}
}

@Configuration
public class AppConfig {

@Bean // 没有指定 destroyMethod,但 close() 会被自动调用
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 {

// 在 Bean 初始化之前调用(@PostConstruct 之前)
default Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}

// 在 Bean 初始化之后调用(@PostConstruct 之后)
default Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}

BeanPostProcessor 在生命周期中的位置

mermaid-diagram-2026-01-03-173719

为什么说它是 AOP 的幕后功臣?

Spring AOP 的实现原理就是通过 BeanPostProcessor 在 Bean 初始化后,将原始对象替换为代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 简化的 AOP 实现原理
public class AopBeanPostProcessor implements BeanPostProcessor {

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 检查这个 Bean 是否需要被代理(是否有切面匹配)
if (needsProxy(bean)) {
// 创建代理对象,替换原始 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 {

// 用于存储每个 Bean 开始初始化的时间
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;
// 只打印耗时超过 10ms 的 Bean
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
// ✅ 推荐:使用 AOP
@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 的坑比较多

  1. 执行时机早BeanPostProcessor 本身也是 Bean,但它会在其他 Bean 之前被创建。如果它依赖其他 Bean,可能会导致问题。
  2. 影响所有 Bean:一个 BeanPostProcessor 会对容器中的所有 Bean 生效,稍有不慎就会影响整个应用。
  3. 调试困难:出问题时很难定位是哪个 BeanPostProcessor 导致的。

什么时候需要自己写 BeanPostProcessor?

只有在以下场景才需要考虑:

  1. 编写框架或基础组件:如自定义的 RPC 框架、自定义注解处理器
  2. 需要在 Bean 创建过程中做全局处理:如上面的耗时统计例子
  3. 需要替换或包装 Bean:如创建自定义代理

3.6. 本章总结与生命周期问题排查指南

摘要回顾

本章我们从一个 “@Autowired 字段为 null” 的 Bug 出发,深入学习了 Spring Bean 的完整生命周期。

首先,我们建立了生命周期的 全景认知:实例化 → 属性填充 → 初始化 → 使用 → 销毁。理解这个顺序是避免很多 Bug 的关键。

接着,我们学习了 作用域 的概念:

  • singleton:默认作用域,整个容器一个实例,要注意线程安全
  • prototype:每次获取都是新实例,Spring 不管理其销毁
  • Web 作用域:requestsession,生命周期与 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

核心认知升级

读完本章,你应该建立起以下认知:

  1. 构造函数 ≠ 初始化完成:构造函数执行时,依赖注入还没发生,不要在构造函数中使用 @Autowired 字段
  2. @PostConstruct 是初始化逻辑的正确位置:此时所有依赖都已就绪,可以安全使用
  3. singleton 是默认且最常用的作用域:但要注意线程安全问题
  4. prototype Bean 的销毁需要自己负责:Spring 只管创建,不管销毁
  5. 单例注入多例是个经典陷阱:使用 ObjectProvider 或 @Lookup 解决
  6. BeanPostProcessor 是高级扩展点:理解原理即可,日常开发不需要自己写