Spring源码深度解析(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-16 10:26:59

点击下载

作者:郝佳

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Spring源码深度解析

Spring源码深度解析试读:

前言

源代码的重要性

Java开发人员都知道,阅读源码是一个非常好的学习方式,在我们日常工作中或多或少都会接触一些开源代码,比如说最常用的Struts、Hibernate、Spring,这些源码的普及与应用程度远远超过我们的想象,正因为很多人使用,也在推动着源码不断地去完善。这些优秀的源码中有着多年积淀下来的精华,这些精华是非常值得我们学习的,不管我们当前是什么水平,通过反复阅读源码能力能有所提升,小到对源码所提供的功能上的使用更加熟练,大到使我们的程序设计更加完美优秀。但是,纵观我们身边的人,能够做到通读源码的真的是少之又少,究其原因不外乎以下几点。

阅读源码绝对算得上是一件费时费力的工作,需要读者耗费大量的时间去完成。而作为开发人员,毕竟精力有限,实在没办法拿出太多的时间放在源码的阅读上。

源码的复杂性。任何一款源码经历了多年的发展与提炼,其复杂程度可想而知。当我们阅读源码的时候,大家都知道需要通过工具来跟踪代码的运行,进而去分析程序。但是,当代码过于复杂,环环相扣绕来绕去的时候,跟进了几十个甚至几百个函数后,这时我们已经不知道自己所处的位置了,不得不再重来,但是一次又一次的,最终发现自己根本无法驾驭它,不得不放弃。

有些源码发展多年,会遇到各种各样的问题,并对问题进行了解决,而这些问题有的对于我们来说甚至可以用莫名其妙来修饰,有时候根本想不出会在什么情况下会发生。我们选择各种查阅资料,查询无果,失去耐心,最终放弃。

无论基于什么样的原因,放弃阅读源码始终不是一个明智的选择,因为你失去了一个跟大师学习的机会。而且,当你读过几个源码之后你会发现,他们的思想以及实现方式是相通的。这就是开源的好处。随着各种开源软件的发展,各家都会融合别家优秀之处来不断完善自己,这样,到最后的结果就是所有的开源软件从设计上或者实现上都会变得越来越相似,也就是说当你读完某个优秀源码后再去读另一个源代码,速度会有很大提升。

以我为例,Spring是我阅读的第一个源码,几乎耗尽了我将近半年的时间,其中各种煎熬可想而知,但是当我读完Spring再去读MyBatis只用了两周时间。当然,暂且不论它们的复杂程度不同,至少我阅读的时候发现有很多相通的东西。当你第一次阅读的时候,你的重点一定是在源码的理解上,但是,当你读完第一个源码再去读下一个的时候,你自然而然地会带着批判或者说挑剔的眼光去阅读:为什么这个功能在我之前看的源码中是那样实现的,而在这里会是这样实现的?这其中的道理在哪里,哪种实现方式更优秀呢?而通过这样的对比及探索,你会发现,自己的进步快得难以想象。

我们已经有些纠结了,既然阅读源码有那么多的好处,但是很多同学却因为时间或者能力的问题而不得不放弃,岂不是太可惜?为了解决这个问题,我撰写了本书,总结了自己的研究心得和实际项目经验,希望能对正在Spring道路上摸索的同仁们提供一些帮助。

本书特点

本书完全从开发者的角度去剖析源码,每一章都会提供具有代表性的实例,并以此为基础进行功能实现的分析,而不是采取开篇就讲解什么容器怎么实现、AOP怎么实现之类的写法。在描述的过程中,本书尽可能地把问题分解,使用剥洋葱的方式一层一层地将逻辑描述清楚,帮助读者由浅入深地进行学习,并把这些难点和问题各个击破,而不是企图一下让读者理解一个复杂的逻辑。

在阅读源码的过程中,我们难免会遇到各种各样的生僻功能,这些功能在特定的场合会非常有用,但是可能多数情况下并不是很常用,甚至都查阅不到相关的使用资料。本书中重点针对这种情况提供了相应的实用示例,让读者更加全面地了解Spring所提供的功能,对代码能知其然还知其所以然。

本书按照每章所提供的示例跟踪Spring源码的流程,尽可能保证代码的连续性,使读者的思维不被打乱,让读者看到Spring的执行流程,旨在尽量使读者在阅读完本书后即使在不阅读 Spring 源码的情况下也可以对 Spring 源码进行优化,甚至通过扩展源码来满足业务需求,这对开发人员来说是一个很高的要求。本书就希望能帮助读者全面提升实战能力。

本书结构

本书分为两部分:核心实现和企业应用。

第一部分 核心实现(第1~7章):是Spring功能的基础,也是企业应用部分的基础,主要对容器以及AOP功能实现做了具体的分析,如果读者之前没有接触过 Spring 源代码,建议认真阅读这个部分,否则阅读企业应用部分时会比较吃力。

第二部分 企业应用(第8~13章):在核心实现部分的基础上围绕企业应用常用的模块进行讨论,这些模块包括Spring整合JDBC、Spring整合MyBatis、事务、SpringMVC、远程服务、Spring 消息服务等,旨在帮助读者在日常开发中更加高效地使用Spring。

本书适用的Spring版本

截至完稿,Spring已经发布了 4.0.0.M1版本。本书虽然是基于Spring 3.2版本编写的,但所讨论的内容都属于Spring的基础和常用的功能,这些功能都经过长时间、大量用户的验证,已经非常成熟,改动的可能性相对较小。而且从目前Spring的功能规划来看,本书所涉及的内容并不在Spring未来改动的范围内,因此在未来的很长一段时间内本书都不会过时的。

感谢

创作的过程是痛苦的,持续时间也远远超乎了我的想象,而本以为自己对Spring已经非常的熟悉,但是在写作的过程中还是会遇到各种各样的问题,但是我很幸运我能坚持下来,在这里我首先应该感谢爸爸妈妈,虽然他们不知道儿子在忙忙碌碌地写些什么,但是他们对我始终如一的支持与鼓励使我更加坚定信心,在这里祝他们身体健康,同时还要感谢我最好的朋友孙亚超在我低落时给予我的关心与问候,当然要感谢的还有张雨绮同学,虽然并不是明星,但是却有着堪比明星般的美丽笑容,与她在一起的讨论总是让我受益匪浅,同时也感谢妹子王晶对稿件提供的建议与意见。最后感谢郭维云、郝云勃、郝俊、李兴全、梁晓颖、陈淼、孙伟超、王璐、刘瑞、单明、姚佳林、闫微微、李娇、时宇、李平、唐广亮、刘阳、黄思文、金施源等在整个编写过程中给予的支持与帮助。

联系作者

在编写本书过程中,以“够用就好”为原则,尽量覆盖到Spring开发的常用功能。所有观点都出自作者的个人见解,疏漏、错误之处在所难免,欢迎大家指正。读者如果有好的建议或者学习本书过程中遇到问题,请发送邮件到haojia_007@163.com,希望能够与大家一起交流和进步。

在看得见的地方学习知识,在看不到的地方学习智慧。祝愿大家在Spring的学习道路上顺风顺水。作者2013年7月

作者简介

郝佳

计算机专业硕士学位,曾发表过多篇论文先后被 EI、SCI 收录, 2008 年辽宁省教育厅科技计划项目研究人之一;长期奋斗于 J2EE 领域,曾任职于某互联网公司软件架构师,擅长系统的性能优化,目前正投身于开发一款基于Java并发多线程管理的开源框架;热衷于研究各种优秀的开源代码并从中进行总结,从而实现个人技能的提高,尤其对Spring、Hibernate、MyBatis、JMS、Tomcat等源码有着深刻的理解和认识。第一部分核心实现第1章Spring整体架构和环境搭建

Spring是于 2003 年兴起的一个轻量级的Java开源框架,由Rod Johnson在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。Spring是为了解决企业应用开发的复杂性而创建的,它使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。1.1 Spring的整体架构

Spring框架是一个分层架构,它包含一系列的功能要素,并被分为大约20个模块,如图1-1所示。图1-1 Spring整体架构图

这些模块被总结为以下几部分。(1)Core Container。

Core Container(核心容器)包含有Core、Beans、Context和Expression Language模块。

Core和Beans模块是框架的基础部分,提供IoC(转控制)和依赖注入特性。这里的基础概念是BeanFactory,它提供对Factory模式的经典实现来消除对程序性单例模式的需要,并真正地允许你从程序逻辑中分离出依赖关系和配置。

Core模块主要包含Spring框架基本的核心工具类,Spring的其他组件要都要使用到这个包里的类,Core模块是其他组件的基本核心。当然你也可以在自己的应用系统中使用这些工具类。

Beans模块是所有应用都要用到的,它包含访问配置文件、创建和管理bean以及进行Inversion of Control / Dependency Injection(IoC/DI)操作相关的所有类。

Context模块构建于Core和Beans模块基础之上,提供了一种类似于JNDI注册器的框架式的对象访问方法。Context模块继承了 Beans的特性,为Spring核心提供了大量扩展,添加了对国际化(例如资源绑定)、事件传播、资源加载和对Context的透明创建的支持。Context模块同时也支持J2EE的一些特性,例如EJB、JMX和基础的远程处理。ApplicationContext接口是Context模块的关键。

Expression Language 模块提供了一个强大的表达式语言用于在运行时查询和操纵对象。它是 JSP 2.1规范中定义的 unifed expression language的一个扩展。该语言支持设置/获取属性的值,属性的分配,方法的调用,访问数组上下文(accessiong the context of arrays)、容器和索引器、逻辑和算术运算符、命名变量以及从Spring的 IoC容器中根据名称检索对象。它也支持list投影、选择和一般的list聚合。(2)Data Access/Integration。

Data Access/Integration层包含有 JDBC、ORM、OXM、JMS和Transaction模块,其中:

JDBC模块提供了一个JDBC抽象层,它可以消除冗长的JDBC编码和解析数据库厂商特有的错误代码。这个模块包含了Spring对JDBC数据访问进行封装的所有类。

ORM模块为流行的对象-关系映射API,如JPA、JDO、Hibernate、iBatis等,提供了一个交互层。利用ORM封装包,可以混合使用所有Spring提供的特性进行O/R映射。如前边提到的简单声明性事物管理。

Spring框架插入了若干个ORM框架,从而提供了ORM的对象关系工具,其中包括JDO、Hibernate和iBatisSQL Map。所有这些都遵从Spring的通用事务和DAO异常层次结构。

OXM模块提供了一个对Object/XML映射实现的抽象层,Object/XML映射实现包括JAXB、Castor、XMLBeans、JiBX和XStream。

JMS(Java Messaging Service)模块主要包含了一些制造和消费消息的特性。

Transaction 模块支持编程和声明性的事物管理,这些事物类必须实现特定的接口,并且对所有的POJO都适用。(3)Web。

Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文。所以, Spring框架支持与Jakarta Struts的集成。Web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。Web层包含了Web、Web-Servlet、Web-Struts和Web-Porlet模块,具体说明如下。

Web模块:提供了基础的面向Web的集成特性。例如,多文件上传、使用 servlet listeners初始化IoC容器以及一个面向Web的应用上下文。它还包含Spring远程支持中Web的相关部分。

Web-Servlet模块web.servlet.jar:该模块包含Spring的model-view-controller(MVC)实现。Spring的MVC框架使得模型范围内的代码和web forms之间能够清楚地分离开来,并与Spring框架的其他特性集成在一起。

Web-Struts模块:该模块提供了对Struts的支持,使得类在Spring应用中能够与一个典型的Struts Web层集成在一起。注意,该支持在Spring 3.0中是 deprecated的。

Web-Porlet模块:提供了用于Portlet环境和Web-Servlet模块的MVC的实现。(4)AOP。

AOP模块提供了一个符合AOP联盟标准的面向切面编程的实现,它让你可以定义例如方法拦截器和切点,从而将逻辑代码分开,降低它们之间的耦合性。利用 source-level 的元数据功能,还可以将各种行为信息合并到你的代码中,这有点像.Net技术中的attribute概念。

通过配置管理特性,Spring AOP模块直接将面向切面的编程功能集成到了Spring框架中,所以可以很容易地使Spring框架管理的任何对象支持AOP。Spring AOP模块为基于Spring的应用程序中的对象提供了事务管理服务。通过使用Spring AOP,不用依赖EJB组件,就可以将声明性事务管理集成到应用程序中。

Aspects模块提供了对AspectJ的集成支持。

Instrumentation模块提供了 class instrumentation支持和 classloader实现,使得可以在特定的应用服务器上使用。(5)Test。

Test模块支持使用JUnit和TestNG对Spring组件进行测试。1.2 环境搭建

Spring已经将源码从 svn迁移到了GitHub。而且也改为基于Gradle的构建来构建项目,它取代了之前的Ant+Ivy系统,所以要构建Spring源码环境首先要安装GitHub以及Gradle。1.2.1 安装GitHub

首先读者需要到GitHub官网去下载安装包,其中Windows系统对应的版本下载地址为:http://windows.github.com/,下载后双击进行安装。安装成功后,快捷菜单中会出现 GitHub 的菜单,如图1-2所示。图1-2 HitHub安装成功后的启动菜单1.2.2 安装Gradle

Gradle是一个基于Groovy的构建工具,它使用Groovy来编写构建脚本,支持依赖管理和多项目创建,类似 Maven,但比其更加简单轻便。Gradle 为 Ivy 提供了一个 layer,提供了build-by-convention 集成,而且它还让你获得许多类似 Maven 的功能。你可以从 http://www. gradle.org/downloads 页面下载 Gradle ,下载后将文件解压放到指定目录中(笔者放在了C:\Program Files目录下),然后开始进行环境变量的配置。(1)根据对应目录创建GRADLE_HOME系统变量,如图1-3所示。(2)将系统变量加入到path中,如图1-4所示。图1-3创建对应于Gralde的系统变量图1-4将Gradle对应的系统变量加入path中(3)测试。

当完成系统变量的配置后打开命令窗口输入命令“gradle –version”,如果安装成功会出现Gradle对应的版本信息,如图1-5所示。图1-5 测试Gradle的环境变量配置1.2.3 下载Spring

因为Spring源码是通过GitHub进行管理的,所以我们首先打开GitHub,单击快捷菜单中的“Git Shell”选项,如图 1-6所示。图1-6 启动GitHub启动菜单

打开GitHub后,你可以通过cd命令将当前操作目录转换到我们想要存储源码的目录,例如,想要将下载的源码存储到e:\test下,则可以执行“cd e:\test”。

输入以下命令:

git clone git://github.com/SpringSource/Spring-framework.git

其中“git://github.com/SpringSource/Spring-framework.git”为Spring的源码地址。执行命令后便进入源码下载状态,如图1-7所示。图1-7 使用GitHub开始下载源码

经过一段时间的等待后源码下载结束,窗口状态如图1-8所示。图1-8 源码下载结束的窗口显示

而这时候我们去查看,对应的文件夹下已经存在了相应的源码信息,如图1-9所示。图1-9 下载的Spring源码

但是当前的源码并不可以直接导入Eclipse中,我们还需要将源码转换为Eclipse可以读取的形式。网上有各种各样的方法,其中出现最多的是告诉大家将所有工程一次性的编译、导入,但是笔者并不推荐这样的方式,因为这样会耗费大量的时间,而且当存在编译错误的时候你不得不重新编译。笔者建议只对我们感兴趣的工程进行 Eclipse 工程转换,比如我们想要查看Spring事务部分的源码,打开命令窗口,将当前目录切换至源码所在目录,例如,这里是Spring-tx文件夹下,执行命令“gradle cleanidea eclipse”,当窗口出现如下状态说明已经开始执行转换过程,如图1-10所示。图1-10 Spring源码转换至eclipse工程

经过一段时间后转换成功,如图1-11所示。

这时,我们再查看对应的文件夹会发现,已经出现了作为 Eclipse 工程所必须的.project与.classpath文件了,如图1-12所示。

打开Eclipse,将工程导入,导入后如图1-13所示。图1-11 Spring源码成功转换至eclipse工程图1-12转换至Eclipse工程后的Spring源码结构图1-13导入Eclipse后的源码工程

你会发现工程名称前面有一个感叹号,这说明存在错误。查看依赖包及工程,会看到当前工程所依赖的包已经完全导入,没有问题,工程所依赖的JAR如图1-14所示。图1-14 工程依赖的JAR

但是,查看依赖的Projects时发现,当前工程还要依赖于其他Spring中的6个工程,这时,读者可以选择以同样的方式继续导入源码工程,或者,直接找到对应的JAR加入编译路径,工程所依赖的Projects如图1-15所示。图1-15 工程所依赖的Projects第2章容器的基本实现

源码分析是一件非常煎熬非常有挑战性的任务,你准备好开始战斗了吗?

在正式开始分析 Spring 源码之前,我们有必要先来回顾一下 Spring 中最简单的用法,尽管我相信您已经对这个例子非常熟悉了。2.1 容器基本用法

bean是Spring中最核心的东西,因为Spring就像是个大水桶,而bean就像是容器中的水,水桶脱离了水便也没什么用处了,那么我们先看看bean的定义。

public class MyTestBean {

  private String testStr = "testStr";

  public String getTestStr() {

   return testStr;

 }

  public void setTestStr(String testStr) {

   this.testStr = testStr;

 }

}

很普通,bean没有任何特别之处,的确,Spring的目的就是让我们的bean能成为一个纯粹的POJO,这也是Spring所追求的。接下来看看配置文件:

 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://www.Springframework.org/schema/beans http://www. Springframework.

 org/schema/beans/Spring-beans.xsd">

 

在上面的配置中我们看到了bean的声明方式,尽管Spring中bean的元素定义着N种属性来支撑我们业务的各种应用,但是我们只要声明成这样,基本上就已经可以满足我们的大多数应用了。好了,你可能觉得还有什么,但是,真没了,Spring的入门示例到这里已经结束,我们可以写测试代码测试了。

@SuppressWarnings("deprecation")

public class BeanFactoryTest {

 @Test

  public void testSimpleLoad(){

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

   MyTestBean bean=(MyTestBean) bf.getBean("myTestBean");

  assertEquals("testStr",bean.getTestStr());

 }

}

相信聪明的读者会很快看到我们期望的结果:在Eclipse中显示了Green Bar。

直接使用BeanFactory作为容器对于Spring的使用来说并不多见,甚至是甚少使用,因为在企业级的应用中大多数都会使用的是ApplicationContext(后续章节我们会介绍它们之间的区别),这里只是用于测试,让读者更快更好地分析Spring的内部原理。

OK,我们又复习了一遍Spring,你是不是会很不屑呢?这样的小例子没任何挑战性。嗯,确实,这样的使用是过于简单了,但是本书的目的并不是介绍如何使用Spring,而是帮助您更好地了解 Spring 的内部原理。读者可以自己先想想,上面的一句简单代码都执行了什么样的逻辑呢?这样一句简单代码其实在Spring中执行了太多太多的逻辑,即使笔者用半本书的文字也只能介绍它的大致原理。那么就让我们快速的进入分析状态吧。2.2 功能分析

现在我们可以来好好分析一下上面测试代码的功能,来探索上面的测试代码中Spring究竟帮助我们完成了什么工作?不管之前你是否使用过Spring,当然,你应该使用过的,毕竟本书面用的是对Spring有一定使用经验的读者,你都应该能猜出来,这段测试代码完成的功能无非就是以下几点。(1)读取配置文件beanFactoryTest.xml。(2)根据 beanFactoryTest.xml 中的配置找到对应的类的配置,并实例化。(3)调用实例化后的实例。

为了更清楚地描述,笔者临时画了设计类图,如图2-1所示,如果想完成我们预想的功能,至少需要3个类。图2-1 最简单的Spring功能架构

ConfigReader:用于读取及验证配置文件。我们要用配置文件里面的东西,当然首先要做的就是读取,然后放置在内存中。

ReflectionUtil:用于根据配置文件中的配置进行反射实例化。比如在上例中beanFactoryTest.xml出现的,我们就可以根据bean.MyTestBean进行实例化。

App:用于完成整个逻辑的串联。

按照最原始的思维方式,整个过程无非如此,但是作为一个风靡世界的优秀源码真的就这么简单吗?2.3 工程搭建

不如我们首先大致看看 Spring 的源码。在 Spring 源码中,用于实现上面功能的是org.Springframework.beans.jar,我们看源码的时候要打开这个工程,如果我们只使用上面的功能,那就没有必要引入Spring的其他更多的包,当然Core是必须的,还有些依赖的包如图2-2所示。图2-2 Spring测试类依赖的JAR

引入依赖的 JAR 消除掉所有编译错误后,终于可以看源码了。或许你已经知道了答案, Spring 居然用了 N 多代码实现了这个看似很简单的功能,那么这些代码都是做什么用的呢?Spring在架构或者编码的时候又是如何考虑的呢?带着疑问,让我们踏上了研读Spring源码的征程。2.4 Spring的结构组成

我们首先尝试梳理一下Spring的框架结构,从全局的角度了解一下Spring的结构组成。2.4.1 beans包的层级结构

笔者认为阅读源码的最好方法是通过示例跟着操作一遍,虽然有时候或者说大多数时候会被复杂的代码绕来绕去,绕到最后已经不知道自己身在何处了,但是,如果配以UML还是可以搞定的。笔者就是按照自己的思路进行分析,并配合必要的UML,希望读者同样可以跟得上思路。

我们先看看整个beans工程的源码结构,如图2-3所示。图2-3 beans工程的源码结构

beans包中的各个源码包的功能如下。

src/main/java用于展现Spring的主要逻辑。

src/main/resources用于存放系统的配置文件。

src/test/java用于对主要逻辑进行单元测试。

src/test/resources用于存放测试用的配置文件。2.4.2 核心类介绍

通过beans工程的结构介绍,我们现在对beans的工程结构有了初步的认识,但是在正式开始源码分析之前,有必要了解一下Spring中最核心的两个类。

1.DefaultListableBeanFactory

XmlBeanFactory继承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整个bean加载的核心部分,是 Spring 注册及加载 bean 的默认实现,而对于 XmlBeanFactory 与DefaultListableBeanFactory 不同的地方其实是在 XmlBeanFactory 中使用了自定义的 XML 读取器XmlBeanDefinitionReader,实现了个性化的BeanDefinitionReader读取,DefaultListableBeanFactory继承了 AbstractAutowireCapableBeanFactory 并实现了 ConfigurableListableBeanFactory 以及BeanDefinitionRegistry接口。以下是ConfigurableListableBeanFactory的层次结构图(见图2-4)以及相关类图(见图2-5)。

从上面的类图以及层次结构图中,我们可以很清晰地从全局角度了解 DefaultListableBean Factory的脉络。如果读者没有了解过Spring源码可能对上面的类图不是很理解,不过没关系,通过后续的学习,你会逐渐了解每个类的作用。那么,让我们先简单地了解一下上面类图中的各个类的作用。

AliasRegistry:定义对alias的简单增删改等操作。

SimpleAliasRegistry:主要使用map作为alias的缓存,并对接口AliasRegistry进行实现。

SingletonBeanRegistry:定义对单例的注册及获取。

BeanFactory:定义获取bean及bean的各种属性。

DefaultSingletonBeanRegistry:对接口SingletonBeanRegistry各函数的实现。

HierarchicalBeanFactory:继承BeanFactory,也就是在BeanFactory定义的功能的基础上增加了对parentFactory的支持。

BeanDefinitionRegistry:定义对BeanDefinition的各种增删改操作。

FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基础上增加了对FactoryBean的特殊处理功能。图2-4 ConfigurableListableBeanFactory的层次结构图图2-5 容器加载相关类图

ConfigurableBeanFactory:提供配置Factory的各种方法。

ListableBeanFactory:根据各种条件获取bean的配置清单。

AbstractBeanFactory:综合 FactoryBeanRegistrySupport和 ConfigurableBeanFactory的功能。

AutowireCapableBeanFactory:提供创建bean、自动注入、初始化以及应用bean的后处理器。

AbstractAutowireCapableBeanFactory:综合AbstractBeanFactory并对接口Autowire Capable BeanFactory进行实现。

ConfigurableListableBeanFactory:BeanFactory配置清单,指定忽略类型及接口等。

DefaultListableBeanFactory:综合上面所有功能,主要是对Bean注册后的处理。

XmlBeanFactory对DefaultListableBeanFactory类进行了扩展,主要用于从XML文档中读取BeanDefinition,对于注册及获取Bean都是使用从父类DefaultListableBeanFactory继承的方法去实现,而唯独与父类不同的个性化实现就是增加了XmlBeanDefinitionReader类型的reader属性。在XmlBeanFactory中主要使用reader属性对资源文件进行读取和注册。

2.XmlBeanDefinitionReader

XML配置文件的读取是Spring中重要的功能,因为Spring的大部分功能都是以配置作为切入点的,那么我们可以从XmlBeanDefinitionReader中梳理一下资源文件读取、解析及注册的大致脉络,首先我们看看各个类的功能。

ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource。

BeanDefinitionReader:主要定义资源文件读取并转换为BeanDefinition的各个功能。

EnvironmentCapable:定义获取Environment方法。

DocumentLoader:定义从资源文件加载到转换为Document的功能。

AbstractBeanDefinitionReader:对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现。

BeanDefinitionDocumentReader:定义读取Docuemnt并注册BeanDefinition功能。

BeanDefinitionParserDelegate:定义解析Element的各种方法。

经过以上分析,我们可以梳理出整个XML配置文件读取的大致流程,如图2-6所示,在XmlBeanDifinitionReader中主要包含以下几步的处理。图2-6 配置文件读取相关类图(1)通过继承自AbstractBeanDefinitionReader中的方法,来使用ResourLoader将资源文件路径转换为对应的Resource文件。(2)通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件。(3)通过实现接口BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析。2.5 容器的基础XmlBeanFactory

好了,到这里我们已经对Spring的容器功能有了一个大致的了解,尽管你可能还很迷糊,但是不要紧,接下来我们会详细探索每个步骤的实现。再次重申一下代码,我们接下来要深入分析以下功能的代码实现:

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

通过XmlBeanFactory初始化时序图(如图2-7所示)我们来看一看上面代码的执行逻辑。图2-7 XmlBeanFactory初始化时序图

时序图从BeanFactoryTest测试类开始,通过时序图我们可以一目了然地看到整个逻辑处理顺序。在测试的BeanFactoryTest中首先调用ClassPathResource的构造函数来构造Resource资源文件的实例对象,这样后续的资源处理就可以用Resource提供的各种服务来操作了,当我们有了Resource后就可以进行XmlBeanFactory的初始化了。那么Resource资源是如何封装的呢?2.5.1 配置文件封装

Spring的配置文件读取是通过 ClassPathResource进行封装的,如 new ClassPathResource ("beanFactoryTest.xml"),那么ClassPathResource完成了什么功能呢?

在Java中,将不同来源的资源抽象成URL,通过注册不同的handler(URLStreamHandler)来处理不同来源的资源的读取逻辑,一般handler的类型使用不同前缀(协议,Protocol)来识别,如“file:”、“http:”、“jar:”等,然而 URL 没有默认定义相对 Classpath 或 ServletContext等资源的handler,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀(协议),比如“classpath:”,然而这需要了解URL的实现机制,而且URL也没有提供一些基本的方法,如检查当前资源是否存在、检查当前资源是否可读等方法。因而 Spring 对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装底层资源。

public interface InputStreamSource {

  InputStream getInputStream() throws IOException;

}

public interface Resource extends InputStreamSource {

  boolean exists();

  boolean isReadable();

  boolean isOpen();

  URL getURL() throws IOException;

  URI getURI() throws IOException;

  File getFile() throws IOException;

  long lastModified() throws IOException;

  Resource createRelative(String relativePath) throws IOException;

  String getFilename();

  String getDescription();

}

InputStreamSource封装任何能返回InputStream的类,比如File、Classpath下的资源和Byte Array等。它只有一个方法定义:getInputStream(),该方法返回一个新的 InputStream对象。

Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法。为了便于操作,Resource 还提供了基于当前资源创建一个相对资源的方法:createRelative()。在错误处理中需要详细地打印出错的资源文件,因而Resource还提供了getDescription()方法用于在错误处理中的打印信息。

对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClassPathResource)、URL资源(UrlResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。相关类图如2-8所示。图2-8 资源文件处理相关类图

在日常的开发工作中,资源文件的加载也是经常用到的,可以直接使用Spring提供的类,比如在希望加载文件时可以使用以下代码:

Resource resource=new ClassPathResource(“beanFactoryTest.xml”);

InputStream inputStream=resource.getInputStream();

得到 inputStream 后,我们就可以按照以前的开发方式进行实现了,并且我们已经可以利用Resource及其子类为我们提供好的诸多特性。

有了Resource接口便可以对所有资源文件进行统一处理。至于实现,其实是非常简单的,以getInputStream为例,ClassPathResource中的实现方式便是通过class或者classLoader提供的底层方法进行调用,而对于 FileSystemResource 的实现其实更简单,直接使用 FileInputStream对文件进行实例化。

ClassPathResource.java

if (this.clazz != null) {

  is = this.clazz.getResourceAsStream(this.path);

}else {

  is = this.classLoader.getResourceAsStream(this.path);

}

FileSystemResource.java

public InputStream getInputStream() throws IOException {

  return new FileInputStream(this.file);

}

当通过Resource相关类完成了对配置文件进行封装后配置文件的读取工作就全权交给XmlBeanDefinitionReader 来处理了。

了解了 Spring 中将配置文件封装为 Resource 类型的实例方法后,我们就可以继续探寻XmlBeanFactory的初始化过程了,XmlBeanFactory的初始化有若干办法,Spring中提供了很多的构造函数,在这里分析的是使用Resource实例作为构造函数参数的办法,代码如下:

XmlBeanFactory.java

public XmlBeanFactory(Resource resource) throws BeansException {

 //调用XmlBeanFactory(Resource,BeanFactory)构造方法,

  this(resource, null);

}

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

//parentBeanFactory为父类BeanFactory用于factory合并,可以为空

public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws

BeansException {

 super(parentBeanFactory);

 this.reader.loadBeanDefinitions(resource);

}

上面函数中的代码this.reader.loadBeanDefinitions(resource) 才是资源加载的真正实现,也是我们分析的重点之一。我们可以看到时序图中提到的XmlBeanDefinitionReader加载数据就是在这里完成的,但是在XmlBeanDefinitionReader加载数据前还有一个调用父类构造函数初始化的过程:super(parentBeanFactory),跟踪代码到父类AbstractAutowireCapableBeanFactory的构造函数中:

AbstractAutowireCapableBeanFactory.java

public AbstractAutowireCapableBeanFactory() {

 super();

 ignoreDependencyInterface(BeanNameAware.class);

 ignoreDependencyInterface(BeanFactoryAware.class);

 ignoreDependencyInterface(BeanClassLoaderAware.class);

}

这里有必要提及一下ignoreDependencyInterface方法。ignoreDependencyInterface的主要功能是忽略给定接口的自动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢?

举例来说,当A中有属性B,那么当Spring在获取A的Bean的时候如果其属性B还没有初始化,那么Spring会自动初始化B,这也是Spring中提供的一个重要特性。但是,某些情况下,B不会被初始化,其中的一种情况就是B实现了BeanNameAware接口。Spring中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析Application上下文注册依赖,类似于 BeanFactory 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过ApplicationContextAware进行注入。2.5.2 加载Bean

之前提到的在XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类型的reader属性提供的方法this.reader.loadBeanDefinitions(resource),而这句代码则是整个资源加载的切入点,我们先来看看这个方法的时序图,如图2-9所示。图2-9 loadBeanDefinitions函数执行时序图

看到图2-9我们才知道什么叫山路十八弯,绕了这么半天还没有真正地切入正题,比如加载XML文档和解析注册Bean,一直还在做准备工作。我们根据上面的时序图来分析一下这里究竟在准备什么?从上面的时序图中我们尝试梳理整个的处理过程如下。( 1 )封装资源文件。当进入 XmlBeanDefinitionReader 后首先对参数 Resource 使用EncodedResource类进行封装。(2)获取输入流。从Resource中获取对应的InputStream并构造InputSource。(3)通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions。

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

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {

  return loadBeanDefinitions(new EncodedResource(resource));

}

那么EncodedResource的作用是什么呢?通过名称,我们可以大致推断这个类主要是用于对资源文件的编码进行处理的。其中的主要逻辑体现在getReader()方法中,当设置了编码属性的时候Spring会使用相应的编码作为输入流的编码。

public Reader getReader() throws IOException {

  if (this.encoding != null) {

   return new InputStreamReader(this.resource.getInputStream(), this.encoding);

 }

  else {

   return new InputStreamReader(this.resource.getInputStream());

 }

}

上面代码构造了一个有编码(encoding)的InputStreamReader。当构造好encodedResource对象后,再次转入了可复用方法 loadBeanDefinitions(new EncodedResource(resource))。

这个方法内部才是真正的数据准备阶段,也就是时序图所描述的逻辑:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {

  Assert.notNull(encodedResource, "EncodedResource must not be null");

  if (logger.isInfoEnabled()) {

   logger.info("Loading XML bean definitions from " + encodedResource. getResource());

 }

//通过属性来记录已经加载的资源

  Set currentResources = this.resourcesCurrentlyBeingLoaded.get();

  if (currentResources == null) {

   currentResources = new HashSet(4);

  this.resourcesCurrentlyBeingLoaded.set(currentResources);

 }

  if (!currentResources.add(encodedResource)) {

   throw new BeanDefinitionStoreException(

    "Detected cyclic loading of " + encodedResource + " - check your

    import definitions!");

 }

  try {

//从encodedResource中获取已经封装的Resource对象并再次从Resource中获取其中的inputStream

  InputStream inputStream = encodedResource.getResource().getInputStream();

  try {

 //InputSource这个类并不来自于Spring,它的全路径是org.xml.sax.InputSource

  InputSource inputSource = new InputSource(inputStream);

  if (encodedResource.getEncoding() != null) {

  inputSource.setEncoding(encodedResource.getEncoding());

 }

 //真正进入了逻辑核心部分

   returndoLoadBeanDefinitions(inputSource, encodedResource.getResource());

  }

   finally {

   //关闭输入流

   inputStream.close();

  }

 }

  catch (IOException ex) {

   throw new BeanDefinitionStoreException(

    "IOException parsing XML document from " + encodedResource.getResource(), ex);

 }

  finally {

  currentResources.remove(encodedResource);

   if (currentResources.isEmpty()) {

   this.resourcesCurrentlyBeingLoaded.remove();

  }

 }

}

我们再次整理一下数据准备阶段的逻辑,首先对传入的resource参数做封装,目的是考虑到Resource可能存在编码要求的情况,其次,通过SAX读取XML文件的方式来准备InputSource对象,最后将准备的数据通过参数传入真正的核心处理部分doLoadBeanDefinitions(inputSource, encodedResource.getResource())。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载