欢迎光临
我们一直在努力

Spring 容器关闭

Spring 容器关闭:@EventListener(ContextClosedEvent) 与 @Order

专题:在应用退出时尽早执行清理逻辑,并结合目中 Netty 关闭实践说明。


1. 什么时候会触发?

当 Spring 应用上下文被关闭时(例如调用 ApplicationContext.close()、SpringApplication 正常退出、或 ConfigurableApplicationContext 进入关闭流程),Spring 会发布 ContextClosedEvent。

要点说明
事件类型 org.springframework.context.event.ContextClosedEvent
触发时机 上下文 开始关闭,但 尚未销毁单例 Bean
与 @PreDestroy 的关系 ContextClosedEvent 监听器 先执行;之后才进入 Bean 销毁(@PreDestroy、DisposableBean.destroy())

因此,适合在监听器里做:停 Netty、拒绝新连接、关闭长连接,避免销毁阶段仍有业务线程访问即将失效的 Bean。


2. @EventListener 用法

2.1 基本写法

@Component
public class MyShutdownListener {

@EventListener(ContextClosedEvent.class)
public void onContextClosed(ContextClosedEvent event) {
// 清理逻辑
}
}

项目要求
类注册 类需被 Spring 管理(如 @Component)
注解位置 标注在 方法 上
方法参数 ContextClosedEvent,或更宽的 ApplicationContextEvent
事件指定 ContextClosedEvent.class 表示只监听该事件

2.2 等价写法:ApplicationListener

@Component
public class MyShutdownListener implements ApplicationListener<ContextClosedEvent> {

@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 清理逻辑
}
}

@EventListener 是声明式写法,底层同样由 Spring 的事件机制调度;本项目 PLC 侧 NettyClientContainer 即采用 ApplicationListener<ContextClosedEvent>。


3. @Order(Ordered.HIGHEST_PRECEDENCE) 做什么?

当 同一个事件 有 多个 监听器时,Spring 按 @Order 值排序:数值越小,越先执行。

常量值含义
Ordered.HIGHEST_PRECEDENCE Integer.MIN_VALUE 最高优先级,最先执行
Ordered.LOWEST_PRECEDENCE Integer.MAX_VALUE 最低优先级,最后执行
自定义 如 @Order(1) 介于两者之间,数字越小越靠前

注意: @Order 只影响 同一事件类型 的监听器之间顺序,不会改变 ContextClosedEvent 与 @PreDestroy 的先后(事件始终在 Bean 销毁之前)。

3.1 多监听器示例

@Component
public class FirstListener {
@Order(Ordered.HIGHEST_PRECEDENCE)
@EventListener(ContextClosedEvent.class)
public void first(ContextClosedEvent e) { /* 1 */ }
}

@Component
public class SecondListener {
@Order(1)
@EventListener(ContextClosedEvent.class)
public void second(ContextClosedEvent e) { /* 2 */ }
}

@Component
public class ThirdListener {
@Order(Ordered.LOWEST_PRECEDENCE)
@EventListener(ContextClosedEvent.class)
public void third(ContextClosedEvent e) { /* 3 */ }
}

执行顺序:first → second → third。


4. Spring 关闭阶段顺序(示意)

SingletonBeans

EventListeners

ApplicationContext

Application

SingletonBeans

EventListeners

ApplicationContext

Application

#mermaid-svg-QqvQw6no9VFZ9ZmA{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QqvQw6no9VFZ9ZmA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QqvQw6no9VFZ9ZmA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QqvQw6no9VFZ9ZmA .error-icon{fill:#552222;}#mermaid-svg-QqvQw6no9VFZ9ZmA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QqvQw6no9VFZ9ZmA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QqvQw6no9VFZ9ZmA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QqvQw6no9VFZ9ZmA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QqvQw6no9VFZ9ZmA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QqvQw6no9VFZ9ZmA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QqvQw6no9VFZ9ZmA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QqvQw6no9VFZ9ZmA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QqvQw6no9VFZ9ZmA .marker.cross{stroke:#333333;}#mermaid-svg-QqvQw6no9VFZ9ZmA svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QqvQw6no9VFZ9ZmA p{margin:0;}#mermaid-svg-QqvQw6no9VFZ9ZmA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QqvQw6no9VFZ9ZmA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QqvQw6no9VFZ9ZmA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QqvQw6no9VFZ9ZmA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QqvQw6no9VFZ9ZmA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QqvQw6no9VFZ9ZmA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QqvQw6no9VFZ9ZmA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QqvQw6no9VFZ9ZmA .sequenceNumber{fill:white;}#mermaid-svg-QqvQw6no9VFZ9ZmA #sequencenumber{fill:#333;}#mermaid-svg-QqvQw6no9VFZ9ZmA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QqvQw6no9VFZ9ZmA .messageText{fill:#333;stroke:none;}#mermaid-svg-QqvQw6no9VFZ9ZmA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QqvQw6no9VFZ9ZmA .labelText,#mermaid-svg-QqvQw6no9VFZ9ZmA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QqvQw6no9VFZ9ZmA .loopText,#mermaid-svg-QqvQw6no9VFZ9ZmA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QqvQw6no9VFZ9ZmA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QqvQw6no9VFZ9ZmA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QqvQw6no9VFZ9ZmA .noteText,#mermaid-svg-QqvQw6no9VFZ9ZmA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QqvQw6no9VFZ9ZmA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QqvQw6no9VFZ9ZmA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QqvQw6no9VFZ9ZmA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QqvQw6no9VFZ9ZmA .actorPopupMenu{position:absolute;}#mermaid-svg-QqvQw6no9VFZ9ZmA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QqvQw6no9VFZ9ZmA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QqvQw6no9VFZ9ZmA .actor-man circle,#mermaid-svg-QqvQw6no9VFZ9ZmA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QqvQw6no9VFZ9ZmA :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

Order 越小越先执行

PreDestroy / DisposableBean

close()

publish ContextClosedEvent

例如 RfidServerShutdownListener

destroy singletons


5. 项目实例:RFID 优先关闭

源码:RfidServerShutdownListener.java

@Component
@Slf4j
public class RfidServerShutdownListener {

@Order(Ordered.HIGHEST_PRECEDENCE)
@EventListener(ContextClosedEvent.class)
public void onContextClosed(ContextClosedEvent event) {
log.info("Spring 容器关闭,停止 RFID 服务…");
RfidServerHandler.setAcceptingMessages(false);
RfidServerHandler.closeAllConnections();
RfidServerManager.stopServerNow();
}
}

5.1 三步分别在做什么?

步骤调用作用
1 setAcceptingMessages(false) Netty 业务线程不再处理新报文(静态开关)
2 closeAllConnections() 关闭所有活跃 Channel,清空连接集合
3 stopServerNow() 立即停止 RFID 服务端线程,并完成 pending 请求

5.2 为什么要 HIGHEST_PRECEDENCE?

RFID 的 RfidServerHandler 在 Netty IO 线程里会通过 SpringUtils.getBean(…) / getBeanSafely(…) 取 Spring Bean(如 ICacheService、IRfidRecordService)。

容器关闭后若 Netty 仍在收包:

  • 可能触发 BeanCreationNotAllowedException(销毁阶段禁止创建/获取 Bean)
  • 或访问已失效的上下文

因此需要在 Bean 销毁之前、且尽量早于其它 ContextClosedEvent 监听器,先停 Netty 并拒收消息。

5.3 配套:getBeanSafely 与 acceptingMessages

SpringUtils.getBeanSafely:容器非 active 或 Bean 不可用时返回 null,避免 Netty 线程在收尾阶段抛异常。

RfidServerHandler 在关键路径使用 getBeanSafely,与关闭监听器形成 双保险(先停服务 + 取 Bean 时容错)。


6. 与本项目其它关闭方式对比

方式典型位置执行阶段特点
@EventListener + @Order(HIGHEST) RfidServerShutdownListener ContextClosedEvent,最早一批 显式最高优先级,专管 RFID
ApplicationListener<ContextClosedEvent> NettyClientContainer 同上,默认 Order 无 @Order,与其它监听器按默认规则排序
@PreDestroy / DisposableBean PLC、Disruptor、WebSocket 等 Bean 销毁时 晚于 ContextClosedEvent
JVM ShutdownHook ApplicationShutdownManager 进程退出 与 Spring 关闭可能叠加,注意勿重复关或顺序冲突

实践建议:

  • 长连接 / 后台线程:优先在 ContextClosedEvent(必要时加 @Order)中停止。
  • 仅释放本 Bean 持有的资源:可用 @PreDestroy。
  • JVM 钩子适合「Spring 未起来或 close() 未走到」的兜底,不宜替代 Spring 内优雅关闭的唯一入口。

7. 何时选用哪种机制?

场景推荐
停 Netty / 消息队列 / 定时任务,且要在 Bean 销毁前完成 ContextClosedEvent + @Order
只关闭当前 Bean 内的连接、线程池 @PreDestroy 或 DisposableBean
需要随 Spring 生命周期自动 start/stop 的组件 SmartLifecycle(stop() 在关闭流程中调用)
进程被 kill、IDE 停止、未调用 context.close() JVM ShutdownHook 作补充

8. 最小模板(可复制)

package com.example.shutdown;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ResourceShutdownListener {

@Order(Ordered.HIGHEST_PRECEDENCE)
@EventListener(ContextClosedEvent.class)
public void onContextClosed(ContextClosedEvent event) {
log.info("ApplicationContext closing, cleaning up resources…");
// 1. 停止接受新请求
// 2. 关闭现有连接
// 3. 停止后台线程 / 服务器
}
}


9. 反模式与注意点

  • 在监听器里长时间阻塞 会拖慢整个上下文关闭;耗时操作应设超时或异步收尾并配合 CountDownLatch(需谨慎,避免进程直接退出)。

  • 在 ContextClosedEvent 里依赖即将销毁的 Bean 其它 Bean 可能已在关闭链路上;优先调用静态/基础设施层关闭 API(如本项目的 RfidServerManager.stopServerNow())。

  • 与 @PreDestroy 重复关闭同一资源 应保证幂等(如 AtomicBoolean isShuttingDown),NettyClientContainer 即采用关闭标志避免重复添加连接。

  • 误以为 @Order 能早于 JVM 钩子 JVM 钩子与 Spring 事件是不同维度;需统一设计「谁先关 Netty、谁再 context.close()」。


  • 10. 相关 Spring 类与文档

    • org.springframework.context.event.ContextClosedEvent
    • org.springframework.context.event.EventListener
    • org.springframework.core.annotation.Order
    • org.springframework.core.Ordered

    赞(0)
    未经允许不得转载:171主机测评 » Spring 容器关闭
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址