《Spring技术内幕——深入解析Spring架构与设计原理》
书名:Spring技术内幕——深入解析Spring架构与设计原理
作者:计文柯
ISBN:9787111288060
丛书名:揭秘系列丛书
出版社:机械工业出版社
出版日期:2010 年1月
开本:16
页码:300
版次:1-1
定价:55元
豆瓣网讨论地址:http://www.douban.com/subject/4199483/
China-pub预订地址:http://www.china-pub.com/196261
图 书 内 容
本书是Spring领域的问鼎之作,由业界拥有10余年开发经验的资深Java专家亲自执笔!Java开发者社区和Spring开发者社区一致强烈推荐。
国内第一本基于Spring 3.0的著作,从源代码的角度对Spring的内核和各个主要功能模块的架构、设计和实现原理进行了深入剖析。你不仅能从本书中参透Spring框架的优秀架构和设计思想,而且还能从Spring优雅的实现源码中一窥Java语言的精髓。此外,本书还展示了阅读源代码的卓越方法,不仅授你以鱼,而且还授你以渔!
如果你以一种淡定的心态翻开这本书,无论你是Java程序员、Spring开发者,还是平台开发人员、系统架构师,抑或是对开源软件源代码着迷的代码狂人,都能从本书中受益。
第一部分 Spring核心实现篇
第2章 Spring Framework的核心:IoC容器的实现
朝辞白帝彩云间,千里江陵一日还。
两岸猿声啼不住,轻舟已过万重山。
—【唐】李白《早发白帝城》
2.1 Spring IoC容器概述
2.1.1 IoC容器和依赖反转模式
子曰:温故而知新。在这里,我们先简要地回顾一下有关依赖反转的相关概念。我们选取维基百科中关于依赖反转的叙述,把这些文字作为我们理解依赖反转概念的参考。这里不会对这些原理进行学理上的考究,只是希望提供一些有用的信息,以便给读者一些启示。这个模式非常重要,它是IoC容器得到广泛应用的基础。
维基百科对“依赖反转”相关概念的叙述
早在2004年,Martin Fowler就提出了“哪些方面的控制被反转了?”这个问题。他得出的结论是:依赖对象的获得被反转了。基于这个结论,他为控制反转创造了一个更好的名字:依赖注入。许多非凡的应用(比HelloWorld.java更加优美、更加复杂)都是由两个或多个类通过彼此的合作来实现业务逻辑,这使得每个对象都需要与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么如你所见,这将导致代码高度耦合并且难以测试。
以上的这段话概括了依赖反转的要义,如果合作对象的引用或依赖关系的管理要由具体对象来完成,会导致代码的高度耦合和可测试性降低,这对复杂的面向对象系统的设计是非常不利的。在面向对象系统中,对象封装了数据和对数据的处理,对象的依赖关系常常体现在对数据和方法的依赖上。这些依赖关系可以通过把对象的依赖注入交给框架或IoC容器来完成,这种从具体对象手中交出控制的做法是非常有价值的,它可以在解耦代码的同时提高代码的可测试性。极限编程中对单元测试和重构等实践的强调体现了软件开发过程中对质量的承诺,这是软件项目成功的一个重要因素。
依赖控制反转的实现方式有很多种。在Spring中,IoC容器是实现这个模式的载体,它可以在对象生成或初始化时直接将数据注入到对象中,也可以通过将对象引用注入到对象数据域中的方式来注入对方法调用的依赖。这种依赖注入是可以递归的,对象被逐层注入。就此而言,这种方案有一种完整而简洁的美感,它把对象的依赖关系有序地建立起来,简化了对象依赖关系的管理,在很大程度上简化了面向对象系统的复杂性。
本篇将对Spring的核心IoC容器和AOP的实现原理进行阐述。IoC容器和AOP是Spring的核心,是Spring系统中其他组件模块和应用开发的基础。从两个核心模块的设计和实现上可以了解到Spring倡导的对企业应用开发所应秉持的思路,比如使用POJO开发企业应用、提供一致的编程模型、强调对接口编程等。对于这些Spring背后的开发思想和设计理念,大家都不会陌生,在Rod Johnson的经典著作里都有全面和深刻的讲解。作为参考,我们可以看到Spring官方网站对Spring项目的描述。如下图所示,Spring的目标和愿景写得很清楚。
首先,Spring的目标在于让Java EE的开发变得更容易,这也就意味着Spring框架的使用也应该是容易的。对于开发人员而言,易用性是第一位的。为什么要让Java EE开发变得更容易,难道以前的Java EE开发很艰难?Spring究竟是如何让Java EE的开发变得更容易的呢?了解Java EE开发历史的读者都知道,正如Rod Johnson在他的著作Expert One-on-One Java EE Design and Development中提到的那样,EJB模型为Java EE开发引入了过度的复杂性,这个开发模型对Java EE的开发并不友好。有没有更好的开发模型呢?有,就是POJO!它让Java洗净铅华,恢复其自然的风采。使用POJO不仅能开发复杂的Java企业应用,而且还可以让Java EE开发在开发成本、开发周期、可维护性和性能上获得更大优势。对一般的企业应用需求而言,重要的是如何方便地使用应用需要的服务,而不是各种各样的开发模型和模式。虽然这些模式为我们描绘了设计高可靠性分布式应用的美妙场景,但这些场景是不是大多数企业应用开发者所要面对的呢?
世上都说Java好,唯有Spring忘不了。喜欢Java,是因为它简洁,不但包含了面向对象的语言特性,同时还可以跨平台,可谓是简洁而又强大。但是,进入到企业应用后,作为门外汉的自己一看到复杂的EJB模型就心生畏惧。这时候,我接触到了Spring,她给人的第一印象就是简洁却又具有丰富的内涵,就像第一次遇到Java一样,被她的这种特质深深地吸引了。她降低了企业应用开发的门槛,还原了POJO的本色,让我们直接依赖于Java语言,直接依赖于面向对象编程,使用无所不在的单元测试来保证代码质量,这样我们就有信心能够开发出高质量的企业应用。
也就是说,我们如何才能让开发既变得容易,又能享受到Java EE中提供的各种服务呢?Spring的目标就是通过自己的努力,让用户体会到这种简单之中的强大。同时,作为应用框架,Spring不想把自己作为另外一种复杂开发模型的替代,也就是说不是用另一种复杂性去替代现有的复杂性,那是换汤不换药,并不能解决问题。这就意味着需要有新的突破。要解决这个问题,需要降低应用的负载和框架的侵入性,Spring是怎样做到这一点的呢?
Spring为我们提供的解决方案就是IoC容器和AOP支持。作为依赖反转模式的具体实现,IoC容器很好地降低了框架的侵入性,同时也可以认为依赖反转模式是Spring体现出来的核心模式。这些核心模式是软件架构设计中非常重要的因素,比如说,我们常常看到的MVC模式就是这样的核心模式。不要小看这些体系结构模式的作用和影响,它们就是框架背后所谓的“道”。有了IoC容器和AOP的支持,用户的开发方式发生了很大的变化,具体说来,就是可以使用POJO来完成开发,对用户来说是简化了,但由于有平台的支持,依然能够实现复杂的企业应用开发。对于依赖反转,在Spring中,Java EE的服务都被抽象到IoC容器和AOP中并进行了有效地封装,而且因为依赖注入的特性,这些复杂的依赖关系的管理被反转了,它们的管理交给了容器。
Spring中各个模块的依赖关系可以用简单的IoC配置文件进行描述,信息集中并且明了。在使用其他组件服务时,只需要在配置文件中配置这些服务与应用组件的依赖关系。对应用开发而言,只需要了解服务的接口和依赖关系的配置。这样一来又很好地体现了Spring的第二个信条:让应用开发对接口编程,而不是对类编程。这样POJO使用Java EE服务时,可以将对这些服务实现的依赖降到最低,尽可能地降低框架的侵入性。
在处理与现有优秀解决方案的关系时,根据Spring的既定策略,它不会与这些第三方的解决方案发生竞争,而是致力于为应用提供使用优秀方案的集成平台。真正地把Spring定位在应用平台的地位,使得自己成为一个兼容并包的开放体系的同时,最大程度地降低开发者对Spring API的依赖,这是怎样实现的呢?答案还是IoC容器和AOP技术,也就是说,Spring API在开发过程中并不是必须使用的。
关于如何反转对依赖的控制,把控制权从具体业务对象手中转交到平台或者框架中,是解决面向对象系统设计复杂性和提高面向对象系统可测试性的一个有效的解决方案。它促进了IoC设计模式的发展,是IoC容器要解决的核心问题。同时,也是产品化的IoC容器出现的推动力。
注意 IoC亦称为“依赖倒置原理”(Dependency Inversion Principle),几乎所有框架都使用了倒置注入(Martin Fowler)技巧,是IoC原理的一项应用。SmallTalk、C++、Java或.NET等面向对象语言的程序员已使用了这些原理。控制反转是Spring框架的核心。
IoC原理的应用在不同的语言中有许多实现,比如SmallTalk、C++、Java等。在同一语言的实现中也会有多个具体的产品,Spring是Java语言实现中最著名的一个。同时,IoC也是Spring框架要解决的核心问题。
注意 应用控制反转后,当对象被创建时,由一个调控系统内的所有对象的外界实体将其所依赖的对象的引用传递给它。也就是说,依赖被注入到对象中。所以,控制反转是关于一个对象如何获取它所依赖的对象的引用的,在这里,反转指的是责任的反转。
我们可以认为上面提到的调控系统是应用平台,或者更具体地说是IoC容器。通过使用IoC容器,对象依赖关系的管理被反转了,转到IoC容器中来了,对象之间的相互依赖关系由IoC容器进行管理,并由容器完成对象的注入。这样就在很大程度上简化了应用的开发,把应用从复杂的对象依赖关系管理中解放出来。简单地说,因为很多对象的依赖关系的建立和维护并不需要和系统运行状态有很强的关联性,所以可以把我们在面向对象编程中常常需要执行的诸如新建对象、给对象引用赋值等操作交由容器统一完成。这样一来,这些散落在不同代码中的功能相同的部分就集中成为容器的一部分,也就是成为面向对象系统的基础设施的一部分。
如果对面向对象系统中的对象进行简单地分类,会发现除了一部分是数据对象外,其他有很大一部分对象都是用来处理数据的。这些对象并不会经常发生变化,是系统中基础的部分。在很多情况下,这些对象在系统中以单件的形式存在就可以满足应用的需求,而且它们也不常涉及数据和状态共享的问题。如果涉及数据共享方面的问题,需要在这些单件的基础上做进一步的处理。
同时,这些对象之间的相互依赖关系也是比较稳定的,一般不会随着应用的运行状态的改变而改变。这些特性使得这些对象非常适合由IoC容器来管理,虽然它们存在于应用系统中,但是应用系统并不承担管理这些对象的责任,而是通过依赖反转把责任交给了容器(或者说平台)。了解了这些背景,Spring IoC容器的原理也就不难理解了。在原理的具体实现上,Spring有着自己的独特思路、实现技巧和丰富的产品特性。关于这些原理的实现,下面会进行详细的分析。
第1章中,我们已经对建立本地源代码环境做了简要的介绍,该源代码环境是我们分析Spring原理前要做的重要准备工作。同时,我们还需要针对IoC容器做一些额外的事情:根据Spring 3.0的源代码组织特点,每个模块作为独立的Eclipse项目存在,所以现在需要在Eclipse中建立与IoC容器和上下文相关的代码项目。这样就可以方便地使用Eclipse的代码分析工具来对相关模块的实现进行分析。这个额外的准备过程在分析其他模块时也是需要的,所以这里会做一个说明。
准备过程如图2-1所示,打开Eclipse,依次选择File→Import→General→Existing Projects into Workspace,然后再选择org.springframework.beans和org.springframework.context两个目录,并将其导入到Eclipse本地环境中。这时即可看到在Package Explorer View中的Spring IoC容器的源代码项目。
图2-1 打开IoC容器的源代码包
2.1.2 Spring的IoC容器系列
IoC容器为开发者管理对象之间的依赖关系提供了很多便利和基础服务,有许多IoC容器供开发者选择,SpringFramework的IoC核心就是其中的一个,它是开源的。那具体什么是IoC容器呢?它在Spring框架中到底长什么样?其实对IoC容器的使用者来说,我们经常接触到的BeanFactory和ApplicationContext都可以看成是容器的具体表现形式。我们通常所说的IoC容器,如果深入到Spring的实现去看,会发现IoC容器实际上代表着一系列功能各异的容器产品,只是容器的功能有大有小,有各自的特点。我们举水桶为例子,在商店中出售的水桶有大有小,制作材料也各不相同,有金属的、塑料的等,总之是各式各样,但只要能装水,具备水桶的基本特性,那就可以作为水桶来出售,来让用户使用。这在Spring中也是一样,Spring有各式各样的IoC容器的实现供用户选择和使用。使用什么样的容器完全取决于用户的需要,但在使用之前如果能够了解容器的基本情况,那对容器的使用是非常有帮助的,就像我们在购买商品前对商品进行考察和挑选那样。图2-2展示了这个容器系列的概况。
就像商品需要有产品规格说明一样,同样,作为IoC容器,也需要为它的具体实现指定基本的功能规范,这个功能规范的设计表现为接口类BeanFactory,它体现了Spring为提供给用户使用的IoC容器所设定的最基本功能规范。还是举前面我们说的百货商店出售的水桶为例子,如果把IoC容器看成一个水桶,那么这个BeanFactory就定义了可以作为水桶的基本功能,比如至少能装水,有个提手什么的。满足了基本的功能,为了不同场合的需要,水桶的生产厂家还在这个基础上为用户设计了其他各式各样的水桶产品,来满足不同的用户需求。这些水桶会提供更丰富的功能,有简约型的,有豪华型的,等等。但是,不管什么水桶,它都需要有一项最基本的功能:能够装水。那对Spring的具体IoC容器实现来说,它需要满足的基本特性是什么呢?它需要满足BeanFactory这个基本的接口定义,所以在图2-2中可以看到,这个BeanFactory接口在继承体系中的地位,它是作为一个最基本的接口类出现在Spring的IoC容器体系中的。
图2-2 Spring的IoC容器系列概况
在这些Spring提供的基本IoC容器的接口定义和实现的基础上,Spring通过定义BeanDefinition来管理基于Spring的应用中的各种对象以及它们之间的相互依赖关系。BeanDefinition抽象了我们对Bean的定义,是让容器起作用的主要数据类型。我们都知道,在计算机的世界里,所有的功能都是建立在用数据对现实进行抽象的基础上完成的。IoC容器是用来管理对象依赖关系的,对IoC容器来说,BeanDefinition就是对依赖反转模式中管理的对象依赖关系的数据抽象,也是容器实现依赖反转功能的核心数据结构,依赖反转功能都是围绕对这个BeanDefinition的处理上完成的。这些BeanDefinition就像是容器里装的水,有了这些基本数据,容器才能够发挥作用。在下面的分析中,BeanDefinition的上镜次数会很多,我们在这里先简单地打个招呼。
同时,在使用IoC容器时,了解BeanFactory和ApplicationContext之间的区别对我们理解和使用IoC容器也是比较重要的。弄清楚了这两种重要容器之间的区别和联系,意味着我们具备辨别容器系列中不同容器产品的能力。还有一个好处就是,如果需要定制特定功能的容器实现,也能比较方便地在容器系列中找到一款恰当的产品作为参考。
2.2 IoC容器系列的实现:BeanFactory和ApplicationContext
2.2.1 BeanFactory对IoC容器的功能定义
从前面的介绍,我们知道BeanFactory定义了IoC容器的基本功能规范,所以,下面我们就从BeanFactory这个最基本的容器定义来进入Spring的IoC容器体系,去了解IoC容器的实现原理。IoC容器的基本接口是由BeanFactory来定义的,也就是说,BeanFactory定义了IoC容器的最基本的形式,并且提供了IoC容器所应该遵守的最基本的服务契约。同时,这也是我们使用IoC容器所应遵守的最底层和最基本的编程规范,这些接口定义勾画出了IoC的基本轮廓。很显然,在Spring的代码实现中,BeanFactory只是一个接口类,并没有给出容器的具体实现,而我们在图2-2中看到的各种具体类,比如DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等都可以看成是容器的附加了某种功能的具体实现,也就是容器体系中的具体容器产品。下面我们来看看BeanFactory是怎样定义IoC容器的基本接口的。下面介绍这个基本接口为用户提供的基本功能。
用户使用容器时,可以使用转义符“&”来得到FactoryBean本身,用来区分通过容器来获取FactoryBean产生的对象和获取FactoryBean本身。举例来说,如果myJndiObject是一个FactoryBean,那么使用&myJndiObject得到的是FactoryBean,而不是myJndiObject这个FactoryBean产生出来的对象。
注意 理解上面这段话需要很好地区分FactoryBean和BeanFactory这两个在Spring中使用频率很高的类,它们在拼写上非常相似。一个是Factory,也就是IoC容器或对象工厂;一个是Bean。在Spring中,所有Bean都是由BeanFactory(也就是IoC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能产生或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。
BeanFactory接口设计了getBean方法,这个方法是使用IoC容器API的主要方法,通过这个方法,可以取得IoC容器中管理的Bean,Bean的取得是通过指定名字来进行索引的。如果需要在获取Bean时对Bean的类型进行检查,BeanFactory接口定义了带有参数的getBean方法,这个方法的使用与getBean方法类似,不同的是增加了对Bean检索的类型的要求。
用户可以通过BeanFactory接口方法getBean来使用Bean名字,从而当获取Bean时,如果需要获取的Bean是prototype类型的,用户还可以为这个prototype类型的Bean生成指定构造函数的对应参数。这使得在一定程度上可以控制生成prototype类型的Bean。有了BeanFactory的定义,用户可以执行以下操作:
* 通过接口方法containsBean让用户能够判断容器是否含有指定名字的Bean。
* 通过接口方法isSingleton来查询指定了名字的Bean是否是Singleton类型的Bean。对于Singleton属性,用户可以在BeanDefinition中指定。
* 通过接口方法isPrototype来查询指定了名字的Bean是否是prototype类型的。与Singleton属性一样,这个属性也可以由用户在BeanDefinition中指定。
* 通过接口方法isTypeMatch来查询指定了名字的Bean的Class类型是否是特定的Class类型。这个Class类型可以由用户来指定。
* 通过接口方法getType来查询指定了名字的Bean的Class类型。
* 通过接口方法getAliases来查询指定了名字的Bean的所有别名,这些别名都是用户在BeanDefinition中定义的。
这些定义的接口方法勾画出了IoC容器的基本特性,因为BeanFactory接口定义了IoC容器,所以下面给出它定义的全部内容来让大家参考,如代码清单2-1所示。
代码清单2-1 BeanFactory接口
public interface BeanFactory {
/**
* Used to dereference a {@link FactoryBean} instance and distinguish it from
* beans created by the FactoryBean. For example, if the bean named
* myJndiObject
is a FactoryBean, getting &myJndiObject
* will return the factory, not the instance returned by the factory.
*/
String FACTORY_BEAN_PREFIX = "&";
/**
* Return an instance, which may be shared or independent, of the specified bean.
* This method allows a Spring BeanFactory to be used as a replacement for the
* Singleton or Prototype design pattern. Callers may retain references to
* returned objects in the case of Singleton beans.
*
Translates aliases back to the corresponding canonical bean name.
* Will ask the parent factory if the bean cannot be found in this factory instance.
* */
Object getBean(String name) throws BeansException;
/**
* Return an instance, which may be shared or independent, of the specified bean.
*
Behaves the same as {@link #getBean(String)}, but provides a measure of type
* safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the
* required type. This means that ClassCastException can't be thrown on casting
* the result correctly, as can happen with {@link #getBean(String)}.
*
Translates aliases back to the corresponding canonical bean name.
* Will ask the parent factory if the bean cannot be found in this factory instance.
* */
T getBean(String name, Class requiredType) throws BeansException;
/**
* Return an instance, which may be shared or independent, of the specified bean.
* Allows for specifying explicit constructor arguments / factory method arguments,
* overriding the specified default arguments (if any) in the bean definition.
* */
Object getBean(String name, Object... args) throws BeansException;
/**
* Does this bean factory contain a bean with the given name? More specifically,
* is {@link #getBean} able to obtain a bean instance for the given name?
*
Translates aliases back to the corresponding canonical bean name.
* Will ask the parent factory if the bean cannot be found in this factory instance.
*/
boolean containsBean(String name);
/**
* Is this bean a shared singleton? That is, will {@link #getBean} always
* return the same instance?
*
Note: This method returning false
does not clearly indicate
* independent instances. It indicates non-singleton instances, which may correspond
* to a scoped bean as well. Use the {@link #isPrototype} operation to explicitly
* check for independent instances.
*
Translates aliases back to the corresponding canonical bean name.
* Will ask the parent factory if the bean cannot be found in this factory instance.
*/
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
/**
* Is this bean a prototype? That is, will {@link #getBean} always return
* independent instances?
*
Note: This method returning false
does not clearly indicate
* a singleton object. It indicates non-independent instances, which may correspond
* to a scoped bean as well. Use the {@link #isSingleton} operation to explicitly
* check for a shared singleton instance.
*
Translates aliases back to the corresponding canonical bean name.
* Will ask the parent factory if the bean cannot be found in this factory instance.
*/
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
/**
* Check whether the bean with the given name matches the specified type.
* More specifically, check whether a {@link #getBean} call for the given name
* would return an object that is assignable to the specified target type.
*
Translates aliases back to the corresponding canonical bean name.
* Will ask the parent factory if the bean cannot be found in this factory instance.
*/
boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException;
/**
* Determine the type of the bean with the given name. More specifically,
* determine the type of object that {@link #getBean} would return for the given name.
*
For a {@link FactoryBean}, return the type of object that the FactoryBean creates,
* as exposed by {@link FactoryBean#getObjectType()}.
*
Translates aliases back to the corresponding canonical bean name.
* Will ask the parent factory if the bean cannot be found in this factory instance.
*/
Class getType(String name) throws NoSuchBeanDefinitionException;
/**
* Return the aliases for the given bean name, if any.
* All of those aliases point to the same bean when used in a {@link #getBean} call.
*
If the given name is an alias, the corresponding original bean name
* and other aliases (if any) will be returned, with the original bean name
* being the first element in the array.
*
Will ask the parent factory if the bean cannot be found in this factory instance.
*/
String[] getAliases(String name);
}
2.2.2 IoC容器XmlBeanFactory的工作原理
这个BeanFactory接口提供了使用IoC容器的规范。在这个基础上,Spring还提供了符合这个IoC容器接口的一系列容器的实现供开发人员使用。例如,在图2-2中,我们可以看到BeanFactory的相关部分的实现。为简单起见,我们浏览一下图2-2的BeanFactory的继承体系,注意AutowireCapableBeanFactory→AbstractAutowireCapableBeanFactory→DefaultListableBeanFactory→XmlBeanFactory IoC容器的实现系列。
我们从这个容器系列的最底层实现XmlBeanFactory开始,这个容器的实现与我们在Spring应用中用到的那些上下文相比,有一个非常明显的特点,它只提供了最基本的IoC容器的功能。从它的名字中可以看出,这个IoC容器可以读取以XML形式定义的BeanDefinition。理解这一点有助于理解ApplicationContext与基本的BeanFactory之间的区别和联系。我们可以认为直接的BeanFactory实现是IoC容器的基本形式,而各种ApplicationContext的实现是IoC容器的高级表现形式。关于ApplicationContext的分析,以及它与BeanFactory相比的增强特性都会在下面进行详细的分析。
让我们回顾一下这个继承体系,从中可以清楚地看到它们之间的联系,它们都是IoC容器系列的组成部分。在设计这个容器系列时,我们可以从继承体系的发展上看到IoC容器各项功能的实现过程。如果要扩展自己的容器产品,建议读者最好在这个继承体系中检验一下,看看Spring是不是已经提供了现成的或相近的容器实现供我们参考。下面就从我们比较熟悉的XmlBeanFactory的实现入手进行分析,来看看一个基本的IoC容器是怎样实现的。
如果仔细阅读XmlBeanFactory的源码,在一开始的注释里面已经对 XmlBeanFactory的功能做了简要的说明,从代码的注释还可以看到,这是Rod Johnson在2001年就写下的代码,可见这个类应该是Spring的元老类了。它是继承DefaultListableBeanFactory这个类的,而且它非常重要,在以后的分析中这个类会经常用到。我们会看到这个DefaultListableBeanFactory实际上包含了IoC容器的重要功能,也是在很多地方都会用到的容器系列中的一个基本产品。
从名字上就可以看出来,在Spring中,实际上是把它作为一个默认的完整功能的IoC容器来使用的。XmlBeanFactory在继承了DefaultListableBeanFactory容器的功能的同时,给DefaultListableBeanFactory增加的功能很容易从XmlBeanFactory的名字上猜到。它是一个与XML相关的BeanFactory,也就是说它可以读取以XML文件方式定义的BeanDefinition的一个IoC容器。
如果说XmlBeanFactory是一个可以读取XML文件方式定义的BeanDefinition的IoC容器,那么这些实现XML读取的功能是怎样实现的呢?对这些XML文件定义信息的处理并不是由XmlBeanFactory来直接处理的。在XmlBeanFactory中,初始化了一个XmlBeanDefini-tionReader对象,有了这个Reader对象,那些以XML的方式定义的BeanDefinition就有了处理的地方。我们可以看到,对这些XML形式的信息的处理实际上是由这个XmlBeanDefini-tionReader来完成的。
构造XmlBeanFactory这个IoC容器时,需要指定BeanDefinition的信息来源,而这个信息来源需要封装成Spring中的Resource类来给出。Resource是Spring用来封装IO操作的类。比如,我们的BeanDefinition信息是以xml文件形式存在的,那么可以使用像ClassPathResource res = new ClassPathResource("beans.xml");这样具体的ClassPathResource来构造需要的Resource,然后作为构造参数传递给XmlBeanFactory构造函数。这样,IoC容器就可以方便地定位到需要的BeanDefinition信息来对Bean完成容器的初始化和依赖注入过程。
XmlBeanFactory的功能是建立在DefaultListableBeanFactory这个基本容器的基础上的,在这个基本容器的基础上实现了其他诸如XML读取的附加功能。对于这些功能的实现原理,看一看XmlBeanFactory的代码实现就能很容易地理解。如代码清单2-2所示,在XmlBeanFactory构造方法中需要得到Resource对象。对XmlBeanDefinitionReader对象的初始化,以及使用这个对象来完成loadBeanDefinitions的调用,就是这个调用启动了从Resource中载入BeanDefinitions的过程,loadBeanDefinitions同时也是IoC容器初始化的重要组成部分。
代码清单2-2 XmlBeanFactory的实现
我们看到XmlBeanFactory使用了DefaultListableBeanFactory作为基类,DefaultListableBeanFactory是很重要的一个IoC实现,在其他IoC容器中,比如ApplicationContext,其实现的基本原理和XmlBeanFactory一样,也是通过持有或者扩展DefaultListableBeanFactory来获得基本的IoC容器的功能的。
参考XmlBeanFactory的实现,我们以编程的方式使用DefaultListableBeanFactory,从中我们可以看到IoC容器使用的一些基本过程。尽管我们在应用中使用IoC容器时很少会使用这样原始的方式,但是了解一下这个基本的过程,对我们了解IoC容器的工作原理却是非常有帮助的。因为这个编程式使用容器的过程很清楚地揭示了在IoC容器实现中的那些关键的类(比如Resource、DefaultListableBeanFactory以及BeanDefinitionReader)之间的相互关系,例如它们是如何把IoC容器的功能解耦的,又是如何结合在一起为IoC容器服务的,等等。在代码清单2-3中可以看到编程式使用IoC容器的过程。
代码清单2-3 编程式使用IoC容器
这样,我们就可以通过factory对象来使用 DefaultListableBeanFactory这个IoC容器了。在使用IoC容器时,需要如下几个步骤:
1)创建IoC配置文件的抽象资源,这个抽象资源包含了BeanDefinition的定义信息。
2)创建一个BeanFactory,这里使用DefaultListableBeanFactory。
3)创建一个载入BeanDefinition的读取器,这里使用XmlBeanDefinitionReader来载入XML文件形式的BeanDefinition,通过一个回调配置给BeanFactory。
4)从定义好的资源位置读入配置信息,具体的解析过程由XmlBeanDefinitionReader来完成。完成整个载入和注册Bean定义之后,需要的IoC容器就建立起来了。这个时候IoC容器就可以直接使用了。
2.2.3 ApplicationContext的特点
我们了解了IoC容器建立的基本步骤。现在可以很方便地通过编程的方式来手工控制这些配置和容器的建立过程了。但是,在Spring中系统已经为用户提供了许多已经定义好的容器实现,而不需要开发人员事必躬亲。相比那些简单拓展BeanFactory的基本IoC容器,开发人员常用的ApplicationContext除了能够提供在上面看到的容器的基本功能外,还为用户提供了以下的附加服务,可以让客户更方便地使用。所以说,ApplicationContext是一个高级形态意义的IoC容器,如图2-3所示,可以看到ApplicationContext在BeanFactory的基础上添加的附加功能,这些功能为ApplicationContext提供了以下BeanFactory不具备的新特性。
图2-3 ApplicationContext的接口关系
* 支持不同的信息源。我们看到ApplicationContext扩展了MessageSource接口,这些信息源的扩展功能可以支持国际化的实现,为开发多语言版本的应用提供服务。
* 访问资源。体现在对ResourceLoader和Resource的支持上,这样我们可以从不同地方得到Bean定义资源。这种抽象使用户程序可以灵活地定义Bean定义信息,尤其是从不同的IO途径得到Bean定义信息。这在接口关系上看不出来,一般来说,具体Applic-ationContext都是继承了DefaultResourceLoader的子类。因为DefaultResourc-eLoader是AbstractApplicationContext的基类,关于Resource在IoC容器中的使用,在2.3节中有详细的讲解。
* 支持应用事件。继承了接口ApplicationEventPublisher,这样在上下文中引入了事件机制。这些事件和Bean的生命周期的结合为Bean的管理提供了便利。
* 在ApplicationContext中提供的附加服务。这些服务使得基本IoC容器的功能更丰富。因为具备了这些丰富的附加功能,使得ApplicationContext与简单的BeanFactory相比,对它的使用是一种面向框架的使用风格,所以一般建议在开发应用时使用ApplicationContext作为IoC容器的基本形式。
2.3 IoC容器的初始化
IoC容器的初始化包括BeanDefinition的Resouce定位、载入和注册这三个基本的过程。在前面的编程式地使用DefaultListableBeanFactory中,我们可以看到定位和载入过程的接口调用。这里将详细分析这三个过程的实现。值得注意的是,Spring在实现中是把这三个过程分开并使用不同的模块来完成的,这样可以让用户更加灵活地对这三个过程进行剪裁和扩展,定义出最适合自己的IoC容器的初始化过程。
BeanDefinition的资源定位由ResourceLoader通过统一的Resource接口来完成,这个Resource对各种形式的BeanDefinition的使用提供了统一接口。对于这些BeanDefinition的存在形式,相信大家都不会感到陌生。比如说,在文件系统中的Bean定义信息可以使用FileSystemResource来进行抽象;在类路径中可以使用前面提到的ClassPathResource来使用,等等。这个过程类似于容器寻找数据的过程,就像用水桶装水先要把水找到一样。
第二个关键的部分是BeanDefinition的载入,该载入过程把用户定义好的Bean表示成IoC容器内部的数据结构,而这个容器内部的数据结构就是BeanDefinition,下面可以看到这个数据结构的详细定义。总地说来,这个BeanDefinition实际上就是POJO对象在IoC容器中的抽象,这个BeanDefinition定义了一系列的数据来使得IoC容器能够方便地对POJO对象也就是Spring的Bean进行管理。即BeanDefinition就是Spring的领域对象。下面我们会对这个载入的过程进行详细的分析,便于大家对整个过程有比较清楚的了解。
第三个过程是向IoC容器注册这些BeanDefinition的过程。这个过程是通过调用BeanDefinitionRegistry接口的实现来完成的,这个注册过程把载入过程中解析得到的BeanDefinition向IoC容器进行注册。可以看到,在IoC容器内部,是通过使用一个HashMap来持有这些BeanDefinition数据的。
值得注意的是,IoC容器和上下文的初始化一般不包含Bean依赖注入的实现。一般而言,依赖注入发生在应用第一次向容器通过getBean索取Bean时。但有一个例外值得注意,在使用IoC容器时有一个预实例化的配置,这个预实例化是可以配置的,具体来说可以通过在Bean定义信息中的lazyinit属性来设定;有了这个预实例化的特性,用户可以对容器初始化过程作一个微小的控制;从而改变这个被设置了lazyinit属性的Bean的依赖注入的发生,使得这个Bean的依赖注入在IoC容器初始化时就预先完成了。有了以上的一个大概的轮廓,下面就详细地看一看在IoC容器的初始化过程中,BeanDefinition的资源定位、载入和解析过程是怎么实现的。
2.3.1 BeanDefinition的Resource定位
以编程的方式使用DefaultListableBeanFactory时,我们可以看到,首先定义一个Resource来定位容器使用的BeanDefinition。这时使用的是ClassPathResource,意味着Spring会在类路径中寻找以文件形式存在的BeanDefinition信息。
ClassPathResource res = new ClassPathResource("beans.xml");
这个定义的Resource并不能让DefaultListableBeanFactory直接使用,Spring是通过BeanDefinitionReader来对这些信息进行处理的。在这里,我们也可以看到使用ApplicationContext相对于直接使用DefaultListableBeanFactory的好处。因为在ApplicationContext中,Spring已经为我们提供了一系列加载不同Resource的读取器的实现,而DefaultListableBeanFactory只是一个纯粹的IoC容器,需要为它配置特定的读取器才能完成这些功能。当然,有利就有弊,使用DefaultListableBeanFactory这种更底层的容器,却能提高我们定制IoC容器的灵活性。
回到我们经常使用的ApplicationContext上来,例如FileSystemXmlApplication-Context、ClassPathXmlApplicationContext以及XmlWebApplicationContext等。简单地从这些类的名字上分析,可以清楚地看到它们可以提供哪些不同的Resource读入功能,比如FileSystemXmlApplicationContext可以从文件系统载入Resource,ClassPathXm-lApplicationContext可以从Class Path载入Resource,XmlWebApplicationContext可以在Web容器中载入Resource,等等。
下面以FileSystemXmlApplicationContext为例,通过分析这个ApplicationContext的实现来看看它是怎样完成这个Resource定位过程的。作为辅助,我们可以在图2-4中看到相应的ApplicationContext继承体系。
图2-4 FileSystemXmlApplicationContext的继承关系
从图2-4中可以看到,这个FileSystemXmlApplicationContext已经通过继承Abstra-ctApplicationContext具备了ResourceLoader读入以Resource定义的BeanDefiniti-on的能力,因为AbstractApplicationContext的基类是DefaultResourceLoader。下面看看FileSystemXmlApplicationContext的具体实现,如代码清单2-4所示。
代码清单2-4 FileSystemXmlApplicationContext的实现
在FileSystemApplicationContext中,我们可以看到实现了两个部分的功能,一部分是在构造函数中,对configuration进行处理,使得所有在配置在文件系统中的XML文件方式的BeanDefnition都能够得到有效的处理,比如实现了getResourceByPath方法,这个方法是一个模板方法,是为读取Resource服务的。对于IoC容器功能的实现,这里没有涉及,因为它继承了AbstractXmlApplicationContext,关于IoC容器功能相关的实现,都是在FileSystemXmlApplicationContext中完成的,但是在构造函数中通过refresh来启动了IoC容器的初始化,这个refresh方法非常重要,也是我们以后分析容器初始化过程实现的一个重要入口。
注意 FileSystemApplicationContext是一个支持XML定义BeanDefinition的ApplicationContext,并且可以指定以文件形式的BeanDefinition的读入,这些文件可以使用文件路径和URL定义来表示。在测试环境和独立应用环境中,这个ApplicationContext是非常的有用的。
根据图2-5的调用关系分析,我们可以清楚地看到整个BeanDefinition资源定位的过程。这个对BeanDefinition资源定位的过程,最初是由refresh来触发的,这个refresh的调用是在FileSystemXmlBeanFactory的构造函数中启动的。
图2-5 getResourceByPath的调用关系
大家看了上面的调用过程可能会比较好奇,这个FileSystemXmlApplicationContext在什么地方定义了BeanDefinition的读入器BeanDefinitionReader,从而完成BeanDefi-nition信息的读入呢?在前面分析过,在IoC容器的初始化过程中,BeanDefinition资源的定位、读入和注册过程是分开进行的,这也是解耦的一个体现。关于这个读入器的配置,可以到FileSystemXmlApplicationContext的基类AbstractRefreshableApplicationContext中看看它是怎样实现的。
我们重点看看AbstractRefreshableApplicationContext的refreshBeanFactory方法的实现,这个refreshBeanFactory被FileSystemXmlApplicationContext构造函数中的refresh调用。在这个方法里,通过createBeanFactroy构建了一个IoC容器供Appl-icationContext使用。这个IoC容器就是我们前面提到过的DefaultListableBeanFactory,同时,它启动了loadBeanDefinitions来载入BeanDefinition,这个过程和我们前面看到的编程式的使用IoC容器(XmlBeanFactory)的过程非常类似。
从代码清单2-4中可以看到,在初始化FileSystmXmlApplicationContext的过程中,通过IoC容器的初始化的refresh来启动整个调用,使用的IoC容器是DefultListableBeanFactory。具体的资源载入在XmlBeanDefinitionReader读入BeanDefinition时完成,在XmlBeanDefinitionReader的基类AbstractBeanDefinitionReader中可以看到这个载入过程的具体实现。对载入过程的启动,可以在AbstractRefreshableApplicationCont-ext的loadBeanDefinitions方法中看到,如代码清单2-5所示。
代码清单2-5 AbstractRefreshableApplicationContext对容器的初始化
前面我们看到的getResourceByPath会被子类FileSystemXmlApplicationContext实现,这个方法返回的是一个 FileSystemResource对象,通过这个对象Spring可以进行相关的IO操作,完成BeanDefinition的定位。分析到这里已经一目了然,它实现的就是对path进行解析,然后生成一个FileSystemResource对象并返回,如代码清单2-6所示。
代码清单2-6 FileSystemXmlApplicationContext生成Resource对象
protected Resource getResourceByPath(String path) {
if (path != null && path.startsWith("/")) {
path = path.substring(1);
}
return new FileSystemResource(path);
}
如果是其他的ApplicationContext,那么对应地会生成其他种类的Resource,比如ClassPathResource、ServletContextResource等。关于Spring中Resource的种类,可以在图2-6中的Resource类的继承关系中了解。作为接口的Resource定义了许多与IO相关的操作,这些操作也都可以从图2-6中Resource的接口定义中看到。这些接口对不同的Resource实现代表着不同的意义,是Resource的实现需要考虑的。
图2-6 Resource的定义和继承关系
从图2-6中我们可以看到Resource的定义和它的继承关系,通过对前面的实现原理的分析,我们以FileSystemXmlApplicationContext的实现原理为例子,了解了Resource定位问题的解决方案,即以FileSystem方式存在的Resource的定位实现。在BeanDefinition定位完成的基础上,就可以通过返回的Resource对象来进行BeanDefinition的载入了。在定位过程完成以后,为BeanDefinition的载入创造了IO操作的条件,但是具体的数据还没有开始读入。这些数据的读入将在下面看到的BeanDefinition的载入和解析中来完成。仍然以水桶为例子,这里就像如果要用水桶去打水,那么先要找到水源。这里完成对Resource的定位,就类似于水源已经找到了,下面就是打水的过程了,类似于把找到的水装到水桶里的过程。找水不简单,但与打水相比,我们发现打水更需要技巧。
2.3.2 BeanDefinition的载入和解析
对IoC容器来说,BeanDefinition的载入过程相当于把我们定义的BeanDefinition在IoC容器中转化成一个Spring内部表示的数据结构的过程。IoC容器对Bean的管理和依赖注入功能的实现,是通过对其持有的BeanDefinition进行各种相关的操作来完成的。这些BeanDefinition数据在IoC容器里通过一个HashMap来保持和维护,当然这只是一种比较简单的维护方式,如果你觉得需要提高IoC容器的性能和容量,完全可以自己做一些扩展。我们从DefaultListableBeanFactory来入手看看IoC容器是怎样完成BeanDefinition载入的。这个DefaultListableBeanFactory已经是我们非常熟悉的基本IoC容器,在前面已经碰到过多次,相信大家对它一定不会感到陌生。为了了解这一点,我们先回到IoC容器的初始化入口,也就是到refresh()方法去看一看。这个方法的最初是在FileSystemXmlApplicationContext的构造函数中被调用的,它的调用意味着容器的初始化或数据更新,这些初始化和更新的数据当然就是BeanDefinition,如代码清单2-7所示。
代码清单2-7 启动BeanDefinition的载入
对于容器的启动来说,refresh是一个很重要的方法,我们看看它的实现。在AbstractApplicationContext类(它是FileSystemXmlApplicationContext的基类)中找到这个方法,它详细地描述了整个ApplicationContext的初始化过程,比如BeanFactory的更新,messagesource和postprocessor的注册,等等。这里看起来更像是对ApplicationContext进行初始化的模板或执行提纲,这个执行过程为IoC容器Bean的生命周期管理提供了条件。这个IoC容器的refresh过程如代码清单2-8所示。
代码清单2-8 对IoC容器的refresh的实现
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
// 这里是在子类中启动refreshBeanFactory()的地方。
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the Bean Factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
}
}
我们进入到AbstractRefreshableApplicationContext的refreshBeanFactory()方法中,在这个方法里创建了BeanFactory。在创建IoC容器前,如果已经有容器存在,那么需要把已有的容器销毁和关闭,保证在refresh以后使用的是新建立起来的IoC容器。这么看来,这个refresh非常像我们对容器的重启动,就像计算机的重启动那样。在建立好当前的IoC容器以后,开始了对容器的初始化过程,比如BeanDefinition的载入,具体的实现如代码清单2-9所示。
代码清单2-9 AbstractRefreshableApplicationContext的refreshBeanFactory方法
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
//创建IoC容器,这里使用的是DefaultListableBeanFactory。
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
//启动对BeanDefintion的载入。
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing XML document
for " + getDisplayName(), ex);
}
}
这里调用的loadBeanDefinitions实际上是一个抽象方法,那么实际的载入过程是在哪里发生的呢?我们看看前面提到的loadBeanDefinitios在AbstractRefreshableApplicationContext的子类AbstractXmlApplicationContext中的实现,在这个loadBeanDefinitions中,初始化了读取器XmlBeanDefinitionReader,然后再把这个读取器在IoC容器中设置好(过程和编程式使用XmlBeanFactory是类似的),最后是启动读入器来完成BeanDefinition在IoC容器中的载入,如代码清单2-10所示。
代码清单2-10 AbstractXmlApplicationContext中的loadBeanDefinitions
public abstract class AbstractXmlApplicationContext extends
AbstractRefreshableConfigApplicationContext {
public AbstractXmlApplicationContext() {
}
public AbstractXmlApplicationContext(ApplicationContext parent) {
super(parent);
}
//这里是实现loadBeanDefinitions的地方。
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
throws IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
/**
*创建XmlBeanDefinitionReader,并通过回调设置到 BeanFactory中去,
*创建BeanFactory的过程可以参考上文对编程式使用IoC容器的相关分析,这里和前面一样,
*使用的也是 DefaultListableBeanFactory。
*/
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader
(beanFactory);
/**
*Configure the bean definition reader with this context's
*resource loading environment.
*/
/**
*这里设置XmlBeanDefinitionReader,为XmlBeanDefinitionReader
*配置ResourceLoader,因为DefaultResourceLoader是父类,所以this可以直接被使用。
*/
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
/**
*Allow a subclass to provide custom initialization of the reader,
*then proceed with actually loading the bean definitions.
*/
// 这是启动Bean定义信息载入的过程。
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}
protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) {
}
接着就是loadBeanDefinitions调用的地方,首先得到BeanDefinition信息的Resource定位,然后直接调用XmlBeanDefinitionReader读取,具体的载入过程是委托给BeanDefinitionReader完成的。因为这里的BeanDefinition是通过XML文件定义的,所以这里使用XmlBeanDefinitionReader来载入BeanDefinition到容器中,如代码清单2-11所示。
代码清单2-11 XmlBeanDefinitionReader载入XmlBeanDefinitionReader
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws
BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
String[] configLocations = getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}
protected Resource[] getConfigResources() {
return null;
}
}
通过以上实现原理的分析,我们可以看到,在初始化FileSystmXmlApplicationContext的过程中,是通过调用IoC容器的refresh来启动整个BeanDefinition的载入过程的,这个初始化是通过定义的XmlBeanDefinitionReader来完成的。同时,我们也知道实际使用的IoC容器是DefultListableBeanFactory,具体的Resource载入在XmlBeanDefinitionReader读入BeanDefinition时实现。因为Spring可以对应不同形式的BeanDefinition。由于这里使用的是XML方式的定义,所以需要使用XmlBeanDefinitionReader。如果使用了其他的BeanDefinition方式,就需要使用其他种类的BeanDefinitionReader来完成数据的载入工作。在XmlBeanDefinitionReader的实现中可以看到,是在reader.loadBeanDefinitions中开始进行BeanDefinition的载入的,而这时XmlBeanDefinitionReader的父类AbstractBeanDefinitionReader已经为BeanDefinition的载入做好了准备,如代码清单2-12所示。
代码清单2-12 AbstractBeanDefinitionReader载入BeanDefinitions