Spring 深入——IoC 容器 02

IoC容器的实现学习——02

回顾

前面学习了 IoC 模式的核心概念,使用场景,以及 Spring 对 IoC

具体实现的两种系列:BeanFactoryApplicationContext

通过两种系列的具体 IoC 容器来帮助我们了解了两个不同的特点,以及面向不同的场景。有利有弊,在开发中需要根据具体需求选择合适的 IoC 具体实现。

其中也通过对 Spring IoC 的具体实现的简单分析,对 IoC 设计的有了初步的了解和想法。那么现在就来开始了解 IoC 容器初始化的过程

IoC 容器的初始化过程:

前面在学习 FileSystemXmlApplicationContext 的时候,构造方法中通过此类调用了 refresh() 方法。IoC 容器的初始化实际上就是通过这个方法来启动的,标志着 IoC 容器正式启动。

IoC 容器的启动包括以下三个基本过程:

  1. BeanDefinition 资源的定位
  2. ~ 的载入
  3. ~ 的注册

期间需要注意的是这是一个顺序过程,同时指的是 IoC 容器的初始化,而 Bean 的依赖注入的实现,一般不包括其中,但是 BeanDefiniton 有一个 lazyinit 的属性,用户可以通过这个属性改变 Bean 的依赖注入过程,eg:一般情况下 Bean 的注入需要在容器初始化之后,第一次调用 getBean() 时才会触发,而通过 lazyinit 属性可以让 Bean 在 IoC 容器初始化时就预先完成了依赖注入。

BeanDefinition 的 Resource 定位

根据前面的学习,我们这一过程的表层应该不难知晓,就是通过定义一个 Resuorce 去定位容器使用的 BeanDefinition。eg:ClassPathResource() 这个类就是在项目中的类路径中寻找以文件形式存在的 BeanDefinition。

应该注意的是,不能把 Resource 的定位 BeanDefinition 资源和 BD 的载入弄混淆了。只是定位资源而已,此时 IoC 容器还不能直接使用这些信息,这些信息是交由 BeanDefinitionReader 来对这些信息进行 BD 的载入处理。

相对于 DefaultListableBeanFactory 容器需要手动配置好特定的 Resource 读取器,ApplicationContext 容器就准备好了一系列的读取器。

但是使用 DefaultListableBeanFactory 这种底层容器可以根据业务定制 IoC 容器的灵活性,有利有弊。

还是通过 FileSystemXmlApplicationContext 这一具体容器来分析是如何完成 Resource 的定位过程。

继承体系:

image-20221021164312140

主要两个功能的源码:

image-20221021164358242

上图表明了 getResourceByPath() 是实现 Resource 定位的方法。但是并不是使用者调用的,查看该方法的调用链:

image-20221021165749280

上图标注了该方法最初是由 refresh() 方法触发的,而 refresh() 是在构造器中调用的。

我们需要通过这个方法来了解过程。

构造器调用的是超类 AbstractApplicationContext 中的 refresh(),查看源码:

public void refresh() throws BeansException, IllegalStateException {
    synchronized(this.startupShutdownMonitor) {
        this.prepareRefresh();
        // 创建beanFactory以及扫描bean信息(beanDefinition),并通过BeanDefinitionRegistry 注册到容器中。
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
        // 省略...

    }
}

obtainFreshBeanFactory() 源码:

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
    this.refreshBeanFactory();
    // 省略...
}

refreshBeanFactory() 是一个抽象方法,有两个具体的实现类:

  • AbstractRefreshableApplicationContext
  • GenericApplicationContext

在这里我们的 FSXAC 继承了 AbstractRefreshableApplicationContext,所以我们看在这个类中 refreshBeanFactory() 的实现:

protected final void refreshBeanFactory() throws BeansException {
//...
    try {
        DefaultListableBeanFactory beanFactory = this.createBeanFactory(); // 1
        beanFactory.setSerializationId(this.getId());
        this.customizeBeanFactory(beanFactory);
        this.loadBeanDefinitions(beanFactory);// 2
///...
    }
}

我们抽出这两行代码进行分析:

  1. 创建 BeanFactory,以 DefaultListableBeanFactory 作为 IoC 容器。
  2. BD 的载入相关启动。

我们到目前位置并没有看到与之相关的 Resource 定位信息,只看到 BD 的载入启动,所以针对 loadBeanDefinitions() 进行进一步分析。该方法调用的是本类的一个抽象方法loadBeanDefinitions(DefaultListableBeanFactory var1),此方法是模板方法,由子类具体实现:

image-20221023183708453

而 FSXAC 就是 AbstractXmlApplicationContext 的子类,所以进而分析这个类的具体实现。

image-20221023184006806

可以看到:

  1. 创建了 XmlBeanDefinitionReader 类,用于将 XML 文件中的 Bean 读取出来并加载。

  2. 调用 XBDR 的 loadBeanDefinitions,开始启动 BeanDefinition 的加载。

  3. 在具体的实现中,分别传入不同的参数,但是在此方法中走判断时,调用了 this.getConfigResources() 这个方法在此类中是返回的 Resource[]null,所以走第二个判断,获取以字符串数组,因为之前在 FSXAC 中就设置好了。

    image-20221023215532354

String[] 传入调用的是 XmlBeanDefinitionReader 的基类 AbstractBeanDefinitionReader 的方法:

image-20221023220101615

这里就是将 String 数组中的字符串,一个一个传入调用本类重载方法,并且对其进行计数

public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
    return this.loadBeanDefinitions(location, (Set)null);
}

public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
    // 取得 ResourceLoader,使用的是 DeaultResourceLoader
    ResourceLoader resourceLoader = this.getResourceLoader(); // 关键代码1
    if (resourceLoader == null) {
        //...略
    } else {
        int loadCount;
        // 调用 DefaultResourceLoader 的 getResource 完成具体的 Resource 定位
        if (!(resourceLoader instanceof ResourcePatternResolver)) { // 关键代码2
            Resource resource = resourceLoader.getResource(location);
            loadCount = this.loadBeanDefinitions((Resource)resource);
            //... 略
        } else {
            // 调用 DefaultResourceLoader 的 getResources 完成具体的 Resource 定位
            try {
                Resource[] resources = ((ResourcePatternResolver)resourceLoader).getResources(location);
                loadCount = this.loadBeanDefinitions(resources);
                //... 略
            } 
            //... 略
        }
    }
}

可以看到最终调用的是两个参数的方法:(String location, Set<Resource> actualResources),通过上面代码的简要分析,我们提取出两个重要的信息:

  1. ResourceLoader 的作用?
  2. DefaultResourceLoader 的 getResource 完成了具体的 Resource 定位

首先第一个,Spring 将资源的定义和加载区分开来,这里需要注意的是资源的加载也就是 Resource 的加载,而不是 BeanDefinition 的加载。Resource 定义了统一的资源(抽象并统一各种资源来源),ResourceLoader 定义了这些资源的统一加载。所以 BeanDefinition 资源的定位过程应该是:将不同 BD 资源获取途径经过 Spring 统一封装为 Resource,再由 ResourceLoader 进行资源加载,获取这些 Resource,给 BeanDefinition 的载入做准备。

而在这个 FSXAC 的例子中,这个 ResourceLoader 就是 DefaultResourceLoader,来看看是怎么具体实现 getResource().

public Resource getResource(String location) {
    Assert.notNull(location, "Location must not be null");
    Iterator var2 = this.protocolResolvers.iterator();

    Resource resource;
    do {
        if (!var2.hasNext()) {
            if (location.startsWith("/")) {
                // 处理以 / 标识的 Resource 定位
                return this.getResourceByPath(location);
            }
			// 处理带有 classpath 表示的 Resource 
            if (location.startsWith("classpath:")) {
                return new ClassPathResource(location.substring("classpath:".length()), this.getClassLoader());
            }

            try {
                // 处理 URL 表示的 Resource 定位
                URL url = new URL(location);
                return new UrlResource(url);
            } catch (MalformedURLException var5) {
                // 处理既不是 classpath 也不是 URL 标识的 Resource 定位
                // 则将 getResource 的责任交给 getResourceByPath(),这个方法时 protected,默认实现是得到一个 ClassPathContextResource 对象,通常会由子类实现该方法。
                return this.getResourceByPath(location);
            }
        }

        ProtocolResolver protocolResolver = (ProtocolResolver)var2.next();
        resource = protocolResolver.resolve(location, this);
    } while(resource == null);

    return resource;
}

通过上述分析,找到了熟悉的方法名: protected Resource getResourceByPath(String path){}

这个方法由子类 FSXAC 实现,这个方法返回的是:FileSystemResource 对象,通过这个对象,Spring 就可以进行相关的 I/O 操作,完成 BeanDefinition 定位。

实际上这么多过程和细节,都是为了实现一个功能,对 path 进行解析,然后生成一个 FileSystemResource 对象,并返回,给 BeanDefinition 载入过程做准备。

实际上 Spring 针对不同类型的 Resource 都准备了对应的实现类,方便我们针对不同场景进行合适的使用,不同的 ApplicationContext 会对应生成其他的 Resource:ClassPathResource、ServletContextResource 等,而且 Resource 接口本身就是继承了 InputStreamSource (这个抽象类唯一的方法是返回一个 InputStream),定义了很多的 I/O 相关的操作,其实现类也主要是针对不同的资源类型做出合适的实现。

小结:

通过 FileSystemXmlApplicationContext 这个 AC 实现原理为例子,初步的了解了 Resource 定位的解决方案,就是通过调用 getResourceByPath() 方法,重写了父类 DefaultResourceLoader 的方法,最后得到了 FileSystemResource 这个类型的 Resource 的定位实现。那么此时这个 Resource 的定位过程已经完成,为 BeanDefinition 的载入创造了 I/O 操作的条件,但是具体的数据还没开始读入。读入就是 BeanDefinition 的载入和解析过程了。

其实 Resource 就是统一了资源的定义,各种 BeanDefinition 定义的资源(File,URL,XML...)都统一抽象成 Resource,所有实现类都需要实现相关的 I/O 操作。

而 ResourceLoader 就是根据某种匹配方式来创建匹配的 Resource,并返回。

将其过程多捋几遍,初步理解其 BeanDefinition 的资源定位过程。下一步就是 BeanDefinition 的载入和解析过程。

posted @ 2022-10-23 23:14  CN_DADA  阅读(301)  评论(0编辑  收藏  举报