在上一篇文章中,我提到了 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 的底层原理。