Spring优雅停机
前言
Spring 优雅停机在某些业务场景下是至关重要的,
比如你的项目里有
- MQ 的消费.
- 消费过程中要去调用外部服务的接口查询所需数据(Feign调用).
- 消费处理完成后,要发送一个 MQ 通知消息.
在停机时,如果不考虑关闭顺序,那么就会出现你还在消费消息,但是外部的接口已经调不通了,或者是你已经处理完成,通知消息却发不出去了,在某些情况下,可以通过消息重试等机制来解决,但是在与资金相关的项目中,停机顺序处理不当,往往就会造成资损.
上面举的这个例子,最佳的关闭顺序应该是:
- 先关闭 MQ 的消费
- 再关闭 Feign 调用和 MQ 的发送
Java 应用的 Shutdown
JVM 为我们提供设置关机 Hook 的 API, 可以像下面这样设置 Hook:
Runtime.getRuntime().addShutdownHook(new Thread(...));
在 Spring 中也是用这种方法注册的 shutdownHook:
AbstractApplicationContext 中的代码如下:
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
this.shutdownHook = new Thread() {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
只要添加这个 shutdown 的 hook,在 JVM 正常关机,比如执行 kill {pid}, kill -15 {pid} 这些命令的时候,就会回调注册的 hook.
但是强制关闭,比如 kill -9 {pid}这些命令执行的时候,就不会调用,毕竟是强杀.
Spring Shutdown流程
通过分析源码,梳理了下,Spring 向 JVM 注册的 shutdownHook 中的具体逻辑.
从上面的流程可以看到,我们可以添加自定义的 Shutdown 逻辑的办法有三种(绿色框):
- 监听 ContextClosedEvent 事件.
- 实现 SmartLifeCycle 接口的 stop() 方法
- 实现 DisposableBean 接口的 destroy() 方法
ContextClosedEvent
监听事件的写法有两种:实现 ApplicationListener 接口,使用 @EventListener 注解.
这里有个需要注意的点:@Order
添加 @Order 注解的目的是为了设定订阅同一事件的 listener 的执行优先顺序.
比如我的 listener 里想手动关闭 Eureka, 即调用 EurekaAutoServiceRegistration.stop() 方法,
但是 EurekaAutoServiceRegistration 中也有一个 ContextClosedEvent 的 listener 中会调用 stop() 方法,
如果不指定 order, 那就没办法保证我自定义的 listener 优先执行.
实现接口
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class ShutdownListener implements ApplicationListener<ContextClosedEvent> { @Override public void onApplicationEvent(final ContextClosedEvent event) { // todo } }
使用注解
@EventListener(ContextClosedEvent.class) @Order(Ordered.HIGHEST_PRECEDENCE) public void onApplicationEvent(ContextClosedEvent event) { // 过滤掉 web 容器, 防止重复处理. if (!event.getApplicationContext().getParent().getClass().getName().equals(SpringApplication.DEFAULT_CONTEXT_CLASS) ) { return; } log.info("on_application_event_context_closed_event"); // 停掉 MQ 的消费 mqConsumer.shutdown(); log.info("context_closed_event, mq_has_been_shutdown"); // 停掉 feignClient eurekaAutoServiceRegistration.stop(); log.info("context_closed_event, feign_client_has_been_stopped"); // 停掉 MQ 的发送 rocketMQTemplate.destroy(); log.info("context_closed_event, mq_template_has_been_destroyed"); }
SmartLifeCycle
SmartLifeCycle 实现了 LifeCycle 接口,是对 LifeCycle 接口的一种增强和扩展:
通过官方文档的描述,可以知道扩展的主要有:
- 继承了 Phased 接口, 用来控制不同的 SmartLifeCycle 的执行优先级( start时,Phased越小,越先执行,shutdown时相反)
- isAutoStartup() 如果该方法返回 true, 则在 SpringApplicationContext 刷新的时候(refesh),会自动调用 start()方法. 而 LifeCycle 中的 start() 方法不会被自动调用.
- 添加了新的方法 stop(Runnable callback), 该方法用于处理并发情况,相应的 SmartLifeCycle 实现类 stop 完成后,应该显式的执行以下 callback 的 run() 方法.
【这里将 callback 方法暴露出去的目的还需要深入探究】
所以,我们可以将自定义的 shutdown 逻辑写在 stop 方法里:
default void stop(Runnable callback) {
my_stop();
callback.run();
}
DisposableBean
实现该接口的 destory() 方法,一般是用来处理当前 Bean 在应用 Shutdown 时的一些善后、清理工作,
如果要实现整个应用级别的优雅停机逻辑,放在这里处理是不合适的.
总结
在选择【监听 ContextClosedEvent】 还是【实现 SmartLifeCycle 接口】时,要根据想要控制的 Bean 默认的 Stop 的时机来决定.
比如,想要在 Feign 关闭之前添加自定义的逻辑, 而却通过【实现 SmartLifeCycle 接口】来实现,那肯定是不可行的,因为 EurekaAutoServiceRegistration 中监听了 ContextClosedEvent 事件,而根据上面分析的 Spring 的 Shutdown 顺序,ContextClosedEvent 事件监听会首先触发执行,在 Spring 执行 SmartLifeCycle 实现类的 stop() 方法时,Feign 已经提早 Stop 完成了.
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。