【从零构建Spring|第七节】 注册虚拟机钩子, 实现Bean初始化及销毁

【从零构建Spring|第七节】 注册虚拟机钩子, 实现Bean初始化及销毁

日志

重点:客制化 Bean 初始化阶段,用于接口暴漏、数据库数据读取、配置文件加载,链接注册中心暴露 RPC 接口以及在 Web 关闭时执行链接断开,内存销毁

目的:把这些操作交给 Spring 容器自动化处理,即满足用户可以在 xml 中配置初始化和销毁的方法,也可以通过实现类的方式处理,比如我们在使用 Spring 时用到的 InitializingBean, DisposableBean 两个接口。 其实还可以有一种是注解的方式处理初始化操作,不过目前还没有实现到注解的逻辑,后续再完善此类功能。

设计:初始化、销毁的生命周期在 Bean 加载以及注册阶段,如图所示

image-20230209214627168

设计

手写Spring-Bean初始化和销毁

适配器模式

销毁方法有两种甚至多种,目前有 实现接口 DisposableBean、配置信息 destroy-method 两种方式。销毁方法是交由 ApplicationContext 应用上下文在注册虚拟机钩子后,虚拟机关闭前执行的操作动作。在销毁执行时,不希望 Spring 还得关注要销毁哪些类型的方法。它的使用更希望有一个统一的接口执行销毁,所以这里新增了适配器模式做统一处理

详细代码:

适配器内部可保存需要销毁 bean 的信息,并且实现了 DisposableBean,本质上就是一个 bean,具体是怎么调用到这个适配器的,可以看看最后面的流程分析

信息的读取

BeanDefinition 新增两个属性:initMethodName、destroyMethodName。目的是为了在 Spring.xml 配置的 Bean 对象中可配置init-method="initMethod" destroy-method="destroyDataMethod" 操作。用接口实现也是一样的,只不过一个是接口方法直接调用,一个是配置文件读取方法反射调用

bean属性定义新增初始化和销毁后,需要在 XmlBeanDefinitionReader 中,添加对新增属性的读取,并将其存入 BeanDefinition 中

销毁方法

销毁核心方法 destroySingletons 接口方法定义在 ConfigurableBeanFactory,实现却不是该接口的子类 AbstractBeanFactory,而是 AbstractBeanFactory 的父类 DefaultSingletonBeanRegistryDefaultSingletonBeanRegistry与 ConfigurableBeanFactory 并无直接继承关系。这对于我们正常程序员来说是几乎没法想到的。思考一下我们写程序的时候,难道不是定义一个接口,通过子类去实现吗。

不过转念一想倒也对,DefaultSingletonBeanRegistry 是 SingletonBeanRegistry 子类实现。接口定义了获取单例对象,其子类就有必要对单例对象的生命周期负责(注册,销毁)

关于接口的定义通常都是接口定义获取手段,而不管怎么生成,怎么销毁。这些的实现统统交给子类,要是看到没有实现,那就再继承一次

DefaultSingletonBeanRegistry 是单例对象创建销毁的基本单位(默认调用器),因此不应将销毁方法放入 AbstractBeanFactory,会导致数据脏污,接口职责混乱。也算是认识到 Spring 架构设计的牛逼之处!

image-20230222161647437

销毁方法的实现在没有直接继承关系的 DefaultSingletonBeanRegistry

销毁初始化方法的数据

初始化和销毁方法不同,因为初始化只需要在读取配置文件检测是否有初始化方法即可,其在实例化 Bean 之前,执行 BeanPostProcesser 之后所调用。而销毁方法,无论有没有,他都是在实例化 Bean 之后所注册(不是调用),调用则是由 ApplicationContext 应用上下文所定义的虚拟机钩子 Hook 来调用。因此这里就不能用和初始化一样的逻辑了。因为 Hook 调用时是将所有需要销毁的 Bean 方法统一销毁。我们是不知道 Bean 对象类型的,因此这里就需要使用适配器模式定义一个统一的接口,Hook 调用这个统一的适配器接口就好了,具体的对接通过继承适配器接口即可。

再来回顾一下刚刚所说的,销毁方法是在虚拟机关闭时统一执行的,怎么知道哪些 bean 是需要销毁的呢?要是我的话就封装一个查找是否有销毁方法的方法来逐一寻找,但是 Spring 很聪明,在设计时,直接把初始化和销毁放在同一个阶段读取与调用(实际上调用的不是销毁方法,而是调用销毁方法的注册表)。我一开始还很纳闷为什么两个作用时期不同的方法要放在一起。实则不然,这样做的好处就是在 Bean 创建对象实例时,会把销毁方法都给保存到内存里,方便后续执行销毁动作时候的调用。

销毁方法的具体信息,会通过

protected void registerDisposableBeanIfNecessary(String beanName, Object bean, BeanDefinition beanDefinition) {
if (bean instanceof DisposableBean || StrUtil.isNotEmpty(beanDefinition.getDestroyMethodName())) {
registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, beanDefinition));
}
}

方法注册到 DefaultSingletonBeanRegistry 中新增的 DisposableBean 待销毁集合 Map<String, DisposableBean> disposableBeanMap

这个接口的方法,最终会被类 AbstractApplicationContext 的 close 方法通过 getBeanFactory().destroySingletons() 调用

在注册销毁方法的时候,会根据是接口类型和配置类型统一交给 DisposableBeanAdapter 销毁适配器类来做统一处理。实现了某个接口的类可以被 instanceof 判断或者强转后调用接口方法

钩子 Hook 销毁程序

Java提供了一个程序退出处理机制:Runtime.getRuntime().addShutdownHook(new Thread()),首先通过Runtime.getRuntime()获得当前的程序对象(这是一个静态方法),然后通过Runtime中的void addShutdownHook(Thread hook)方法来向java的虚拟机(JVM)注册一个shutdown的钩子事件,这样程序一旦结束,就会运行线程hook。在实际业务中,我们只需要将程序结束之前需要做的一些工作放在线程hook来完成就可以了。

Runtime.getRuntime().addShutdownHook(new Thread(this::close)); 

用于捕捉程序退出时刻,在程序退出时处理必要的退出准备,如关闭网路、关闭文件

对于单线程程序而言,我们退出程序无非使用 System.exit(0)进行关闭

但对于多线程程序而言,很难把握程序退出的时机,但有些业务情况很需要捕捉程序退出的一刻对程序进行必要处理,就好像 Spring 框架中需要及时销毁已经实例化的 Bean,防止内存愈发庞大。因此可以使用钩子方法

这个方法在一些中间件和监控系统的设计也能用到,例如监测服务器宕机,执行备机启动操作

参考:

Bean 对象内部继承InitializingBean, DisposableBean两个接口实现

流程

本节实现了上一节上下文中没有完成的初始化,如图:

上文提到初始化的调用在 Bean 实例化、注入属性之后,即在应用上下文容器创建时期

  1. 初始化 Bean,反射获取实际的初始化方法并调用,初始化数据加载到内存中

  2. 之后注册销毁方法,往需要销毁的集合里存入适配器,适配器里保存了 Bean 信息,待钩子方法使用 close 将其销毁

销毁方法调用在程序关闭时,这个程序关闭时到底是什么时候呢?可通过打断点的方式找到执行钩子事件 close() 的时机