在上一篇文章中,咱们对 IoC 容器整个执行流程的准备阶段已经分析完毕,如今就差最核心、最复杂的逻辑了。对应到开篇中的代码,也就是下面这段:java
beanDefinitionReader.loadBeanDefinitions(resource);
复制代码
因为这部分涉及的逻辑很复杂,所以我主要分为 4 个步骤来分析:ide
这一篇主要就是解析前的准备工做相关的一些分析了。函数
虽然以前 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();
}
}
}
复制代码
能够发现,在真正执行读取资源以前,Spring 还作了诸多的准备工做:日志
而后才是真正的解析逻辑:doLoadBeanDefinitions(inputSource, encodedResource.getResource());
在真正处理完咱们传入的 resource 后,也会在内存中移除相应的对象,避免内存泄漏的问题。
OK,在下一篇咱们真正进入到解析的逻辑。