为什么 spring 要做优雅停机
我们现在的服务一般都是在 spring 容器运行,如果不做优雅停机,会有以下问题
1、程序中的任务运行到一半,被强行结束,影响到正常业务
2、出现 spring 容器已经关闭,但任务仍在运行的情况,这个时候用到 spring 的部分就会报错
所以理想状态下,停机的时候,先停止我们自己的任务,然后再关闭 spring 的容器
spring 怎么做优雅停机
在用 kill pid 进行停机时,会触发 jvm 钩子函数,spring 很好的利用了这个特性,来看下源码
注册一个钩子函数
// 注册一个钩子,org.springframework.context.support.AbstractApplicationContext#registerShutdownHook 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); } } |
在 doClose 方法里面,会先发布 ContextClosedEvent 事件,然后关闭 spring 容器
// org.springframework.context.support.AbstractApplicationContext#doClose protected void doClose() { // Check whether an actual close attempt is necessary... if ( this .active.get() && this .closed.compareAndSet( false , true )) { // 省略相关代码 try { // Publish shutdown event. // spring 的事件默认是同步执行 publishEvent( new ContextClosedEvent( this )); } catch (Throwable ex) { logger.warn( "Exception thrown from ApplicationListener handling ContextClosedEvent" , ex); } // 省略关闭 spring 容器相关代码 。。。 } } |
因此,我们可以利用 ContextClosedEvent 事件,对应用的一些任务做优雅关停
@Component public class SpringContextClosedListener implements ApplicationListener<ContextClosedEvent> { @Override public void onApplicationEvent(ContextClosedEvent event) { // 关闭应用的任务,比如关闭 kafka 消费,关闭定时任务等等 } } |
spring-boot 优雅停机
spring boot 的 actuator 组件提供了shutdown 端点,可以让我们通过调接口的方式提前对容器进行关闭,而不必等到 jvm 关闭的钩子函数触发时再关闭
直接通过 http 调用 curl localhost:8080/actuator/shutdown 即可
我们来看下源码
// org.springframework.boot.actuate.context.ShutdownEndpoint public Map<String, String> shutdown() { if ( this .context == null ) { return NO_CONTEXT_MESSAGE; } try { return SHUTDOWN_MESSAGE; } finally { Thread thread = new Thread( this ::performShutdown); thread.setContextClassLoader(getClass().getClassLoader()); thread.start(); } } private void performShutdown() { try { Thread.sleep(500L); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } // 主动调用 ApplicationContext 的关闭方法 this .context.close(); } |
主动调用 ApplicationContext 的关闭方法, 如果之前注册过关闭事件的钩子函数,会取消掉
// org.springframework.context.support.AbstractApplicationContext#close public void close() { synchronized ( this .startupShutdownMonitor) { doClose(); // If we registered a JVM shutdown hook, we don't need it anymore now: // We've already explicitly closed the context. if ( this .shutdownHook != null ) { try { Runtime.getRuntime().removeShutdownHook( this .shutdownHook); } catch (IllegalStateException ex) { // ignore - VM is already shutting down } } } } |
可以看到和上面 spring 钩子函数执行的方法一样,因此,我们依然可以用 ContextClosedEvent 监听到,然后自定义自己的逻辑
我可以直接用 Runtime.getRuntime().addShutdownHook 自定义自己的关停逻辑吗
可以,但不建议,因为 jvm 的钩子函数是 并发+ 无序 执行的,你保证不了你的钩子函数和 spring 钩子函数的顺序
除非你的任务逻辑不依赖任何上下文,也不依赖 spring ,否则还是建议放到 spring 的 ContextClosedEvent 事件
多个 ContextClosedEvent 事件的顺序问题
如果有多个 ContextClosedEvent 事件,并且事件有相互依赖关系,请使用 spring 的 order 指定顺序
之前的血泪教训 20200928-上线后cdimond动态配置不生效bug
ContextClosedEvent 事件为什么不生效
1、首先 springboot 的话都会自己注册 spring 关闭事件钩子,所以都是生效的
2、但非 springboot 并且非 web 项目默认没有注册 spring 的关闭钩子,因此需要自己注册下,否则 ContextClosedEvent 事件不会生效
Core Technologies