@PostConstruct、@PreDestroy 和 @DependsOn注解的使用和区别

这三个注解是 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,无法使用。
实战场景

  1. 缓存预热:应用启动时,把热点数据从数据库加载到内存。
  2. 启动定时任务:启动一个后台线程池或调度器。
  3. 资源检查:检查数据库连接、文件路径是否存在。
实战代码:缓存预热与异步启动

这是一个非常典型的 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 销毁前),执行清理逻辑。
核心价值 :防止内存泄漏,保证优雅停机
实战场景

  1. 关闭线程池:防止应用停止后,后台线程还在跑。
  2. 关闭连接:关闭数据库连接池、Redis 连接、文件流。
  3. 数据落盘:将内存中的临时统计数据写入数据库。
实战代码:优雅停机

配合上面的 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

    XML 复制代码
    1<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. 执行顺序

如果你混用了多种初始化方式,它们的执行顺序是固定的:

  1. 构造方法
  2. @Autowired 注入
  3. @PostConstruct
  4. InitializingBean.afterPropertiesSet() (Spring 接口)
  5. 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 杀进程不执行
相关推荐
Devin~Y4 小时前
大厂Java面试:Spring Boot + Redis/Kafka + Spring Cloud + JVM + RAG/向量检索(小Y翻车实录)
java·jvm·spring boot·redis·spring cloud·kafka·mybatis
是宇写的啊4 小时前
SpringBoot 统一功能处理
java·spring boot·后端
等....4 小时前
Spring Boot多模块项目部署
java·spring boot·后端
20岁30年经验的码农4 小时前
Spring Boot 配置文件生效规则
spring boot·后端·pycharm
斌果^O^4 小时前
SpringBoot 实战:@Async + CompletableFuture 实现多 SQL 并行统计查询
java·spring boot·sql
有趣灵魂5 小时前
Java Spring Boot根据Word模板和动态数据生成Word文件
java·spring boot·word·apache
霸道流氓气质5 小时前
SpringBoot+LangChain4j+Ollama+MCP实现智能天气工具调用示例
java·spring boot·后端