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 中,如果同时存在多种销毁机制:
@PreDestroy最先执行(由CommonAnnotationBeanPostProcessor处理)DisposableBean.destroy()其次执行(由 Spring 内部在@PreDestroy之后调用)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 代理机制,以保持知识点的原子性和教学清晰度。