【重写SpringFramework】生命周期管理(chapter 3-10)容器的生命周期是如何驱动的?容器的生命周期
1. 前言
在第一章 beans 模块中,我们介绍了 Bean 的生命周期,所有单例的初始化与销毁都是由 BeanFactory
驱动的。事实上,BeanFactory
本身也存在生命周期。而 BeanFactory
主要专注于管理单例,因此其自身的生命周期是委托给 ApplicationContext
来实现的。
本节我们将讨论以下几个问题,容器的生命周期是如何驱动的?容器的生命周期又如何传导到具体组件的?Spring 框架设计 Lifecycle 组件的初衷是什么,又是如何与现有体系进行互补的?
2. Lifecycle 组件
2.1 概述
Lifecycle 组件都是单例,而 BeanFactory
已经有了一套针对 Bean 的管理机制,包括依赖注入、指定初始化和销毁操作等。Lifecycle 组件作为一种特殊的单例,必然存在着通用处理逻辑不能覆盖的场景。我们可以将 Lifecycle 组件的特点归纳为两点,一是独立性,二是异步操作。
BeanFactory
接口中定义的所有 getBean
方法都抛出异常,而 getBean
方法会触发对象的创建流程。在此期间,实例化、填充对象、初始化等任何操作都有可能抛出异常。接下来的问题是,谁来处理这些异常?实际上这些异常是在 AbstractApplicationContext
的 refresh
方法中被捕获,执行一些销毁操作,然后再次抛出异常,最终导致的结果就是 Spring 容器创建失败。
public abstract class AbstractApplicationContext {
@Override
public void refresh() {
try {
//初始化操作,略
} catch(Exception e) {
destroyBeans(); //销毁已创建的单例
throw e; //抛出异常,容器创建失败
}
}
}
从这里可以看出,普通单例与容器的关系十分密切,任何一个单例的初始化失败,都会导致容器创建失败。作为对比 Lifecycle 组件在执行 start
方法时,允许出现某些异常而不影响容器的创建,这就是所谓的独立性。第二个特点是异步性。异步操作可以看做是独立性的延申,相对独立意味着能以异步的方式来运行,适合执行一些耗时的操作。
2.2 相关 API
Lifecycle
接口定义了初始化和销毁操作,start
和 stop
方法都没有抛出异常,对比 InitializingBean
和 DisposableBean
接口定义的方法都抛出了异常。
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
LifecycleProcessor
接口的作用是管理 Lifecycle 组件,onRefresh
和 onClose
方法会将启动和停止的信号传递给所有的 Lifecycle 组件。
public interface LifecycleProcessor{
void onRefresh();
void onClose();
}
DefaultLifecycleProcessor
是 LifecycleProcessor
接口的默认实现类,持有一个 BeanFactory
的实例。先来看 onRefresh
方法,调用 getLifecycleBeans
方法寻找实现了 Lifecycle
接口的单例,然后依次遍历并执行 start
方法。onClose
方法的实现与之类似,也是遍历容器中的所有 Lifecycle
组件,并调用 stop
方法。
public class DefaultLifecycleProcessor implements LifecycleProcessor {
private ConfigurableBeanFactory beanFactory;
@Override
public void onRefresh() {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
for (Map.Entry<String, Lifecycle> entry : lifecycleBeans.entrySet()) {
Lifecycle bean = entry.getValue();
if(!bean.isRunning()){
bean.start();
}
}
}
@Override
public void onClose() {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
for (Lifecycle lifecycle : lifecycleBeans.values()) {
lifecycle.stop();
}
}
private Map<String, Lifecycle> getLifecycleBeans() {
//遍历Spring容器,寻找实现了Lifecycle接口的单例
Map<String, Lifecycle> beans = new LinkedHashMap<>();
List<String> beanNames = this.beanFactory.getBeanNamesForType(Lifecycle.class);
for (String beanName : beanNames) {
Lifecycle bean = this.beanFactory.getBean(beanName, Lifecycle.class);
beans.put(beanName, bean);
}
return beans;
}
}
2.3 深入思考
我们已经知道了 Lifecycle
组件的特点,那么它是否是必须的呢?换句话说,能否在 Bean 的初始化过程中执行异步操作吗?答案是不行。试想一下,如果某个单例的初始化操作是异步的,并且需要耗费一定的时间。与此同时,Spring 容器创建成功,开始对外提供服务。然后这个单例的异步任务执行失败,Spring 容器又会被关闭,这种情况是绝对不允许发生的。归根结底,Spring 在设计 Bean 的生命周期时,期望的是任何单例初始化失败,需抛出异常,终止容器的创建,这一点与 Java 集合中的快速失败机制(fail-fast)类似。
为了加深理解,我们来看一个实际使用的例子。在微服务系统中,存在两种类型的应用。一种是注册中心,另一种是客户端应用。注册中心负责管理所有的客户端应用,客户端应用则向外界提供服务。以 Spring Cloud 为例,Eureka 服务是一个注册中心,客户端应用则是基于 Spring Boot 的 Web 服务。客户端应用启动时需要完成两个操作,一是启动 Spring Boot 应用,也就是完成容器的创建。二是向 Eureka 服务注册自身的相关信息。
注册操作是一个比较复杂的行为,涉及到远程的网络请求。网络请求可能会遇到各种问题,就拿超时来说,可能是注册中心没有启动或者出现异常,也可能是网络延迟所导致的。因此注册操作允许重试,直到连接上注册中心为止。执行注册操作的组件是 EurekaAutoServiceRegistration
,实现了 Lifecycle
接口。感兴趣的读者可以自行查看源码,我们将在第三部 Spring Cloud 中进行详细分析。
3. 容器生命周期
3.1 概述
无论是 Bean 的生命周期,还是 Lifecycle 机制,我们讨论的对象都是单例。实际上,Spring 容器本身也存在生命周期。外层容器的生命周期驱动着内部组件生命周期的变化,而容器的生命周期又受外部环境的影响。所谓的外部环境有多种表现形式,列举几个比较常见的:
- 在 IDE 上点击启动或停止的图标
- 在命令行窗口中输入
java -jar
或kill
等命令 - 由 Servlet 容器或虚拟机在适当的时机触发
3.2 容器的初始化
容器的初始化就是 Spring 容器的创建过程,核心流程就是 AbstractApplictionContext
的 refresh
方法。虽然一直没有强调容器的初始化这一概念,但本章的大多数内容都与之有关。我们已经介绍了很多机制,这里只关注 Lifecycle 组件是如何初始化的。
在 refresh
方法中,finishRefresh
方法是最后的步骤,创建了 DefaultLifecycleProcessor
实例,并调用 onRefresh
方法即可。Spring 将 Lifecycle 组件的初始化放在了整个流程中非常靠后的位置,仅次于发布事件,从这一点也反应出 Lifecycle 组件是相对独立的。
public abstract class AbstractApplicationContext {
private LifecycleProcessor lifecycleProcessor;
protected void finishRefresh() {
this.lifecycleProcessor = new DefaultLifecycleProcessor(getBeanFactory());
this.lifecycleProcessor.onRefresh();
//发送ContextRefreshedEvent事件(TODO)
}
}
3.3 容器的关闭
关闭容器的操作定义在 ConfigurableApplicationContext
接口中。触发容器关闭有两种方式,close
方法表示手动关闭,registerShutdownHook
方法则是向虚拟机注册一个钩子函数,在虚拟机停止运行之前,以回调的方式通知 Spring 容器,执行销毁流程。在实际使用中,传统的 Web 应用属于第一种方式,而 Spring Boot 应用则是第二种方式。
public interface ConfigurableApplicationContext {
void close();
void registerShutdownHook();
}
AbstractApplicationContext
实现了这两种关闭方式,registerShutdownHook
方法的核心是向虚拟机注册了一个异步操作。close
方法需要注意的是,移除可能存在的钩子函数,避免重复执行 Spring 容器销毁操作。registerShutdownHook
和 close
方法只是触发的条件不同,实际的销毁操作都是由 doClose
方法完成的。
public abstract class AbstractApplicationContext {
private Thread shutdownHook;
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread() {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
@Override
public void close() {
synchronized (this.startupShutdownMonitor) {
//执行实际关闭逻辑
doClose();
//移除已注册的虚拟机钩子,close方法已经显式地关闭了上下文
if (this.shutdownHook != null) {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
}
}
}
在 doClose
方法中,首先检查 active
和 closed
两个标记位,确保容器的状态是活跃且未关闭的。接下来主要执行了以下四步,代码都比较简单,仅罗列如下:
- 调用
publishEvent
方法,发布容器关闭事件(先略过)。 - 调用
LifecycleProcessor
的onClose
方法,销毁 Lifecycle 组件。 - 调用
destoryBeans
方法,销毁所有的单例 Bean,实际调用了BeanFactory
的destroySingletons
方法。 - 调用
onClose
方法,子类可以完成一些自定义操作。比如在 Spring Boot 应用中,EmbeddedWebApplicationContext
关闭了内嵌的 Servlet 容器。
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
//1. 发送容器关闭事件(TODO)
//2. 销毁Lifecycle组件
if (this.lifecycleProcessor != null) {
this.lifecycleProcessor.onClose();
}
//3. 销毁单例Bean
destroyBeans();
//4. 扩展方法,由子类实现
onClose();
this.active.set(false);
}
}
4. 测试
4.1 手动关闭容器
回顾之前与 ApplicationContext
有关的测试方法,我们只调用了 refresh
方法,实际上容器并没有执行关闭流程,而是在代码执行完毕后被虚拟机回收了。本次测试有两个目的,一是触发容器的关闭操作,二是观察 Lifecycle 组件的生命周期的变化。首先定义一个 Lifecycle 组件,在 start
和 stop
方法中打印日志。
//测试类,Lifecycle组件的简单实现
public class LifeCycleBean implements Lifecycle {
@Override
public void start() {
System.out.println("启动Lifecycle组件...");
}
@Override
public void stop() {
System.out.println("销毁Lifecycle组件...");
}
}
在测试方法中,调用 refresh
方法刷新容器,执行 Lifecycle 组件的初始化操作。最后调用 close
方法关闭容器,在此过程中,LifecycleProcessor
会通知所有的 Lifecycle 组件执行销毁操作。
//测试方法
@Test
public void testCloseMethod() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("lifecycleBean", new RootBeanDefinition(LifeCycleBean.class));
context.refresh();
//手动触发
context.close();
}
从测试结果可以看出,LifeCycleBean
分别执行了初始化和销毁操作。
启动Lifecycle组件...
销毁Lifecycle组件...
4.2 自动关闭容器
在测试方法中,调用 registerShutdownHook
方法向虚拟机注册钩子函数。当代码执行完毕,虚拟机在退出之前,以回调的方式通知容器执行关闭操作。
//测试方法
@Test
public void testJvmHook() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("lifecycleBean", new RootBeanDefinition(LifeCycleBean.class));
//注册虚拟机钩子,当程序结束运行
context.registerShutdownHook();
context.refresh();
}
测试结果与上个测试完全一致,说明手动关闭和自动关闭的效果是一样的。
启动Lifecycle组件...
销毁Lifecycle组件...
5. 总结
本节我们介绍了 Spring 的生命周期管理,包括容器的生命周期、Bean 的生命周期,以及 Lifecycle 机制。容器的生命周期由外部环境控制,容器本身驱动着 Bean 和 Lifecycle 组件的生命周期的改变。Bean 的生命周期和 Lifecycle 机制主要是为单例服务的,它们之间既有联系又有区别,以初始化操作为例简单说明:
- Bean 的初始化操作在创建 Bean 的过程中执行,Lifecycle 的初始化操作在刷新容器的最后阶段执行。
- Bean 的初始化操作与容器密切相关,一旦报错会立即终止容器的运行。Lifecycle 的初始化操作较为独立,即使报错也未必影响容器的运行。
- Bean 的初始化是同步操作,应尽量减少耗时,使应用更快的启动。Lifecycle 的初始化操作可以异步的,适合耗时较长的操作。
容器的生命周期包括初始化和关闭两个操作,初始化其实就是容器刷新流程,容器关闭有两种途径。一是手动调用 close
方法关闭容器,二是向虚拟机注册一个钩子函数,当虚拟机关闭的时候,触发容器的关闭操作。这两种关闭操作的效果是一样的,都会触发 Bean 和 Lifecycle 组件的销毁操作。
6. 项目信息
新增修改一览,新增(5),修改(2)。
context
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring
│ └─ context
│ ├─ support
│ │ ├─ AbstractApplicationContext.java (*)
│ │ └─ DefaultLifecycleProcessor.java (+)
│ ├─ ConfigurableApplicationContext.java (*)
│ ├─ Lifecycle.java (+)
│ └─ LifecycleProcessor.java (+)
└─ test
└─ java
└─ context
└─ lifecycle
├─ LifecycleBean.java (+)
└─ LifecycleTest.java (+)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。
转载自:https://juejin.cn/post/7412548172453249076