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

978-7-115-49914-1
作者: 郝佳
译者:
编辑: 傅道坤
分类: Spring

图书目录:

详情

本书从核心实现和企业应用两个方面,由浅入深、由易到难地对Spring源码展开了系统的讲解,包括Spring的设计理念和整体架构、容器的基本实现、默认标签的解析、自定义标签的解析、bean的加载、容器的功能扩展、AOP、数据库连接JDBC、整合MyBatis、事务、SpringMVC、远程服务、Spring消息服务等内容。   

图书摘要

版权信息

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

ISBN:978-7-115-49914-1

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

编  著 郝 佳

责任编辑 傅道坤

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书从核心实现、企业应用和Spring Boot这3个方面,由浅入深、由易到难地对Spring源码展开了系统的讲解,包括Spring 整体架构和环境搭建、容器的基本实现、默认标签的解析、自定义标签的解析、bean的加载、容器的功能扩展、AOP、数据库连接JDBC、整合MyBatis、事务、SpringMVC、远程服务、Spring消息、Spring Boot体系原理等内容。

本书不仅介绍了使用Spring框架开发项目必须掌握的核心概念,还指导读者使用Spring框架编写企业级应用,并针对在编写代码的过程中如何优化代码、如何使得代码高效给出了切实可行的建议,从而帮助读者全面提升实战能力。

本书语言简洁,示例丰富,可帮助读者迅速掌握使用Spring进行开发所需的各种技能。本书适合于已具有一定Java编程基础的读者,以及在Java平台下进行各类软件开发的开发人员、测试人员等。


郝佳,计算机专业硕士学位,曾发表过多篇论文并先后被EI、SCI收录;2008 年辽宁省教育厅科技计划项目研究人之一;长期奋斗于J2EE领域,2013年入职阿里巴巴,目前担任业务中间件软件架构师;一直专注于中间件领域,拥有6项技术专利,擅长系统的性能优化;热衷于研究各种优秀的开源代码并从中进行总结,从而实现个人技能的提高,尤其对Spring、Hibernate、MyBatis、JMS、Tomcat等源码有着深刻的理解和认识。


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

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

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

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

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

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

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

本书分为3部分:核心实现、企业应用和Spring Boot。

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

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

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

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


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


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

第2章 容器的基本实现

第3章 默认标签的解析

第4章 自定义标签的解析

第5章 bean的加载

第6章 容器的功能扩展

第7章 AOP


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

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

图1-1 Spring整体架构图

这些模块被总结为以下几部分。

1.Core Container

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

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

2.Data Access/Integration

Data Access/Integration层包含JDBC、ORM、OXM、JMS和Transaction模块。

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

3.Web

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

4.AOP

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

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

5.Test

Test模块支持使用JUnit和TestNG对Spring组件进行测试。

由于在上一版本中作者对环境搭建描述得比较粗糙,导致非常多的读者询问,作者在此表示歉意,并在这一版本中补充了非常详细的Spring环境搭建流程。如果是第一次接触Spring源码的环境搭建,确实还是比较麻烦的。

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

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.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-13所示。

图1-13 错误信息获取

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

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')

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

图1-16 问题发现

aspect关键字Java语法违背

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

图1-17 aspect关键字Java语法违背

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

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.下载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 删除Facets

5.更改编译器

编译器要改为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的环境搭建工作,还有一些单测类的错误已经不影响源码阅读,没有必要浪费时间去解决,删掉就好,有兴趣的读者可以自己解决。


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

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

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所追求的。接下来看看配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.Springframework.org/schema/beans"
     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 id="myTestBean" class="bean.MyTestBean"/>

</beans>

在上面的配置中我们看到了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中执行了太多太多的逻辑,即使作者用半本书的文字也只能介绍它的大致原理。那么就让我们快速地进入分析状态吧。

现在我们可以来好好分析一下上面测试代码的功能,来探索上面的测试代码中Spring究竟帮助我们完成了什么工作?不管之前你是否使用过Spring,当然,你应该使用过的,毕竟本书面向的是对Spring有一定使用经验的读者,你都应该能猜出来,这段测试代码完成的功能无非就是以下几点。

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

图2-1 最简单的Spring功能架构

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

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

图2-2 Spring测试类依赖的JAR

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

我们首先尝试梳理Spring的框架结构,从全局的角度了解Spring的结构组成。

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

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

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

图2-3 beans工程的源码结构

通过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中各个类的作用。

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

2.XmlBeanDefinitionReader

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

经过以上分析,我们可以梳理出整个XML配置文件读取的大致流程,如图2-6所示,在XmlBeanDifinitionReader中主要包含以下几步的处理。

图2-6 配置文件读取相关类图

1.通过继承自AbstractBeanDefinitionReader中的方法,来使用ResourLoader将资源文件路径转换为对应的Resource文件。

2.通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件。

3.通过实现接口BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析。

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

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

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

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

图2-7 XmlBeanFactory初始化时序图

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进行注入。

之前提到的在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<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
         if (currentResources == null) {
             currentResources = new HashSet<EncodedResource>(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());
             }
             //真正进入了逻辑核心部分
                 return doLoadBeanDefinitions(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)
             throws BeanDefinitionStoreException {
         try {
             int validationMode = getValidationModeForResource(resource);
             Document doc = this.documentLoader.loadDocument(
                     inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
             return registerBeanDefinitions(doc, resource);
         }
         catch (BeanDefinitionStoreException ex) {
             throw ex;
         }
         catch (SAXParseException ex) {
             throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                     "Line " + ex.getLineNumber() + " in XML document from " + resource  
 + " is invalid", ex);
         }
         catch (SAXException ex) {
             throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                     "XML document from " + resource + " is invalid", ex);
         }
         catch (ParserConfigurationException ex) {
             throw new BeanDefinitionStoreException(resource.getDescription(),
                     "Parser configuration exception parsing XML from " + resource,ex);
         }
         catch (IOException ex) {
             throw new BeanDefinitionStoreException(resource.getDescription(),
                     "IOException parsing XML document from " + resource, ex);
         }
         catch (Throwable ex) {
             throw new BeanDefinitionStoreException(resource.getDescription(),
                     "Unexpected exception parsing XML document from " + resource,ex);
         }
     }

在上面冗长的代码中假如不考虑异常类的代码,其实只做了三件事,这三件事的每一件都必不可少。

这3个步骤支撑着整个Spring容器部分的实现,尤其是第3步对配置文件的解析,逻辑非常的复杂,我们先从获取XML文件的验证模式讲起。

了解XML文件的读者都应该知道XML文件的验证模式保证了XML文件的正确性,而比较常用的验证模式有两种:DTD和XSD。它们之间有什么区别呢?

DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。 一个DTD文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则。

要使用DTD验证模式的时候需要在XML文件的头部声明,以下是在Spring中使用DTD声明方式的代码:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework.org/ dtd/ Spring-beans-2.0.dtd">
<beans>
... ...
</beans>

而以Spring为例,具体的Spring-beans-2.0.dtd部分如下:

<!ELEMENT beans (
     description?,
     (import | alias | bean)*
)>
<!ATTLIST beans default-lazy-init (true | false) "false">
<!ATTLIST beans default-merge (true | false) "false">
<!ATTLIST beans default-autowire (no | byName | byType | constructor|autodetect)"no">
<!ATTLIST beans default-dependency-check (none | objects | simple | all) "none">
<!ATTLIST beans default-init-method CDATA #IMPLIED>
<!ATTLIST beans default-destroy-method CDATA #IMPLIED>
... ...

XML Schema语言就是XSD(XML Schemas Definition)。XML Schema描述了XML文档的结构。可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。文档设计者可以通过XML Schema指定XML文档所允许的结构和内容,并可据此检查XML文档是否是有效的。XML Schema本身是XML文档,它符合XML语法结构。可以用通用的XML解析器解析它。

在使用XML Schema文档对XML实例文档进行检验,除了要声明名称空间外(xmlns= http://www.Springframework.org/schema/beans),还必须指定该名称空间所对应的XML Schema文档的存储位置。通过schemaLocation属性来指定名称空间所对应的XML Schema文档的存储位置,它包含两个部分,一部分是名称空间的URI,另一部分就是该名称空间所标识的XML Schema文件位置或URL地址(xsi:schemaLocation="http://www.springframework.org/schema/beans http://www. Springframework.org/schema/beans/Spring-beans.xsd)。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.Springframework.org/schema/beans"
     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">
     ... ...
</beans>

Spring-beans-3.0.xsd部分代码如下:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.Springframework.org/schema/beans"
         xmlns:xsd="http://www.w3.org/2001/XMLSchema"
         targetNamespace="http://www.Springframework.org/schema/beans">

     <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>

     <xsd:annotation>
         <xsd:documentation><![CDATA[
     ... ...
         ]]></xsd:documentation>
     </xsd:annotation>

     <!-- base types -->
     <xsd:complexType name="identifiedType" abstract="true">
         <xsd:annotation>
             <xsd:documentation><![CDATA[
     The unique identifier for a bean. The scope of the identifier
     is the enclosing bean factory.
             ]]></xsd:documentation>
         </xsd:annotation>
         <xsd:attribute name="id" type="xsd:ID">
             <xsd:annotation>
                 <xsd:documentation><![CDATA[
     The unique identifier for a bean.
                 ]]></xsd:documentation>
             </xsd:annotation>
         </xsd:attribute>
     </xsd:complexType>
     ... ...

</xsd:schema>

我们只是简单地介绍一下XML文件的验证模式的相关知识,目的在于让读者对后续知识的理解能有连续性,如果对XML有兴趣的读者可以进一步查阅相关资料。

了解了DTD与XSD的区别后我们再去分析Spring中对于验证模式的提取就更容易理解了。通过之前的分析我们锁定了Spring通过getValidationModeForResource方法来获取对应资源的的验证模式。

protected int getValidationModeForResource(Resource resource) {
         int validationModeToUse = getValidationMode();
         //如果手动指定了验证模式则使用指定的验证模式
         if (validationModeToUse != VALIDATION_AUTO) {
             return validationModeToUse;
         }
         //如果未指定则使用自动检测
         int detectedMode = detectValidationMode(resource);
         if (detectedMode != VALIDATION_AUTO) {
             return detectedMode;
         }
         return VALIDATION_XSD;
}

方法的实现其实还是很简单的,无非是如果设定了验证模式则使用设定的验证模式(可以通过对调用XmlBeanDefinitionReader中的setValidationMode方法进行设定),否则使用自动检测的方式。而自动检测验证模式的功能是在函数detectValidationMode方法中实现的,在detectValidationMode函数中又将自动检测验证模式的工作委托给了专门处理类XmlValidationMode- Detector,调用了XmlValidationModeDetector的validationModeDetector方法,具体代码如下:

protected int detectValidationMode(Resource resource) {
         if (resource.isOpen()) {
             throw new BeanDefinitionStoreException(
                     "Passed-in Resource [" + resource + "] contains an open stream: " +
                     "cannot determine validation mode automatically. Either pass in a Resource " +
                     "that is able to create fresh streams, or explicitly specify the validationMode " +
                     "on your XmlBeanDefinitionReader instance.");
         }

         InputStream inputStream;
         try {
             inputStream = resource.getInputStream();
         }
         catch (IOException ex) {
             throw new BeanDefinitionStoreException(
                     "Unable to determine validation mode for [" + resource + "]:   
cannot open InputStream. " +
                     "Did you attempt to load directly from a SAX InputSource without specifying the " +
                     "validationMode on your XmlBeanDefinitionReader instance?", ex);
         }

         try {
             return this.validationModeDetector.detectValidationMode(inputStream);
         }
         catch (IOException ex) {
             throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
                     resource + "]: an error occurred whilst reading from the   
InputStream.", ex);
         }
}

XmlValidationModeDetector.java

     public int detectValidationMode(InputStream inputStream) throws IOException {
         BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream));
         try {
             boolean isDtdValidated = false;
             String content;
             while ((content = reader.readLine()) != null) {
                 content = consumeCommentTokens(content);
                 //如果读取的行是空或者是注释则略过
                 if (this.inComment || !StringUtils.hasText(content)) {
                     continue;
                 }
                 if (hasDoctype(content)) {
                     isDtdValidated = true;
                     break;
                 }
                 //读取到<开始符号,验证模式一定会在开始符号之前
                 if (hasOpeningTag(content)) {
                     break;
                 }
             }
             return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
         }
         catch (CharConversionException ex) {
             // Choked on some character encoding...
             // Leave the decision up to the caller.
             return VALIDATION_AUTO;
         }
         finally {
             reader.close();
         }
}

     private boolean hasDoctype(String content) {
             return (content.indexOf(DOCTYPE) > -1);
     }

只要我们理解了XSD与DTD的使用方法,理解上面的代码应该不会太难,Spring用来检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。

经过了验证模式准备的步骤就可以进行Document加载了,同样XmlBeanFactoryReader类对于文档读取并没有亲力亲为,而是委托给了DocumentLoader去执行,这里的DocumentLoader是个接口,而真正调用的是DefaultDocumentLoader,解析代码如下:

DefaultDocumentLoader.java

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
         ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

         DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
         if (logger.isDebugEnabled()) {
             logger.debug("Using JAXP provider [" + factory.getClass().getName()+"]");
         }
         DocumentBuilder builder = createDocumentBuilder(factory, entityResolver,   
errorHandler);
         return builder.parse(inputSource);
     }

对于这部分代码其实并没有太多可以描述的,因为通过SAX解析XML文档的套路大致都差不多,Spring在这里并没有什么特殊的地方,同样首先创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析inputSource来返回Document对象。对此感兴趣的读者可以在网上获取更多的资料。这里有必要提及一下EntityResolver,对于参数entityResolver,传入的是通过getEntityResolver()函数获取的返回值,如下代码:

protected EntityResolver getEntityResolver() {
         if (this.entityResolver == null) {
             // Determine default EntityResolver to use.
             ResourceLoader resourceLoader = getResourceLoader();
             if (resourceLoader != null) {
                 this.entityResolver = new ResourceEntityResolver(resourceLoader);
             }
             else {
                 this.entityResolver=new DelegatingEntityResolver (getBeanClassLoader());
             }
         }
         return this.entityResolver;
}

那么,EntityResolver到底是做什么用的呢?

在loadDocument方法中涉及一个参数EntityResolver,何为EntityResolver?官网这样解释: 如果SAX应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityResolver方法向SAX 驱动器注册一个实例。也就是说,对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义,以便对文档进行一个验证。默认的寻找规则,即通过网络(实现上就是声明的DTD的URI地址)来下载相应的DTD声明,并进行认证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的DTD声明没有被找到的原因。

EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如我们将DTD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可。这样就避免了通过网络来寻找相应的声明。

首先看entityResolver的接口方法声明:

InputSource resolveEntity(String publicId, String systemId)

这里,它接收两个参数publicId和systemId,并返回一个inputSource对象。这里我们以特定配置文件来进行讲解。

1.如果我们在解析验证模式为XSD的配置文件,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.Springframework.org/schema/beans"
     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">
     ... ...
</beans>

读取到以下两个参数。

2.如果我们在解析验证模式为DTD的配置文件,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework. org/dtd/Spring-beans-2.0.dtd">
<beans>
... ...
</beans>

读取到以下两个参数。

之前已经提到过,验证文件默认的加载方式是通过URL进行网络下载获取,这样会造成延迟,用户体验也不好,一般的做法都是将验证文件放置在自己的工程里,那么怎么做才能将这个URL转换为自己工程里对应的地址文件呢?我们以加载DTD文件为例来看看Spring中是如何实现的。根据之前Spring中通过getEntityResolver()方法对EntityResolver的获取,我们知道,Spring中使用DelegatingEntityResolver类为EntityResolver的实现类,resolveEntity实现方法如下:

DelegatingEntityResolver.java

     public InputSource resolveEntity(String publicId, String systemId) throws   
SAXException, IOException {
         if (systemId != null) {
             if (systemId.endsWith(DTD_SUFFIX)) {
             //如果是dtd从这里解析
                 return this.dtdResolver.resolveEntity(publicId, systemId);
             }
             else if (systemId.endsWith(XSD_SUFFIX)) {
                 //通过调用META-INF/Spring.schemas解析
                 return this.schemaResolver.resolveEntity(publicId, systemId);
             }
         }
         return null;
}

我们可以看到,对不同的验证模式,Spring使用了不同的解析器解析。这里简单描述一下原理,比如加载DTD类型的BeansDtdResolver的resolveEntity是直接截取systemId最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity是默认到META-INF/Spring.schemas文件中找到systemid所对应的XSD文件并加载。

BeansDtdResolver.java

public InputSource resolveEntity(String publicId, String systemId)throws IOException {
         if (logger.isTraceEnabled()) {
             logger.trace("Trying to resolve XML entity with public ID [" + publicId +
                     "] and system ID [" + systemId + "]");
         }
     // DTD_EXTENSION = ".dtd";
         if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
             int lastPathSeparator = systemId.lastIndexOf("/");
             for (String DTD_NAME : DTD_NAMES) {
                 // DTD_NAMES = {"Spring-beans-2.0", "Spring-beans"};
                 int dtdNameStart = systemId.indexOf(DTD_NAME);
                 if (dtdNameStart > lastPathSeparator) {
                     String dtdFile = systemId.substring(dtdNameStart);
                     if (logger.isTraceEnabled()) {
                         logger.trace("Trying to locate [" + dtdFile +"]in Spring jar");
                     }
                     try {
                         Resource resource = new ClassPathResource(dtdFile, getClass());
                         InputSource source = new InputSource(resource.getInputStream());
                         source.setPublicId(publicId);
                         source.setSystemId(systemId);
                         if (logger.isDebugEnabled()) {
                             logger.debug("Found beans DTD [" + systemId + "] in   
classpath: " + dtdFile);
                         }
                         return source;
                     }
                     catch (IOException ex) {
                         if (logger.isDebugEnabled()) {
                             logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in class path", ex);
                         }
                     }

                 }
             }
         }
         return null;
}

当把文件转换为Document后,接下来的提取及注册bean就是我们的重头戏。继续上面的分析,当程序已经拥有XML文档文件的Document实例对象时,就会被引入下面这个方法。

XmlBeanDefinitionReader.java

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStore Exception {
         //使用DefaultBeanDefinitionDocumentReader实例化BeanDefinitionDocumentReader
         BeanDefinitionDocumentReader documentReader=createBeanDefinitionDocumentReader();
         //将环境变量设置其中
         documentReader.setEnvironment(this.getEnvironment());
         //在实例化BeanDefinitionReader时候会将BeanDefinitionRegistry传入,默认使用继承自DefaultListableBeanFactory的子类
         //记录统计前BeanDefinition的加载个数
         int countBefore = getRegistry().getBeanDefinitionCount();
     //加载及注册bean
         documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
         //记录本次加载的BeanDefinition个数
         return getRegistry().getBeanDefinitionCount() - countBefore;
}

其中的参数doc是通过上一节loadDocument加载转换出来的。在这个方法中很好地应用了面向对象中单一职责的原则,将逻辑处理委托给单一的类进行处理,而这个逻辑处理类就是BeanDefinitionDocumentReader。BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocument- Reader后,发现这个方法的重要目的之一就是提取 root,以便于再次将root作为参数继续BeanDefinition的注册。

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

     logger.debug("Loading bean definitions");
     Element root = doc.getDocumentElement();

     doRegisterBeanDefinitions(root);
 }

经过艰难险阻,磕磕绊绊,我们终于到了核心逻辑的底部doRegisterBeanDefinitions(root),至少我们在这个方法中看到了希望。

如果说以前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions算是真正地开始进行解析了,我们期待的核心部分真正开始了。

 protected void doRegisterBeanDefinitions(Element root) {
     //处理profile属性
     String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
     if (StringUtils.hasText(profileSpec)) {
         Assert.state(this.environment !=null, "environment property must not be null");
         String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec,   
BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
         if (!this.environment.acceptsProfiles(specifiedProfiles)) {
             return;
         }
     }
     //专门处理解析
     BeanDefinitionParserDelegate parent = this.delegate;
     this.delegate = createHelper(readerContext, root, parent);

     //解析前处理,留给子类实现
     preProcessXml(root);
     parseBeanDefinitions(root, this.delegate);
     //解析后处理,留给子类实现
     postProcessXml(root);

     this.delegate = parent;
 }

通过上面的代码我们看到了处理流程,首先是对profile的处理,然后开始进行解析,可是当我们跟进preProcessXml(root)或者postProcessXml(root)发现代码是空的,既然是空的写着还有什么用呢?就像面向对象设计方法学中常说的一句话,一个类要么是面向继承的设计的,要么就用final修饰。在DefaultBeanDefinitionDocumentReader中并没有用final修饰,所以它是面向继承而设计的。这两个方法正是为子类而设计的,如果读者有了解过设计模式,可以很快速地反映出这是模版方法模式,如果继承自DefaultBeanDefinitionDocumentReader的子类需要在Bean解析前后做一些处理的话,那么只需要重写这两个方法就可以了。

我们注意到在注册Bean的最开始是对PROFILE_ATTRIBUTE属性的解析,可能对于我们来说,profile属性并不是很常用。让我们先了解一下这个属性。

分析profile前我们先了解下profile的用法,官方示例代码片段如下:

<beans xmlns="http://www.Springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www. Springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">
     ... ...    
<beans profile="dev">
       ... ...
 </beans>
    <beans profile="production">
        ... ...
    </beans>
</beans>

集成到Web环境中时,在web.xml中加入以下代码:

     <context-param>
    <param-name>Spring.profiles.active</param-name>
    <param-value>dev</param-value>
</context-param>

有了这个特性我们就可以同时在配置文件中部署两套配置来适用于生产环境和开发环境,这样可以方便的进行切换开发、部署环境,最常用的就是更换不同的数据库。

了解了profile的使用再来分析代码会清晰得多,首先程序会获取beans节点是否定义了profile属性,如果定义了则会需要到环境变量中去寻找,所以这里首先断言environment不可能为空,因为profile是可以同时指定多个的,需要程序对其拆分,并解析每个profile是都符合环境变量中所定义的,不定义则不会浪费性能去解析。

处理了profile后就可以进行XML的读取了,跟踪代码进入parseBeanDefinitions(root, this.delegate)。

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
     //对beans的处理    
     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)) {
                         //对bean的处理
                         parseDefaultElement(ele, delegate);
                     }
                     else {
                         //对bean的处理
                         delegate.parseCustomElement(ele);
                     }
                 }
             }
         }
         else {
             delegate.parseCustomElement(root);
         }
}

上面的代码看起来逻辑还是蛮清晰的,因为在Spring的XML配置里面有两大类Bean声明,一个是默认的,如:

<bean id="test" class="test.TestBean"/>

另一类就是自定义的,如:

<tx:annotation-driven/>

而两种方式的读取及解析差别是非常大的,如果采用Spring默认的配置,Spring当然知道该怎么做,但是如果是自定义的,那么就需要用户实现一些接口及配置了。对于根节点或者子节点如果是默认命名空间的话则采用parseDefaultElement方法进行解析,否则使用delegate.parseCustomElement方法对自定义命名空间进行解析。而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()获取命名空间,并与Spring中固定的命名空间http://www.springframework.org/schema/beans进行比对。如果一致则认为是默认,否则就认为是自定义。而对于默认标签解析与自定义标签解析我们将会在下一章中进行讨论。


相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
云原生Spring实战Spring Boot与?Kubernetes实践
云原生Spring实战Spring Boot与?Kubernetes实践
Spring实战(第6版)
Spring实战(第6版)
Java研发自测入门与进阶
Java研发自测入门与进阶
Spring核心技术和案例实战
Spring核心技术和案例实战
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程