第2章
Spring Framework的核心:IoC容器的实现
朝辞白帝彩云间,千里江陵一日还。
两岸猿声啼不住,轻舟已过万重山。
—【唐】李白《早发白帝城》
本章内容
- Spring IoC容器概述
- IoC容器系列的设计与实现:BeanFactory和ApplicationContext
- IoC容器的初始化过程
- IoC容器的依赖注入
- 容器其他相关特性的设计与实现
2.1 Spring IoC容器概述
2.1.1 IoC容器和依赖反转模式
子曰:温故而知新。在这里,我们先简要地回顾一下依赖反转的相关概念。我们选取维基百科中关于依赖反转的叙述,把这些文字作为我们理解依赖反转这个概念的参考。这里不会对这些原理进行学理上的考究,只是希望提供一些有用的信息,以便给读者一些启示。这个模式非常重要,它是IoC容器得到广泛应用的基础。
维基百科对“依赖反转”相关概念的叙述
早在2004年,Martin Fowler就提出了“哪些方面的控制被反转了?”这个问题。他得出的结论是:依赖对象的获得被反转了。基于这个结论,他为控制反转创造了一个更好的名字:依赖注入。许多非凡的应用(比HelloWorld.java更加优美、更加复杂)都是由两个或多个类通过彼此的合作来实现业务逻辑的,这使得每个对象都需要与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么如你所见,这将导致代码高度耦合并且难以测试。
以上的这段话概括了依赖反转的要义,如果合作对象的引用或依赖关系的管理由具体对象来完成,会导致代码的高度耦合和可测试性的降低,这对复杂的面向对象系统的设计是非常不利的。在面向对象系统中,对象封装了数据和对数据的处理,对象的依赖关系常常体现在对数据和方法的依赖上。这些依赖关系可以通过把对象的依赖注入交给框架或IoC容器来完成,这种从具体对象手中交出控制的做法是非常有价值的,它可以在解耦代码的同时提高代码的可测试性。在极限编程中对单元测试和重构等实践的强调体现了在软件开发过程中对质量的承诺,这是软件项目成功的一个重要因素。
依赖控制反转的实现有很多种方式。在Spring中,IoC容器是实现这个模式的载体,它可以在对象生成或初始化时直接将数据注入到对象中,也可以通过将对象引用注入到对象数据域中的方式来注入对方法调用的依赖。这种依赖注入是可以递归的,对象被逐层注入。就此而言,这种方案有一种完整而简洁的美感,它把对象的依赖关系有序地建立起来,简化了对象依赖关系的管理,在很大程度上简化了面向对象系统的复杂性。
关于如何反转对依赖的控制,把控制权从具体业务对象手中转交到平台或者框架中,是降低面向对象系统设计复杂性和提高面向对象系统可测试性的一个有效的解决方案。它促进了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容器完成对象的注入。这样就在很大程度上简化了应用的开发,把应用从复杂的对象依赖关系管理中解放出来。简单地说,因为很多对象依赖关系的建立和维护并不需要和系统运行状态有很强的关联性,所以可以把在面向对象编程中需要执行的诸如新建对象、为对象引用赋值等操作交由容器统一完成。这样一来,这些散落在不同代码中的功能相同的部分就集中成为容器的一部分,也就是成为面向对象系统的基础设施的一部分。
如果对面向对象系统中的对象进行简单分类,会发现除了一部分是数据对象外,其他很大一部分对象是用来处理数据的。这些对象并不常发生变化,是系统中基础的部分。在很多情况下,这些对象在系统中以单件的形式起作用就可以满足应用的需求,而且它们也不常涉及数据和状态共享的问题。如果涉及数据共享方面的问题,需要在这些单件的基础上再做进一步的处理。
同时,这些对象之间的相互依赖关系也是比较稳定的,一般不会随着应用的运行状态的改变而改变。这些特性使这些对象非常适合由IoC容器来管理,虽然它们存在于应用系统中,但是应用系统并不承担管理这些对象的责任,而是通过依赖反转把责任交给了容器(或者说平台)。了解了这些背景,Spring IoC容器的原理也就不难理解了。在原理的具体实现上,Spring有着自己的独特思路、实现技巧和丰富的产品特性。关于这些原理的实现,下面会进行详细的分析。
2.1.2 Spring IoC的应用场景
在Java EE企业应用开发中,前面介绍的IoC(控制反转)设计模式,是解耦组件之间复杂关系的利器, Spring IoC模块就是这个模式的一种实现。在以Spring为代表的轻量级Java EE开发风行之前,我们更熟悉的是以EJB为代表的开发模式。在EJB模式中,应用开发人员需要编写EJB组件,而这种组件需要满足EJB容器的规范,才能够运行在EJB容器中,从而获取事务管理、生命周期管理这些组件开发的基本服务。从获取的基本服务上看,Spring提供的服务和EJB容器提供的服务并没有太大的差别,只是在具体怎样获取服务的方式上,两者的设计有很大的不同:在Spring中,Spring IoC提供了一个基本的JavaBean容器,通过IoC模式管理依赖关系,并通过依赖注入和AOP切面增强了为JavaBean这样的POJO对象赋予事务管理、生命周期管理等基本功能;而对于EJB,一个简单的EJB组件需要编写远程/本地接口、Home接口以及Bean的实现类,而且EJB运行是不能脱离EJB容器的,查找其他EJB组件也需要通过诸如JNDI这样的方式,从而造成了对EJB容器和技术规范的依赖。也就是说Spring把EJB组件还原成了POJO对象或者JavaBean对象,降低了应用开发对传统J2EE技术规范的依赖。
同时,在应用开发中,以应用开发人员的身份设计组件时,往往需要引用和调用其他组件的服务,这种依赖关系如果固化在组件设计中,就会造成依赖关系的僵化和维护难度的增加,这个时候,如果使用IoC容器,把资源获取的方向反转,让IoC容器主动管理这些依赖关系,将这些依赖关系注入到组件中,那么会让这些依赖关系的适配和管理更加灵活。在具体的注入实现中,接口注入(Type 1 IoC)、setter注入(Type 2 IoC)、构造器注入(Type 3 IoC)是主要的注入方式。在Spring的IoC设计中,setter注入和构造器注入是主要的注入方式;相对而言,使用Spring时setter注入是常见的注入方式,而且为了防止注入异常,Spring IoC容器还提供了对特定依赖的检查。
另一方面,在应用管理依赖关系时,可以通过IoC容器将控制进行反转,在反转的实现中,如果能通过可读的文本来完成配置,并且还能通过工具对这些配置信息进行可视化的管理和浏览,那么肯定是能够提高对组件关系的管理水平,并且如果耦合关系需要变动,并不需要重新修改和编译Java源代码,这符合在面向对象设计中的开闭准则,并且能够提高组件系统设计的灵活性,同时,如果结合OSGi的使用特性,还可以提高应用的动态部署能力。
在具体使用Spring IoC容器的时候,我们可以看到,Spring IoC容器已经是一个产品实现。作为产品实现,它对多种应用场景的适配是通过Spring设计的IoC容器系列来实现的,比如在某个容器系列中可以看到各种带有不同容器特性的实现,可以读取不同配置信息的各种容器,从不同I/O源读取配置信息的各种容器设计,更加面向框架的容器应用上下文的容器设计等。这些丰富的容器设计,已经可以满足广大用户对IoC容器的各种使用需求,这时的Spring IoC容器,已经不是原来简单的Interface21框架了,已经成为一个IoC容器的工业级实现。下面,我们会对IoC容器系列的设计和实现进行详细分析。