容器的基本用法

熟悉 Spring 的朋友应该都很了解下段代码:

public void testBeanFactory() {
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
TestBean testBean = bf.getBean("testBean");
}

一段简单的通过容器获取 Bean 的代码,它所完成的功能无非就是以下几点:

  • 读取配置文件 beanFactoryTest.xml
  • 根据 beanFactoryTest.xml 中的配置找到对应类的配置,并实例化
  • 获取实例化后的实例

接下来我们来分析这段代码的实现原理

Spring 核心类介绍

在开始正式的源码分析之前,有必要先了解 Spring 核心的两个类

1. DefaultListableBeanFactory

XmlBeanFactory 继承自 DefaultListableBeanFactory,DefaultListableBeanFactory 是整个 bean 加载的核心部分,是 Spring 注册及加载 bean 的默认实现。XmlBeanFactory 与 DefaultListableBeanFactory 的不同之处就在于 XmlBeanFactory 使用了自定义的 XML 读取器 XmlBeanDefinitionReader

2. XmlBeanDefinitionReader

在 XmlBeanDefinitionReader 中主要包含以下几步处理:

  1. 通过继承自 AbstractBeanDefinitionReader 的方法,使用 ResourceLoader 将资源文件路径转换为对应的 Resoure 文件
  2. 通过 DocumentLoader 对 Resource 文件进行转换,将 Resource 文件转换为 Document 文件
  3. 通过实现 BeanDefinitionDocumentReader 的 DefaultBeanDefinitionDocumentReader 类对 Document 进行解析,并使用 BeanDefinitionParserDelegate 对 Element 进行解析

容器基础 XmlBeanFactory

接下来我们将深入分析以下代码的功能实现

BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));

通过 XmlBeanFactory 初始化时序图,我们来看一看上面代码的执行逻辑

1. 封装配置文件

Spring 的配置文件读取是通过 ClassPathResource 封装成 Resource,Resource 的结构如下:

public interface Resource extends InputStreamSource {
boolean exists();
default boolean isReadable() {
return this.exists();
}
default boolean isOpen() {
return false;
}
default boolean isFile() {
return false;
}
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(this.getInputStream());
}
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String var1) throws IOException;
@Nullable
String getFilename();
String getDescription();
}

Resource 接口抽象了所有 Spring 内部使用的底层资源:File、URL、Classpath 等等,并定义了有关资源操作的方法。对于不同来源的资源文件,都有对应的 Resource 实现:文件(FileSystemResource)、Classpath(ClasspathResource)、URL(UrlResource )、InputStream(InputStreamResource)、Byte(ByteResource)等等,有了 Resource 接口就可以对所有资源文件进行统一处理,至于处理的实现其实很简单,以 ClasspathResource 为例,实现方式就是通过 class 或者 classLoader 提供的底层方式进行调用

2. 数据准备阶段

通过 Resource 完成配置文件的封装以后,就将 Resource 作为 XmlBeanFactory 的构造函数参数传入,代码如下:

public XmlBeanFactory(Resource resource) throws BeansException {
this(resource, (BeanFactory)null);
}

构造函数内部再次调用内部构造函数:

public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
super(parentBeanFactory);
this.reader = new XmlBeanDefinitionReader(this);
this.reader.loadBeanDefinitions(resource);
}

this.reader.loadBeanDefinitions(resource); 是整个资源加载的切入点,这个方法的处理过程如下:

  1. 对参数 Resource 使用 EncodedResource 类进行封装
  2. 从 Resource 获取对应的 InputStream 并构造 InputSource
  3. 通过构造的 InputSource 实例和 Resource 实例继续调用函数 doLoadBeanDefinitions

我们来看一下 loadBeanDefinitions 函数具体的实现过程:

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return this.loadBeanDefinitions(new EncodedResource(resource));
}

EncodedResource 的作用是对资源文件的编码进行处理,可以通过设置编码属性指定 Spring 使用响应的编码进行处理

当构造好 EncodedResource 对象后,再次转入到 loadBeanDefinitions(new EncodedResource(resource));

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (this.logger.isTraceEnabled()) {
this.logger.trace("Loading XML bean definitions from " + encodedResource);
}
// 获取已经加载的资源
Set<EncodedResource> currentResources = (Set)this.resourcesCurrentlyBeingLoaded.get();
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
} else {
int var6;
try {
// 从已经封装的 Resource 对象获取 InputStream
InputStream inputStream = encodedResource.getResource().getInputStream();
Throwable var4 = null; try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 进入真正的逻辑核心部分
var6 = this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());
} catch (Throwable var24) {
var4 = var24;
throw var24;
} finally {
if (inputStream != null) {
if (var4 != null) {
try {
inputStream.close();
} catch (Throwable var23) {
var4.addSuppressed(var23);
}
} else {
inputStream.close();
}
} }
} catch (IOException var26) {
throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), var26);
} finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
} } return var6;
}
}

再次整理数据准备阶段的逻辑,首先对传入的 Resource 参数进行编码处理,将准备的数据传入到真正的核心处理部分 doLoadBeanDefinitions 方法

3. 获取 Document

doLoadBeanDefinitions 方法的代码如下:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException {
try {
Document doc = this.doLoadDocument(inputSource, resource);
int count = this.registerBeanDefinitions(doc, resource);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Loaded " + count + " bean definitions from " + resource);
} return count;
} catch (BeanDefinitionStoreException var5) {
throw var5;
} catch (SAXParseException var6) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(), "Line " + var6.getLineNumber() + " in XML document from " + resource + " is invalid", var6);
} catch (SAXException var7) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(), "XML document from " + resource + " is invalid", var7);
} catch (ParserConfigurationException var8) {
throw new BeanDefinitionStoreException(resource.getDescription(), "Parser configuration exception parsing XML from " + resource, var8);
} catch (IOException var9) {
throw new BeanDefinitionStoreException(resource.getDescription(), "IOException parsing XML document from " + resource, var9);
} catch (Throwable var10) {
throw new BeanDefinitionStoreException(resource.getDescription(), "Unexpected exception parsing XML document from " + resource, var10);
}
} protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, this.getEntityResolver(), this.errorHandler, this.getValidationModeForResource(resource), this.isNamespaceAware());
}

不考虑处理异常的代码,其实只做了三件事:

  1. 获取对 XML 文件的验证模式
  2. 加载 XML 文件,并得到对应的 Document
  3. 根据返回的 Document 注册 Bean 信息

获取 XML 验证模式是为了保证 XML 文件的正确性,常用的验证模式有 DTD 和 XSD 两种。Spring 通过 getValidationModeForResource 方法获取对应资源的验证模式,这里不再赘述

protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = this.getValidationMode();
// 如果手动指定了验证模式就使用指定的验证模式
if (validationModeToUse != 1) {
return validationModeToUse;
} else {
// 如果未指定就使用自动检测
int detectedMode = this.detectValidationMode(resource);
return detectedMode != 1 ? detectedMode : 3;
}
}

XmlBeanDefinitionReader 将文档读取交由 DocumentLoader 去处理,DocumentLoader 是个接口,真正调用的是 DefaultDocumentLoader,解析代码如下:

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = this.createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {
logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = this.createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}

对于这部分代码没有太多可以描述的,因为通过 SAX 解析 XML 文档的套路大都相同,解析完成返回一个 Document 对象

4. 解析及注册 BeanDefinitions

当程序拥有 Document 对象后,就会被引入下面这个方法:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 使用 DefaultBeanDefinitionDocumentReader 实例化 BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = this.createBeanDefinitionDocumentReader();
// 记录统计前 BeanDefinition 的加载个数
int countBefore = this.getRegistry().getBeanDefinitionCount();
// 加载及注册 bean
documentReader.registerBeanDefinitions(doc, this.createReaderContext(resource));
// 记录本次加载的 BeanDefinition 个数
return this.getRegistry().getBeanDefinitionCount() - countBefore;
}

BeanDefinitionDocumentReader 是一个接口,通过 createBeanDefinitionDocumentReader 方法完成实例化,实际类型是 DefaultBeanDefinitionDocumentReader,registerBeanDefinitions 方法代码如下:

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
this.doRegisterBeanDefinitions(doc.getDocumentElement());
}

getDocumentElement 方法的重要目的之一是提取 root,以便于再次将 root 作为参数继续 BeanDefinition 的注册

再次进入 doRegisterBeanDefinitions 方法:

protected void doRegisterBeanDefinitions(Element root) {
// 专门处理解析
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = this.createDelegate(this.getReaderContext(), root, parent);
// 处理 profile 属性
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute("profile");
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, ",; ");
if (!this.getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + this.getReaderContext().getResource());
} return;
}
}
}
// 解析前处理,留给子类实现
this.preProcessXml(root);
this.parseBeanDefinitions(root, this.delegate);
// 解析后处理,留给子类实现
this.postProcessXml(root);
this.delegate = parent;
}

这里使用了模板方法设计模式,如果继承自 DefaultBeanDefinitionDocumentReader 的子类需要在 Bean 解析前后做一些处理的话,可以重写 preProcessXml 和 postProcessXml 方法

在注册 Bean 的最开始是先对 profile 属性解析,profile 属性可用于在配置文件中部署两套配置分别适用生产环境和开发环境,做到方便的的切换环境

处理完 profile 属性以后就可以进行 XML 的读取,跟踪代码进入 parseBeanDefinitions 方法

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes(); for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element)node;
if (delegate.isDefaultNamespace(ele)) {
this.parseDefaultElement(ele, delegate);
} else {
delegate.parseCustomElement(ele);
}
}
}
} else {
delegate.parseCustomElement(root);
}
}

根节点或者子节点是默认命名空间的话采用 parseDefaultElement 方法解析,否则使用 delegate.parseCustomElement 方法解析,而对于标签的解析,我们放到下一篇文章作讲解

Spring XmlBeanFactory 容器的基本实现的更多相关文章

  1. MyEclipse Spring 学习总结一 Spring IOC容器

    一.Spring IOC容器---- Spring AllicationContext容器 程序的结构如下: 1.首先在MyEclipse 创建创建Java Project 2.创建好后,添加spin ...

  2. 解读Spring Ioc容器设计图

    在Spring Ioc容器的设计中,有俩个主要的容器系列:一个是实现BeanFactory接口的简单容器系列,这系列容器只实现了容器最基本的功能:另外一个是ApplicationContext应用上下 ...

  3. Spring IOC容器基本原理

    2.2.1 IOC容器的概念IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化.定位.配置应用程序中的对象及建立这些对象间的依赖.应用程序无需直接在代码中new相关的对象,应用程序由IOC容器 ...

  4. Spring:容器基本用法

    bean是Spring 最核心的东西,打个比方,假设Spring是一个水桶,那么bean就是水桶里的水,水桶离开水后,就没啥作用了.我们先来看一下bean的定义: public class Perso ...

  5. spring揭密学习笔记(3)-spring ioc容器:Spring的IoC容器之BeanFactory

    1. Spring的IoC容器和IoC Service Provider的关系 Spring的IoC容器和IoC Service Provider所提供的服务之间存在一定的交集,二者的关系如图4-1所 ...

  6. 一头扎进Spring之---------Spring核心容器----------

    1.什么是 IOC/DI? IOC(Inversion of Control)控制反转:所谓控制反转,就是把原先我们代码里面需要实现的对象创建.依赖的代码,反转给容器来帮忙实现.那么必然的我们需要创建 ...

  7. Spring Ioc容器核心类继承图

    Spring IOC容器其实就是BeanFactory的实例,Spring中BeanFactory的类关系结构如下图: 从上图可以看出Beanfactory作为根接口又细化出三个二级接口,最后又有Co ...

  8. Spring IoC容器与应用上下文的设计与实现

    一.前言 写这篇博文的主要目的如下: 通过相关类和接口分析IoC容器到底长什么样. 阐述笔者对Spring上下文和容器的理解. 介绍重要的类辅助理解SpringBoot的启动流程. 二.Spring ...

  9. 详解Spring IoC容器

    一.Spring IoC容器概述 1.依赖反转(依赖注入):依赖对象的获得被反转了. 如果合作对象的引用或依赖关系的管理由具体对象来完成,会导致代码的高度耦合和可测试性的降低,这对复杂的面向对象系统的 ...

随机推荐

  1. 2022.02.21 UB

    2022.02.21 UB 参考资料: https://zhuanlan.zhihu.com/p/141467895 https://blog.csdn.net/ghscarecrow/article ...

  2. K8S+Jenkins自动化构建微服务项目(后续)

    因为之前写过基于K8S部署jenkins master/slave平台,在这个的基础上构建微服务到K8S集群中 Jenkins-slave构建微服务项目到K8S集群 1.微服务项目上传到git仓库 这 ...

  3. 【面试普通人VS高手系列】b树和b+树的理解

    数据结构与算法问题,困扰了无数的小伙伴. 很多小伙伴对数据结构与算法的认知有一个误区,认为工作中没有用到,为什么面试要问,问了能解决实际问题? 图灵奖获得者: Niklaus Wirth 说过: 程序 ...

  4. 【SpringBoot实战】视图技术-Thymeleaf

    前言 在一个Web应用中,通常会采用MVC设计模式实现对应的模型.视图和控制器,其中,视图是用户看到并与之交互的界面.对最初的Web应用来说,视图是由HTML元素组成的静态界面:而后期的Web应用更倾 ...

  5. python学习-Day8

    目录 作业讲解 数据类型内置方法3 字典 ( dict ,可变类型) 类型转换 -- dict() 需要掌握的方法 按key取值(可存可取) 统计字典中键值对的个数(len) 成员运算 删除键值对 获 ...

  6. XCTF练习题---MISC---normal_png

    XCTF练习题---MISC---normal_png flag:flag{B8B68DD7007B1E406F3DF624440D31E0} 解题步骤: 1.观察题目,下载附件 2.拿到手以后发现是 ...

  7. 就是要让你搞懂Nginx,这篇就够了!

    开源Linux 长按二维码加关注~ 作者:渐暖° 出处:blog.csdn.net/yujing1314/article/details/107000737 来源:公众号51CTO技术栈 Nginx ...

  8. MyCat 使用中问题记录

    MyCat问题记录: Unknown charsetIndex:255 异常消息: jvm 1 | 2022-04-27 14:09:13,337 [WARN ][$_NIOREACTOR-13-RW ...

  9. 弃用!Github 上用了 Git.io 缩址服务的都注意了

    GitHub 是面向开源及私有软件项目的托管平台,因为只支持 Git 作为唯一的版本库格式进行托管,故名 GitHub.对程序员来说,GitHub 可以说是开源精神之所系.在 GitHub 任何职业程 ...

  10. 管家婆财贸ERP系列功能对比财贸c3-c8-c9功能对比介绍

    管家婆财贸ERP系列功能对比财贸c3-c8-c9功能对比介绍 管家婆财贸ERP产品功能 序号 名称 说明 一 采购管理 对日常订货.入库.退货.估价入库等业务进行处理,多种方便灵活的订单定制方式,实现 ...