Spring源码深度解析

978-7-115-32568-6
作者: 郝佳
译者:
编辑: 杜洁
分类: Spring

图书目录:

详情

本书以Spring 3.2为基础,从核心实现、企业应用两个方面展开讨论,包括了容器的基本实现、默认标签的解析、自定义标签、Bean加载、AOP、JDBCConnection、第8章 与ibatis的整合、事务、远程服务、Spring消息、处理Web请求等内容。

图书摘要

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等源码有着深刻的理解和认识。

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

在正式开始分析 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有一定使用经验的读者,你都应该能猜出来,这段测试代码完成的功能无非就是以下几点。

(1)读取配置文件beanFactoryTest.xml。

(2)根据 beanFactoryTest.xml 中的配置找到对应的类的配置,并实例化。

(3)调用实例化后的实例。

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

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

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

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

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

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

不如我们首先大致看看 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所示。

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

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

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

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

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

src/test/resources用于存放测试用的配置文件。

通过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进行解析。

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

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

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

图2-7 XmlBeanFactory初始化时序图

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

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());

 }

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

   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)

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

 }

}

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

(1)获取对XML文件的验证模式。

(2)加载XML文件,并得到对应的Document。

(3)根据返回的Document注册Bean信息。

这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,

  Exception {

  ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws

  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>

读取到以下两个参数。

publicId:null

systemId:http://www.Springframework.org/schema/beans/Spring-beans.xsd

(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>

读取到以下两个参数。

publicId:-//Spring//DTD BEAN 2.0//EN

systemId:http://www.Springframework.org/dtd/Spring-beans-2.0.dtd

之前已经提到过,验证文件默认的加载方式是通过 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了,进入DefaultBeanDefinition Document 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 进行比对。如果一致则认为是默认,否则就认为是自定义。而对于默认标签解析与自定义标签解析我们将会在下一章中进行讨论。

图书在版编目(CIP)数据

Spring源码深度解析/郝佳编著.--北京:人民邮电出版社,2013.9

ISBN 978-7-115-32568-6

Ⅰ.①S… Ⅱ.①郝… Ⅲ.①JAVA语言—程序设计 Ⅳ.①TP312

中国版本图书馆CIP数据核字(2013)第165971号

内容提要

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

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

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

◆编著 郝佳

责任编辑 杜洁

责任印制 程彦红 杨林杰

◆人民邮电出版社出版发行  北京市崇文区夕照寺街14号

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

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

北京艺辉印刷有限责任公司印刷

◆开本:800×1000 1/16

印张:24.75

字数:545千字  2013年9月第1版

印数:1–3000册  2013年9月北京第1次印刷

定价:69.00元

读者服务热线:(010)67132692  印装质量热线:(010)67129223

反盗版热线:(010)67171154

相关图书

深入浅出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)

相关文章

相关课程