Spring 中 @PostConstruct 与 @PreDestroy 的完整与实战

1. 基础概念与核心思想

1.1 @PostConstruct 的定义

@PostConstruct 是 Java 标准注解(来源于 JSR-250),用于在 依赖注入完成后立即执行初始化逻辑。 在 Spring 中,它通常用于对 Bean 进行补充初始化,例如加载配置、建立连接、校验参数等。

其执行时机可以用一个简单模型表示:

Bean 实例化 → 属性注入 → @PostConstruct 方法执行 → Bean 初始化完成

1.2 @PreDestroy 的定义

同样来自 JSR-250,@PreDestroy 用于在 Bean 销毁前执行清理逻辑,例如关闭连接、释放资源、写入缓存或日志等。

它的执行流程通常如下:

容器准备销毁 Bean → 执行 @PreDestroy 方法 → Bean 从容器移除

1.3 为什么 Spring 要支持这两个注解

在 Spring IoC 生命周期中,存在多个可插入点:

  • 实例化阶段
  • 属性注入阶段
  • 初始化阶段
  • 销毁阶段

@PostConstruct@PreDestroy 的出现,让开发者可以在 “介于自动注入和Bean可用状态之间” 插入定制逻辑,而不需要:

  • 实现 BeanPostProcessor
  • 实现 InitializingBean / DisposableBean
  • 在 XML 或 @Bean 中手动指定 initMethod/destroyMethod

VS 其他方式对比如下:

(前后空行保证表格正常渲染)

方式 初始化方法 销毁方法 侵入性 推荐度
@PostConstruct / @PreDestroy ★ 非侵入 ★★★★★
InitializingBean / DisposableBean ★★★ 需实现接口 ★★★
@Bean(initMethod, destroyMethod) ★★ XML 或 Java Config ★★★★
BeanPostProcessor ✔(高级) ✔(高级) ★★★★★ 非常复杂

简单来说: 它们是最优雅、最简洁的 Spring Bean 生命周期扩展方式。


2. 使用场景:什么时候需要它们?

2.1 @PostConstruct 常见使用场景

1)加载缓存或预热配置

例如系统启动后,将数据库某些配置读取到内存中。

2)建立第三方连接

例如 Elasticsearch、Kafka、Redis 客户端初始化。

3)异步线程池启动检查

验证核心线程池是否按照配置创建完成。

4)参数校验

例如检查配置文件加载的值是否符合要求。

2.2 @PreDestroy 常见使用场景

1)连接释放

通常用于关闭数据库连接池、消息客户端、线程池等。

2)持久化内存缓存

比如应用关闭前把缓存中的信息保存到 Redis 或数据库。

3)清理临时文件

例如一些需要删除的本地文件目录。


3. Spring Bean 生命周期与注解执行时机详解

为了理解这些注解执行的位置,必须掌握 Spring Bean 的完整生命周期。以伪流程图解释如下:

1. 扫描类并解析 BeanDefinition
2. Bean 实例化(构造方法)
3. 填充属性(依赖注入)
4. 调用 BeanNameAware
5. 调用 BeanFactoryAware
6. 调用 ApplicationContextAware
7. BeanPostProcessor#postProcessBeforeInitialization()
8. @PostConstruct   ← 本文主角
9. InitializingBean#afterPropertiesSet()
10. @Bean(initMethod)
11. BeanPostProcessor#postProcessAfterInitialization()
12. Bean 正式初始化完成,可被使用
---------------------------------------------
13. 容器关闭:执行 @PreDestroy  ← 本文主角
14. DisposableBean#destroy()
15. @Bean(destroyMethod)
16. Bean 销毁完成

注解在其中的位置

  • @PostConstructpostProcessBeforeInitialization 与 afterPropertiesSet 之间
  • @PreDestroy 在 DisposableBean 之前执行

4. 示例代码(最小可运行示例)

下面提供一个最小可运行 Spring Boot 项目结构,包含完整 Java 类与 application.yml

4.1 Maven 项目结构

src
 └── main
     ├── java
     │   └── com.example.demo
     │       ├── DemoApplication.java
     │       └── InitExampleService.java
     └── resources
         └── application.yml

5. 代码实现:@PostConstruct 与 @PreDestroy 的使用

5.1 DemoApplication.java

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

5.2 InitExampleService.java

package com.example.demo;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;

@Service
public class InitExampleService {

    private String cacheValue;

    @PostConstruct
    public void init() {
        System.out.println("[@PostConstruct] 系统开始预加载配置");
        cacheValue = "初始化配置值";
    }

    public String getCacheValue() {
        return cacheValue;
    }

    @PreDestroy
    public void destroy() {
        System.out.println("[@PreDestroy] 系统即将关闭,执行数据清理");
    }
}

5.3 application.yml

server:
  port: 8080

spring:
  application:
    name: postconstruct-demo

运行项目后,你会在控制台看到:

[@PostConstruct] 系统开始预加载配置
...
[@PreDestroy] 系统即将关闭,执行数据清理

6. 实战案例:常见业务场景的完整实现

6.1 场景一:应用启动后预加载数据库配置(含建表 SQL)

假设系统有一张配置表,需要在项目启动时加载:

数据库表 SQL

CREATE TABLE sys_config (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    config_key VARCHAR(255) NOT NULL,
    config_value VARCHAR(255) NOT NULL
);

插入示例数据:

INSERT INTO sys_config (config_key, config_value) VALUES
('site_name', 'MyPlatform'),
('cache_timeout', '300');

Java 代码:读取后缓存到内存

@Service
public class ConfigService {

    private final JdbcTemplate jdbcTemplate;
    private Map<String, String> configCache = new HashMap<>();

    public ConfigService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostConstruct
    public void loadConfig() {
        System.out.println("[ConfigService] 开始加载系统配置");
        jdbcTemplate.query("SELECT config_key, config_value FROM sys_config",
                rs -> configCache.put(rs.getString("config_key"), rs.getString("config_value"))
        );
    }

    @PreDestroy
    public void beforeShutdown() {
        System.out.println("[ConfigService] 系统关闭,配置缓存无需清理,但可选择持久化日志等");
    }

    public String get(String key) {
        return configCache.get(key);
    }
}

此类场景在后台管理系统中极其常见。


7. 实战案例:线程池初始化与关闭

线程池若不安全退出可能导致:

  • 线程泄露
  • 应用无法正常退出
  • 数据丢失

7.1 ThreadPoolService

@Service
public class ThreadPoolService {

    private ExecutorService executorService;

    @PostConstruct
    public void init() {
        System.out.println("[ThreadPoolService] 初始化线程池");
        executorService = Executors.newFixedThreadPool(5);
    }

    public void submitTask(Runnable task) {
        executorService.submit(task);
    }

    @PreDestroy
    public void destroy() {
        System.out.println("[ThreadPoolService] 销毁线程池");
        executorService.shutdown();
    }
}

8. 深入原理:Spring 如何处理 @PostConstruct 与 @PreDestroy

Spring 并不是在 Bean 中直接查找这些注解,而是通过:

CommonAnnotationBeanPostProcessor

这是一个 BeanPostProcessor,其职责包括:

  • 查找 @PostConstruct
  • 查找 @PreDestroy
  • 创建对应的调用任务
  • 注册到生命周期管理容器中

执行逻辑如下:

1. 扫描所有 Bean
2. 找出标有 @PostConstruct 的方法
3. 在 Bean 初始化后调用
4. 找出标有 @PreDestroy 的方法
5. 在容器关闭时执行

注意

Spring 5 之后默认启用对 JSR-250 的支持,只要加上 jakarta.annotation 或 javax.annotation。


9. @PostConstruct 与构造方法的区别与对比

(前后空行)

项目 构造方法 @PostConstruct
执行时机 Bean 实例化阶段 依赖注入完成之后
能否使用 @Autowired 注入结果 ❌ 不可靠 ✔ 完全可靠
是否可访问应用上下文 一般不行 可以
使用难度 简单 非常简单
适合任务 字段初始化 配置读取、连接建立

核心区别: 构造方法时 Bean 的依赖还未注入完成,而 @PostConstruct 一定在注入完成后执行。

例如下面代码会失败:

public class TestService {

    @Autowired
    private UserService userService;

    public TestService() {
        // userService 是 null
        System.out.println(userService);
    }
}

而使用 @PostConstruct:

@PostConstruct
public void init() {
    // userService 已经是可用对象
    System.out.println(userService);
}

10. @PreDestroy 为什么有时不执行?

这是开发者最常见的疑惑,原因包括:

10.1 应用被强制 kill(kill -9)

不会触发 Spring 容器关闭。

10.2 使用非可管理 Bean

通过 new 创建的对象没有生命周期管理。

10.3 使用 @ConfigurationProxyBeanMethods = false 但没有使用 @Bean 管理 Bean

10.4 线程池或非守护线程阻塞应用关闭

导致执行不到关闭阶段。

10.5 注解扫描未启用(极老旧项目)


11. 常见错误与排查方法

11.1 方法必须为 void,没有参数

以下写法会导致注解失效:

@PostConstruct
public int init() {
    return 1;
}

@PreDestroy
public void destroy(String param) {
}

正确写法:

@PostConstruct
public void init() {}

11.2 方法不能是 static

错误:

@PostConstruct
public static void init() {}

11.3 方法不能抛出 checked exception

错误:

@PostConstruct
public void init() throws Exception {}

12. 最佳实践

12.1 内部执行逻辑必须轻量

不要在 @PostConstruct 中执行:

  • 大量循环
  • 长耗时逻辑
  • 网络 IO

应把重任务放入线程池。

12.2 建议明确日志输出

@PostConstruct
public void init() {
    log.info("Init xxx service");
}

12.3 如果 Bean 范围不是 singleton,谨慎使用

尤其是 prototype,不会调用 @PreDestroy。

12.4 使用场景边界明确

适用于:

  • 初始化配置
  • 资源建立
  • 内存预热

不适用于:

  • Controller 逻辑
  • 业务流程
  • 数据库写入流程

13. 作者经验总结

  • @PostConstruct 是最干净、最优雅的初始化方式,能保证依赖注入完成后执行。
  • @PreDestroy 在实际生产非常关键,可用来确保资源不会泄露。
  • 线程池、消息客户端、文件句柄等必须在 @PreDestroy 中清理,否则会造成内存泄漏。
  • 如果初始化逻辑过重,应拆分为异步任务,而不是让 Spring 容器启动变慢。
  • 与构造方法相比,@PostConstruct 更适合实际业务场景。
  • 在 Kubernetes 环境中(如 Spring Boot + Docker),只要不是 kill -9@PreDestroy 都会正常执行,非常适合做优雅停机处理。

正文到此结束
评论插件初始化中...
Loading...