【超详细的Spring源码分析 —— 02 Spring IoC 对于 Bean 管理的核心组件分析 - 准备阶段】

469 阅读6分钟

在上一篇文章中,我提到了 Spring 在宏观上的 IoC 执行流程,也粗略地拆解了整个流程中的核心组件,这些组件包括了:

  • 资源抽象 - Resource
  • 注册器,也可称之为工厂 - DefaultListableBeanFactory
  • 读取器 - BeanDefinitionReader

那么在这一篇文章中,我们来看看 IoC 容器在解析并装配 Bean 之前都需要完成哪些准备工作。

整个分析流程我们参照着 IoC 容器初始化的流程走:

public class SpringClient {
    public static void main(String[] args) {
      	// 1. 首先是创建资源
        Resource resource = new ClassPathResource("applicationContext.xml");

        // 2. 创建工厂(也可以理解为创建BeanDefinition注册器)
      	DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
			
      	// 3. 创建读取器
        BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);

      	// 4. 读取资源, 完成bean装配
        beanDefinitionReader.loadBeanDefinitions(resource);
       
        // 5. 依赖注入        
        Student student = defaultListableBeanFactory.getBean("student", Student.class);
    }
}

一、资源抽象 Resource 的创建

这里我们分析的资源抽象是基于 classpath 的实现 —— ClassPathResource 。下面我们就来详细看一下,Spring 从 classpath 获取资源对象时,底层究竟完成了那些工作。

首先我们来看一下 ClassPathResource 初始化流程涉及到的代码:

public class ClassPathResource extends AbstractFileResolvingResource {

    private final String path; // 资源路径

    // 用于加载资源的类加载器
    // 一般情况下, 我们都是通过 ClassLoader 去加载资源
    @Nullable
    private ClassLoader classLoader; 
   
    // 用于加载资源的类对象
    // 本质上还是会用它获取到对应的 ClassLoader 去加载资源
    @Nullable
    private Class<?> clazz;  

    // 调用一个重载构造
    public ClassPathResource(String path) {
    	this(path, (ClassLoader) null);
    }
    
    public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
    	Assert.notNull(path, "Path must not be null");
    	// 这里会调用一个字符串工具类, 对path进行一些转换, 保证能够被Spring识别并可用
        String pathToUse = StringUtils.cleanPath(path);
        // classLoader不允许path以 "/" 开头 
    	if (pathToUse.startsWith("/")) {
            pathToUse = pathToUse.substring(1);
    	}
        // 将解析后的资源路径赋值到全局变量
    	this.path = pathToUse;
        // 当传入的类加载器不为空时, 就采用指定的类加载器
        // 若传入的类加载器为空, 就通过工具类获取一个类加载器
    	this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
    }
}

Resource 抽象需要完成的工作有两点:

  • 让我们传入的 path 被 Spring 解析为可用的路径字符串。

  • 获取一个 ClassLoader 用于加载配置文件。这个 ClassLoader 会用于读取 xml 配置文件的输入流。

    如果是通过传入 Class 对象构造 Resource,本质上还是会通过这个 Class 获取到 ClassLoader 进行资源加载。

二、DefaultListableBeanFactory 的创建

工厂的创建涉及到的代码都不是很关键,我认为它最关键的是内部封装的实例,像单例缓存池、beanDefinitionMap 这些属性都是我们分析的重中之中。

首先我们需要重点了解单例缓存池,Spring 通过了三级缓存来解决循环依赖的问题,这几个缓存容器分别定义在了 DefaultListableBeanFactory 的父类 DefaultSingletonBeanRegistry 中,它们分别是:

  • private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);,一级缓存,用于缓存已经被完整实例化的 Bean 实例。
  • private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); 二级缓存,用于缓存那些还未被完整实例化的 Bean 实例(比如属性还未被注入的 Bean 实例)。
  • private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); 三级缓存,用于缓存 ObjectFactory,这个对象可以返回一个属性未被注入的 Bean 实例。

至于为什么要使用三个缓存,主要的目的在于解决循环依赖。其实两个缓存就能够解决 Normal Bean 之间的循环依赖问题了,多加一个缓存的目的是为了解决两个代理对象之间的循环依赖问题。

循环依赖相关的内容会在后续进行详细分析。

除了单例缓存池,我们还需要了解 BeanDefinitionMap,private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);,这个 ConcurrentHashMap 保存了通过配置文件解析出的 BeanDefinition 键值对。在后续初始化 Bean 的流程中,会依赖这里存放的 BeanDefinition。

三、BeanDefinitionReader 的创建

按照流程来看,我们的资源、工厂都准备完毕了,之后的工作就是创建 Bean 定义读取器,紧接着需要将资源解析为 BeanDefinition 并组装到工厂。

由于我们采用的是 xml 的形式配置元数据,因此分析 XmlBeanDefinitionReader 相关的内容。

BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);

// 首先是构造方法, 需要传入一个Bean定义注册器,也就是工厂类
// 我们会把解析出来的BeanDefinition注册到这个工厂中
public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
    // 这边会调用父类的构造方法
    super(registry);
}

接下来,我们跟进 XmlBeanDefinitionReader 的父类构造方法:

AbstractBeanDefinitionReader.AbstractBeanDefinitionReader()

// 构造方法的访问限制符是 protect,它主要是交给子类继承的
protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
    //断言:register不允许为空
    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
    // 将注册器实例赋值给全局变量
    this.registry = registry;
    // 如果注册器还同时具备了ResourceLoader的特性
    // 也就是说如果注册器还具备资源读取的功能
    if (this.registry instanceof ResourceLoader) {
        // 那么将该注册器赋值到全局变量resourceLoader, 用于资源加载
        this.resourceLoader = (ResourceLoader) this.registry;
    } else { // 若注册器不提供资源读取的功能, 那么就会初始化一个资源加载器
        // 这里底层会调用工具类获取到一个类加载器
        // 简单来说, resourceLoader这个实例内部封装了类加载器
        this.resourceLoader = new PathMatchingResourcePatternResolver();
    }
    
    // 如果注册器实现了EnvironmentCapable接口(这个接口提供了获取Environment实例的方法)
    // 那么会通过注册器获取到相应的Environment实例
    if (this.registry instanceof EnvironmentCapable) {
        this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
    } else { // 否则会实例化一个标准的Environment
        // Environment 实例会处理 profile、property 相关的内容
        this.environment = new StandardEnvironment();
    }
}

以上就是读取器在创建时需要完成的工作,主要包含如下几个部分:

  • 将传入的注册器赋值到全局变量, 用于后续的 BeanDefinition 注册工作。

  • 配置读取器的 ResourceLoader,用于资源加载。

  • 配置读取器的 Environment,用于处理环境相关的工作,比如程序运行时采用的属性配置、程序运行在什么环境下。

四、总结

截止目前,解析 BeanDefinition 之前的大部分的准备工作都已经就绪,剩下的就是解析 Resource 然后注册 BeanDefinition 到容器中了。

整个准备阶段,注册器这一块相对要更加重要,我们必须清晰地了解注册器中几个重要的容器都负责什么工作,这样才能更好地理解 Spring 装配 Bean 的底层原理。