Spring 源码阅读 02:ApplicationContext 初始化 Spring 容器

1,028 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

基于 Spring Framework v5.2.6.RELEASE

在使用 XML 文件初始化 Spring 容器时,通常会用到以下的方式:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = context.getBean("user", User.class);
user.sayHello();

其中,beans.xml 文件中定义了一些 Bean,在 ClassPathXmlApplicationContext 的构造方法中,通过读取 beans.xml 中的配置,创建了 Spring 容器。ClassPathXmlApplicationContextApplicationContext 的实现类,ApplicationContext 继承自 BeanFactory。我们可以通过它的 getBean 方法,从 Spring 容器中获取 Bean 对象。

ClassPathXmlApplicationContext 的构造方法

我们从 ClassPathXmlApplicationContext 的构造方法开始,在源码中探索 XML 配置文件是如何加载进来的。

上面的代码中,我们使用了以下的构造方法:

public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
   this(new String[] {configLocation}, true, null);
}

在这里,调用了另外一个构造方法,定义如下:

public ClassPathXmlApplicationContext(
      String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
      throws BeansException {
   super(parent);
   setConfigLocations(configLocations);
   if (refresh) {
      refresh();
   }
}

综合看以上的两个构造方法,这里使用了三个参数:

  • configLocations 是我们创建 ClassPathXmlApplicationContext 时提供的配置文件路径,从方法签名中看出,这里可以提供多个配置文件。
  • refresh 表示是否自动刷新上下文。这里刷新上下文还包括重新加载所有的 BeanDefinition、创建单例 Bean 等。这里的代码中调用方法时的值时 true
  • parent 是当前容器的父容器,这里代码中直接提供了 null

接下来我们看方法体中的内容,主要有三句代码,我们一一来探索。

ClassPathXmlApplicationContext 构造方法中的内容

调用父类构造方法

第一步,以 parent 为参数调用了父类的构造方法。下图是 ClassPathXmlApplicationContext 类的继承关系图:

顺着父类构造方法的调用一路跟踪,我们可以找到以下代码:

public AbstractApplicationContext(@Nullable ApplicationContext parent) {
    this();
    setParent(parent);
}

这里最终调用到了 AbstractApplicationContext 的构造方法,其中有两行逻辑。

  1. 调用了无参构造方法
  2. 调用了 setParent(parent)

我们分别来看。

无参构造方法的代码如下:

public AbstractApplicationContext() {
   this.resourcePatternResolver = getResourcePatternResolver();
}
protected ResourcePatternResolver getResourcePatternResolver() {
   return new PathMatchingResourcePatternResolver(this);
}

这里初始化了 resourcePatternResolver 成员变量,PathMatchingResourcePatternResolver 是一个基于路径匹配的资源解析器。

之后的 setParent 方法源码如下:

public void setParent(@Nullable ApplicationContext parent) {
   this.parent = parent;
   if (parent != null) {
      Environment parentEnvironment = parent.getEnvironment();
      if (parentEnvironment instanceof ConfigurableEnvironment) {
         getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
      }
   }
}

这里将传进来的 parent 参数复制给了对应的成员变量。因为前面的代码中,实际传入的值为 null,因此之后的 if 语句块中的内容我们先不看。

setConfigLocations 处理配置文件路径中的占位符

ClassPathXmlApplicationContext 构造方法的第二步,调用了 setConfigLocations 方法:

public void setConfigLocations(@Nullable String... locations) {
   if (locations != null) {
      Assert.noNullElements(locations, "Config locations must not be null");
      this.configLocations = new String[locations.length];
      for (int i = 0; i < locations.length; i++) {
         this.configLocations[i] = resolvePath(locations[i]).trim();
      }
   }
   else {
      this.configLocations = null;
   }
}

这里的主要步骤,是把我们实例化 ClassPathXmlApplicationContext 时传递的配置文件路径,放到了一个新创建的字符串数组中,并将其赋值给 configLocations 成员变量。

在放入新输入之前,还调用了 resolvePath 方法对文件路径字符串进行了处理,我们看一下这个方法的源码:

protected String resolvePath(String path) {
    return getEnvironment().resolveRequiredPlaceholders(path);
}

再通过 resolveRequiredPlaceholders 方法的接口定义,我们可以了解到,这里主要是为了处理文本中的 ${} 占位符。也就是说,这里给定的路径中可以包含 ${} 占位符,并且在创建 Spring 容器时,Spring 可以处理这些占位符,前提是,在当前的 Spring 环境中,能通过这些占位符找到匹配的值。

refresh 核心方法

在创建容器的构造方法中,refresh 是最核心的方法,这个方法在 AbstractApplicationContext 中定义。我们先看看 refresh 方法的源码:

@Override
public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      // Prepare this context for refreshing.
      // 初始化上下文的信息
      prepareRefresh();

      // Tell the subclass to refresh the internal bean factory.
      // 这里会调用模版方法,通过子类的实现,初始化 BeanFactory 并解析 XML 配置
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      // Prepare the bean factory for use in this context.
      // 对 BeanFactory 进行初识配置
      prepareBeanFactory(beanFactory);

      try {
         // Allows post-processing of the bean factory in context subclasses.
         // 这里默认是一个空的模版方法,子类可以在此处实现逻辑,来注册后处理器
         postProcessBeanFactory(beanFactory);

         // Invoke factory processors registered as beans in the context.
         // 执行 BeanFactory 的后处理器
         invokeBeanFactoryPostProcessors(beanFactory);

         // Register bean processors that intercept bean creation.
         // 注册 Bean 的后处理器
         registerBeanPostProcessors(beanFactory);

         // Initialize message source for this context.
         // 初始化消息源
         initMessageSource();

         // Initialize event multicaster for this context.
         // 初始化时间广播器
         initApplicationEventMulticaster();

         // Initialize other special beans in specific context subclasses.
         // 这里也是一个空的模版方法,子类可以在此处实现逻辑,在容器中的单例 Bean 初始化之前,来初始化一些特殊的 Bean
         onRefresh();

         // Check for listener beans and register them.
         // 注册化监听器
         registerListeners();

         // Instantiate all remaining (non-lazy-init) singletons.
         // 完成 BeanFactory 初始化,实例化所有非延迟加载的单例 Bean
         finishBeanFactoryInitialization(beanFactory);

         // Last step: publish corresponding event.
         // 初始化结束,广播相应的事件
         finishRefresh();
      }

      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }

         // Destroy already created singletons to avoid dangling resources.
         destroyBeans();

         // Reset 'active' flag.
         cancelRefresh(ex);

         // Propagate exception to caller.
         throw ex;
      }

      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}

为了方便理解,我在代码里添加了中文注释,如果你对 Spring 容器初始化的流程有大概的了解,就可以发现,在这个方法中,包含了 Spring 容器初始化的所有核心步骤。

本文先介绍这么多,后面会对 refresh() 方法的详细流程进行探索。