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


发布时间:2020-08-02 03:28:20

点击下载

作者:郝佳

出版社:人民邮电出版社

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

Spring源码深度解析(第2版)

Spring源码深度解析(第2版)试读:

前言

源代码的重要性

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

的时间去完成。而作为开发人员,毕竟精力有限,实在没办法拿

出太多的时间放在源码的阅读上。● 源码的复杂性。任何一款源码经历了多年的发展与提炼,其复杂

程度可想而知。当我们阅读源码的时候,大家都知道需要通过工

具来跟踪代码的运行,进而去分析程序。但是,当代码过于复杂,

环环相扣绕来绕去的时候,跟进了几十个甚至几百个函数后,这

时我们已经不知道自己所处的位置了,不得不再重来,但是一次

又一次地,最终发现自己根本无法驾驭它,不得不放弃。● 有些源码发展多年,会遇到各种各样的问题,并对问题进行了解

决,而其中有些问题对于我们来说甚至可以用莫名其妙来修饰,

有时候根本想不出会在什么情况下发生。我们查阅各种资料,查

询无果后,会失去耐心,最终放弃。

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

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

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

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

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

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

本书分为3部分:核心实现、企业应用和Spring Boot。● 第1部分,核心实现(第1~7章):是Spring功能的基础,也是企

业应用部分的基础,主要对容器以及AOP功能实现做了具体的分

析。如果读者之前没有接触过Spring源代码,建议认真阅读这个

部分,否则阅读企业应用部分时会比较吃力。● 第2部分,企业应用(第8~13章):在核心实现部分的基础上围

绕企业应用常用的模块进行讨论,这些模块包括Spring整合

JDBC、Spring整合MyBatis、事务、SpringMVC、远程服务、

Spring消息服务等,旨在帮助读者在日常开发中更加高效地使用

Spring。● 第3部分,Spring Boot(第14章):对近期流行的Spring Boot的

体系原理进行分析,剥离其神秘的面纱。Spring Boot作为Spring

外的一个独立分支,可以说将Spring的扩展能力应用得出神入化,

仔细研读后一定会受益匪浅。本书适用的Spring版本

截至完稿,Spring已经发布了5.x 版本。本书所讨论的内容属于Spring的基础和常用的功能,这些功能都经过长时间、大量用户的验证,已经非常成熟,改动的可能性相对较小,即使Spring后续更新到10.x,相信这些内容也不会过时,因此值得读者去阅读。而且从目前Spring的功能规划来看,本书所涉及的内容并不在Spring未来改动的范围内,因此在未来的很长一段时间内本书都不会过时。感谢

创作本书的过程是痛苦的,持续时间也远远超乎了我的想象,而且本以为自己对Spring已经非常熟悉,没想到在写作的过程中还是会遇到各种各样的问题,但是我很幸运我能坚持下来。在这里我首先应该感谢爸爸妈妈,虽然他们不知道儿子在忙忙碌碌地写些什么,但是他们对我始终如一的支持与鼓励使我更加坚定信心,在这里祝他们身体健康!同时还要感谢我的妻子,在刚刚生下宝宝后没有过哺乳期的情况下也一直默默支持我,没有因为缺少我的陪伴而埋怨。同时也感谢妹子王晶对稿件提出的建议与意见。最后感谢郭维云、郝云勃、郝俊、李兴全、梁晓颖、陈淼、孙伟超、王璐、刘瑞、单明、姚佳林、闫微微、李娇、时宇、李平、唐广亮、刘阳、黄思文、金施源等在本书编写过程中给予的支持与帮助。联系作者

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

在看得见的地方学习知识,在看不到的地方学习智慧。祝愿大家在Spring的学习道路上顺风顺水。—— 第1部分 ——核心实现

第1章 Spring整体架构和环境搭建

第2章 容器的基本实现

第3章 默认标签的解析

第4章 自定义标签的解析

第5章 bean的加载

第6章 容器的功能扩展

第7章 AOP  第1章 Spring整体架构和环境搭建

Spring是于 2003 年兴起的一个轻量级Java开源框架,由Rod Johnson在其著作Expert One-On-One J2EE Design and Development中阐述的部分理念和原型衍生而来。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中已被弃用。● 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环境搭建流程。如果是第一次接触Spring源码的环境搭建,确实还是比较麻烦的。

作者使用的编译器为目前流行的IntelliJ IDEA,版本为 2018 旗舰版。Eclipse用户还需要自己揣摩环境搭建方法,这里不再赘述。1.2.1 源码链接获取

1.输入GitHub官网网址并搜索spring,如图1-2所示。图1-2 GitHub上的spring搜索 

2.找到对应的spring-framework的工程,点击链接进入,如图1-3所示。图1-3 GitHub上的spring-framework

3.切换为最新的Spring 5.0.x版本源码,如图1-4所示。图1-4 切换为最新的Spring 5.0.x版本源码

4.获取Git分支链接,如图1-5所示。图1-5 获取Git分支链接1.2.2 源码下载及IDEA导入

1.IDEA下Spring Git拉取分支,如图1-6所示。图1-6 IDEA下Spring Git拉取分支

2.本地安装目录设置,如图1-7所示。图1-7 本地安装目录设置

3.拉取等待,如图1-8所示。图1-8 拉取等待

4.IDEA导入,如图1-9所示。图1-9 IDEA导入

5.Gradle项目导入,如图1-10所示。图1-10 Gradle项目导入

6.工程属性设置,如图1-11所示。图1-11 工程属性设置

7.导入后界面展示,如图1-12所示。图1-12 导入后界面展示1.3 cglib和objenesis的编译错误解决1.3.1 问题发现及原因

错误信息获取,如图1-13所示。图1-13 错误信息获取

为了避免第三方class的冲突,Spring把最新的cglib和objenesis给重新打包(repack)了,它并没有在源码里提供这部分的代码,而是直接将其放在jar包当中,这也就导致了我们拉取代码后出现编译错误。那么为了通过编译,我们要把缺失的jar补回来。1.3.2 问题解决

1.缺失jar引入,如图1-14所示。图1-14 缺失jar引入

2.新增jar在Gradle中生效,如图1-15所示。图1-15 新增jar在Gradle中生效

因为整个Spring都在Gradle环境中,所以要使得jar生效就必须更改Gradle配置文件:compile fileTree(dir: 'libs' ,include : '*.jar')1.4 AspectJ编译问题解决1.4.1 问题发现

当完成以上的jar包导入工作并进行重新编译后,发现还是有编译错误提醒,真是山路十八弯。查看编译错误原因,如图1-16所示,发现居然是类找不到。图1-16 问题发现aspect关键字Java语法违背

可是我们明明能看到对应的类就在工程里面,为什么会找不到呢?于是打开对应的类查看,如图1-17所示。图1-17 aspect关键字Java语法违背

发现类的声明居然使用了aspect而不是class,我瞬间吸了口凉气,太久没充电了,JDK新出的语法变化这么大都没关注,经过几番周折后终于在AspectJ的使用上找到了答案。1.4.2 问题原因

AOP(Aspect Orient Programming,面向切面编程)作为面向对象编程的一种补充,当前已经成为一种比较成熟的编程思想。其实AOP问世的时间并不长,甚至在国内的翻译还不太统一(另有人翻译为“面向方面编程”)。

而AOP在Spring中也占据着举足轻重的作用,可以说没有AOP就没有Spring现在的流行,当然AOP的实现有些时候也依赖于AspectJ。AspectJ实现AOP

脱离了Spring,我们可以单独看看AspectJ的使用方法。

AspectJ的用法很简单,就像我们使用JDK编译、运行Java程序一样。下面通过一个简单的程序来示范AspectJ的用法:public class HelloWorld { public void sayHello(){ System.out.println("Hello AspectJ!"); } public static void main(String args[]) { HelloWorld h = new HelloWorld(); h.sayHello(); }}

毫无疑问,结果将输出“Hello AspectJ!”字符串。假设现在客户需要在执行sayHello方法前启动事务,当该方法结束时关闭事务,则在传统编程模式下,我们必须手动修改sayHello方法——如果改为使用AspectJ,则可以无须修改上面的sayHello方法。下面我们定义一个特殊的“类”:public aspect TxAspect { void around():call(void sayHello()) { System.out.println("Transaction Begin"); proceed(); System.out.println("Transaction End"); }}

可能有人已经发现,在上面的类文件中不是使用class、interface或者enum来定义Java类,而是使用aspect——难道Java语言又增加关键字了?No!上面的TxAspect根本不是一个Java类,所以aspect也不是Java支持的关键字,它只是AspectJ才认识的关键字。

上面void around中的内容也不是方法,它只是指定当程序执行HelloWorld对象的sayHello方法时,执行这个代码块,其中proceed表示调用原来的sayHello方法。正如前面提到的,因为Java无法识别TxAspect.java文件中的内容,所以我们需要使用ajc.exe来执行编译:ajc HelloWorld.java TxAspect.java

我们可以把ajc命令理解为javac命令,两者都用于编译Java程序,区别是ajc命令可以识别AspectJ的语法。从这个角度看,我们可以将ajc命令当成增强版的javac命令。

运行该HelloWorld类依然无须任何改变,其结果如下: Transaction Begin Hello AspectJ! Transaction End

从上面的运行结果来看,我们可以完全不修改HelloWorld.java文件,也不用修改执行HelloWorld的命令,就可以实现上文中的实现事务管理的需求。上面的Transaction Begin和Transaction End仅仅是模拟事务的事件,实际开发中,用代码替换掉这段输出即可实现事务管理。1.4.3 问题解决1.下载AspectJ的最新稳定版本

安装AspectJ之前,请确保系统已经安装了JDK。

下载下来后是一个jar包,如图1-18所示。图1-18 下载AspectJ的最新版本2.AspectJ 安装

打开命令行,cd到该jar包所在的文件夹,运行java -jar aspectj-1.9.0.jar命令,打开AspectJ的安装界面。第一个界面是欢迎界面,直接点击Next,如图1-19所示。图1-19 AspectJ安装

在图1-20所示的第二个界面中选择jre的安装路径,继续点击Next。图1-20 AspecJ JDK设置

在图1-21所示的第三个界面中选择AspectJ的安装路径,点击Install。因为安装过程的实质是解压一个压缩包,并不需要太多地依赖于系统,因此路径可以任意选择,这里选择和Java安装在一起。图1-21 AspecJ 安装目录

至此,AspectJ安装完成。3.IDEA对Ajc支持官方文档(使用AspectJ编译器)

此功能仅在Ultimate 版本中得到支持。

默认情况下,IntelliJ IDEA 使用 Javac 编译器。要使用 AspectJ 编译器Ajc (而不是与 javac 组合),应对相应的 IDE 设置进行更改。

项目级别指定的Ajc 设置可以在各个模块进行微调。与模块相关的 AspectJ用于此目的。

请注意,Ajc不与IntelliJ IDEA 捆绑在一起,它是AspectJ 发行版的一部分,您可以从 AspectJ 网站下载。

将Ajc与Javac结合使用可以优化编译性能,IntelliJ IDEA可把二者组合起来,而无须在IDE 设置中切换编译器。

首先,您应该选择Ajc作为项目编译器(在 Java 编译器页面上的 Use 编译器字段)。

如果您想要同时使用Javac,请打开“Delegate to Javac”选项。如果启用此选项,那么没有 aspect 的模块将被编译为Javac (通常更快),并且包含 aspect 的模块将用Ajc 编译(如果此选项为 off,则Ajc用于项目中的所有模块)。

您可以在各个模块级别对编译器(Ajc 和 Javac)之间的任务分配进行微调。对于只包含 @Aspect-annotated 的 Java 类(在 .java 文件中)的形式的模块,您可以指定Ajc 仅应用于后编译的编织(weaving)。如果这样做,则Javac将用于编译所有源文件,然后 Ajc将其应用于编译的类文件进行编织。因此,整个过程(编译+编织)(compilation + weaving)将花费更少的时间。

如果打开了“Javac代理选项(Delegate to Javac)”,则通过在与模块关联的AspectJ Facets中打开相应的选项来启用Ajc的编译后编织模式。

请注意,不应为包含代码样式aspect的模块(在 .aj 文件中定义的 aspect)启用此选项。4.为spring-aspect工程添加Facets属性

按照IDEA官网说明文档尝试对AspectJ项目加Facets,如图1-22~图1-26所示。图1-22 设置Facets(1)图1-23 设置Facets(2)图1-24 设置Facets(3)图1-25 设置Facets(4)图1-26 删除Facets5.更改编译器

编译器要改为Ajc,同时要设置Ajc的安装目录,如图1-27所示。记住,要选择到aspectjtools.jar这个层面,同时,务必要选择Delegate to Javac选项,它的作用是只编译AspectJ的Facets项目,而其他则使用JDK代理,如图1-28所示。如果不勾选,则全部使用Ajc编译,那么会导致编译错误。如图1-29所示,编译器改为Ajc。图1-27 Gradle入口更改编译器图1-28 选中Delegate to Javac选项图1-29 编译器改为Ajc

至此,我们已经完成了整个Spring的环境搭建工作,还有一些单测类的错误已经不影响源码阅读,没有必要浪费时间去解决,删掉就好,有兴趣的读者可以自己解决。  第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所追求的。接下来看看配置文件:

在上面的配置中我们看到了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有一定使用经验的读者,你都应该能猜出来,这段测试代码完成的功能无非就是以下几点。● 读取配置文件beanFactoryTest.xml。● 根据beanFactoryTest.xml中的配置找到对应的类的配置,并实例

化。● 调用实例化后的实例。

为了更清楚地描述,作者临时画了设计类图,如图2-1所示,如果想完成我们预想的功能,至少需要3个类。● ConfigReader:用于读取及验证配置文件。我们要用配置文件里

面的东西,当然首先要做的就是读取,然后放置在内存中。● ReflectionUtil:用于根据配置文件中的配置进行反射实例化。比

如在上例中beanFactoryTest.xml出现的

class="bean.MyTestBean"/>,我们就可以根据bean.MyTestBean

进行实例化。● App:用于完成整个逻辑的串联。图2-1 最简单的Spring功能架构

按照原始的思维方式,整个过程无非如此,但是作为一个风靡世界的优秀源码真的就这么简单吗?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所示。

beans包中的各个源码包的功能如下。● src/main/java用于展现Spring的主要逻辑。● src/main/resources用于存放系统的配置文件。● src/test/java用于对主要逻辑进行单元测试。● src/test/resources用于存放测试用的配置文件。图2-3 beans工程的源码结构2.4.2 核心类介绍

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

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

从上面的类图以及层次结构图中,我们可以很清晰地从全局角度了解DefaultListableBean- Factory的脉络。如果读者没有了解过Spring源码可能对上面的类图不是很理解,不过没关系,通过后续的学习,你会逐渐了解每个类的作用。那么,让我们先简单地了解图2-5中各个类的作用。● AliasRegistry:定义对alias的简单增删改等操作。● SimpleAliasRegistry:主要使用map作为alias的缓存,并对接口

AliasRegistry进行实现。● SingletonBeanRegistry:定义对单例的注册及获取。● BeanFactory:定义获取bean及bean的各种属性。● DefaultSingletonBeanRegistry:对接口SingletonBeanRegistry各

函数的实现。● HierarchicalBeanFactory:继承BeanFactory,也就是在

BeanFactory定义的功能的基础上增加了对parentFactory的支

持。● BeanDefinitionRegistry:定义对BeanDefinition的各种增删改操作。● FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基

础上增加了对FactoryBean的特殊处理功能。● 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所示)我们来看一看上面代码的执行逻辑。

时序图从BeanFactoryTest测试类开始,通过时序图我们可以一目了然地看到整个逻辑处理顺序。在测试的BeanFactoryTest中首先调用ClassPathResource的构造函数来构造Resource资源文件的实例对象,这样后续的资源处理就可以用Resource提供的各种服务来操作了,当我们有了Resource后就可以进行XmlBeanFactory的初始化了。那么Resource资源是如何封装的呢?图2-7 XmlBeanFactory初始化时序图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.javaif (this.clazz != null) { is = this.clazz.getResourceAsStream(this.path); }else { is = this.classLoader.getResourceAsStream(this.path); }FileSystemResource.javapublic InputStream getInputStream() throws IOException { return new FileInputStream(this.file); }

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

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

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

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载