前言

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 中的具体逻辑.

2023-03-21T11:56:18.png

从上面的流程可以看到,我们可以添加自定义的 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 完成了.

文章目录