【超详细的Spring源码分析 —— 03 Spring对于Bean管理的核心组件源码分析 - 解析前的准备工做】

在上一篇文章中,咱们对 IoC 容器整个执行流程的准备阶段已经分析完毕,如今就差最核心、最复杂的逻辑了。对应到开篇中的代码,也就是下面这段:java

beanDefinitionReader.loadBeanDefinitions(resource);
复制代码

因为这部分涉及的逻辑很复杂,所以我主要分为 4 个步骤来分析:ide

  1. 解析前的准备工做
  2. 解析 Bean Definition
  3. 初始化 Bean Definition
  4. 注册到工厂

这一篇主要就是解析前的准备工做相关的一些分析了。函数

1、解析前的准备工做

虽然以前 Spring 已经作了足够多的准备工做了,可是在解析以前,还须要完成一些准备事项,下面咱们一步一步来看这部分的源码。学习

首先咱们进入到 loadBeanDefinitions() 这个函数:this

/** * 该方法须要传入须要解析的资源对象 * * 它会从指定的资源中, 读取bean定义 * 这里的资源, 就是咱们以前建立的Resource对象 * * 这个方法的返回值,就是解析的Bean个数 */
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    // 这里会先对resource封装一些编码、字符集的处理,而后调用一个重载方法
    
    return loadBeanDefinitions(new EncodedResource(resource));
}
复制代码

咱们接着深刻EncodedResource这个构造方法:编码

/** * 根据给定的resource建立一个新的EncodedResource * 这个构造方法没有指定编码格式或字符集 */
public EncodedResource(Resource resource) {
    // 这里会调用下面贴出的构造方法
    this(resource, null, null);
}

// 注意这个构造方法是私有的
// 由于编码和字符集咱们只须要指定其一便可, 不须要两者都指定
// Spring 也分别提供了参数为(resource + encoding) 和 (resource + charset)的构造方法
// 这种设计方式也是十分值得学习借鉴的
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;
}

// 针对encoding + resource的构造
public EncodedResource(Resource resource, @Nullable String encoding) {
    this(resource, encoding, null);
}

// 针对encoding + charset的构造
public EncodedResource(Resource resource, @Nullable Charset charset) {
    this(resource, null, charset);
}
复制代码

其实在 Spring 在给 resource 封装编码属性时,所采起的设计方式是很是优雅的。spa

由于编码字符处理与读取资源并无任何逻辑关联,所以 Spring 并无在 loadBeanDefinitions() 方法中增长两个 encoding、charset 参数,而是将 resource、encoding、charset 封装到一个 EncodedResource 类中,而后经过重载来实现读取资源的方法。简直是将面向对象的封装特性发挥地淋漓尽致。线程

回到正题,当编码字符集封装完毕后,会进入到 loadBeanDefinitions() 的另外一个重载方法,这个方法须要传入一个封装了字符处理的资源对象。设计

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    // 断言:encodedResource 不为空
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    // 记录一下日志
    if (logger.isTraceEnabled()) {
        logger.trace("Loading XML bean definitions from " + encodedResource);
    }
    // this.resourcesCurrentlyBeingLoaded是一个ThreadLocal<Set<EncodedResource>>类型的对象
    // 它用于存储当前线程私有的EncodedResource集合
    // 这里会获取到当前线程私有的资源集合
    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
    // 若是获取到的资源集合Set为null
    if (currentResources == null) {
        // 那么会初始化一个set, 而后将其set到threadLocal中
        currentResources = new HashSet<>(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }
    // 尝试将resource资源添加到set, 若是添加失败了则抛出异常
    // 这里提一下:
    // Spring提供了 <import> 标签, 用于在xml配置文件中导入其余多个配置文件
    // 若是 A.xml import B.xml, 且 B.xml import A.xml
    // 那么在加载 A.xml 的时候也会加载 B.xml
    // 但 B.xml 是依赖于 A.xml 的, 这就会致使A、B之间的循环导入
    // 为了不这种状况, 这里经过set来进行一个去重
    // 简单来讲, 就是解决资源的循环依赖问题
    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    }
    // 到这里, 就是真正读取配置文件的核心逻辑了
    // 首先是一个 try-with-resource 语句, 获取到了resource的输入流
    try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
        // 而后会将inputStream封装为InputSource
        // 这里提一下:
        // 解析xml的方式有两种
        // 1. DOM: 将整个xml读取到内存进行解析
        // 2. SAX: 以文件流的形式逐行加载到内存并解析
        // 这里采用的是后者
        InputSource inputSource = new InputSource(inputStream);
        // 若是encodedResource被设置了编码格式, 那么在解析的时候采用指定编码格式
        // 默认状况下, 是没有设置编码格式、字符集的, 这里以前源码中有分析到
        if (encodedResource.getEncoding() != null) {
            inputSource.setEncoding(encodedResource.getEncoding());
        }
        // 而后就是真正的解析逻辑了
        // 这里会调用 doLoadBeanDefinitions, 传入一个InputSource、以及 resource
        // 还有一个小细节须要注意:Spring中的大部份内部方法都是以 ”do“ 开头的
        // 这些方法是 Spring 不但愿咱们调用的
        return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
    } catch (IOException ex) {
        throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), ex);
    } finally {
        // 为了不内存泄漏
        // 在资源被加载完毕后, 会在Set集合中删除掉resource
        currentResources.remove(encodedResource);
        // 而且当资源集合为空时, 删除掉threadLocal中的元素
        if (currentResources.isEmpty()) {
            this.resourcesCurrentlyBeingLoaded.remove();
        }
    }
}
复制代码

2、总结

能够发现,在真正执行读取资源以前,Spring 还作了诸多的准备工做:日志

  • 字符集处理
  • 建立/获取线程私有的资源Set集合
  • 处理配置文件的循环依赖
  • 定义解析 xml 的方式(sax)

而后才是真正的解析逻辑:doLoadBeanDefinitions(inputSource, encodedResource.getResource());

在真正处理完咱们传入的 resource 后,也会在内存中移除相应的对象,避免内存泄漏的问题。

OK,在下一篇咱们真正进入到解析的逻辑。

相关文章
相关标签/搜索