Spring Shutdown Hook工作机制揭秘
前言
上篇文章,我们讨论了在Spring环境中正确关闭线程池的姿势,抛出了问题并给出了解决方案。本篇,将接着讨论解决方案背后的原理:Spring Shutdown Hook工作机制
源码解析
源码基于Spring Boot 2.1.0.RELEASE
注册Spring Shutdown Hook的时机
首先要找到入口在哪,即Spring Shutdown Hook是在哪注册的,很容易猜想,应该是在应用启动过程中注册的,找到如下源码位置:org.springframework.boot.SpringApplication#refreshContext
(Spring Boot)
Spring Boot 在启动过程中,刷新Context之后,如果registerShutdownHook
开启[默认为true],则会注册一个Shutdown Hook
org.springframework.context.support.AbstractApplicationContext#registerShutdownHook (spring-context) 如下:
image-20200722131219294这里有一点需要注意的是:提供Spring Shutdown Hook能力的是spring-context
,即spring framework
本身的能力,但是将shutdown hook注册进JVM shutdown hook的行为,却是Spring Boot
提供的。也就是说,如果在纯Spring环境下,需要自己手动调用AbstractApplicationContext#registerShutdownHook
注册shutdown hook来支持Spring的优雅关闭
画外音:哪有什么岁月静好,只不过有人(Spring Boot) 替你负重前行
Spring Shutdown Hook的逻辑
接下来看看Spring Shutdown Hook的具体实现逻辑,在org.springframework.context.support.AbstractApplicationContext#doClose
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
// ...(省略)
// 发布Spring 应用上下文的关闭事件,让监听器们有机会在应用关闭之前做出一些响应
publishEvent(new ContextClosedEvent(this));
// 执行lifecycleProcessor的关闭方法,让Lifecycle们有机会在应用关闭之前做出一些响应
this.lifecycleProcessor.onClose();
// 销毁IOC容器里所有单例Bean
destroyBeans();
// 关闭BeanFactory
closeBeanFactory();
// 勾子函数,让子类实现后做各自的资源清理,比如ServletWebServerApplicationContext会实现该勾子函数关闭内嵌的WebServer(Tomcat)
onClose();
this.active.set(false);
}
}
Spring Shutdown Hook 一共做了5件事:
- 发布Spring应用上下文的关闭事件,让监听器们有机会在应用关闭之前做出一些响应
- 执行lifecycleProcessor的关闭方法,让Lifecycle们有机会在应用关闭之前做出一些响应
- 销毁IOC容器里所有单例Bean
- 关闭BeanFactory
- 执行勾子函数,子类实现后做各自的资源清理,比如ServletWebServerApplicationContext会实现该勾子函数关闭内嵌的WebServer(Tomcat)
不得不赞称,站在上层的角度去理解,该段逻辑非常清晰,这样的代码鲜明地为我们展示了编码原则:一个方法内部,代码尽量保持在同一抽象层次
其中第1、第2件事,正是我们在Spring环境中正确关闭线程池的姿势利用到的解决方案:即在第3件事情开始前,通过某些机制通知应用程序对事件做出响应
第1件事与第2件事看起来很像,都是让应用关闭之前做出一些响应,但是有使用场景的区别:
- ContextClosedEvent是应用级别的事件,因此对之做出的响应更适用于全局性的行为
- Lifecycle一般是Bean级别的通知,因此对之做出的响应更适用于单个Bean的行为
接下来看第3件事:org.springframework.context.support.AbstractApplicationContext#destroyBeans
image-20200722205332016这是个模板方法,默认情况下会销毁IOC容器里的单例Bean,子类可以覆盖它并添加一些额外的行为,但是迄今为止,也没有子类覆盖该方法
org.springframework.beans.factory.support.DefaultListableBeanFactory#destroySingletons 方法如下:
image-20200722210024779destroySingletons
是个重载方法,核心逻辑在父类DefaultSingletonBeanRegistry中,调用完父类方法后就清理一下本类涉及的的一些本地缓存数据。我们接着看父类方法的逻辑:
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroySingletons
public void destroySingletons() {
// ...(省略)
String[] disposableBeanNames;
// disposableBeans 是个Map,Key为bean name,value为disposable bean实例
// 即 <beanName, disposableInstance>
// private final Map<String, Object> disposableBeans = new LinkedHashMap<>();
synchronized (this.disposableBeans) {
disposableBeanNames = StringUtils.toStringArray(this.disposableBeans.keySet());
}
// 依次销毁 disposableInstances
for (int i = disposableBeanNames.length - 1; i >= 0; i--) {
destroySingleton(disposableBeanNames[i]);
}
// 清除本类使用到的一些本地缓存
this.containedBeanMap.clear();
this.dependentBeanMap.clear();
this.dependenciesForBeanMap.clear();
// 清除单例缓存
clearSingletonCache();
}
/**
* Clear all cached singleton instances in this registry.
* @since 4.3.15
*/
protected void clearSingletonCache() {
synchronized (this.singletonObjects) {
this.singletonObjects.clear();
this.singletonFactories.clear();
this.earlySingletonObjects.clear();
this.registeredSingletons.clear();
this.singletonsCurrentlyInDestruction = false;
}
}
这个段方法的逻辑也很简单:
- 拿到所有的disposable beans(即实现了DisposableBean接口的bean),依次执行destroySingleton方法,进行资源回收
- 清除本类使用到的一些本地缓存
- 清除单例缓存
2、3清除缓存的动作很简单,就是调用Map#clear\Set#clear
方法,将集合清空
这里有两个缓存Map需要注意:dependentBeanMap
与dependenciesForBeanMap
,它们的定义如下:
/** Map between dependent bean names: bean name to Set of dependent bean names. */
private final Map<String, Set<String>> dependentBeanMap = new ConcurrentHashMap<>(64);
/** Map between depending bean names: bean name to Set of bean names for the bean's dependencies. */
private final Map<String, Set<String>> dependenciesForBeanMap = new ConcurrentHashMap<>(64);
- dependentBeanMap: Bean名称和所有依赖于Bean的名称的映射关系,即:谁依赖我
- dependenciesForBeanMap: Bean名称和Bean所依赖的所有名称的映射关系,即:我依赖谁
命名上很像,不好理解。我举个例子帮助理解:假设A依赖B(即A->B),A依赖C(即A->C),那么,
- dependentBeanMap: <B, [A]>与<C, [A]>
- dependenciesForBeanMap: <A, [B,C]>
此处请先将两个Map映射关系记住,至于具体作用会在下文解释
还有一个缓存Map: containedBeanMap,定义如下:
/** Map between containing bean names: bean name to Set of bean names that the bean contains. */
private final Map<String, Set<String>> containedBeanMap = new ConcurrentHashMap<>(16);
- containedBeanMap: Bean名称和Bean所包含的所有Bean的名称的映射关系,即:我包含谁
这种"我包含谁"的关系在主流的Annotation-Base的场景下已经比较少出现了,要构造这种映射关系,需要是XML-Base,
假设Foo包含Bar,需要通过如下Spring的配置文件进行配置,才会将这种"包含"关系放入containedBeanMap
public class Foo {
private Bar bar;
public Foo(Bar bar) {
this.bar = bar;
}
}
public class Bar {
}
// Spring 配置文件
<bean id="foo" class="com.example.demo.Foo">
<constructor-arg>
<bean class="com.example.demo.Bar"/>
</constructor-arg>
</bean>
从另一个角度看,这也是一种依赖关系:Foo依赖Bar。由于使用该场景的人越来越少,因此简单了解一下containedBeanMap的含义即可
接下来看org.springframework.beans.factory.support.DefaultListableBeanFactory#destroySingleton方法,销毁单个bean,注意跟上文提到的方法的区别,上文是destroySingletons
image-20200722220501030同样的,destroySingleton也是个重载方法,核心逻辑也是在父类DefaultSingletonBeanRegistry中,接着看:org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroySingleton
image-20200723101422658 image-20200723101835006接下来看org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroyBean方法:
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroyBean
protected void destroyBean(String beanName, @Nullable DisposableBean bean) {
// 1. 首先回收所有依赖"我"的beans
Set<String> dependencies;
synchronized (this.dependentBeanMap) {
// Within full synchronization in order to guarantee a disconnected Set
dependencies = this.dependentBeanMap.remove(beanName);
}
for (String dependentBeanName : dependencies) {
// 递归调用DefaultSingletonBeanRegistry#destroySingleton
destroySingleton(dependentBeanName);
}
// 2. 执行DisposableBean的destroy方法,进行资源的回收
bean.destroy();
// 3. 回收"我"包含的所有beans
Set<String> containedBeans;
synchronized (this.containedBeanMap) {
// Within full synchronization in order to guarantee a disconnected Set
containedBeans = this.containedBeanMap.remove(beanName);
}
if (containedBeans != null) {
for (String containedBeanName : containedBeans) {
destroySingleton(containedBeanName);
}
}
// 4. 解除"我"对其他Bean的依赖关系(dependentBeanMap)
synchronized (this.dependentBeanMap) {
for (Iterator<Map.Entry<String, Set<String>>> it = this.dependentBeanMap.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, Set<String>> entry = it.next();
Set<String> dependenciesToClean = entry.getValue();
dependenciesToClean.remove(beanName);
if (dependenciesToClean.isEmpty()) {
it.remove();
}
}
}
// 5. 解除"我"对其他Bean的依赖关系(dependenciesForBeanMap)
this.dependenciesForBeanMap.remove(beanName);
}
这个方法一共做了5件事:
- 首先回收所有依赖"我"的beans
- 执行DisposableBean的destroy方法,进行资源的回收
- 回收"我"包含的所有beans(containedBeanMap)
- 解除"我"对其他Bean的依赖关系(dependentBeanMap)
- 解除"我"对其他Bean的依赖关系(dependenciesForBeanMap)
为了便于理解,我举个例子来分析这整个过程:
- 假设A\B\C三个Bean,A\C都是普通的被Spring管理的Bean,B实现了
DisposableBean
接口,同样被Spring管理 - A依赖B,B依赖C
上图显示初始状态下的依赖关系,以及三个Map各自的数据
此时,要销毁Bean B
-
首先回收所有依赖"我"的beans。通过dependentBeanMap找到"谁依赖我",递归执行
image-20200723133055164destroySingleton
将依赖我的对象先回收掉,由图可知A依赖了B,因此先回收A。该步骤执行完之后,状态如下示: -
执行DisposableBean的destroy方法,进行资源的回收。此处,要执行B的destroy方法,完成资源的回收。一旦该方法执行完毕,说明B就已经完成其使命,可以被回收掉
-
回收"我"包含的所有beans(containedBeanMap)。 由于此处不构造containedBeanMap,为空,此步骤跳过
-
解除"我"对其他Bean的依赖关系(dependentBeanMap)。B被销毁之后,已经是一个"无用"的Bean,但是它本身可能还引用着其它的Bean,这种引用关系仍然被保存在dependentBeanMap里,因此需要把这种引用关系断掉,来保证逻辑语义的正确
-
解除"我"对其他Bean的依赖关系(dependenciesForBeanMap)。同第4步,引用关系仍然可能被保存在dependenciesForBeanMap里,因此需要把这种引用关系断掉,来保证逻辑语义的正确
第4、第5件事是从不同的Map中断掉这种引用关系,因此本质上是同一回事。经过4、5之后,如图示:
image-20200723133241196注意:本文中提及的解除引用关系是指在Map上把依赖关系给删除,而不是真正把对象间的引用给解除;
同理,销毁(回收)Bean同样指的是执行destroy方法进行了资源的回收,并不是真的把Bean给销毁、回收
至此,Spring Shutdown Hook整个执行过程我们已经分析完毕,为了更好理解,接下来会用上篇文章的案例来分析Spring Shutdown Hook的执行过程
案例解析
为了阅读的连续性,此处再把案例阐述一遍
@Resource
private RedisTemplate<String, Integer> redisTemplate;
// org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
@Resource
private ThreadPoolTaskExecutor executor;
@GetMapping("/incr")
public void incr() {
executor.execute(() -> {
// 依赖Redis进行计数
redisTemplate.opsForValue().increment("demo", 1L);
});
}
- 使用Spring的ThreadPoolTaskExecutor,用于异步任务的执行
- 高并发请求/incr接口,每次请求该接口,都会往线程池中添加一个任务,任务异步执行的过程中依赖Redis
此时,上游流量被切断且应用程序收到停机请求,在应用启动之初注册的Spring Shutdown Hook被激活
- 我们此处并不自定义上篇文章中提到的ContextClosedEvent,也不实现Lifecycle接口,因此
发布Spring应用上下文的关闭事件
、执行lifecycleProcessor的关闭方法
这两个过程略过(如果有疑问:这样做,上篇文章提到的问题不就出现了么?不就不能实现优雅关闭线程池了?别急,下面会有答案) - 接着会销毁所有实现了DisposableBean的Bean,很巧的是,ThreadPoolTaskExecutor与JedisConnectionFactory都实现了该接口,因此,依赖关系如图所示:
org.springframework.data.redis.connection.jedis.JedisConnectionFactory#destroy
imageorg.springframework.scheduling.concurrent.ExecutorConfigurationSupport#destroy
image image按照我们上文的分析,ThreadPoolTaskExecutor、JedisConnectionFactory的destroy方法都会被执行:
-
ThreadPoolTaskExecutor#destroy: 执行线程池的优雅关闭
-
JedisConnectionFactory#destroy: 关闭Jedis连接池,回收Jedis连接
那ThreadPoolTaskExecutor与JedisConnectionFactory执行destroySingleton
方法的先后不同,会导致结果的不同吗?
- 假设ThreadPoolTaskExecutor先执行。此时XXXController会先被destroy,然后执行ThreadPoolTaskExecutor#destroy,由于支持优雅关闭,任务理论上已经执行完毕,不再需要使用到RedisTemplate,因此这种情况OK
- 假设JedisConnectionFactory行先执行。此时RedisTemplate会先要求被destroy,进而引发XXXController与ThreadPoolTaskExecutor先行被destroy。此时就进入了第一种情况,因此这种情况也是OK的
可以发现,无论ThreadPoolTaskExecutor、JedisConnectionFactory谁先执行destroySingleton
,结果都是一样的,都能使得线程池被优雅关闭,根本原因就是Spring会找到引用链中的头节点先行销毁,然后依着引用链依次销毁Bean,使得最底层被依赖的对象最晚被销毁
那么为什么上篇文章还会出现Spring环境下线程池未优雅关闭的问题?
那是因为,很多代码会直接使用自定义的JDK线程池,未被Spring管理,也没有找到合适的地方执行shutdown(Now)
+ awaitTermination
。Spring Shutdown Hook执行的时候,只能找到它管理的Bean进行销毁,而我们使用的自定义的JDK线程池既不被Spring管理,也没有实现DisposableBean,Spring必然"看不见"该线程池的存在,直接就把JedisConnectionFactory给回收了,导致线程池里的任务获取连接失败
所以你瞧,使用ThreadPoolTaskExecutor还有这种福利,真是个意外的惊喜,建议大家在Spring环境中都使用它代替直接使用JDK线程池类。 当然,如果有定制线程池的需要,也可以自定义线程池类,然后再实现DisposableBean接口同时把相应的destroy方法实现,同时将实例交给Spring管理,效果也是等价的
那些非DisposableBean beans是如何销毁的?
需要实现资源回收的Bean,需要关注Bean销毁事件的Bean才需要实现DisposableBean接口。我们一般开发过程中使用到的无状态的Controller、Service,是不需要实现DisposableBean接口的--->我们何时关心过它们的销毁呢?所以,我们不关心,Spring当然也不关心,Spring Shutdown Hook 的第3件事"销毁IOC容器里所有单例Bean",只是执行DisposableBean的destroy方法完成资源回收工作,以及清空各种依赖关系的Map和Singleon Cache,但对象本身并没有真实被销毁。因此对于非DisposableBean beans,在接下来应用关闭之后就自动死亡
总结
本篇文章主要分析了Spring Shutdown Hook的执行流程,从源码层面可以看出作者的代码功力非常强,考虑到了多种扩展角度(扩展点机制、模板方法、勾子方法),代码从布局上也非常清晰,同一抽象语义的代码在同一个方法里,易于理解跟阅读,这是非常值得我们学习的地方(敲重点:能从源码中学到什么?)。其次从功能层面可以看到,做为一个成熟的框架,Spring考虑的非常全面:哪些Bean需要先销毁哪些Bean需要后销毁,哪些Bean需要执行资源回收方法,哪些Bean不需要执行资源回收方法都是有考量的,资源回收之后还清理各种本地缓存和映射关系,确保程序逻辑语义的正确。正是Spring考虑的多,所以我们才可以心安理得考虑的少:哪有什么岁月静好,只不过有人替你负重前行