likes
comments
collection
share

【重写SpringFramework】生命周期管理(chapter 3-10)容器的生命周期是如何驱动的?容器的生命周期

作者站长头像
站长
· 阅读数 18

1. 前言

在第一章 beans 模块中,我们介绍了 Bean 的生命周期,所有单例的初始化与销毁都是由 BeanFactory 驱动的。事实上,BeanFactory 本身也存在生命周期。而 BeanFactory 主要专注于管理单例,因此其自身的生命周期是委托给 ApplicationContext 来实现的。

本节我们将讨论以下几个问题,容器的生命周期是如何驱动的?容器的生命周期又如何传导到具体组件的?Spring 框架设计 Lifecycle 组件的初衷是什么,又是如何与现有体系进行互补的?

2. Lifecycle 组件

2.1 概述

Lifecycle 组件都是单例,而 BeanFactory 已经有了一套针对 Bean 的管理机制,包括依赖注入、指定初始化和销毁操作等。Lifecycle 组件作为一种特殊的单例,必然存在着通用处理逻辑不能覆盖的场景。我们可以将 Lifecycle 组件的特点归纳为两点,一是独立性,二是异步操作。

BeanFactory 接口中定义的所有 getBean 方法都抛出异常,而 getBean 方法会触发对象的创建流程。在此期间,实例化、填充对象、初始化等任何操作都有可能抛出异常。接下来的问题是,谁来处理这些异常?实际上这些异常是在 AbstractApplicationContextrefresh 方法中被捕获,执行一些销毁操作,然后再次抛出异常,最终导致的结果就是 Spring 容器创建失败。

public abstract class AbstractApplicationContext {
    @Override
    public void refresh() {
        try {
            //初始化操作,略
        } catch(Exception e) {
            destroyBeans();     //销毁已创建的单例
            throw e;            //抛出异常,容器创建失败
        }
    }
}

从这里可以看出,普通单例与容器的关系十分密切,任何一个单例的初始化失败,都会导致容器创建失败。作为对比 Lifecycle 组件在执行 start 方法时,允许出现某些异常而不影响容器的创建,这就是所谓的独立性。第二个特点是异步性。异步操作可以看做是独立性的延申,相对独立意味着能以异步的方式来运行,适合执行一些耗时的操作。

2.2 相关 API

Lifecycle 接口定义了初始化和销毁操作,startstop 方法都没有抛出异常,对比 InitializingBeanDisposableBean 接口定义的方法都抛出了异常。

public interface Lifecycle {
    void start();
    void stop();
    boolean isRunning();
}

LifecycleProcessor 接口的作用是管理 Lifecycle 组件,onRefreshonClose 方法会将启动和停止的信号传递给所有的 Lifecycle 组件。

public interface LifecycleProcessor{
    void onRefresh();
    void onClose();
}

DefaultLifecycleProcessorLifecycleProcessor 接口的默认实现类,持有一个 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)类似。

【重写SpringFramework】生命周期管理(chapter 3-10)容器的生命周期是如何驱动的?容器的生命周期

为了加深理解,我们来看一个实际使用的例子。在微服务系统中,存在两种类型的应用。一种是注册中心,另一种是客户端应用。注册中心负责管理所有的客户端应用,客户端应用则向外界提供服务。以 Spring Cloud 为例,Eureka 服务是一个注册中心,客户端应用则是基于 Spring Boot 的 Web 服务。客户端应用启动时需要完成两个操作,一是启动 Spring Boot 应用,也就是完成容器的创建。二是向 Eureka 服务注册自身的相关信息。

注册操作是一个比较复杂的行为,涉及到远程的网络请求。网络请求可能会遇到各种问题,就拿超时来说,可能是注册中心没有启动或者出现异常,也可能是网络延迟所导致的。因此注册操作允许重试,直到连接上注册中心为止。执行注册操作的组件是 EurekaAutoServiceRegistration,实现了 Lifecycle 接口。感兴趣的读者可以自行查看源码,我们将在第三部 Spring Cloud 中进行详细分析。

3. 容器生命周期

3.1 概述

无论是 Bean 的生命周期,还是 Lifecycle 机制,我们讨论的对象都是单例。实际上,Spring 容器本身也存在生命周期。外层容器的生命周期驱动着内部组件生命周期的变化,而容器的生命周期又受外部环境的影响。所谓的外部环境有多种表现形式,列举几个比较常见的:

  • 在 IDE 上点击启动或停止的图标
  • 在命令行窗口中输入 java -jarkill 等命令
  • 由 Servlet 容器或虚拟机在适当的时机触发

【重写SpringFramework】生命周期管理(chapter 3-10)容器的生命周期是如何驱动的?容器的生命周期

3.2 容器的初始化

容器的初始化就是 Spring 容器的创建过程,核心流程就是 AbstractApplictionContextrefresh 方法。虽然一直没有强调容器的初始化这一概念,但本章的大多数内容都与之有关。我们已经介绍了很多机制,这里只关注 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 容器销毁操作。registerShutdownHookclose 方法只是触发的条件不同,实际的销毁操作都是由 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 方法中,首先检查 activeclosed 两个标记位,确保容器的状态是活跃且未关闭的。接下来主要执行了以下四步,代码都比较简单,仅罗列如下:

  • 调用 publishEvent 方法,发布容器关闭事件(先略过)。
  • 调用 LifecycleProcessoronClose 方法,销毁 Lifecycle 组件。
  • 调用 destoryBeans 方法,销毁所有的单例 Bean,实际调用了 BeanFactorydestroySingletons 方法。
  • 调用 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 组件,在 startstop 方法中打印日志。

//测试类,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 的初始化操作可以异步的,适合耗时较长的操作。

【重写SpringFramework】生命周期管理(chapter 3-10)容器的生命周期是如何驱动的?容器的生命周期

容器的生命周期包括初始化和关闭两个操作,初始化其实就是容器刷新流程,容器关闭有两种途径。一是手动调用 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
评论
请登录