乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@PreDestroy

定位:JSR-250 标准注解,标记在容器销毁 Bean 之前执行的清理方法。它是 Spring 销毁回调的首选推荐方式,与 Spring API 零耦合,确保应用在关闭时优雅释放资源。


定义与作用

@PreDestroy 是 Java 通用注解规范(JSR-250)定义的注解,位于 javax.annotation 包下(Spring 6 / Jakarta EE 9+ 后迁移至 jakarta.annotation)。

其核心语义是:在 Spring 容器关闭、Bean 实例被销毁之前,执行被标注的方法。该方法用于执行资源清理逻辑,例如:

  • 关闭数据库连接池、释放文件句柄
  • 停止后台线程、注销定时任务
  • 清理临时文件、刷新缓冲区到磁盘
  • 向外部服务发送下线通知(如 Eureka 注销、Nacos 下线)

与 DisposableBean.destroy() 相比,@PreDestroy 不强制实现 Spring 接口,代码可在 Jakarta EE 应用服务器等非 Spring 容器中复用。


适用位置与常用属性

项目说明
标注位置方法上(实例方法,不能是静态方法)
方法签名public / protected / package-private,返回类型必须为 void,无参数
执行次数每个 Bean 实例生命周期内仅执行一次
执行时机容器关闭时、DisposableBean.destroy() 之前
属性该注解本身无属性,纯标记注解

约束:一个类中只能有一个方法被 @PreDestroy 标注。若存在多个,Spring 容器关闭时将抛出 BeanCreationException。

作用域限制:@PreDestroy 仅对 singleton 作用域有效。prototype 作用域的 Bean 创建后容器不再跟踪,销毁回调永远不会执行。


核心原理

Spring 容器在 ApplicationContext.close() 或 JVM 关闭钩子触发时,遍历所有 singleton Bean 的销毁回调。CommonAnnotationBeanPostProcessor 负责识别 @PreDestroy 注解,通过反射调用被标注的方法。

与 DisposableBean 的执行顺序

在同一个 Bean 中,如果同时存在 @PreDestroy 和 DisposableBean:

  1. @PreDestroy 方法先执行
  2. destroy() 后执行

这个顺序与初始化阶段的 @PostConstruct / InitializingBean 顺序一致,确保 JSR-250 标准注解优先于 Spring 专有接口。


完整示例:飞翔科技学生管理系统的资源优雅释放

场景简述

飞翔科技后端开发小崔负责学生管理系统的成绩导入模块。该模块使用 ExecutorService 处理批量成绩 Excel 文件的异步解析。CTO 大翔在架构评审会上强调:"系统重启或下线时,正在处理的任务必须完成或安全中断,线程池不能暴力关闭,否则可能丢失学生成绩数据。"

操作前:无销毁回调的代码

小崔最初的实现没有资源清理逻辑,依赖 JVM 退出时强制终止线程:

@Service
public class GradeImportService {

    private final ExecutorService executor = Executors.newFixedThreadPool(4);

    public void importGradesAsync(String filePath) {
        executor.submit(() -> {
            // 解析 Excel、写入数据库
            System.out.println("[GradeImport] 正在导入:" + filePath);
        });
    }

    // 问题:没有清理方法!
    // JVM 退出时线程池中的任务可能被强制中断,导致数据不完整
}

运行结果:

[GradeImport] 正在导入:/uploads/2024_spring_grades.xlsx
[GradeImport] 正在导入:/uploads/2024_autumn_grades.xlsx
# 此时 Tomcat 热部署或应用重启
# 线程被强制中断,数据库中可能出现部分导入的成绩记录

问题分析:

  • 线程池未优雅关闭,正在执行的任务被 Thread.interrupt() 强制中断
  • 已解析但未写入数据库的成绩记录丢失
  • 临时文件未清理,磁盘空间持续增长
  • MySQL 连接池连接未显式释放,可能导致连接泄漏

操作后:使用 @PreDestroy 的完整代码

小崔重构代码,添加 @PreDestroy 方法实现优雅关闭:

import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.io.File;
import java.util.Arrays;

@Service
public class GradeImportService {

    private final ExecutorService executor = Executors.newFixedThreadPool(4);
    private final String tempDir = System.getProperty("java.io.tmpdir") + "/grade_import";

    public GradeImportService() {
        // 确保临时目录存在
        File dir = new File(tempDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        System.out.println("[GradeImportService] 线程池初始化完成,临时目录:" + tempDir);
    }

    public void importGradesAsync(String filePath) {
        executor.submit(() -> {
            try {
                System.out.println("[GradeImport] 开始导入:" + filePath);
                // 模拟解析和写入耗时
                Thread.sleep(2000);
                System.out.println("[GradeImport] 导入完成:" + filePath);
            } catch (InterruptedException e) {
                System.out.println("[GradeImport] 导入任务被中断:" + filePath);
                Thread.currentThread().interrupt();
            }
        });
    }

    /**
     * 容器关闭前,优雅释放资源
     * 1. 停止接收新任务
     * 2. 等待已有任务完成(最多 30 秒)
     * 3. 强制关闭未完成的任务
     * 4. 清理临时文件
     */
    @PreDestroy
    public void shutdownGracefully() {
        System.out.println("[GradeImportService] @PreDestroy 开始执行优雅关闭...");

        // 步骤1:停止接收新任务
        executor.shutdown();
        System.out.println("[GradeImportService] 线程池已停止接收新任务");

        try {
            // 步骤2:等待已有任务完成
            if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
                System.out.println("[GradeImportService] 部分任务未在 30 秒内完成,强制关闭");
                executor.shutdownNow();
            } else {
                System.out.println("[GradeImportService] 所有任务已完成");
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }

        // 步骤3:清理临时文件
        File dir = new File(tempDir);
        if (dir.exists()) {
            File[] files = dir.listFiles();
            if (files != null) {
                Arrays.stream(files).forEach(File::delete);
            }
            dir.delete();
            System.out.println("[GradeImportService] 临时文件已清理");
        }

        System.out.println("[GradeImportService] @PreDestroy 优雅关闭完成");
    }
}

Spring Boot 启动验证:

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

@SpringBootApplication
public class StudentManagementApplication {
    public static void main(String[] args) throws InterruptedException {
        ConfigurableApplicationContext context =
            SpringApplication.run(StudentManagementApplication.class, args);

        GradeImportService service = context.getBean(GradeImportService.class);

        // 提交两个异步导入任务
        service.importGradesAsync("/uploads/2024_spring_grades.xlsx");
        service.importGradesAsync("/uploads/2024_autumn_grades.xlsx");

        // 模拟业务运行 1 秒后系统关闭
        Thread.sleep(1000);
        System.out.println("\n[Main] 系统即将关闭...\n");
        context.close();
    }
}

运行结果及分析:

[GradeImportService] 线程池初始化完成,临时目录:/tmp/grade_import
[GradeImport] 开始导入:/uploads/2024_spring_grades.xlsx
[GradeImport] 开始导入:/uploads/2024_autumn_grades.xlsx

[Main] 系统即将关闭...

[GradeImportService] @PreDestroy 开始执行优雅关闭...
[GradeImportService] 线程池已停止接收新任务
[GradeImport] 导入完成:/uploads/2024_spring_grades.xlsx
[GradeImport] 导入完成:/uploads/2024_autumn_grades.xlsx
[GradeImportService] 所有任务已完成
[GradeImportService] 临时文件已清理
[GradeImportService] @PreDestroy 优雅关闭完成

执行顺序验证:

  1. 容器启动:GradeImportService 实例化,线程池创建
  2. 业务运行:两个导入任务提交到线程池
  3. 容器关闭:context.close() 触发销毁流程
  4. @PreDestroy 执行:停止新任务提交 → 等待已有任务完成 → 清理临时文件
  5. 所有成绩数据完整导入,无资源泄漏

易错场景与面试考点

易错场景一:prototype 作用域 Bean 的 @PreDestroy 不执行

小崔曾将成绩解析器配置为 prototype,期望每次导入都使用新实例,并在导入完成后自动清理:

@Component
@Scope("prototype")
public class GradeParser {

    private final List<String> tempFiles = new ArrayList<>();

    public void parse(String filePath) {
        String tempFile = createTempCopy(filePath);
        tempFiles.add(tempFile);
        // 解析逻辑...
    }

    @PreDestroy
    public void cleanup() {
        // 错误!这个方法永远不会被调用
        tempFiles.forEach(path -> new File(path).delete());
    }
}

问题:prototype Bean 创建后容器不再持有其引用,@PreDestroy 永远不会执行。tempFiles 中的临时文件将持续占用磁盘空间,直到 JVM 退出或手动清理。

解决方案:

// 方案1:改回 singleton,由调用方显式清理
@Component
public class GradeParser {
    public void parse(String filePath) {
        String tempFile = createTempCopy(filePath);
        try {
            // 解析逻辑...
        } finally {
            new File(tempFile).delete(); // 立即清理
        }
    }
}

// 方案2:使用 try-with-resources 或 finally 块在业务方法内清理
// 方案3:自定义作用域管理器(高级用法,不在本章范围)

易错场景二:在 @PreDestroy 中调用可能已销毁的依赖

@Service
public class GradeExportService {

    private final DataSource dataSource;

    public GradeExportService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @PreDestroy
    public void flushBuffer() {
        // 危险!DataSource 可能已先被销毁
        try (Connection conn = dataSource.getConnection()) {
            // 刷新缓冲区...
        } catch (SQLException e) {
            System.out.println("[GradeExportService] 刷新失败:" + e.getMessage());
        }
    }
}

问题分析:Spring 销毁 Bean 的顺序与依赖关系相反,但同一层级内的销毁顺序不保证。若 DataSource 在 GradeExportService 之前销毁,dataSource.getConnection() 将抛出异常。

正确做法:在 @PreDestroy 中仅操作当前 Bean 自身的资源,不依赖其他 Bean 的状态。若必须依赖,使用 @DependsOn 显式控制销毁顺序:

@Service
@DependsOn("studentDataSource") // 确保 DataSource 在本 Bean 之后销毁
public class GradeExportService {
    // ...
}

易错场景三:@PreDestroy 方法执行时间过长阻塞关闭流程

@Service
public class ReportService {

    @PreDestroy
    public void generateFinalReport() {
        // 错误!生成报表可能需要数分钟,阻塞容器关闭
        generateMonthlyReport();
        generateYearlyReport();
    }
}

问题:@PreDestroy 在容器关闭的主线程中同步执行。若方法耗时过长,将导致应用停止时间延长,在 Kubernetes 等环境中可能触发强制终止(SIGKILL)。

正确做法:@PreDestroy 中只做轻量级清理(关闭连接、释放句柄)。耗时操作应在应用关闭前通过业务逻辑或独立钩子完成。

面试考点

Q1:@PreDestroy 对 prototype 作用域有效吗?

无效。prototype Bean 创建后容器不再跟踪其生命周期,销毁回调永远不会执行。若需清理 prototype Bean 的资源,应在业务方法内使用 try-finally 或 try-with-resources 即时清理。

Q2:@PreDestroy 和 DisposableBean.destroy() 的执行顺序?

@PreDestroy 先执行,destroy() 后执行。标准注解优先于 Spring 专有接口,与初始化阶段的顺序规则一致。

Q3:Spring Boot 应用如何确保 @PreDestroy 在 JVM 异常退出时也能执行?

注册 JVM Shutdown Hook:Runtime.getRuntime().addShutdownHook(...)。Spring 的 ConfigurableApplicationContext.registerShutdownHook() 已默认注册,确保 context.close() 在 JVM 退出时被调用。但 SIGKILL(kill -9)无法捕获,此时 @PreDestroy 不会执行。

Q4:@PreDestroy 方法可以抛出异常吗?

可以,但不建议。@PreDestroy 方法抛出的异常会被 Spring 捕获并记录,不会阻止其他 Bean 的销毁流程继续执行。但异常可能导致当前 Bean 的后续清理逻辑中断。


本文边界说明

本文档仅讲解 @PreDestroy 注解。关于初始化阶段的对应注解 @PostConstruct、Spring 接口 InitializingBean / DisposableBean、以及容器扩展点 BeanPostProcessor / BeanFactoryPostProcessor,请分别参阅本章其他独立文档。严禁在讲解 @PreDestroy 时混入 @PostConstruct 或 AOP 代理的详细实现,以保持知识点的原子性和教学清晰度。

上一页
@PostConstruct
下一页
InitializingBean