Appearance
Spring 核心:IoC 容器 - Part1
Tip:基于 Spring Core 5.3.30 版本。
1. Spring IoC 容器和 Bean 简介
这一章涵盖了 Spring Framework 对控制反转(IoC)原则的实现。IoC 也被称为依赖注入(DI)。它是一个过程,对象通过构造函数参数、工厂方法的参数或在对象实例构造后或从工厂方法返回后设置的属性来定义它们的依赖关系(即它们使用的其他对象)。然后容器在创建 Bean 时注入这些依赖关系。这个过程基本上是对象本身控制其依赖项的实例化或定位的逆过程(因此称为 “控制反转”),它不是通过直接构造类或使用 Service Locator 模式等机制来实现的。
org.springframework.beans 和 org.springframework.context 包是 Spring Framework IoC 容器的基础。BeanFactory 接口提供了一个高级配置机制,能够管理任何类型的对象。ApplicationContext 是 BeanFactory 的子接口。它添加了以下功能:
- 更容易集成 Spring 的 AOP 功能;
- 消息资源处理(用于国际化);
- 事件发布;
- 用于 Web 应用程序的
WebApplicationContext等应用层特定上下文;
简而言之,BeanFactory 提供了配置框架和基本功能,而 ApplicationContext 添加了更多面向企业的功能。ApplicationContext 是 BeanFactory 的完整超集,并且在本章中专门用于描述 Spring 的 IoC 容器。有关使用 BeanFactory 而不是 ApplicationContext 的更多信息,请参阅 BeanFactory API 小节。
在 Spring 中,由 Spring IoC 容器管理的构成应用程序骨干的对象称为 Bean。Bean 是由 Spring IoC 容器实例化、组装和管理的对象。否则,Bean 只是应用程序中的众多对象之一。Bean 及其之间的依赖关系在容器使用的配置元数据中反映出来。
2. 容器概述
org.springframework.context.ApplicationContext 接口代表了 Spring IoC 容器,负责实例化、配置和组装 Bean。容器通过读取配置元数据来获取关于要实例化、配置和组装哪些对象的指令。配置元数据可以用 XML、Java 注解或 Java 代码表示。它允许您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系。
Spring 提供了多个 ApplicationContext 接口的实现。在独立应用程序中,通常会创建 ClassPathXmlApplicationContext 或 FileSystemXmlApplicationContext 的实例。虽然 XML 一直是定义配置元数据的传统格式,但您可以通过提供少量 XML 配置来声明性地启用对这些额外元数据格式的支持,从而指示容器使用 Java 注解或代码作为元数据格式。
在大多数应用程序场景中,通常不需要显式的用户代码来实例化一个或多个 Spring IoC 容器的实例。例如,在 Web 应用程序场景中,通常只需要在应用程序的 web.xml 文件中提供简单的八行左右的样板 Web 描述符 XML 就足够了(请参阅 Web 应用程序的 ApplicationContext 便捷实例化小节)。如果您使用 Spring Tools for Eclipse(一种基于 Eclipse 的开发环境),您可以轻松地通过几次鼠标点击或按键来创建这个样板配置。
下图显示了 Spring 的高级视图。您的应用程序类与配置元数据结合在一起,因此在创建和初始化 ApplicationContext 后,您就拥有了一个完全配置和可执行的系统或应用程序。

2.1. 配置元数据
如图 1.1 所示,Spring IoC 容器使用某种形式的配置元数据。这个配置元数据代表了您作为应用程序开发者告诉 Spring 容器如何实例化、配置和组装应用程序中的对象。
配置元数据传统上是以简单直观的 XML 格式提供的,这也是本章大部分内容用来传达 Spring IoC 容器的关键概念和特性的方式。
Note:基于 XML 的元数据配置并不是唯一允许的配置元数据形式。Spring IoC 容器本身与实际编写配置元数据的格式完全解耦。如今,许多开发人员选择为他们的 Spring 应用程序使用基于 Java 的配置。
有关在 Spring 容器中使用其他形式的元数据的信息,请参阅以下内容:
基于注解的配置:Spring 2.5 引入了对基于注解的配置元数据的支持。
基于 Java 的配置:从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多功能已成为核心 Spring Framework 的一部分。因此,您可以使用 Java 而不是 XML 文件在应用程序类外部定义 Bean。要使用这些新功能,请参阅
@Configuration、@Bean、@Import和@DependsOn注解。
Spring 配置至少包含一个通常包含多个 Bean 定义,容器必须管理这些 Bean。基于 XML 的配置元数据将这些 Bean 配置为 <beans/> 元素内的 <bean/> 元素。Java 配置通常使用 @Configuration 类中的 @Bean 注解方法。
这些 Bean 定义对应于构成您应用程序的实际对象。通常,您会定义服务层对象、数据访问对象(DAO)、表示层对象(如 Struts Action 实例)、基础设施对象(如 Hibernate SessionFactories、JMS Queues 等)。通常情况下,不会在容器中配置细粒度的领域对象,因为创建和加载领域对象通常是 DAO 和业务逻辑的责任。但是,您可以使用 Spring 与 AspectJ 的集成来配置在 IoC 容器之外创建的对象。请参阅使用 AspectJ 和 Spring 对领域对象进行依赖注入。
以下示例显示了基于 XML 的配置元数据的基本结构:
XML
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions go here -->
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
id属性是一个字符串,用于标识单个 Bean 定义;class属性定义了 Bean 的类型,并使用了完全限定的类名;
id 属性的值是指向协作对象的。本示例中未显示用于引用协作对象的 XML。有关更多信息,请参阅依赖小节。
2.2. 实例化容器
提供给 ApplicationContext 构造函数的位置路径或路径是资源字符串,它们让容器可以从各种外部资源(如本地文件系统、Java CLASSPATH 等)加载配置元数据。
Java
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");Note:在了解了 Spring 的 IoC 容器之后,您可能想进一步了解 Spring 的资源抽象(如《Spring 核心:资源》中所述)。它提供了一种方便的机制,用于从 URI 语法中定义的位置读取
InputStream。特别是,用于构建应用程序上下文的资源路径。
以下示例显示了服务层对象的配置文件 services.xml:
XML
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="itemDao" ref="itemDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
以下示例显示了数据访问对象的配置文件 daos.xml:
XML
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for data access objects go here -->
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在前面的示例中,服务层由 PetStoreServiceImpl 类和两个数据访问对象 JpaAccountDao 和 JpaItemDao 组成。property 元素中的 name 属性指的是 JavaBean 属性的名称,而 ref 元素指的是另一个 Bean 定义的名称。id 和 ref 元素之间的这种关联表示了协作对象之间的依赖关系。有关配置对象依赖项的详细信息,请参阅依赖小节。
2.2.1. 编写基于 XML 的配置元数据
跨多个 XML 文件定义 Bean 可以很有用。通常,每个单独的 XML 配置文件代表架构中的一个逻辑层或模块。
您可以使用应用程序上下文的构造函数从所有这些 XML 片段加载 Bean 定义。这个构造函数接受多个资源位置,就像前面的部分所示。或者,可以使用一个或多个 <import/> 元素来从另一个文件或多个文件中加载 Bean 定义。以下示例显示了如何实现这一点:
XML
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
在上述示例中,外部 Bean 定义从三个文件中加载:services.xml、messageSource.xml 和 themeSource.xml。所有位置路径都相对于执行导入操作的定义文件,因此 services.xml 必须与执行导入操作的文件位于相同的目录或类路径位置,而 messageSource.xml 和 themeSource.xml 必须位于执行导入操作的文件的 resources 路径下。如您所见,前导斜杠被忽略了。但是,考虑到这些路径是相对的,最好根本不要使用斜杠。被导入文件的内容,包括顶级的 <beans/> 元素,必须符合 Spring Schema 的有效 XML Bean 定义。
Note
可以使用相对的
../路径引用父目录中的文件,但不建议这样做。这样做会创建对当前应用程序之外的文件的依赖关系。特别是对于classpath:URL(例如,classpath:../services.xml),这种引用并不推荐,因为运行时解析过程会选择 “最近” 的classpath根目录,然后查找其父目录。classpath配置的更改可能会导致选择不同的、不正确的目录。您始终可以使用完全限定的资源位置而不是相对路径,例如,
file:C:/config/services.xml或classpath:/config/services.xml。但是,请注意,这样做将您的应用程序配置与特定的绝对位置耦合在一起。通常最好保持对这些绝对位置的间接引用,例如,通过${…}占位符,这些占位符在运行时根据 JVM 系统属性进行解析。
命名空间本身提供了导入指令功能。除了普通的 Bean 定义之外,Spring 提供的一系列 XML 命名空间中还有更多的配置功能,例如,context 和 util 命名空间。
2.2.2. Groovy Bean 定义 DSL
作为外部化配置元数据的进一步示例,我们也可以在 Spring 的 Groovy Bean 定义 DSL 中表达 Bean 定义,这在 Grails 框架中是众所周知的。通常,这种配置存在于一个 .groovy 文件中,其结构如下面的示例所示:
Groovy
beans {
dataSource(BasicDataSource) {
driverClassName = "org.hsqldb.jdbcDriver"
url = "jdbc:hsqldb:mem:grailsDB"
username = "sa"
password = ""
settings = [mynew:"setting"]
}
sessionFactory(SessionFactory) {
dataSource = dataSource
}
myService(MyService) {
nestedBean = { AnotherBean bean ->
dataSource = dataSource
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这种配置风格在很大程度上等同于 XML 的 Bean 定义,甚至支持 Spring 的 XML 配置命名空间。它还允许通过 importBeans 指令导入 XML Bean 定义文件。
2.3. 使用容器
ApplicationContext 是一个高级工厂的接口,能够维护不同 Bean 及其依赖项的注册表。通过使用方法 T getBean(String name, Class<T> requiredType),你可以检索你的 Bean 的实例。
ApplicationContext 允许你读取 Bean 定义并访问它们,如下面的示例所示:
Java
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// use configured instance
List<String> userList = service.getUsernameList();1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
使用 Groovy 配置,引导程序看起来非常相似。它有一个不同的上下文实现类,这个类能够理解 Groovy(但也理解 XML Bean 定义)。以下示例显示了 Groovy 配置:
Java
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");最灵活的选项是结合读取器代理的 GenericApplicationContext,例如,对于 XML 文件,可以使用 XmlBeanDefinitionReader,如下例所示:
Java
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();1
2
3
2
3
你也可以使用 GroovyBeanDefinitionReader 来读取 Groovy 文件,如下例所示:
Java
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();1
2
3
2
3
你可以在同一个 ApplicationContext 上混合并匹配这些读取器代理,从各种配置源读取 Bean 定义。
然后,你可以使用 getBean 来检索你的 Bean 实例。ApplicationContext 接口还有一些其他的方法用于检索 Bean,但是,理想情况下,你的应用程序代码永远不应该使用它们。实际上,你的应用程序代码根本不应该调用 getBean() 方法,因此完全不依赖于 Spring API。例如,Spring 与 Web 框架的集成为各种 Web 框架组件(如控制器和 JSF 管理的 Bean)提供了依赖注入,让你通过元数据(如自动装配注解)声明对特定 Bean 的依赖。
3. Bean 概述
Spring IoC 容器管理一个或多个 Bean。这些 Bean 是根据你提供给容器的配置元数据创建的(例如,以 XML <bean/> 定义的形式)。
在容器内部,这些 Bean 定义被表示为 BeanDefinition 对象,其中包含(但不限于)以下元数据:
包限定的类名:通常是正在定义的 Bean 的实际实现类;
Bean 行为配置元素,说明 Bean 在容器中的行为应如何(作用域、生命周期回调等);
需要为 Bean 工作的其他 Bean 的引用。这些引用也被称为协作者或依赖项;
在新创建的对象中设置的其他配置设置。例如,管理连接池的 Bean 中使用的池的大小限制或连接数量;
这些元数据转化为构成每个 Bean 定义的一组属性。以下表格描述了这些属性:
| 属性 | 解释 |
|---|---|
| 类 | 实例化 Bean |
| 名称 | Bean 命名 |
| 作用域 | Bean 作用域 |
| 构造函数参数 | 依赖注入 |
| 属性 | 依赖注入 |
| 自动装配模式 | 自动装配协作者 |
| 延迟初始化模式 | 延迟初始化的 Bean |
| 初始化方法 | 初始化回调 |
| 销毁方法 | 销毁回调 |
除了包含如何创建特定 Bean 的信息的 Bean 定义之外,ApplicationContext 的实现还允许注册由用户在容器外部创建的现有对象。这是通过访问 ApplicationContext 的 BeanFactory 来完成的,通过 getBeanFactory() 方法,该方法返回 DefaultListableBeanFactory 的实现。DefaultListableBeanFactory 通过 registerSingleton(...) 和 registerBeanDefinition(...) 方法支持这种注册。然而,典型的应用程序仅与通过常规 Bean 定义元数据定义的 Bean 一起工作。
Note:Bean 元数据和手动提供的单例实例需要尽早注册,以便容器在自动装配和其他内省步骤中正确地推理它们。虽然在一定程度上支持覆盖现有的元数据和现有的单例实例,但是在运行时(与工厂的实时访问并发)注册新的 Bean 并不被官方支持,这可能会导致并发访问异常,或者使 Bean 容器的状态不一致,或者两种情况同时出现。
3.1. Bean 命名
每个 Bean 都有一个或多个标识符。这些标识符在托管 Bean 的容器中必须是唯一的。一个 Bean 通常只有一个标识符。然而,如果需要多于一个的标识符,额外的标识符可以被视为别名。
在基于 XML 的配置元数据中,你可以使用 id 属性、name 属性或两者都用来指定 Bean 的标识符。id 属性让你指定一个确切的 Id。通常,这些名称是字母与数字的组合(例如 myBean、someService 等),但也可以包含特殊字符。如果你想为 Bean 引入其他别名,则你也可以在 name 属性中指定它们,并用逗号(,)、分号(;)或空格进行分隔。
Note:在 Spring 3.1 之前的版本中,
id属性被定义为xsd:ID类型,这限制了可用的字符。从 3.1 版本开始,它被定义为xsd:string类型。请注意,尽管不再由 XML 解析器强制执行 Bean Id 的唯一性,但容器仍然会强制执行。
你可以不用为 Bean 提供 name 或 id。如果你没有明确提供 name 或 id,容器会为该 Bean 生成一个唯一的名称。然而,如果你想通过名称引用该 Bean,通过使用 ref 元素或服务定位器的方式查找,则你必须提供一个名称。不提供名称的动机与使用内部 Bean 和自动装配协作者有关。
Note:Bean 命名规范
命名 Bean 时,约定是使用标准的 Java 实例字段名称的约定。也就是说,Bean 的名称以小写字母开头,然后使用驼峰式命名。这样的名称的例子包括
accountManager、accountService、userDao、loginController等。一致地命名 Bean 可以使你的配置更容易阅读和理解。此外,如果你使用 Spring AOP,当对一组通过名称相关联的 Bean 应用建议时,它会有很大的帮助。
Note:在类路径中进行组件扫描时,Spring 会为未命名的组件生成 Bean 名称,并遵循前面描述的规则:基本上,取简单的类名并将其初始字符转为小写。然而,在特殊情况下,当有多于一个字符且第一和第二个字符都是大写时,则原始的大小写保持不变。这些规则与
java.beans.Introspector.decapitalize定义的规则相同(Spring 在这里使用了它)。
3.1.1. 在 Bean 定义之外为 Bean 设置别名
在 Bean 定义本身中,你可以为 Bean 提供多个名称,通过使用由 id 属性指定的最多一个名称和 name 属性中的任意数量的其他名称的组合。这些名称可以是同一 Bean 的等效别名,对于某些情况很有用,例如让应用程序中的每个组件通过使用特定于该组件本身的 Bean 名称来引用公共依赖项。
然而,只在 Bean 实际定义的地方指定所有别名并不总是足够的。有时候,可能希望为在其他地方定义的 Bean 引入一个别名。这通常是在大型系统中的情况,其中配置在每个子系统之间被分割,每个子系统都有自己的对象定义集。在基于 XML 的配置元数据中,你可以使用 <alias/> 元素来实现这点:
XML
<alias name="fromName" alias="toName"/>在这种情况下,一个名为 fromName 的 Bean(在同一个容器中)也可以在使用这个别名定义后被称为 toName。
例如,子系统 A 的配置元数据可能通过 subsystemA-dataSource 的名称引用一个 DataSource。子系统 B 的配置元数据可能通过 subsystemB-dataSource 的名称引用一个 DataSource。当组成使用这两个子系统的主应用程序时,主应用程序通过 myApp-dataSource 的名称引用 DataSource。为了让这三个名称都引用同一个对象,你可以将以下别名定义添加到配置元数据中:
XML
<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>1
2
2
现在,每个组件和主应用程序都可以通过一个独特的名称来引用 DataSource,这个名称保证不会与任何其他定义冲突(有效地创建了一个命名空间),但它们都引用同一个 Bean。
Note:如果你使用 Java 配置,
@Bean注解可以用来提供别名。详细信息请参见 Bean 别名小节。
3.2. 实例化 Bean
一个 Bean 定义本质上是创建一个或多个对象的配方。当容器被要求创建一个命名的 Bean 时,它会查看该 Bean 的定义,使用被该 Bean 定义封装的配置元数据来创建(或获取)一个实际的对象。
如果你使用基于 XML 的配置元数据,你可以在 <bean/> 元素的 class 属性中指定要实例化的对象的类型(或类)。这个 class 属性(在 BeanDefinition 实例上内部表示为一个 Class 属性)通常是必需的(有一些例外情况,请参见通过实例工厂方法实例化和 Bean 定义继承小节)。你可以用两种方式之一使用 Class 属性:
通常情况下,用于指定要构造的 Bean 类,在容器自身通过调用其构造函数反射地创建 Bean 的情况下,这与使用
new操作符的 Java 代码有些等价;在较少见的情况下,容器会从包含指定静态工厂方法的类上调用该静态工厂方法来创建 Bean。从调用静态工厂方法返回的对象类型可以是相同的类,也可以是完全不同的类;
Tip:嵌套类名称
如果你想为嵌套类配置一个 Bean 定义,你可以使用嵌套类的二进制名称或源名称。
例如,如果你有一个名为
SomeThing的类在com.example包中,而这个SomeThing类有一个名为OtherThing的静态嵌套类,它们可以用美元符号$或点号.分隔。因此,在 Bean 定义中,class属性的值可以是com.example.SomeThing$OtherThing或com.example.SomeThing.OtherThing。
3.2.1. 通过构造函数实例化
当你使用构造函数方法创建一个 Bean 时,所有普通的类都可以被 Spring 使用并与之兼容。也就是说,被开发的类不需要实现任何特定的接口,也不需要按照特定的方式编码。只需要指定 Bean 的类即可。然而,根据你为该特定 Bean 使用的 IoC 类型的不同,你可能需要一个默认(空)构造函数。
Spring IoC 容器可以管理几乎任何你想要管理的类。它不限于管理真正的 JavaBean。大多数 Spring 用户更喜欢实际的 JavaBean,它只包含一个默认(无参数)构造函数和根据容器中属性模型设计的适当的 Setter 和 Getter。你也可以在容器中使用更多奇异的非 Bean 风格的类。例如,如果你需要使用一个完全不符合 JavaBean 规范的旧连接池,Spring 也可以对其进行管理。
通过基于 XML 的配置元数据,你可以如下指定你的 Bean 类:
XML
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>1
2
3
2
3
关于向构造函数提供参数(如果需要)以及在对象构造后设置对象实例属性的机制的详细信息,请参阅依赖注入小节。
3.2.2. 通过静态工厂方法实例化
当定义一个使用静态工厂方法创建的 Bean 时,可以使用 class 属性指定包含静态工厂方法的类,并使用名为 factory-method 的属性指定工厂方法的名称。你应该能够调用这个方法,并返回一个活动对象,随后该对象将被视为通过构造函数创建的对象。这样一个 Bean 定义的用途之一是在旧代码中调用静态工厂方法。
以下 Bean 定义指定了将通过调用工厂方法创建该 Bean。该定义没有指定返回对象的类型(类),而是指定了包含工厂方法的类。在这个例子中,createInstance() 方法必须是一个静态方法。以下示例展示了如何指定工厂方法:
XML
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>1
2
3
2
3
以下示例展示了一个可以与前面的 Bean 定义配合使用的类:
Java
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
有关向工厂方法提供(可选)参数以及在从工厂返回对象后设置对象实例属性的机制的详细信息,请参阅依赖和配置细节小节。
3.2.3. 通过实例工厂方法实例化
与使用静态工厂方法进行实例化类似,使用实例工厂方法进行实例化会调用容器中现有 Bean 的非静态方法来创建一个新的 Bean。要使用该机制,首先需要将 class 属性留空,然后在 factory-bean 属性中指定当前(或父级或祖先)容器中包含要调用以创建对象的实例方法的 Bean 的名称。使用 factory-method 属性设置工厂方法的名称。以下示例展示了如何配置这样的 Bean:
XML
<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>
<!-- the bean to be created via the factory bean -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
以下示例展示了相应的类:
Java
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
一个工厂类也可以包含多个工厂方法,如下面的示例所示:
XML
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
<bean id="accountService"
factory-bean="serviceLocator"
factory-method="createAccountServiceInstance"/>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
以下示例展示了相应的类:
Java
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
private static AccountService accountService = new AccountServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
public AccountService createAccountServiceInstance() {
return accountService;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
这种方法表明工厂 Bean 本身也可以通过依赖注入(DI)进行管理和配置。详细信息请参阅依赖和配置细节小节。
Note:在 Spring 文档中,“工厂 Bean” 指的是在 Spring 容器中配置的一个通过实例或静态工厂方法创建对象的 Bean。相比之下,“FactoryBean” 指的是 Spring 特定的
FactoryBean实现类。
3.2.4. 确定 Bean 的运行时类型
确定特定 Bean 的运行时类型并不是一件简单的事情。在 Bean 元数据定义中指定的类只是一个初始类引用,可能会与声明的工厂方法结合,或者是一个 FactoryBean 类,这可能导致 Bean 的运行时类型不同,或者在实例级别的工厂方法的情况下根本没有设置(这种情况下将通过指定的 factory-bean 名称来解析)。此外,AOP 代理可能会使用基于接口的代理包装 Bean 实例,限制了目标 Bean 实际类型的暴露(只暴露其实现的接口)。
确定特定 Bean 的实际运行时类型的推荐方法是使用 BeanFactory.getType 调用指定的 Bean 名称。这将考虑到上述所有情况,并返回 BeanFactory.getBean 调用将返回的对象类型。
4. 依赖
典型的企业应用程序不只是由单个对象(或者在 Spring 术语中叫做 Bean)组成。即使是最简单的应用程序也有几个对象共同工作,以呈现给最终用户看到的统一的应用程序。接下来的部分将解释如何从定义一些独立的 Bean 定义,过渡到一个完全实现的应用程序,在这个应用程序中,对象相互协作以共同实现目标。
4.1. 依赖注入
依赖注入(DI)是一种过程,对象通过构造函数参数、工厂方法的参数或在对象实例构造、从工厂方法返回后设置的属性,来定义它们的依赖关系(即它们所使用的其他对象)。然后容器在创建 Bean 时注入这些依赖关系。这个过程基本上是对象本身控制其依赖项的实例化或位置的反转(因此称为控制反转),而不是通过直接构造类或服务定位器模式来实现。
采用 DI 原则编写的代码更清晰,当对象被提供其依赖项时,解耦效果更好。对象不会查找其依赖项,也不知道依赖项的位置或类。因此,您的类在使用接口或抽象基类作为依赖项时更容易进行测试,这样可以在单元测试中使用存根(stub)或模拟(mock)实现。
DI 主要有两种变体:基于构造函数的依赖注入和基于 Setter 的依赖注入。
4.1.1. 基于构造函数的依赖注入
基于构造函数的依赖注入是通过容器调用具有多个参数的构造函数来实现的,每个参数代表一个依赖项。使用特定参数调用静态工厂方法来构造 Bean 几乎是等效的,本文档将构造函数的参数和静态工厂方法的参数视为相似的。以下示例展示了一个只能通过构造函数注入依赖项的类:
Java
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private final MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
请注意,这个类并没有什么特别之处。它是一个普通的 POJO,没有依赖于容器特定的接口、基类或注解。
4.1.1.1. 构造函数参数解析
构造函数参数解析匹配是根据参数的类型进行的。如果在 Bean 定义的构造函数参数中不存在潜在的歧义,那么在 Bean 定义中定义的构造函数参数的顺序就是在实例化 Bean 时向适当的构造函数提供这些参数的顺序。考虑以下类:
Java
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
假设 ThingTwo 和 ThingThree 类没有继承关系,那么就不会存在潜在的歧义。因此,以下配置可以正常工作,并且你不需要在 <constructor-arg/> 元素中显式地指定构造函数参数的索引或类型。
XML
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
</beans>1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
当另一个 Bean 被引用时,类型是已知的,可以进行匹配(就像前面的例子一样)。当使用简单类型时,比如 <value>true</value>,Spring 无法确定值的类型,因此在没有帮助的情况下无法按类型进行匹配。考虑以下类:
Java
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private final int years;
// The Answer to Life, the Universe, and Everything
private final String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
构造函数参数类型匹配
在上述场景中,如果你通过使用 type 属性来显式指定构造函数参数的类型,容器就可以对简单类型使用类型匹配,如下面的示例所示:
XML
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>1
2
3
4
2
3
4
构造函数参数索引
你可以使用 index 属性来显式指定构造函数参数的索引,如下面的示例所示:
XML
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>1
2
3
4
2
3
4
除了解决多个简单值的歧义之外,指定索引还可以解决构造函数具有两个相同类型参数的歧义。
构造函数参数名称
你还可以使用构造函数参数的名称来消除歧义,如下面的示例所示:
XML
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>1
2
3
4
2
3
4
请注意,为了使此功能开箱即用,你的代码必须使用启用了调试标志的方式编译,以便 Spring 可以从构造函数中查找参数名。如果你无法或不想使用调试标志编译代码,你可以使用 @ConstructorProperties JDK 注解来显式命名你的构造函数参数。然后样例类将如下所示:
Java
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
4.1.2. 基于 Setter 的依赖注入
基于 Setter 的依赖注入是通过容器在调用无参构造函数或无参静态工厂方法实例化 Bean 后,调用 Bean 的 Setter 方法来实现的。
以下示例展示了一个只能通过纯 Setter 注入进行依赖注入的类。这个类是传统的 Java 类,是一个普通的 POJO,没有依赖于容器特定的接口、基类或注解。
Java
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
ApplicationContext 支持构造函数和 Setter 两种方式的依赖注入。它还支持在一些依赖项已经通过构造函数注入后,再使用 Setter 进行注入。你可以通过 BeanDefinition 的形式配置这些依赖项,然后结合 PropertyEditor 实例来将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户不直接(即以编程方式)使用这些类,而是使用 XML bean 定义、带有 @Component、@Controller 等注解的组件,或者基于 Java 配置的 @Configuration 类中的 @Bean 方法。然后,这些源会在内部被转换为 BeanDefinition 实例,并用于加载整个 Spring IoC 容器实例。
Tip
在选择构造函数注入还是 Setter 注入时,通常的经验法则是使用构造函数注入来处理必需的依赖项,而使用 Setter 方法或配置方法来处理可选的依赖项。需要注意的是,可以使用
@Required注解在 Setter 方法上,将属性声明为必需的依赖项。但是,最好使用带有程序验证参数的构造函数注入。Spring 团队通常倾向于使用构造函数注入,因为这样可以将应用程序组件实现为不可变对象,并确保必需的依赖项不为
null。此外,通过构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。另外,需要注意的是,大量的构造函数参数是一个糟糕的代码特征,意味着该类可能承担了太多的职责,应该重构以更好地解决关注点分离的问题。Setter 注入主要应该用于可以在类内部分配合理默认值的可选依赖项。否则,代码中使用依赖项时必须进行非空检查。Setter 注入的一个好处是,Setter 方法使得该类的对象可以在以后重新配置或重新注入。因此,通过 JMX MBeans 进行管理是 Setter 注入的一个强有力的用例。
应该根据特定类选择最合适的 DI 风格。有时,当处理第三方类时,你可能没有源代码,因此选择可能已经为你做出。例如,如果第三方类没有暴露任何 Setter 方法,那么构造函数注入可能是唯一可用的 DI 形式。
4.1.3. 依赖解析过程
容器执行 Bean 的依赖项解析如下:
ApplicationContext是通过描述所有 Bean 的配置元数据创建和初始化的。配置元数据可以由 XML、Java 代码或注解来指定;对于每个 Bean,其依赖项以属性、构造函数参数或静态工厂方法的参数形式表达(如果你使用静态工厂方法而不是普通的构造函数)。这些依赖项在实际创建 Bean 时提供给它;
每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 Bean 的引用;
每个作为值的属性或构造函数参数都会从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,如
int、long、String、boolean等;
Spring 容器在创建时会验证每个 Bean 的配置。然而,直到 Bean 实际创建时,Bean 的属性才会被设置。那些单例作用域的 Bean 并且被设置为预先实例化(默认)在容器创建时就会被创建。作用域在 Bean Scopes 中定义。否则,只有在请求时才会创建 Bean。创建一个 Bean 可能会导致一组的 Bean 被创建,因为 Bean 的依赖项以及其依赖项的依赖项(等等)都会被创建和分配。请注意,这些依赖项之间的解析不匹配可能会晚些出现 —— 也就是在首次创建受影响的 Bean 时。
Tip:循环依赖
如果你主要使用构造函数注入,可能会创建一个无法解决的循环依赖场景。
例如:类 A 通过构造函数注入需要一个类 B 的实例,而类 B 通过构造函数注入需要一个类 A 的实例。如果你配置类 A 和 B 的 Bean 互相注入,Spring IoC 容器会在运行时检测到这个循环引用,并抛出一个
BeanCurrentlyInCreationException。一种可能的解决方案是编辑一些类的源代码,使其通过 Setter 而不是构造函数进行配置。或者,避免使用构造函数注入,只使用 Setter 注入。换句话说,虽然不推荐,但你可以使用 Setter 注入来配置循环依赖。
与典型情况(没有循环依赖)不同,Bean A 和 Bean B 之间的循环依赖迫使其中一个 Bean 在自身完全初始化之前被注入到另一个 Bean 中(一个经典的鸡和蛋的场景)。
你通常可以信任 Spring 做正确的事情。它在容器加载时检测配置问题,如引用到不存在的 Bean 和循环依赖。Spring 在 Bean 实际创建时尽可能晚地设置属性和解决依赖。这意味着,一个正确加载的 Spring 容器在你请求一个对象时可能会产生一个异常,如果创建该对象或其依赖项存在问题 —— 例如,Bean 由于缺少或无效的属性而抛出一个异常。一些配置问题可能会延迟显示,这就是为什么 ApplicationContext 实现默认预先实例化单例 Bean。尽管在实际需要之前创建这些 Bean 会增加一些前期时间和内存的开销,但这样你在 ApplicationContext 创建时就能发现配置问题,而不是在后面的某个时候。你可以覆盖这个默认行为,使得单例 Bean 延迟初始化,而不是急切地预先实例化。
如果不存在循环依赖,当一个或多个协作 Bean 被注入到一个依赖 Bean 中时,每个协作 Bean 在被注入到依赖 Bean 之前都被会完全配置。这意味着,如果 Bean A 依赖于 Bean B,那么 Spring IoC 容器在调用 Bean A 的 Setter 方法之前就完全配置了 Bean B。换句话说,Bean 被实例化(如果它不是一个预先实例化的单例),它的依赖项被设置,并且相关的生命周期方法(如配置的 init 方法或 InitializingBean 回调方法)被调用。
4.1.4. 依赖注入的示例
以下示例使用基于 XML 的配置元数据进行基于 Setter 的依赖注入。Spring XML 配置文件的一部分如下所示,它指定了一些 Bean 的定义:
XML
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
以下示例展示了对应的 ExampleBean 类:
Java
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在前面的例子中,声明了与 XML 文件中指定的属性相匹配的 Setter。以下示例使用基于构造函数的依赖注入:
XML
<bean id="exampleBean" class="examples.ExampleBean">
<!-- constructor injection using the nested ref element -->
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<!-- constructor injection using the neater ref attribute -->
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
以下示例展示了对应的 ExampleBean 类:
Java
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public ExampleBean(
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
this.beanOne = anotherBean;
this.beanTwo = yetAnotherBean;
this.i = i;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
在 Bean 定义中指定的构造函数参数将作为 ExampleBean 构造函数的参数使用。
现在考虑这个示例的一个变体,在这个变体中,不使用构造函数,而是告诉 Spring 调用一个静态工厂方法来返回对象的实例:
XML
<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
<constructor-arg ref="anotherExampleBean"/>
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
以下示例展示了对应的 ExampleBean 类:
Java
public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}
// a static factory method; the arguments to this method can be
// considered the dependencies of the bean that is returned,
// regardless of how those arguments are actually used.
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
静态工厂方法的参数由 <constructor-arg/> 元素提供,与实际使用构造函数时完全相同。由工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在这个示例中是相同的)。一个实例(非静态)工厂方法可以以基本相同的方式使用(除了使用 factory-bean 属性而不是 class 属性之外),所以我们不在这里讨论这些细节。
4.2. 依赖和配置细节
正如前面所述,您可以将 Bean 属性和构造函数参数定义为对其他托管 Bean(协作者)的引用,也可以定义为内联值。Spring 的基于 XML 的配置元数据支持 <property/> 和 <constructor-arg/> 元素内的子元素类型,用于实现此目的。
4.2.1. 直接值(原始类型、字符串等)
<property/> 元素的 value 属性将属性或构造函数参数指定为可读的字符串表示。Spring 的转换服务用于将这些值从字符串转换为属性或参数的实际类型。以下示例展示了设置各种值的情况:
XML
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="misterkaoli"/>
</bean>1
2
3
4
5
6
7
2
3
4
5
6
7
以下示例使用 p: 命名空间来实现更加简洁的 XML 配置:
XML
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="root"
p:password="misterkaoli"/>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
以上的 XML 更加简洁。然而,除非您使用支持在创建 Bean 定义时自动完成属性的 IDE(如 IntelliJ IDEA 或 Spring Tools for Eclipse),否则只会在运行时而不是设计时发现拼写错误。强烈推荐使用这样的 IDE 功能。
您还可以配置 java.util.Properties 实例,如下所示:
XML
<bean id="mappings"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<!-- typed as a java.util.Properties -->
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
</value>
</property>
</bean>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Spring 容器通过使用 JavaBeans PropertyEditor 机制将 <value/> 元素内部的文本转换为 java.util.Properties 实例。这是一个很好的快捷方式,也是 Spring 团队在少数几个地方更倾向于使用嵌套的 <value/> 元素而不是 value 属性风格的原因之一。
4.2.1.1. idref 元素
idref 元素是一种简单而可靠的方法,用于将容器中另一个 Bean 的 id(一个字符串值,而不是一个引用)传递给 <constructor-arg/> 或 <property/> 元素。以下示例展示了如何使用它:
XML
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>1
2
3
4
5
6
7
2
3
4
5
6
7
在运行时,前面的 Bean 定义片段与以下片段完全等效:
XML
<bean id="theTargetBean" class="..."/>
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>1
2
3
4
5
2
3
4
5
第一种形式比第二种形式更可取,因为使用 idref 标签可以让容器在部署时验证被引用的命名 Bean 是否实际存在。在第二种变体中,对传递给 client Bean 的 targetName 属性的值不执行任何验证。拼写错误只有在 client Bean 实际实例化时才会被发现(很可能导致严重的结果)。如果 client Bean 是一个原型 Bean,则可能要在容器部署后很长时间才能发现这种拼写错误和由此导致的异常。
Note:在 4.0 版本的 Beans XSD 中,
idref元素上不再支持local属性,因为它不再比常规的 Bean 引用提供任何价值。在升级到 4.0 架构时,请将现有的idref local引用更改为idref bean。
在 Spring 2.0 之前的版本中,<idref/> 元素带来价值的一个常见场景是在 ProxyFactoryBean Bean 定义中配置 AOP 拦截器。在指定拦截器名称时使用 <idref/> 元素可以防止拼写拦截器 ID 错误。
4.2.2. 对其他 Bean 的引用(协作者)
<ref/> 元素是 <constructor-arg/> 或 <property/> 定义元素中的最后一个元素。在这里,您将一个 Bean 的指定属性的值设置为对容器管理的另一个 Bean(协作者)的引用。被引用的 Bean 是要设置其属性的 Bean 的依赖项,并且在需要设置属性之前按需初始化(如果协作者是单例 Bean,则可能已经被容器初始化)。所有引用最终都是对另一个对象的引用。作用域和验证取决于您是否通过 bean 或 parent 属性指定了另一个对象的 ID 或名称。
通过 <ref/> 标签的 bean 属性指定目标 Bean 是最通用的形式,允许创建对同一容器或父容器中的任何 Bean 的引用,而不管它是否在同一个 XML 文件中。bean 属性的值可以与目标 Bean 的 id 属性相同,也可以与目标 Bean 的 name 属性中的值之一相同。以下示例展示了如何使用 ref 元素:
XML
<ref bean="someBean"/>通过 parent 属性指定目标 Bean 创建对当前容器的父容器中的 Bean 的引用。parent 属性的值可以与目标 Bean 的 id 属性或 name 属性中的值之一相同。目标 Bean 必须在当前容器的父容器中。当你有一个分层的容器,你想用一个与父级 Bean 同名的代理来包装父级容器中的现有 Bean 时,你应该使用这种 Bean 引用变体。以下示例展示了如何使用 parent 属性:
XML
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required here -->
</bean>1
2
3
4
2
3
4
XML
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
</property>
<!-- insert other configuration and dependencies as required here -->
</bean>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Note:在 4.0 版本的 Beans XSD 中,不再支持
ref元素上的local属性,因为它不再比常规的 Bean 引用提供任何价值。在升级到 4.0 架构时,请将现有的ref local引用更改为ref bean。
4.2.3. 内部 Bean
在 <property/> 或 <constructor-arg/> 元素内部的 <bean/> 元素定义了一个内部 Bean,如下例所示:
XML
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
内部 Bean 定义不需要定义的 ID 或名称。如果指定了,容器不会将这样的值用作标识符。容器在创建时也会忽略 scope 标志,因为内部 Bean 始终是匿名的,并且始终与外部 Bean 一起创建。无法独立访问内部 Bean,也无法将其注入到除封闭 Bean 之外的协作 Bean 中。
作为一个特殊情况,有可能从自定义作用域接收到销毁回调 —— 例如,对于包含在单例 Bean 中的请求作用域内的内部 Bean。内部 Bean 实例的创建与其包含的 Bean 绑定,但销毁回调允许它参与请求作用域的生命周期。这不是一个常见的场景。内部 Bean 通常只是简单地共享其包含的 Bean 的作用域。
4.2.4. 集合
<list/>、<set/>、<map/> 和 <props/> 元素分别设置 Java 集合类型 List、Set、Map 和 Properties 的属性和参数。以下示例展示了如何使用它们:
XML
<bean id="moreComplexObject" class="example.ComplexObject">
<!-- results in a setAdminEmails(java.util.Properties) call -->
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Map 的键或值,或者 Set 的值,还可以是以下任何元素:
Text
bean | ref | idref | list | set | map | props | value | null4.2.4.1. 集合合并
Spring 容器还支持集合合并。应用程序开发人员可以定义一个父 <list/>、<map/>、<set/> 或 <props/> 元素,并让子 <list/>、<map/>、<set/> 或 <props/> 元素继承并覆盖父集合中的值。也就是说,子集合的值是父集合和子集合元素合并的结果,子集合中的元素会覆盖父集合中指定的值。
本节关于合并的内容讨论了父子 Bean 机制。不熟悉父子 Bean 定义的读者可能希望在继续阅读之前阅读相关部分。
以下示例演示了集合合并:
XML
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.com</prop>
<prop key="support">support@example.com</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- the merge is specified on the child collection definition -->
<props merge="true">
<prop key="sales">sales@example.com</prop>
<prop key="support">support@example.co.uk</prop>
</props>
</property>
</bean>
<beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
请注意在 child Bean 定义的 adminEmails 属性的 <props/> 元素上使用了 merge=true 属性。当容器解析并实例化 child Bean 时,生成的实例具有一个 adminEmails Properties 集合,其中包含了子级的 adminEmails 集合与父级的 adminEmails 集合合并的结果。以下列表显示了结果:
Text
administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk1
2
3
2
3
子级 Properties 集合的值集从父级 <props/> 继承了所有属性元素,并且子级对 support 值的设置覆盖了父级集合中的值。
这种合并行为同样适用于 <list/>、<map/> 和 <set/> 集合类型。在 <list/> 元素的特定情况下,保留了与 List 集合类型相关联的语义(即,有序值的集合概念),父级的值位于所有子级列表值之前。对于 Map、Set 和 Properties 集合类型,不存在排序。因此,对于容器内部使用的关联 Map、Set 和 Properties 实现类型的集合类型,没有排序语义。
4.2.4.2. 集合合并的限制
您不能合并不同的集合类型(例如 Map 和 List)。如果尝试这样做,将会抛出相应的异常。merge 属性必须在较低级别的、继承的、子级上指定。在父级集合定义上指定 merge 属性是多余的,并且不会产生期望的合并。
4.2.4.3. 强类型集合
由于 Java 对泛型类型的支持,您可以使用强类型集合。也就是说,可以声明一个 Collection 类型,使其只能包含(例如)String 元素。如果您使用 Spring 将一个强类型的 Collection 注入到 Bean 中,您可以利用 Spring 的类型转换支持,使得您强类型的 Collection 实例的元素在添加到 Collection 之前被转换为适当的类型。以下是如何做到这一点的 Java 类和 Bean 定义的示例:
Java
public class SomeClass {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
XML
<beans>
<bean id="something" class="x.y.SomeClass">
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
</bean>
</beans>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
在准备将 something Bean 的 accounts 属性注入时,可以通过反射获得关于强类型 Map<String, Float> 元素类型的泛型信息。因此,Spring 的类型转换基础设施将各个 value 元素识别为 Float 类型,并将字符串值(9.99、2.75 和 3.99)转换为实际的 Float 类型。
4.2.5. 空值和空字符串值
Spring 将属性等的空参数视为空字符串。以下基于 XML 的配置元数据片段将 email 属性设置为空字符串值 "":
XML
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>1
2
3
2
3
上述示例等同于以下的 Java 代码:
Java
exampleBean.setEmail("");<null/> 元素用于处理 null 值。以下示例展示了一个例子:
XML
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>1
2
3
4
5
2
3
4
5
上述配置等同于以下的 Java 代码:
Java
exampleBean.setEmail(null);4.2.6. 使用 p: 命名空间的 XML 快捷方式
p: 命名空间允许您使用 bean 元素的属性(而不是嵌套的 <property/> 元素)来描述您的属性值、协作 Bean,或者两者兼而有之。
Spring 支持基于 XML Schema 定义的命名空间,从而实现可扩展的配置格式。本章讨论的 beans 配置格式是在一个 XML Schema 文档中定义的。然而,p: 命名空间没有在 XSD 文件中定义,而是仅存在于 Spring 的核心中。
以下示例展示了两个 XML 片段(第一个使用标准的 XML 格式,第二个使用 p: 命名空间),它们会解析为相同的结果:
XML
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="someone@somewhere.com"/>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
该示例显示了 p: 命名空间中的一个属性,该属性在 Bean 定义中被称为 email。这告诉 Spring 包含一个属性声明。如前所述,p: 命名空间没有模式定义,所以你可以将属性的名称设置为属性名。
接下来的示例包括另外两个 Bean 定义,它们都引用了另一个 Bean:
XML
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>
<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>
<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这个例子不仅使用了 p: 命名空间来声明属性值,还使用了一种特殊的格式来声明属性引用。第一个 Bean 定义使用 <property name="spouse" ref="jane"/> 来创建从 Bean john 到 Bean jane 的引用,而第二个 Bean 定义则使用 p:spouse-ref="jane" 作为属性来完成完全相同的操作。在这种情况下,spouse 是属性名称,而 -ref 部分表示这不是一个直接的值,而是对另一个 Bean 的引用。
Note:
p:命名空间不像标准的 XML 格式那样灵活。例如,声明属性引用的格式与以Ref结尾的属性发生冲突,而标准的 XML 格式则不会出现这种情况。我们建议您仔细选择您的方法,并将此与团队成员沟通,以避免生成同时使用这三种方法的 XML 文档。
4.2.7. 使用 c: 命名空间的 XML 快捷方式
类似于 p: 命名空间的 XML 快捷方式,c: 命名空间在 Spring 3.1 中引入,允许使用内联属性来配置构造函数参数,而不是嵌套的 constructor-arg 元素。
以下示例使用 c: 命名空间来完成与基于构造函数的依赖注入相同的操作:
XML
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
<!-- traditional declaration with optional argument names -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>
<!-- c-namespace declaration with argument names -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c: 命名空间使用与 p: 命名空间相同的约定(用于 Bean 引用的尾随 -ref)来按名称设置构造函数参数。同样,即使 c: 命名空间没有在 XSD 模式中定义(它存在于 Spring 核心中),但它仍然需要在 XML 文件中声明。
对于极少数情况下构造函数参数名不可用的情况(通常是因为字节码是在没有调试信息的情况下编译的),您可以使用参数索引作为备用方法,如下所示:
XML
<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
c:_2="something@somewhere.com"/>1
2
3
2
3
Tip:由于 XML 语法的限制,索引表示法要求前面加上下划线
_,因为 XML 属性名不能以数字开头(即使一些 IDE 允许)。对于<constructor-arg>元素也有相应的索引表示法,但通常不常用,因为在那里通常只使用声明的顺序即可。
在实践中,构造函数的解析机制在匹配参数时非常高效,因此除非您确实需要,否则我们建议您在整个配置中都使用名称表示法。
4.2.8. 复合属性名
你可以在设置 Bean 属性时使用复合或嵌套属性名称,只要路径上除了最后一个属性名之外的所有组件都不为 null。考虑以下的 Bean 定义:
XML
<bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>1
2
3
2
3
something Bean 具有一个 fred 属性,该属性具有一个 bob 属性,bob 属性又具有一个 sammy 属性,最终这个 sammy 属性被设置为值 123。为了使这个过程顺利进行,something 的 fred 属性和 fred 的 bob 属性在 Bean 构建完成后不能为 null。否则,会抛出一个 NullPointerException。
4.3. 使用 depends-on
如果一个 Bean 是另一个 Bean 的依赖项,通常意味着一个 Bean 被设置为另一个 Bean 的属性。通常情况下,你可以在基于 XML 的配置元数据中使用 <ref/> 元素来实现这一点。然而,有时 Bean 之间的依赖关系并不那么直接。一个例子是当类中的静态初始化程序需要被触发时,比如数据库驱动程序的注册。depends-on 属性可以明确地强制在使用此元素的 Bean 初始化之前初始化一个或多个 Bean。以下示例使用 depends-on 属性来表示对单个 Bean 的依赖:
XML
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />1
2
2
要表示对多个 Bean 的依赖关系,可以将 Bean 的名称列表作为 depends-on 属性的值提供(逗号、空格和分号都是有效的分隔符):
XML
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />1
2
3
4
5
6
2
3
4
5
6
Tip:
depends-on属性既可以指定初始化时的依赖关系,也可以(只针对单例 Bean)指定对应的销毁时依赖关系。定义了与给定 Bean 的depends-on关系的依赖 Bean 会在给定 Bean 被销毁之前首先被销毁。因此,depends-on也可以控制关闭顺序。
4.4. 延迟初始化的 Bean
默认情况下,ApplicationContext 实现会在初始化过程中急切地创建和配置所有的单例 Bean。通常情况下,这种预实例化是可取的,因为可以立即发现配置或周围环境中的错误,而不是在几个小时甚至几天后才发现。当这种行为不可取时,你可以通过将 Bean 定义标记为延迟初始化来阻止单例 Bean 的预实例化。一个延迟初始化的 Bean 告诉 IoC 容器在首次请求时创建 Bean 实例,而不是在启动时。
在 XML 中,可以通过 <bean/> 元素上的 lazy-init 属性来控制这种行为,如下例所示:
XML
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>1
2
2
当上述配置被 ApplicationContext 使用时,当 ApplicationContext 启动时,lazy Bean 不会被急切地预实例化,而 not.lazy Bean 则会被急切地预实例化。
然而,当一个延迟初始化的 Bean 是一个非延迟初始化的单例 Bean 的依赖项时,ApplicationContext 会在启动时创建延迟初始化的 Bean,因为它必须满足单例 Bean 的依赖关系,延迟初始化的 Bean 需要被注入到另一个非延迟初始化的单例 Bean 中。
你还可以通过在 <beans/> 元素上使用 default-lazy-init 属性来在容器级别控制延迟初始化,如下例所示:
XML
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>1
2
3
2
3
4.5. 自动装配协作者
Spring 容器可以自动装配协作 Bean 之间的关系。你可以让 Spring 自动检索 ApplicationContext 的内容,为你的 Bean 自动解析协作对象(其他 Bean)。自动装配具有以下优点:
自动装配可以显著减少需要指定属性或构造函数参数的情况(本章其他地方讨论的 Bean 模板等机制在这方面也很有用);
自动装配可以随着你的对象的演变而更新配置。例如,如果你需要向一个类添加一个依赖,那么这个依赖可以在你不需要修改配置的情况下自动满足。因此,在开发过程中,自动装配可以特别有用,而且当代码库变得更稳定时,也可以切换到显式装配的选项;
当使用基于 XML 的配置元数据(参见依赖注入小节)时,可以使用 <bean/> 元素的 autowire 属性为 Bean 定义指定自动装配模式。自动装配功能有四种模式。你可以为每个 Bean 指定自动装配方式,因此可以选择哪些 Bean 进行自动装配。以下表格描述了这四种自动装配模式:
| 模式 | 描述 |
|---|---|
no | (默认)不进行自动装配。Bean 引用必须由 ref 元素定义。不建议在较大的部署中更改默认设置,因为显式指定协作对象可以提供更大的控制和清晰度。在某种程度上,它还可以记录系统的结构 |
byName | 按属性名称自动装配。Spring 查找与需要自动装配的属性名称相同的 Bean。例如,如果一个 Bean 定义被设置为按名称自动装配,并且它包含一个 master 属性(即具有一个 setMaster(..) 方法),Spring 将查找名为 master 的 Bean 定义,并使用它来设置属性 |
byType | 如果容器中恰好存在一个与属性类型匹配的 Bean,则允许自动装配该属性。如果存在多个匹配的 Bean,则会抛出致命异常,表示不能对该 Bean 使用 byType 自动装配。如果没有匹配的 Bean,则不会发生任何操作(属性不会被设置) |
constructor | 类似于 byType,但适用于构造函数参数。如果容器中没有恰好一个与构造函数参数类型匹配的 Bean,则会引发致命错误 |
在 byType 或 constructor 自动装配模式下,你可以连接数组和类型化集合。在这种情况下,容器中所有与期望类型匹配的自动装配候选项都将被提供以满足依赖关系。如果强类型化的 Map 实例期望的键类型是 String,则也可以自动装配。一个自动装配的 Map 实例的值由所有与期望类型匹配的 Bean 实例组成,而 Map 实例的键包含相应的 Bean 名称。
4.5.1. 自动装配的限制和缺点
自动装配在整个项目中保持一致时效果最好。如果通常不使用自动装配,那么仅仅用于连接一个或两个 Bean 定义可能会让开发者感到困惑。
考虑一下自动装配的限制和缺点:
显式设置
property和constructor-arg中的依赖关系始终会覆盖自动装配。你无法自动装配简单属性,比如原始类型、字符串和类(以及这些简单属性的数组)。这种限制是有意设计的;自动装配不如显式装配精确。尽管在前面的表中已经指出,Spring 在存在歧义时会小心避免猜测,以免产生意外结果。但是,你的 Spring 管理对象之间的关系不再被显式地记录下来;
装配信息可能无法被从 Spring 容器生成文档的工具获取;
容器中可能有多个 Bean 定义与要自动装配的 Setter 方法或构造函数参数指定的类型匹配。对于数组、集合或
Map实例,这不一定是一个问题。但是,对于期望单一值的依赖关系,这种歧义不会被随意解决。如果没有唯一的 Bean 定义可用,就会抛出异常;
对于最后一种情况,您有几个选项:
放弃自动装配,改为显式装配;
通过将 Bean 定义的
autowire-candidate属性设置为false来避免对 Bean 定义进行自动装配,具体描述请参见下一节;通过将
<bean/>元素的primary属性设置为true来指定单个 Bean 定义为主要候选者;实现基于注解的配置提供的更精细的控制,具体描述请参见基于注解的容器配置小节;
4.5.2. 从自动装配中排除一个 Bean
在每个 Bean 的基础上,你可以将一个 Bean 从自动装配中排除。在 Spring 的 XML 格式中,将 <bean/> 元素的 autowire-candidate 属性设置为 false。容器会使得该特定的 Bean 定义对自动装配基础设施(包括注解风格的配置,如 @Autowired)不可用。
Tip:
autowire-candidate属性仅影响基于类型的自动装配。它不会影响按名称的显式引用,即使指定的 Bean 没有标记为自动装配候选,按名称进行自动装配仍然会注入一个 Bean(如果名称匹配的话)。
你还可以根据 Bean 名称的模式匹配来限制自动装配的候选项。顶层的 <beans/> 元素接受一个或多个模式,这些模式可以在其 default-autowire-candidates 属性中定义。例如,要将自动装配的候选项限制为以 Repository 结尾的任何 Bean,可以提供值 *Repository。要提供多个模式,可以用逗号分隔的列表来定义它们。对于 Bean 定义的 autowire-candidate 属性,显式指定的 true 或 false 始终优先,对这类 Bean 不适用以上模式匹配规则。
这些技术对于那些你永远不希望通过自动装配被注入到其他 Bean 中的 Bean 非常有用。这并不意味着被排除的 Bean 不能使用自动装配进行配置,而是该 Bean 本身不会成为自动装配其他 Bean 的候选项。
4.6. 方法注入
在大多数应用场景中,容器中的大多数 Bean 都是单例的。当一个单例 Bean 需要与另一个单例 Bean 或非单例 Bean 配合工作时,通常会通过将一个 Bean 定义为另一个 Bean 的属性来处理依赖关系。当 Bean 的生命周期不同时会出现问题。假设单例 Bean A 需要使用非单例(原型)Bean B,可能在每次对 A 的方法调用时都需要。容器只会创建单例 Bean A 一次,因此只有一次机会设置属性。容器无法在每次需要时为 Bean A 提供 Bean B 的新实例。
一种解决方案是放弃一些控制反转。你可以通过实现 ApplicationContextAware 接口使 Bean A 感知容器,并通过向容器发出 getBean("B") 调用,在 Bean A 需要时请求(通常是新的)Bean B 实例。以下示例展示了这种方法:
Java
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;
// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
前面的方法并不理想,因为业务代码意识到并与 Spring 框架耦合在一起。方法注入是 Spring IoC 容器的一个相对高级的特性,可以让你更清晰地处理这种用例。
Note:你可以在这篇博客中了解更多关于方法注入背后动机的内容。
4.6.1. 查找方法注入
查找方法注入是指容器能够覆盖容器管理的 Bean 上的方法并返回容器中另一个命名的 Bean 的查找结果。查找通常涉及原型 Bean,就像前面部分描述的场景一样。Spring 框架通过使用 CGLIB 库进行字节码生成来实现此方法注入,动态生成一个子类来重写方法。
Tip
为了使这种动态子类化工作,Spring Bean 容器子类化的类不能是
final的,并且要被重写的方法也不能是final的;对一个包含
abstract方法的类进行单元测试,需要你自己对这个类进行子类化,并提供一个abstract方法的存根(stub)实现;具体方法也是组件扫描所必需的,因为组件扫描需要具体类来识别;
另一个关键的限制是查找方法不能与工厂方法一起使用,特别是不能与配置类中的
@Bean方法一起使用,因为在这种情况下,容器不负责创建实例,因此无法动态地创建一个运行时生成的子类;
在上一个代码片段中的 CommandManager 类的情况下,Spring 容器动态地重写了 createCommand() 方法的实现。正如重写后的示例所示,CommandManager 类并不具有任何 Spring 依赖:
Java
package fiona.apple;
// no more Spring imports!
public abstract class CommandManager {
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在包含要被注入的方法的客户端类(在这种情况下是 CommandManager)中,要被注入的方法需要具有以下形式的签名:
ABNF
("public" / "protected") ["abstract"] return-type method-name "();"如果方法是 abstract 的,动态生成的子类会实现这个方法。否则,动态生成的子类会重写原始类中定义的具体方法。考虑以下示例:
XML
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
被标识为 commandManager 的 Bean 在需要新 myCommand Bean 实例时会调用自己的 createCommand() 方法。如果实际上需要的是原型 Bean,你必须小心地将 myCommand Bean 部署为一个原型。如果它是一个单例,每次都会返回相同的 myCommand Bean 实例。
或者,在基于注解的组件模型中,你可以通过 @Lookup 注解声明一个查找方法,如下面的示例所示:
Java
public abstract class CommandManager {
public Object process(Map commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
或者更符合惯用法的是,你可以依赖于查找方法的返回类型声明来解析目标 Bean:
Java
public abstract class CommandManager {
public Object process(Map commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract Command createCommand();
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
请注意,你通常应该使用具体的存根实现来声明这样的带注解查找方法,以便它们与 Spring 的组件扫描规则兼容,其中默认情况下会忽略抽象类。这个限制不适用于显式注册或显式导入的 Bean 类。
Tip
另一种访问不同作用域目标 Bean 的方法是使用
ObjectFactory/Provider注入点。请参阅作为依赖项的作用域 Bean 小节。你可能还会发现
ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包中)很有用。
4.6.2. 任意方法替换
与查找方法注入相比,一种不太有用的方法注入形式是能够用另一个方法实现替换托管 Bean 中的任意方法。除非你确实需要这种功能,否则你可以安全地跳过本节的其余部分。
在基于 XML 的配置元数据中,你可以使用 replaced-method 元素来替换已部署 Bean 的现有方法实现为另一个方法实现。考虑以下类,它有一个名为 computeValue 的方法,我们想要覆盖它:
Java
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}1
2
3
4
5
6
7
2
3
4
5
6
7
一个实现 org.springframework.beans.factory.support.MethodReplacer 接口的类提供了新的方法定义,如下例所示:
Java
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
用于部署原始类并指定方法覆盖的 Bean 定义将类似于以下示例:
XML
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- arbitrary method replacement -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
你可以在 <replaced-method/> 元素内使用一个或多个 <arg-type/> 元素来指示被覆盖方法的方法签名。只有在方法被重载并且类中存在多个变体时,才需要参数的签名。为了方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,以下所有内容都匹配 java.lang.String:
Text
java.lang.String
String
Str1
2
3
2
3
因为参数的数量通常足以区分每种可能的选择,这种快捷方式可以节省大量输入,只需输入与参数类型匹配的最短字符串即可。