第二章 容器的基本实现

2.1 容器基本用法

下面将由一个简单的实例来开始 spring 容器的学习:

public class BeanFactoryTest {
  //省略了配置文件和 TestBean 
  @Test
  public void testSimpleLoad() {
      BeanFactory bf = new XmlBeanFactory(new ClasspathResource("beanFactoryTest.xml");
      TestBean bean = (TestBean) bf.getBean("testBean");
  }
}

这个例子很简单,直接使用 BeanFactory 作为容器对于 Spring 的使用来说并不多见,大多数时候都是使用 ApplicationContext,这里只是用于测试帮助更好的了解 Spring 的内部原理。

2.2 功能分析

这段测试代码主要完成三个功能:

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

2.5 容器的基础 XmlBeanFactory

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

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

我们将通过 XmlBeanFactory 的初始化时序图来分析上面代码的执行逻辑:

  1. 在 BeanFactoryTest 中首先调用 ClassPathResource 的构造函数来构造Resource 资源文件对象;
  2. 使用 Resource 对象构造 XmlBeanFactory 对象;
  3. 在构造过程中通过 Resource 进行读取和配置文件。
XmlBeanFactory 初始化时序图

2.5.1 配置文件封装

之前已经知道 spring 通过 Resource 对文件进行读取,在这里使用的是
Resource 的实现类 ClassPathResource。这一节主要了解封装配置文件的类 Resource。

使用 Resource 封装文件的原因

在 Java 中可以将不同来源的资源抽象成 URL,但是 URL 没有默认定义相对 Classpath 或 ServletContext 等资源的 handler,虽然可以注册自己的 URLStream-Handler 来解析特定的 URL 前缀,然而这需要了解 URL 的实现机制,而且 URL 也没有提供一些基本的方法,比如检查当前资源是否在存在、是否可读等方法。所以 Spring 对其内部使用到的资源实现了自己的抽象机构:Resource 接口 封装底层资源。

public interface InputStreamSource {
    // 返回任何实现了 InputStream 接口的类
    InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
    boolean exists();        // 存在性   
    boolean isReadable();    //可读性
    boolean isOpen();        // 是否处于打开状态
    URL getURL() throws IOException;     //封装 URL 类型
    URI getURI() throws IOException;     // 封装 URI 类型
    File getFile() throws IOException;   // 封装 File 类型
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String relativePath) throws IOException;
    String getFilename();      // 获得不带路径信息的文件名
    String getDescription(); 
}

Resource 接口抽象了 Spring 内部使用到的所有底层资源:File 、URI、URL 等。有了 Resource 接口就可以对所有资源文件进行统一处理,至于实现其实很简单。以 getInputStream 为例,ClassPathResource 中的实现方式是通过 class 或者 classLoader 提供的底层方法实现的。

public InputStream getInputStream() throws IOException {
        InputStream is;
        if (this.clazz != null) {
            is = this.clazz.getResourceAsStream(this.path);
        } else if (this.classLoader != null) {
            is = this.classLoader.getResourceAsStream(this.path);
        } else {
            is = ClassLoader.getSystemResourceAsStream(this.path);
        }
        ...
        return is;
    }
使用 Resource 构造 XmlBeanFactory

Resource 封装对象之后将交由 XmlBeanFactory 进行处理,这里分析使用 Resource 作为实例的构造函数。时序图中提到的 XmlBeanFactory 加载数据就是在方法 reader.loadBeanDefinitions(resource) 中完成的。

    public XmlBeanFactory(Resource resource) throws BeansException {
        this(resource, null);
    }
    public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        // 调用父类的构造方法
        super(parentBeanFactory);
        // 资源加载的真正实现的地方
        this.reader.loadBeanDefinitions(resource);
    }

2.5.2 加载Bean

之前只是在了解 Resource 类,这里开始分析 XmlBeanFactory 的加载过程。在上一节知道 XmlBeanFactory 通过 XmlBeanDefinitionReader 类型的 reader属性调用 loadBeanDefinitions(resource) 方法,这句代码是整个资源加载的切入点,资源加载的真正开始的地方。我们将通过 loadBeanDefinitions(resource) 的时序图来梳理整个处理过程。

  1. 封装资源文件。当进入 XmlBeanDefinitionReader 后首先对参数 Resource 使用 EncodedResource 类进行封装;
  2. 获取输入流。从 Resource 中获取对应的 InputStream 并构造 InputSource;
  3. 通过构造的 InputSource 实例和 Resource 实例继续调用 doLoadBeanDifinitions。
loadBeanDefinitions 函数流程图

EncodedResource 类

首先观察在上一节调用的方法 loadBeanDefinitions(resource),发现这个方法只是将 Resource 封装成 EncodedResource,接着就调用另一个重载方法 loadBeanDefinitions(new EncodedResource(resource))。那么 EncodedResource 是用来干什么的呢?

public int loadBeanDefinitions(Resource resource)  {
        // 将 Resource 封装成 EncodedResource,并调用其重载方法
        return loadBeanDefinitions(new EncodedResource(resource));
}

EncodedResource 顾名思义可以知道这个类主要是用于对资源文件的编码进行处理。其主要逻辑体现在 getReader() 方法中,当设置了编码属性的 Spring 会使用相应的编码作为输入流的编码:

public Reader getReader() {
    if (this.charset != null) {
        return new InputStreamReader(this.resource.getInputStream(), this.charset);
    } else if (this.encoding != null) {
        return new InputStreamReader(this.resource.getInputStream(), this.encoding);
    } else 
        return new InputStreamReader(this.resource.getInputStream());
}

loadBeanDefinitions(EncodedResource resource) 方法

这个方法内部是真正的数据准备阶段,主要是构造对应编码和流的 InputSource 并调用 doLoadBeanDefinitions(inputSource, encodedResource.getResource()) 方法,直到这里仍主要是对流进行处理,在下一个方法将对 XML 文件进行处理。

public int loadBeanDefinitions(EncodedResource encodedResource) {
        // 日志和判断...
        // 记录已经加载的资源...
        try {
            // 从 encodedResource 获取已经封装好的 Resource 对象
            // 再次从 Resource 中获取期中的 InputStream
            InputStream inputStream = encodedResource.getResource().getInputStream();
            try {
                // 这个类并不是来自 Spring,它的全路径是 org.xml.sax.InputSource
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    // EncodedSource 的主要作用在这里,和携带 InputSource
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                // 这里真正进入逻辑核心部分
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }
        }
        // 异常处理和其他一些逻辑...
    }

doLoadBeanDefinitions(InputSource ..., Resource ...) 方法

从方法名(do...)就知道对 XML 文件的真正处理在这里,获取验证模式并将 XML 文件封装成 Document 类,最后通过 registerBeanDefinitions(doc, resource) 方法注册 Bean 信息。接下来将针对下面方法的三个步骤进行讲解。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) {
        // 获取对 XML 文件的验证模式
        int validationMode = getValidationModeForResource(resource);
        // 加载 XML 文件,并得到对应的 Document 信息
        Document doc = this.documentLoader.loadDocument(inputSource, 
                                        getEntityResolver(), this.errorHandler, 
                                        validationMode,      isNamespaceAware());
        // 根据返回的 Document 注册 Bean 信息
        return registerBeanDefinitions(doc, resource);
    }

2.6 XML 的验证模式

为什么需要 XML 的验证模式呢?因为 XML 通过验证模式保证 XML 文件的正确性,而比较常用的验证模式有两种 DTD 和 XSD。对 DTD 和 XSD 的相关知识不在这里进行展开。通过获取 XML 的验证模式采取不同的策略对 XML 进行封装和注册。

2.7 获取 Document

XmlBeanDefinitionReader 并没有将对 Document 的加载逻辑封装在内部,而是使用 DocumentLoader 的实现类完成对 XML 的加载,这里使用的实现类是 DefaultDocumentLoader 。

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
    ErrorHandler errorHandler, int validationMode, boolean namespaceAware)  {
    // 创建 DocumentBuilderFactory 
    DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
    // 通过 DocumentBuilderFactory 创建 DocumentBuilder 
    DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
    // 通过 DocumentBuilder 完成对 Document 的加载
    return builder.parse(inputSource);
}

EntityResolver

这里讲解一下 EntityResolver 的用法。Spring 解析一个 XML 文件首先会读取该 XML 文档上的声明,然后根据声明去寻找相应的 DTD 定义,以便对文档进行验证。但默认的寻找规则是通过网络下载,这过程可能会比较长而且有局限性(网络中断或不可用)。
EntityResolver 就用于解决这一问题的,它的作用就是项目本身可以提供一个寻找 DTD 声明的方法,由程序实现寻找 DTD 声明的过程。

2.8 解析及注册 BeanDefinitions

之前主要还是对流的处理或者文件的封装,这里将开始真正的对 XML 进行解析。XmlBeanDefinitionReader 将对 Document 的加载委托给了 DefaultBeanDefinitionDocumentReader 类,应用了面向对象中单一职责的原则。

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        //使用 createBeanDefinitionDocumentReader 创建 BeanDefinitionDocumentReader
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        //设置环境变量
        documentReader.setEnvironment(getEnvironment());
        //记录注册前已加载数量
        int countBefore = getRegistry().getBeanDefinitionCount();
        //加载及注册 bean
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        //记录本次加载的 bean 个数
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }
registerBeanDefinitions 方法
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
        this.readerContext = readerContext;
        Element root = doc.getDocumentElement();
        doRegisterBeanDefinitions(root);
    }

4
4
4
4
4
4
4
4
4
4

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,301评论 6 344
  • 我们习惯被抛弃 我们忍受被抛弃 我们总是被抛弃 我们讨厌被抛弃 (一) 没有在深夜痛哭过的人,哪资格谈论生活?但是...
    吴祉祺阅读 221评论 2 0
  • 心为何跳动? 分别几日 原来是思念驱使 做...
    坡上风阅读 226评论 0 0