【从零构建Spring|第七节】 注册虚拟机钩子, 实现Bean初始化及销毁
【从零构建Spring|第七节】 注册虚拟机钩子, 实现Bean初始化及销毁
Shio【从零构建Spring|第七节】 注册虚拟机钩子, 实现Bean初始化及销毁
日志
重点:客制化 Bean 初始化阶段,用于接口暴漏、数据库数据读取、配置文件加载,链接注册中心暴露 RPC 接口以及在 Web 关闭时执行链接断开,内存销毁
目的:把这些操作交给 Spring 容器自动化处理,即满足用户可以在 xml 中配置初始化和销毁的方法,也可以通过实现类的方式处理,比如我们在使用 Spring 时用到的 InitializingBean, DisposableBean 两个接口。 其实还可以有一种是注解的方式处理初始化操作,不过目前还没有实现到注解的逻辑,后续再完善此类功能。
设计:初始化、销毁的生命周期在 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
的父类 DefaultSingletonBeanRegistry
。 DefaultSingletonBeanRegistry与 ConfigurableBeanFactory 并无直接继承关系。这对于我们正常程序员来说是几乎没法想到的。思考一下我们写程序的时候,难道不是定义一个接口,通过子类去实现吗。
不过转念一想倒也对,DefaultSingletonBeanRegistry 是 SingletonBeanRegistry 子类实现。接口定义了获取单例对象,其子类就有必要对单例对象的生命周期负责(注册,销毁)
关于接口的定义通常都是接口定义获取手段,而不管怎么生成,怎么销毁。这些的实现统统交给子类,要是看到没有实现,那就再继承一次
DefaultSingletonBeanRegistry 是单例对象创建销毁的基本单位(默认调用器),因此不应将销毁方法放入 AbstractBeanFactory,会导致数据脏污,接口职责混乱。也算是认识到 Spring 架构设计的牛逼之处!
销毁方法的实现在没有直接继承关系的 DefaultSingletonBeanRegistry
销毁初始化方法的数据
初始化和销毁方法不同,因为初始化只需要在读取配置文件检测是否有初始化方法即可,其在实例化 Bean 之前,执行 BeanPostProcesser 之后所调用。而销毁方法,无论有没有,他都是在实例化 Bean 之后所注册(不是调用),调用则是由 ApplicationContext 应用上下文所定义的虚拟机钩子 Hook 来调用。因此这里就不能用和初始化一样的逻辑了。因为 Hook 调用时是将所有需要销毁的 Bean 方法统一销毁。我们是不知道 Bean 对象类型的,因此这里就需要使用适配器模式定义一个统一的接口,Hook 调用这个统一的适配器接口就好了,具体的对接通过继承适配器接口即可。
再来回顾一下刚刚所说的,销毁方法是在虚拟机关闭时统一执行的,怎么知道哪些 bean 是需要销毁的呢?要是我的话就封装一个查找是否有销毁方法的方法来逐一寻找,但是 Spring 很聪明,在设计时,直接把初始化和销毁放在同一个阶段读取与调用(实际上调用的不是销毁方法,而是调用销毁方法的注册表)。我一开始还很纳闷为什么两个作用时期不同的方法要放在一起。实则不然,这样做的好处就是在 Bean 创建对象实例时,会把销毁方法都给保存到内存里,方便后续执行销毁动作时候的调用。
销毁方法的具体信息,会通过
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, BeanDefinition 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 实例化、注入属性之后,即在应用上下文容器创建时期
初始化 Bean,反射获取实际的初始化方法并调用,初始化数据加载到内存中
之后注册销毁方法,往需要销毁的集合里存入适配器,适配器里保存了 Bean 信息,待钩子方法使用 close 将其销毁
销毁方法调用在程序关闭时,这个程序关闭时到底是什么时候呢?可通过打断点的方式找到执行钩子事件 close() 的时机