Spring 源码阅读 06:加载 BeanDefinition 的过程(准备阶段)

前情提要

加载 BeanDefinition 的准备工作

这里先简单介绍一下 BeanDefinition。它是 Spring 框架中一个重要的接口,用来描述 Spring 容器中一个 Bean 实例的各种信息。在 Spring 实例化 Bean 之前,先要加载这些 Bean 的信息,这些信息就会保存在 BeanDefinition 中。

loadBeanDefinitions重载方法

言归正传,先看loadBeanDefinitions 方法的代码:

@Override
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
   Assert.notNull(locations, "Location array must not be null");
   int count = 0;
   for (String location : locations) {
      count += loadBeanDefinitions(location);
   }
   return count;
}

这里对参数中传入的所有配置文件路径进行了遍历,对每一个路径调用重载方法按顺序分别解析。这里调用的重载方法定义如下:

@Override
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
   return loadBeanDefinitions(location, null);
}

这里又调用了一个重载方法,并且我们留意到,增加了第二个参数,传入值为空。方法代码如下:

public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
   ResourceLoader resourceLoader = getResourceLoader();
   if (resourceLoader == null) {
      throw new BeanDefinitionStoreException(
            "Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
   }

   if (resourceLoader instanceof ResourcePatternResolver) {
      // Resource pattern matching available.
      try {
         Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
         int count = loadBeanDefinitions(resources);
         if (actualResources != null) {
            Collections.addAll(actualResources, resources);
         }
         if (logger.isTraceEnabled()) {
            logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
         }
         return count;
      }
      catch (IOException ex) {
         throw new BeanDefinitionStoreException(
               "Could not resolve bean definition resource pattern [" + location + "]", ex);
      }
   }
   else {
      // Can only load single resources by absolute URL.
      Resource resource = resourceLoader.getResource(location);
      int count = loadBeanDefinitions(resource);
      if (actualResources != null) {
         actualResources.add(resource);
      }
      if (logger.isTraceEnabled()) {
         logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
      }
      return count;
   }
}

方法提虽然长,但是我们稍微仔细看一下就会发现,其实这里核心就做了一件事,就是将方法参数中传递进来的配置文件路径,加载成了一个 Resource 对象,然后对这个 Resource 对象调用了另外一个以 Resource 类型为参数的loadBeanDefinitions重载方法。

Resource 是 Spring 中一个非常重要的接口,它是 Spring 对底层资源访问的一个抽象,通过实现 Resource 接口,我们可以开发各种访问底层资源的能力。之前的一篇笔记 Spring 源码阅读:Resource 资源抽象 中曾经详细介绍过 Resource 接口,这里不再做过多介绍。

总之,这里把我们给配置文件路径转换成了一个资源对象,然后从这个资源对象中加载 BeanDefinition。

再看下一个重载方法:

public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
   Assert.notNull(resources, "Resource array must not be null");
   int count = 0;
   for (Resource resource : resources) {
      count += loadBeanDefinitions(resource);
   }
   return count;
}

还是遍历了所有的 Resource 对象,对其调用另一个以单个 Resource 对象作为参数的重载方法。当在 IDE 中查找这个方法定义的时候就会发现,这个重载方法定义在BeanDefinitionReader中,而上面我们看过的几个同名的重载方法都定义在AbstractBeanDefinitionReader中。此时,我们需要找到这个方法的具体实现。

Spring 源码阅读 06:加载 BeanDefinition 的过程(准备阶段)

在之前的源码阅读(Spring 源码阅读:BeanFactory 初始化 )中可以知道,BeanDefinitionReader 对象创建时,它的类型是XmlBeanDefinitionReader,因此我们去这里找方法的实现。

方法体如下:

@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
   return loadBeanDefinitions(new EncodedResource(resource));
}

这里将 Resource 资源封装成了 EncodedResource 后再一次调用了一个重载方法。为了不影响接着向下找 BeanDefinition 加载的逻辑,我们稍后再回过头来看 EncodedResource。继续找到下一个重载方法:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
   Assert.notNull(encodedResource, "EncodedResource must not be null");
   if (logger.isTraceEnabled()) {
      logger.trace("Loading XML bean definitions from " + encodedResource);
   }

   Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();

   if (!currentResources.add(encodedResource)) {
      throw new BeanDefinitionStoreException(
            "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
   }

   try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
      InputSource inputSource = new InputSource(inputStream);
      if (encodedResource.getEncoding() != null) {
         inputSource.setEncoding(encodedResource.getEncoding());
      }
      return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
   }
   catch (IOException ex) {
      throw new BeanDefinitionStoreException(
            "IOException parsing XML document from " + encodedResource.getResource(), ex);
   }
   finally {
      currentResources.remove(encodedResource);
      if (currentResources.isEmpty()) {
         this.resourcesCurrentlyBeingLoaded.remove();
      }
   }
}

这个方法体重,可以看到一句关键的方法调用:

doLoadBeanDefinitions(inputSource, encodedResource.getResource());

根据 Spring 源码中的命名习惯,看到 doXXX() 的方法名,就知道真正的 BeanDefinition 加载逻辑终于找到了。在这个方法的参数列表中,一个是用 Resource 的输入流封装成的 InputSource,用于 XML 的读取,另一个是 Resource 对象本身。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
      throws BeanDefinitionStoreException {

   try {
      Document doc = doLoadDocument(inputSource, resource);
      int count = registerBeanDefinitions(doc, resource);
      if (logger.isDebugEnabled()) {
         logger.debug("Loaded " + count + " bean definitions from " + resource);
      }
      return count;
   }
   /* 略掉一些异常处理的代码 */
}

doLoadBeanDefinitions方法中,主要通过两个步骤来加载 BeanDefinition,第一步是对 XML 文件进行解析,将资源加载到一个 Document 对象中,第二步是调用registerBeanDefinitions方法注册 BeanDefinition。

EncodedResource

到这儿,我们先看一下前面跳过的 EncodedResource 类型。虽然这个类的名字叫做 EncodedResource,但是它并不是 Resource 接口的实现类,不过它实现了 InputStreamSource 接口,也就是 Resource 接口继承的那个接口。

这里重点来分析一下,为什么在执行具体的加载之前,要把 Resource 封装成 EncodedResource,也就是它的作用是什么。先从前文的代码中调用的 EncodedResource 构造方法开始入手:

public EncodedResource(Resource resource) {
   this(resource, null, null);
}
private EncodedResource(Resource resource, @Nullable String encoding, @Nullable Charset charset) {
   super();
   Assert.notNull(resource, "Resource must not be null");
   this.resource = resource;
   this.encoding = encoding;
   this.charset = charset;
}

这里可以看到,除了封装了 Resource 之外,EncodedResource 还有两个成员变量,分别是编码和字符集,这两个会在通过字符流读取资源输入流的时候用到,不过我们这里给到的两个成员变量值都是null

后续

这篇先告一段落,从第一次调用 BeanDefinitionReaderloadBeanDefinitions方法一直找到doLoadBeanDefinitions方法,了解了在真正执行 BeanDefinition 之前,都进行了哪些准备工作。下篇继续探索后面的逻辑。