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

    • 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 核心注解速查表

DisposableBean

定位:Spring 框架专有接口,通过实现 destroy() 方法在容器销毁 Bean 时执行资源释放逻辑。了解即可,新项目优先使用 @PreDestroy。


定义与作用

DisposableBean 是 org.springframework.beans.factory 包下的接口,定义如下:

public interface DisposableBean {
    void destroy() throws Exception;
}

其核心语义是:在 Spring 容器关闭、Bean 实例即将被销毁时,调用 destroy() 方法。开发者可在该方法中执行:

  • 关闭数据库连接池、释放网络连接
  • 停止后台线程、取消定时任务
  • 清理临时文件、关闭文件流
  • 注销外部服务注册(如 Eureka、Nacos)

DisposableBean 是 Spring 早期版本提供的主要销毁机制,在 JSR-250 的 @PreDestroy 出现之前被广泛使用。与 InitializingBean 一样,它强制业务代码依赖 Spring API,现代项目已不推荐直接使用。


适用位置与常用方法

项目说明
实现方式类实现 DisposableBean 接口
必须实现的方法void destroy() throws Exception
执行次数每个 Bean 实例生命周期内仅执行一次
执行时机容器关闭时、@PreDestroy 之后
异常处理方法签名允许抛出 Exception,容器会捕获并记录,不阻止其他 Bean 销毁
作用域限制仅对 singleton 有效,prototype Bean 的 destroy() 永远不会被调用

Spring 官方立场:Spring Framework 官方文档明确说明,DisposableBean 接口不建议在新代码中使用,因为它不必要地将代码与 Spring 耦合。推荐使用 @PreDestroy 或指定 destroy-method。


核心原理

Spring 容器在 ConfigurableApplicationContext.close() 或 JVM 关闭钩子触发时,调用 DefaultSingletonBeanRegistry.destroySingletons() 遍历所有 singleton Bean。对于每个 Bean,先检查是否实现了 DisposableBean,若是则调用 destroy();随后检查是否配置了 destroy-method。

三种销毁方式的执行顺序

在同一个 Bean 中,如果同时存在多种销毁机制:

  1. @PreDestroy 最先执行(由 CommonAnnotationBeanPostProcessor 处理)
  2. DisposableBean.destroy() 其次执行(由 Spring 内部在 @PreDestroy 之后调用)
  3. destroy-method 最后执行(由 Spring 内部在 destroy() 之后调用)

这个顺序与初始化阶段的 @PostConstruct → InitializingBean → init-method 完全对称。


完整示例:飞翔科技学生管理系统的消息队列连接释放

场景简述

飞翔科技架构师白歌在设计学生管理系统的消息通知模块时,要求系统关闭前主动断开与 RabbitMQ 的连接,避免连接在 broker 端残留导致资源浪费。白歌选择用 DisposableBean 演示这一需求,同时向团队说明为何后续应迁移到 @PreDestroy。

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

小崔最初的实现没有连接释放逻辑:

@Service
public class NotificationPublisher {

    private Connection rabbitConnection;
    private Channel channel;

    public NotificationPublisher() throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("rabbitmq.learnto.cn");
        this.rabbitConnection = factory.newConnection();
        this.channel = rabbitConnection.createChannel();
        System.out.println("[NotificationPublisher] RabbitMQ 连接已建立");
    }

    public void publish(String message) {
        // 发送消息...
        System.out.println("[NotificationPublisher] 发送消息:" + message);
    }

    // 问题:没有 destroy 方法!
    // 容器关闭时 rabbitConnection 和 channel 无人关闭
    // RabbitMQ broker 端连接残留,直到心跳超时(默认 60 秒)才释放
}

运行结果:

[NotificationPublisher] RabbitMQ 连接已建立
[NotificationPublisher] 发送消息:成绩发布通知
# 应用重启或关闭
# RabbitMQ 管理后台显示连接数未下降,持续 60 秒后变为 0

问题分析:

  • 每个应用实例在重启时创建新连接,旧连接在 broker 端残留
  • 频繁部署(如 CI/CD 流水线)导致 broker 连接数持续攀升
  • 通道(Channel)未关闭,可能触发 RabbitMQ 的通道数上限告警

操作后:使用 DisposableBean 的完整代码

白歌重构代码,使用 DisposableBean 实现优雅关闭:

import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Service;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

@Service
public class NotificationPublisher implements DisposableBean {

    private Connection rabbitConnection;
    private Channel channel;

    public NotificationPublisher() throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("rabbitmq.learnto.cn");
        factory.setPort(5672);
        factory.setUsername("feixiang");
        factory.setPassword("secret");

        this.rabbitConnection = factory.newConnection();
        this.channel = rabbitConnection.createChannel();
        // 声明队列(幂等操作)
        channel.queueDeclare("student.notification", true, false, false, null);
        System.out.println("[NotificationPublisher] RabbitMQ 连接已建立,队列已声明");
    }

    public void publish(String message) throws Exception {
        channel.basicPublish("", "student.notification", null, message.getBytes());
        System.out.println("[NotificationPublisher] 发送消息到队列:" + message);
    }

    /**
     * 容器关闭时,优雅释放 RabbitMQ 资源
     * 先关闭通道,再关闭连接,顺序不可颠倒
     */
    @Override
    public void destroy() throws Exception {
        System.out.println("[NotificationPublisher] DisposableBean.destroy() 执行资源释放...");

        if (channel != null && channel.isOpen()) {
            channel.close();
            System.out.println("[NotificationPublisher] RabbitMQ Channel 已关闭");
        }

        if (rabbitConnection != null && rabbitConnection.isOpen()) {
            rabbitConnection.close();
            System.out.println("[NotificationPublisher] RabbitMQ Connection 已关闭");
        }

        System.out.println("[NotificationPublisher] 资源释放完成");
    }
}

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 Exception {
        ConfigurableApplicationContext context =
            SpringApplication.run(StudentManagementApplication.class, args);

        NotificationPublisher publisher = context.getBean(NotificationPublisher.class);
        publisher.publish("成绩发布通知:2024 春季学期期末成绩已开放查询");

        System.out.println("\n[Main] 系统即将关闭...\n");
        context.close();
    }
}

运行结果及分析:

[NotificationPublisher] RabbitMQ 连接已建立,队列已声明
[NotificationPublisher] 发送消息到队列:成绩发布通知:2024 春季学期期末成绩已开放查询

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

[NotificationPublisher] DisposableBean.destroy() 执行资源释放...
[NotificationPublisher] RabbitMQ Channel 已关闭
[NotificationPublisher] RabbitMQ Connection 已关闭
[NotificationPublisher] 资源释放完成

关键差异:

  • 操作前:容器关闭时连接无人管理,broker 端连接残留 60 秒,频繁部署导致连接数攀升
  • 操作后:destroy() 在容器关闭时同步执行,通道和连接立即释放,broker 端连接数实时下降

与 @PreDestroy 的对比与迁移

白歌在团队技术分享会上,用同一份需求展示了 DisposableBean 和 @PreDestroy 的差异:

DisposableBean 版本(不推荐)

@Service
public class ResourceCleaner implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        // 清理逻辑
    }
}

缺点:

  • 类必须实现 Spring 接口,与 Spring API 强耦合
  • 若需迁移到 Jakarta EE 或其他 IoC 容器,必须重写销毁逻辑
  • 接口只能有一个 destroy() 方法,无法拆分多个清理步骤

@PreDestroy 版本(推荐)

@Service
public class ResourceCleaner {
    @PreDestroy
    public void cleanup() {
        // 清理逻辑
    }
}

优点:

  • 标准注解,与 Spring 解耦
  • 代码更简洁,意图更明确
  • 可在 Jakarta EE 应用服务器中直接复用

易错场景与面试考点

易错场景一:prototype 作用域实现 DisposableBean

@Component
@Scope("prototype")
public class ReportGenerator implements DisposableBean {

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

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

问题:与 @PreDestroy 一样,DisposableBean.destroy() 对 prototype Bean 无效。容器创建 prototype 实例后不再跟踪,销毁回调永远不会触发。

解决方案:在业务方法内使用 try-finally 即时清理,或改用 singleton 作用域。

易错场景二:destroy() 中抛出异常导致后续 Bean 销毁中断

@Service
public class ServiceA implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        throw new RuntimeException("ServiceA 清理失败");
    }
}

@Service
public class ServiceB implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        System.out.println("[ServiceB] 清理完成");
    }
}

问题:虽然 Spring 会捕获 destroy() 抛出的异常并记录日志,但异常可能导致当前 Bean 的后续清理逻辑中断。更重要的是,若 destroy() 中抛出的异常未被正确处理,在某些旧版本 Spring 中可能影响同一线程内后续 Bean 的销毁流程。

正确做法:在 destroy() 内部捕获所有异常,确保资源释放的健壮性:

@Override
public void destroy() {
    System.out.println("[NotificationPublisher] 开始资源释放...");
    try {
        if (channel != null) channel.close();
    } catch (Exception e) {
        System.out.println("[NotificationPublisher] 通道关闭异常:" + e.getMessage());
    }
    try {
        if (rabbitConnection != null) rabbitConnection.close();
    } catch (Exception e) {
        System.out.println("[NotificationPublisher] 连接关闭异常:" + e.getMessage());
    }
}

易错场景三:在 destroy() 中访问已销毁的依赖

@Service
public class GradeService implements DisposableBean {

    private final NotificationPublisher publisher;

    public GradeService(NotificationPublisher publisher) {
        this.publisher = publisher;
    }

    @Override
    public void destroy() throws Exception {
        // 危险!NotificationPublisher 可能已先被销毁
        publisher.publish("系统即将下线");
    }
}

问题:Spring 销毁 Bean 的顺序与依赖关系相反,但同一层级内的顺序不保证。若 NotificationPublisher 先被销毁,其 channel 和 connection 已关闭,此时调用 publish() 将抛出 AlreadyClosedException。

正确做法:destroy() 中只操作当前 Bean 自身的资源。若必须通知其他组件,使用 Spring 的 ApplicationEvent 机制在关闭前广播事件。

面试考点

Q1:DisposableBean 和 @PreDestroy 有什么区别?

两者都在容器销毁 Bean 前执行清理逻辑,但 DisposableBean 是 Spring 专有接口,侵入性强;@PreDestroy 是 JSR-250 标准注解,与 Spring 解耦,推荐优先使用。执行顺序上,@PreDestroy 先于 destroy()。

Q2:destroy()、@PreDestroy、destroy-method 的执行顺序?

@PreDestroy → destroy() → destroy-method。这个顺序与初始化阶段的 @PostConstruct → afterPropertiesSet() → init-method 完全对称。

Q3:为什么 Spring 官方不推荐 DisposableBean?

因为它将业务代码与 Spring API 强耦合。实现 DisposableBean 的类在非 Spring 容器中无法复用,且接口的单一方法限制了销毁逻辑的拆分。@PreDestroy 和 destroy-method 提供了同等能力且零侵入。

Q4:DisposableBean 的 destroy() 抛出异常会怎样?

Spring 会捕获异常并记录到日志,不会阻止其他 Bean 的销毁流程继续执行。但当前 Bean 的 destroy() 方法中,异常抛出点之后的代码不会执行。因此建议在 destroy() 内部使用 try-catch 包裹每个资源的释放操作。


本文边界说明

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

上一页
InitializingBean
下一页
BeanPostProcessor