这三个注解是 Spring 容器管理 Bean 生命周期的核心工具。如果把 Bean 比作一个员工,那么:
@DependsOn是入职门槛(必须签入职合同,我才能入职)。@PostConstruct是入职仪式(拿到电脑和账号后,开机、登录、准备工作)。@PreDestroy是离职交接(关电脑、交钥匙、保存文档)。
下面我将结合 Spring Boot 实战场景,详细拆解这三个注解的用法、原理和避坑指南。
1. @DependsOn:控制"谁先谁后"
核心作用
强制指定 Bean 的初始化顺序。
虽然 Spring 的 @Autowired 能自动解决大部分依赖,但在以下场景会失效,必须用 @DependsOn:
- 隐式依赖(副作用) :Bean A 没有直接持有 Bean B 的引用(没有
@Autowired),但 A 的初始化逻辑依赖于 B 产生的副作用(比如 B 往系统属性里放了值,或者 B 初始化了某个静态资源/单例注册中心)。 - 第三方库 Bean :无法修改源码加
@Autowired,但必须等它先启动。
实战代码
场景:CacheManager 必须等 RedisConfig 加载完配置(副作用)后才能启动,但代码里没有直接注入 RedisConfig。
java
1import org.springframework.context.annotation.Bean;
2import org.springframework.context.annotation.Configuration;
3import org.springframework.context.annotation.DependsOn;
4import org.springframework.stereotype.Component;
5
6@Component
7class RedisConfig {
8 public RedisConfig() {
9 System.out.println("1. RedisConfig 正在加载配置...");
10 try { Thread.sleep(1000); } catch (Exception e) {} // 模拟耗时
11 System.out.println(" -> RedisConfig 加载完毕");
12 }
13}
14
15@Component
16@DependsOn("redisConfig") // 【关键】强制 Spring 先创建 redisConfig,哪怕没有注入它
17public class CacheManager {
18
19 public CacheManager() {
20 System.out.println("2. CacheManager 正在启动...");
21 // 此时可以确信 RedisConfig 已经初始化完成
22 System.out.println(" -> CacheManager 启动完毕");
23 }
24}
注意:
- 值必须是对方的 Bean 名称(默认是类名首字母小写,如
redisConfig)。 - 如果指定的 Bean 不存在,Spring Boot 启动会直接报错
BeanCreationException。
2. @PostConstruct:初始化的"黄金位置"
核心作用
在 Bean 实例化并且 所有依赖注入(@Autowired)完成后执行。这是执行初始化逻辑(如连接数据库、加载缓存、启动线程)的最佳时机。
为什么不用构造方法?
构造方法执行时,@Autowired 的字段还是 null,无法使用。
实战场景:
- 缓存预热:应用启动时,把热点数据从数据库加载到内存。
- 启动定时任务:启动一个后台线程池或调度器。
- 资源检查:检查数据库连接、文件路径是否存在。
实战代码:缓存预热与异步启动
这是一个非常典型的 Spring Boot 业务场景。
java
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.stereotype.Service;
3import javax.annotation.PostConstruct;
4import java.util.concurrent.Executors;
5import java.util.concurrent.ScheduledExecutorService;
6import java.util.concurrent.TimeUnit;
7
8@Service
9public class DataCacheService {
10
11 @Autowired
12 private UserRepository userRepository; // 假设这是数据库操作类
13
14 private ScheduledExecutorService scheduler;
15
16 // 1. 构造方法
17 public DataCacheService() {
18 System.out.println("构造方法:此时 userRepository 是 null,不能用!");
19 }
20
21 // 2. @PostConstruct:依赖注入已完成,可以安全使用 userRepository
22 @PostConstruct
23 public void init() {
24 System.out.println("@PostConstruct:开始加载缓存...");
25
26 // 模拟耗时操作:从数据库加载所有用户到内存
27 // List<User> users = userRepository.findAll();
28 System.out.println(" - 缓存已加载:" + "模拟数据");
29
30 // 启动后台定时任务(每 10 秒刷新一次)
31 scheduler = Executors.newScheduledThreadPool(1);
32 scheduler.scheduleAtFixedRate(() -> {
33 System.out.println(" - [后台任务] 正在刷新缓存...");
34 }, 0, 10, TimeUnit.SECONDS);
35
36 System.out.println("@PostConstruct:初始化完成,服务就绪!");
37 }
38}
3. @PreDestroy:资源释放的"最后一道防线"
核心作用
在 Spring 容器关闭前(Bean 销毁前),执行清理逻辑。
核心价值 :防止内存泄漏,保证优雅停机 。
实战场景:
- 关闭线程池:防止应用停止后,后台线程还在跑。
- 关闭连接:关闭数据库连接池、Redis 连接、文件流。
- 数据落盘:将内存中的临时统计数据写入数据库。
实战代码:优雅停机
配合上面的 DataCacheService,我们需要在应用关闭时停止线程池。
java
1import javax.annotation.PreDestroy;
2
3// 接上面的类
4 // ...
5
6 @PreDestroy
7 public void destroy() {
8 System.out.println("@PreDestroy:应用正在关闭,正在清理资源...");
9
10 if (scheduler != null && !scheduler.isShutdown()) {
11 scheduler.shutdown(); // 停止接收新任务
12 try {
13 // 等待任务结束,最多等 5 秒
14 if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
15 scheduler.shutdownNow(); // 强制关闭
16 }
17 } catch (InterruptedException e) {
18 scheduler.shutdownNow();
19 Thread.currentThread().interrupt();
20 }
21 }
22 System.out.println("@PreDestroy:资源清理完毕,再见!");
23 }
4. 重点:JDK 11/17/21 与 Spring Boot 3 的替代方案
在 JDK 9 之后,javax.annotation 模块被移除。
- Spring Boot 2.x (JDK 8) : 使用
javax.annotation.PostConstruct - Spring Boot 3.x (JDK 17/21) : 使用
jakarta.annotation.PostConstruct
如果你不想处理包名问题,或者想使用更 Spring 原生的方式,可以使用以下替代方案:
替代方案 1:Spring 原生接口(无依赖问题)
这是 Spring 提供的标准接口,不依赖任何外部规范,完全兼容所有 JDK 版本。
java
1import org.springframework.beans.factory.DisposableBean;
2import org.springframework.beans.factory.InitializingBean;
3import org.springframework.stereotype.Component;
4
5@Component
6public class NativeLifecycleBean implements InitializingBean, DisposableBean {
7
8 @Override
9 public void afterPropertiesSet() throws Exception {
10 // 替代 @PostConstruct
11 System.out.println("InitializingBean: 初始化完成");
12 }
13
14 @Override
15 public void destroy() throws Exception {
16 // 替代 @PreDestroy
17 System.out.println("DisposableBean: 销毁开始");
18 }
19}
替代方案 2:@EventListener(推荐用于全局启动)
如果你不是要初始化某个 Bean,而是想等整个应用启动完 (Tomcat 就绪)再做某事,这个比 @PostConstruct 更好,因为它解耦了业务逻辑。
java
1import org.springframework.boot.context.event.ApplicationReadyEvent;
2import org.springframework.context.event.EventListener;
3import org.springframework.stereotype.Component;
4
5@Component
6public class AppStartupListener {
7
8 @EventListener(ApplicationReadyEvent.class)
9 public void onAppReady() {
10 System.out.println("🚀 整个应用已完全启动,可以开始接客了!");
11 // 适合做:发送启动通知、开始消费 MQ 消息
12 }
13}
5. 综合避坑指南与注意事项
A. 异常处理(生死攸关)
@PostConstruct抛异常 = 启动失败 :
如果这个方法里抛出未捕获的异常(比如数据库连不上),Spring Boot 会认为应用不可用,直接停止启动 。- 建议 :在方法内部使用
try-catch包裹关键逻辑,记录日志,或者决定是继续运行还是主动System.exit(1)。
- 建议 :在方法内部使用
@PreDestroy抛异常 = 忽略并继续 :
如果销毁方法报错,Spring 通常会记录错误日志,然后继续销毁下一个 Bean。
B. JDK 9+ 的依赖问题
@PostConstruct 和 @PreDestroy 属于 javax.annotation 包。
-
JDK 8:自带,无需配置。
-
JDK 11/17/21 :已被移除。如果你的项目运行在高版本 JDK 上,必须在
pom.xml中引入依赖,否则会报ClassNotFoundException:XML1<dependency> 2 <groupId>javax.annotation</groupId> 3 <artifactId>javax.annotation-api</artifactId> 4 <version>1.3.2</version> 5</dependency>(注:Spring Boot 2.3+ 通常通过
spring-boot-starter间接管理了这个依赖,但如果是纯 Java 项目需注意)
C. 执行顺序
如果你混用了多种初始化方式,它们的执行顺序是固定的:
- 构造方法
@Autowired注入@PostConstructInitializingBean.afterPropertiesSet()(Spring 接口)init-method(XML 或 @Bean 配置)
建议 :统一使用 @PostConstruct,不要混用,否则代码很难维护。
D. 静态方法无效
@PostConstruct 只能修饰非静态方法。因为它是针对 Bean 实例(对象)的,而静态方法属于类。
E. 单例 vs 多例
- 单例 (Singleton, 默认) :
@PostConstruct在容器启动时执行一次;@PreDestroy在容器关闭时执行。 - 多例 (Prototype) :每次
getBean都会执行@PostConstruct,但 Spring 不会管理多例 Bean 的销毁 ,所以@PreDestroy不会执行!
总结表格
| 注解 | 核心用途 | 执行时机 | 常见坑 |
|---|---|---|---|
| @DependsOn | 强制顺序 | 实例化之前 | Bean 名称写错导致启动报错 |
| @PostConstruct | 资源初始化 | 注入完成后,使用前 | 抛异常导致启动失败;JDK9+ 缺依赖 |
| @PreDestroy | 资源清理 | 容器关闭前 | 多例 Bean 不执行;kill -9 杀进程不执行 |