15. Spring IoC
15.1. 简介
我们的目标是探索 Spring 框架(http://www.springframework.org)的配置与集成能力,并定义和使用 IoC(控制反转)的概念,该概念也称为依赖注入
请考虑我们刚刚构建的三层应用程序:
![]() |
为了响应用户请求,[Application] 控制器必须与 [service] 层进行通信。在我们的示例中,这是一个 [DaoImpl] 类型的实例。[Application] 控制器在其 [init] 方法中获取了 [service] 层的引用(第 14.8.3 节):
- 第 6 行:通过显式创建 [DaoImpl] 实例来实例化 [dao] 层
- 第 9 行:通过显式创建 [ServiceImpl] 实例,实例化了 [service] 层
请注意,[DaoImpl] 和 [ServiceImpl] 类分别实现了 [IDao] 和 [IService] 接口。在未来的版本中,[IDao] 接口将由一个管理数据库中人员列表的类来实现。为了便于说明,我们暂且将该类命名为 [DaoBD]。 若将 [dao] 层的 [DaoImpl] 实现替换为 [DaoBD] 实现,则需要重新编译 [web] 层。事实上,上文第 6 行原本使用 [DaoImpl] 类型实例化 [dao] 层,现在必须改用 [DaoBD] 类型进行实例化。 因此,我们的 [web] 层依赖于 [dao] 层。上文第 9 行显示,它还依赖于 [service] 层。
Spring IoC 将使我们能够创建一个三层应用程序,其中各层彼此独立,即更改一层无需更改其他层。这为应用程序的演进提供了极大的灵活性。
之前的架构将演变为如下形式:
![]() |
借助 [Spring IoC],[应用程序] 控制器将通过以下方式从 [服务] 层获取所需的引用:
- 在其 [init] 方法中,它将请求 [Spring IoC] 层提供对 [服务] 层的引用
- 随后,[Spring IoC] 将使用一个配置 XML 文件,该文件指定了应实例化哪个类以及如何对其进行初始化。
- [Spring IoC] 将创建的 [服务] 层引用返回给 [应用程序] 控制器。
此方案的优势在于,实例化各层的类名不再硬编码在控制器的 [init] 方法中,而是简单地列在配置文件中。更改某层的实现只需修改该配置文件,而无需修改控制器。
现在让我们通过示例来探索 [Spring IoC] 的功能。
15.2. Spring IoC 实践
15.2.1. Spring
[Spring IoC] 是 [http://www.springframework.org/](2006年5月)上发布的一个大型项目的一部分:
![]() | ![]() |
![]() |
- [1]: Spring 使用了多种第三方技术,本文中统称为依赖项。您必须下载包含依赖项的版本,以免日后需要单独下载这些第三方工具库。
- [2]: 下载的 zip 文件的目录结构
- [3]: [Spring] 发行版,即 Spring 项目本身的 .jar 归档文件,不含其依赖项。Spring 的 [IoC] 功能由 [spring-core.jar, spring-beans.jar] 归档文件提供。
- [4,5]:第三方工具归档文件
15.2.2. 示例的 Eclipse 项目
我们将构建三个示例来演示 Spring IoC 的使用。这些示例都将位于以下 Eclipse 项目中:

[springioc-examples] 项目已配置为将源文件和编译后的类放置在项目文件夹的根目录下:
![]() |
- [1]: [Eclipse] 项目文件夹结构
- [2]: Spring 配置文件位于项目根目录下,因此处于应用程序的类路径中
- [3]: 示例 1 中的类
- [4]: 示例 2 中的类
- [5]: 示例 3 中的类
- [6]: 项目库 [spring-core.jar, spring-beans.jar] 位于 Spring 发行版的 [dist] 文件夹中,而 [commons-logging.jar] 位于 [lib/jakarta-commons] 文件夹中。这三个存档文件已包含在应用程序的类路径中。
15.2.3. 示例 1
示例 1 的组件已放置在项目的 [springioc01] 包中:
![]()
[Person] 类如下所示:
该类包含:
- 第 6-7 行:两个私有字段 name 和 age
- 第 23–38 行:这两个字段的 get 和 set 方法
- 第 10-12 行:一个 toString 方法,用于将 [Person] 对象的值转换为字符串
- 第 15-21 行:一个 init 方法(将在 Spring 创建对象时被调用),以及一个 close 方法(将在对象销毁时被调用)
要使用 Spring 实例化 [Person] 类型的对象,我们将使用以下 [spring-config-01.xml] 文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="personne1" class="istia.st.springioc01.Personne" init-method="init" destroy-method="close">
<property name="nom" value="Simon" />
<property name="age" value="40" />
</bean>
<bean id="personne2" class="istia.st.springioc01.Personne" init-method="init" destroy-method="close">
<property name="nom" value="Brigitte" />
<property name="age" value="20" />
</bean>
</beans>
- 第 3 行、第 12 行:<beans> 标签是 Spring 配置文件的根标签。在此标签内,使用 <bean> 标签来定义要创建的各种对象。
- 第 4–7 行:Bean 的定义
- 第 4 行:该 Bean 的名称为 [person1](id 属性),是类 [istia.st.springioc01.Person] 的实例(class 属性)。该实例创建后将调用其 [init] 方法(init-method 属性),销毁前将调用其 [close] 方法(destroy-method 属性)。
- 第 5 行:定义要赋给所创建 [Person] 实例的 [name] 属性(name 属性)的值。为了执行此初始化,Spring 将使用 [setName] 方法。因此,该方法必须存在。本例中正是如此。
- 第 6 行:与上文针对 [age] 属性的定义相同。
- 第 8–11 行:对名为 [person2] 的 Bean 进行了类似的定义
[Main] 测试类如下:
注释:
- 第 10 行:为了获取 [spring-config-01.xml] 文件中定义的 Bean,我们使用了一个 [XmlBeanFactory] 对象,它允许我们实例化 XML 文件中定义的 Bean。 [spring-config-01.xml] 文件将放置在应用程序的 [ClassPath] 中,即 Java 虚拟机在查找应用程序引用的类时会搜索的目录之一。 [ClassPathResource] 对象用于在应用程序的 [ClassPath] 中搜索资源,在本例中即 [spring-config-01.xml] 文件。生成的 [bf] 对象(Bean Factory)允许我们通过语句 bf.getBean("XX") 获取名为 "XX" 的 Bean 的引用。
- 第 12 行:请求 [spring-config-01.xml] 文件中名为 [person1] 的 Bean 的引用。
- 第 13 行:显示对应的 [Person] 对象的值。
- 第 15–16 行:我们对名为 [person2] 的 Bean 执行相同的操作。
- 第 18–19 行:我们再次请求名为 [person2] 的 Bean。
- 第 21 行:移除 [bf] 中的所有 Bean,即那些由 [spring-config-01.xml] 文件创建的 Bean。
执行 [Main] 类将产生以下结果:
注释:
- 第 1 行是通过执行 [Main] 中的第 12 行获得的。该操作
强制创建了 [person1] bean。由于 [person1] bean 的定义中写有 [init-method="init"],因此执行了所创建 [Person] 对象的 [init] 方法。并显示了相应的消息。
- 第 2 行:[Main] 的第 13 行显示了创建的 [Person] 对象的值。
- 第 3–4 行:对于名为 [person2] 的 Bean,该过程重复进行。
- 第 5 行:[Main] 的第 18–19 行中的操作
personne2 = (Personne) bf.getBean("personne2");
System.out.println("personne2=" + personne2.toString());
并未导致创建一个新的 [Person] 类型对象。如果确实如此,[init] 方法就会被调用。这就是单例原则。 默认情况下,Spring 仅在其配置文件中创建 Bean 的单个实例。它是一个对象引用服务。如果被请求提供尚未创建的对象的引用,它会创建该对象并返回一个引用;如果对象已经创建,Spring 则直接返回对其的引用。在此处,由于 [person2] 已经创建,Spring 直接返回对其的引用。
- 第 6–7 行的输出是由 [Main] 的第 21 行触发的,该行请求销毁 [XmlBeanFactory bf] 对象引用的所有 Bean,即 [person1] 和 [person2] 这两个 Bean。由于这两个 Bean 都具有 [destroy-method="close"] 属性,因此两者的 [close] 方法都会被执行,从而导致第 6–7 行被显示出来。
既然我们已经介绍了 Spring 配置的基础知识,接下来的讲解就可以稍微加快一些了。
15.2.4. 示例 2
示例 2 的元素位于项目的 [springioc02] 包中:

首先通过复制粘贴 [springioc01] 包来创建 [springioc02] 包,然后将 [Car] 类添加到该包中,并根据新示例对 [Main] 类进行调整。
[Car] 类如下所示:
该类包含:
- 第 5–7 行:三个私有字段:type、make 和 owner。这些字段可通过第 26–48 行中的公共 get 和 set 方法进行初始化和读取。它们也可以通过第 13–17 行中定义的 Car(String, String, Person) 构造函数进行初始化。该类还具有一个无参构造函数,以符合 JavaBean 标准。
- 第 20–23 行:一个 toString 方法,用于将 [Car] 对象的值转换为字符串
- 第 51–57 行:`init` 方法(将在对象创建后立即由 Spring 调用)和 `close` 方法(将在对象销毁时调用)
要创建 [Car] 类型的对象,我们将使用以下 Spring 配置文件 [spring-config-02.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="personne1" class="istia.st.springioc02.Personne" init-method="init" destroy-method="close">
<property name="nom" value="Simon" />
<property name="age" value="40" />
</bean>
<bean id="personne2" class="istia.st.springioc02.Personne" init-method="init" destroy-method="close">
<property name="nom" value="Brigitte" />
<property name="age" value="20" />
</bean>
<bean id="voiture1" class="istia.st.springioc02.Voiture" init-method="init" destroy-method="close">
<constructor-arg index="0" value="Peugeot" />
<constructor-arg index="1" value="307" />
<constructor-arg index="2">
<ref local="personne2" />
</constructor-arg>
</bean>
</beans>
该文件向 [spring-config-01.xml] 中定义的 Bean 集合(第 12–17 行)添加了一个键为 "car1"、类型为 [Car] 的 Bean。要初始化该 Bean,我们可以这样编写:
与示例 1 中介绍的方法不同,我们在此选择了使用该类的 Car(String, String, Person) 构造函数。
- 第 12 行:定义 Bean 的名称、其类、实例化后要执行的方法以及销毁后要执行的方法。
- 第 13 行:构造函数 [Car(String, String, Person)] 的第一个参数的值。
- 第 14 行:构造函数 [Car(String, String, Person)] 的第二个参数的值。
- 第 15–17 行:构造函数 [Car(String, String, Person)] 的第三个参数的值。该参数的类型为 [Person]。我们为其提供在同一文件中定义的 Bean [person2] 的引用(ref 标签)(本地属性)。
在测试中,我们将使用以下 [Main] 类:
[main] 方法获取 [car1] Bean 的引用(第 12 行),并将其显示出来(第 13 行)。结果如下:
注释:
- [main] 方法请求 [car1] Bean 的引用(第 12 行)。由于该 Bean 尚未创建(单例模式),Spring 开始创建 [car1] Bean。由于 [car1] Bean 引用了 [person2] Bean,因此后者也被相应地构建出来。 [person2] Bean 已被创建。随后执行其 [init] 方法(结果中的第 1 行)。接着 [car1] Bean 被实例化。随后执行其 [init] 方法(结果中的第 2 行)。
- 结果中的第 3 行来自 [main] 的第 13 行:显示了 bean [car1] 的值。
- [main] 的第 15 行请求销毁所有现有的 Bean,这导致结果中的第 4 行和第 5 行被显示。
15.2.5. 示例 3
示例 3 的元素位于项目的 [springioc03] 包中:

首先通过复制粘贴 [springioc01] 包来创建 [springioc03] 包,然后添加 [GroupePersonnes] 类,删除 [Voiture] 类,并根据新示例调整 [Main] 类。
[GroupePersonnes] 类如下所示:
它的两个私有成员是:
第 8 行:members:一个包含该组成员的数组
第 9 行:workingGroups:一个字典,将人员与工作组进行映射
请注意,[PersonGroup] 类并未定义无参构造函数。回顾一下,如果没有显式定义构造函数,则会存在一个“默认”构造函数,即不执行任何操作的无参构造函数。
此处的目的是演示 Spring 如何允许您初始化复杂对象,例如具有数组或字典字段的对象。示例 3 的 Bean 配置文件 [spring-config-03.xml] 如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="personne1" class="istia.st.springioc03.Personne" init-method="init" destroy-method="close">
<property name="nom" value="Simon" />
<property name="age" value="40" />
</bean>
<bean id="personne2" class="istia.st.springioc03.Personne" init-method="init" destroy-method="close">
<property name="nom" value="Brigitte" />
<property name="age" value="20" />
</bean>
<bean id="groupe1" class="istia.st.springioc03.GroupePersonnes" init-method="init" destroy-method="close">
<property name="membres">
<list>
<ref local="personne1" />
<ref local="personne2" />
</list>
</property>
<property name="groupesDeTravail">
<map>
<entry key="Brigitte" value="Marketing" />
<entry key="Simon" value="Ressources humaines" />
</map>
</property>
</bean>
</beans>
- 第 14–17 行:<list> 标签允许您为数组类型的字段或实现 List 接口的字段初始化不同的值。
- 第 20–23 行:<map> 标签允许您对实现 Map 接口的字段执行相同操作。
在我们的测试中,我们将使用以下 [Main] 类:
- 第 12-13 行:我们向 Spring 请求 [group1] Bean 的引用,并显示其值。
结果如下:
注释:
- 在 [Main] 的第 12 行,请求了对 Bean [group1] 的引用。Spring 开始创建该 Bean。由于 Bean [group1] 引用了 Bean [person1] 和 [person2],因此创建了这两个 Bean(结果中的第 1 行和第 2 行)。随后,Bean [group1] 被实例化,并执行了其 [init] 方法(结果中的第 3 行)。
- [Main] 的第 13 行显示了结果的第 4 行。
- [Main] 的第 15 行显示了结果的第 5–7 行。
15.3. 使用 Spring 配置多层应用程序
假设有一个具有以下结构的三层应用程序:
用户数据业务层 [business]数据访问层 [dao]用户界面层 [ui]
在此,我们将演示使用 Spring 构建此类架构的优势。
- 通过使用 Java 接口,这三个层将实现相互独立
- 这三层的集成将由 Spring 负责
Eclipse中的应用程序结构可能如下所示:
![]() |
- [1]:[DAO] 层:
- [IDao]:该层的接口
- [Dao1, Dao2]:该接口的两个实现
- [2]: [业务]层:
- [IMetier]:该层的接口
- [Business1, Business2]:该接口的两个实现
- [3]: [UI] 层:
- [IUi]:该层的接口
- [Ui1, Ui2]:该接口的两个实现
- [4]:应用程序的 Spring 配置文件。我们将通过两种方式配置应用程序。
- [5]:应用程序所需的库。这些是前面的示例中使用的库。
- [6]: 测试包。[Main1] 将使用 [spring-config-01.xml] 配置,而 [Main2] 将使用 [spring-config-02.xml] 配置。
本示例旨在说明,我们可以更改应用程序一个或多个层的实现,而对其他层完全不产生影响。所有操作都在 Spring 配置文件中完成。
[dao] 层
[dao] 层实现了以下 [IDao] 接口:
实现 [Dao1] 将如下所示:
实现 [Dao2] 将如下所示:
[业务]层
[业务]层实现了以下[IMetier]接口:
[Business1] 的实现如下:
[Metier2] 的实现如下:
[ui] 层
[ui] 层实现了以下 [IUi] 接口:
实现 [Ui1] 将如下所示:
实现 [Ui2] 将如下所示:
Spring 配置文件
第一个 [spring-config-01.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- the dao class -->
<bean id="dao" class="istia.st.springioc.troistier.dao.Dao1"/>
<!-- the business class -->
<bean id="metier" class="istia.st.springioc.troistier.metier.Metier1">
<property name="dao">
<ref local="dao" />
</property>
</bean>
<!-- the UI class -->
<bean id="ui" class="istia.st.springioc.troistier.ui.Ui1">
<property name="metier">
<ref local="metier" />
</property>
</bean>
</beans>
第二个 [spring-config-02.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- the dao class -->
<bean id="dao" class="istia.st.springioc.troistier.dao.Dao2"/>
<!-- the business class -->
<bean id="metier"
class="istia.st.springioc.troistier.metier.Metier2">
<property name="dao">
<ref local="dao" />
</property>
</bean>
<!-- the UI class -->
<bean id="ui" class="istia.st.springioc.troistier.ui.Ui2">
<property name="metier">
<ref local="metier" />
</property>
</bean>
</beans>
测试程序
[Main1] 程序如下:
[Main1] 程序使用 [spring-config-01.xml] 配置文件,因此采用了 [Ui1、Metier1、Dao1] 这三个层的实现。Eclipse 控制台中显示的结果:
[Main2] 程序如下:
[Main2] 程序使用 [spring-config-02.xml] 配置文件,因此采用了 [Ui2、Metier2、Dao2] 层级的实现。Eclipse 控制台中显示的结果:
15.4. 结论
我们构建的应用程序具有高度的可扩展性。 我们只需通过配置即可更改某一层级的实现,而其他层级的代码则保持不变。这是通过 IoC(控制反转)概念实现的,它是 Spring 的两大支柱之一。另一大支柱是 AOP(面向切面编程),我们尚未涉及该内容。它允许您通过配置向类方法添加“行为”,而无需修改方法的代码。






