1. Spring IoC Container 和 Bean 介绍
IoC(控制反转),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
IoC模式,系统中通过引入实现了IoC模式的IoC容器,即可由IoC容器来管理对象的生命周期、依赖关系等,从而使得应用程序的配置和依赖性规范与实际的应用程序代码分离。其中一个特点就是通过文本的配置文件进行应用程序组件间相互关系的配置,而不用重新修改并编译具体的代码。
org.springframework.beans
和org.springframework.context
包是Spring框架的IoC容器的基础。其中BeanFactory
接口提供了一种能够管理任何类型对象的高级配置机制。ApplicationContext
是BeanFactory
的子接口。它补充了:
- 更容易与 Spring 的 AOP 特性集成
- 消息资源处理(用于国际化)
- 活动发布
- 应用层特定的上下文,例如WebApplicationContext 在 Web 应用程序中使用的 。
简而言之,BeanFactory
提供了配置框架和基本功能,并在ApplicationContext
添加了更多企业特定的功能。ApplicationContext
是对BeanFactory
的一个完整超集。
在 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
通常就足够了。
下图显示了 Spring 是如何工作的。您的应用程序类与配置元数据相结合,因此,在ApplicationContext
创建和初始化之后,您就有了一个完全配置且可执行的系统或应用程序。
2.1. 配置元数据
配置元数据传统上以简单直观的 XML 格式提供,本章的大部分内容使用这种格式来传达 Spring IoC 容器的关键概念和特性。
但是,基于 XML 的元数据并不是唯一允许的配置元数据形式。Spring IoC 容器本身与此配置元数据实际写入的格式完全分离。
- 基于注解的配置:
Spring 2.5 引入了对基于注解的配置元数据的支持。 - 基于java的配置:
从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多特性成为核心 Spring Framework 的一部分。因此,您可以使用 Java 而不是 XML 文件来定义应用程序类外部的 bean。要使用这些新功能,请参阅@Configuration
,@Bean
,@Import
,和@DependsOn
注释。
Spring 配置包含了多个必须容器管理的 bean 定义。基于 XML 的配置元数据将这些 bean 配置为顶级元素<bean><beans/>
中的元素。Java 配置通常在@Configuration类中使用@Bean注解的方法。
这些 bean 定义对应于构成应用程序的实际对象。通常,您定义服务层对象、数据访问对象 (DAO)、表示对象(例如 Action实例)、基础设施对象(例如 Hibernate SessionFactories、JMSQueues等)。通常,不会在容器中配置细粒度的领域对象,因为创建和加载领域对象通常是 DAO 和业务逻辑的责任。
示例,基于 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">
<!--id属性是一个字符串,用于标识单个 bean 定义。class属性定义 bean 的类型并使用完全限定的类名。-->
<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>
2.2. 实例化一个容器
提供给ApplicationContext构造函数的一个或多个位置路径资源字符串,它允许容器从各种外部资源(例如本地文件系统、Java CLASSPATH等)加载配置元数据。
示例
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
服务层对象services.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>
数据访问对象daos.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>
前面的例子中,服务层由PetStoreServiceImpl类和两个类型为JpaAccountDao和JpaItemDao(基于JPA ORM)的数据访问对象组成。该property name元素是指JavaBean属性的名称,以及ref元素指的是另一个bean定义的名称。id和ref元素之间的这种联系表达了协作对象之间的依赖关系。
编写基于 XML 的配置元数据
bean的定义可以跨越多个 XML 文件。通常,每个单独的 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>
在前面的例子中,外部豆定义是从三个文件加载: services.xml
,messageSource.xml
,和themeSource.xml
。所有位置路径都相对于执行导入的定义文件,因此services.xml
必须与执行导入的文件位于同一目录或类路径位置, messageSource.xml
并且themeSource.xml
必须位于resources
导入文件位置下方的位置。如您所见,前导斜杠被忽略。但是,鉴于这些路径是相对的,最好根本不使用斜杠。根据 Spring Schema,被导入文件的内容,包括顶级元素<beans/>
,必须是有效的 XML bean 定义。
可以但不建议使用相对“../”路径引用父目录中的文件。这样做会创建对当前应用程序之外的文件的依赖关系。
您始终可以使用完全限定的资源位置而不是相对路径:例如,
file:C:/config/services.xml
或classpath:/config/services.xml
。
示例
<beans>
<import resource="classpath:spring/frame-mvc.xml"/>
<import resource="classpath:spring/frame-server.xml"/>
<import resource="classpath:spring/base-action.xml"/>
<import resource="classpath:spring/base-server.xml"/>
<import resource="classpath:spring/main-server.xml"/>
<import resource="classpath:spring/flowable.xml"/>
<import resource="classpath:spring/liquibase.xml"/>
</beans>
2.3. 使用容器
ApplicationContext
是一个高级工厂的接口,能够维护不同 bean 及其依赖项的注册表。通过使用方法 T getBean(String name, Class<T> requiredType)
,您可以检索 bean 的实例。
示例
// 创建和配置 beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
// 获取实例
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// 使用实例
List<String> userList = service.getUsernameList();
ApplicationContext
接口还有一些其他方法来检索 bean,但理想情况下,您的应用程序代码永远不应该使用它们。实际上,您的应用程序代码根本不应该调用该 getBean()方法,可以完全不依赖于 Spring API。例如,Spring 与 Web 框架的集成为各种 Web 框架组件(例如controllers)提供了依赖注入,让您可以通过元数据(例如@Autowired自动装配注释)声明对特定 bean 的依赖。
3. Bean概述
一个 Spring IoC 容器管理一个或多个 bean。这些 bean 是使用您提供给容器的配置元数据创建的(例如,以 XML<bean/>
定义的形式 )。
在容器本身内,这些 bean的定义表示为BeanDefinition
对象,其中包含以下元数据:
- 一个包限定的类名:通常是被定义的 bean 的实际实现类。
- Bean 行为配置元素,它说明 Bean 在容器中的行为方式(范围、生命周期回调等)。
- 对 bean 执行其工作所需的其他 bean 的引用。这些引用也称为协作者或依赖项。
- 在新创建的对象中设置的其他配置设置——例如,池的大小限制或在管理连接池的 bean 中使用的连接数。
此元数据转换为组成每个 BeanDefinition
对象定义的一组属性。下表描述了这些属性:
属性 | 详细介绍 |
---|---|
Class | 实例化 Bean |
Name | 命名 Bean |
Scope | Bean 作用域 |
Constructor arguments | 依赖注入 |
Properties | 依赖注入 |
Autowiring mode | 自动装配协作对象 |
Lazy initialization mode | 延迟初始化的 Bean |
Initialization method | 初始化回调 |
Destruction method | 销毁回调 |
除了包含有关如何创建特定 bean 信息的 BeanDefinition
对象定义之外,ApplicationContext
还允许注册在容器外(由用户)创建的现有对象。这是通过方法getBeanFactory()
返回的BeanFactoryDefaultListableBeanFactory
实现的。DefaultListableBeanFactory
通过registerSingleton(..)
和 registerBeanDefinition(..)
方法支持此注册。但是,典型的应用程序仅使用通过常规 bean 定义元数据定义的 BeanDefinition
对象。
Bean 元数据和手动提供的单例实例需要尽早注册,官方不支持在运行时注册新 bean
3.1. 命名 Bean
每个 bean 都有一个或多个标识符。这些标识符在承载 bean 的容器中必须是唯一的。一个 bean 通常只有一个标识符。但是,如果它需要多个,则可以将多余的视为别名。
在基于 XML 的配置元数据中,您可以使用id属性、name属性或两者来指定 bean 标识符。该id属性允许您指定一个 id。通常,这些名称是字母数字('myBean'、'someService' 等),但它们也可以包含特殊字符。如果要为 bean 引入其他别名,也可以在name 属性中指定它们,用逗号 ( ,)、分号 ( ;) 或空格分隔。作为历史记录,在 Spring 3.1 之前的版本中,id属性被定义为一种xsd:ID类型,它限制了可能的字符。从 3.1 开始,它被定义为一种xsd:string类型。请注意,bean 的id唯一性仍然由容器强制执行,但不再由 XML 解析器强制执行。
您不需要一定给 bean提供 name或 id。如果您未明确提供name或id,则容器会为该 bean 生成一个唯一name。但是,如果您想通过名称引用该 bean,通过使用ref元素或服务定位器样式查找,您必须提供name。不提供名称的动机与使用内部 bean和自动装配协作者有关。
Bean 命名约定:
约定在命名 bean 时,对实例字段名称使用标准 Java 约定。也就是说,bean 名称以小写字母开头,并从那里开始使用驼峰式大小写。此类名称的示例包括accountManager
、accountService
、userDao
、loginController
等等。
始终如一地命名 bean 使您的配置更易于阅读和理解。此外,如果您使用 Spring AOP,则在将建议应用于一组按名称相关的 bean 时会很有帮助。
通过类路径中的组件扫描,Spring 为未命名的组件生成 bean 名称,遵循前面描述的规则:本质上,采用简单的类名并将其初始字符转换为小写。但是,在有多个字符且第一个和第二个字符都是大写的(不寻常的)特殊情况下,原始大小写将被保留。这些与
java.beans.Introspector.decapitalize
(Spring 在这里使用)定义的规则相同。
在 Bean 定义之外给 Bean 取别名
在 bean 定义本身中,您可以通过使用id
属性指定的最多一个名称和属性中任意数量的其他名称的组合,为 bean 提供多个名称name
。这些名称可以是同一个 bean 的等效别名,并且在某些情况下很有用,例如让应用程序中的每个组件通过使用特定于该组件本身的 bean 名称来引用公共依赖项。
然而,在实际定义 bean 的地方指定所有别名并不总是足够的。有时需要为在别处定义的 bean 引入别名。这在大型系统中很常见,其中配置在每个子系统之间拆分,每个子系统都有自己的一组对象定义。在基于 XML 的配置元数据中,您可以使用<alias/>
元素来完成此操作。
例如,子系统 A 的配置元数据可能引用名为 的数据源subsystemA-dataSource
。子系统 B 的配置元数据可以通过名称来引用数据源subsystemB-dataSource
。在组合使用这两个子系统的主应用程序时,主应用程序通过名称引用 DataSource myApp-dataSource
。要让所有三个名称都引用同一个对象,您可以将以下别名定义添加到配置元数据中:
<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>
现在每个组件和主应用程序都可以通过一个唯一的名称来引用数据源,并且保证不会与任何其他定义发生冲突(有效地创建一个命名空间),但它们引用同一个 bean。
Java-configuration:
@Bean
注解可用于提供别名
3.2. 实例化 Bean
bean 定义本质上是创建一个或多个对象的方法。容器在询问时查看命名 bean 的配置,并使用该 bean 定义的配置元数据来创建(或获取)实际对象。
如果您使用基于 XML 的配置元数据,请在<bean/>
元素的class属性中指定要实例化的对象的类型(或类)。这个class属性(在内部,它是一个BeanDefinition 实例的属性Class)通常是强制性的。(对于例外情况,请参阅 使用实例工厂方法和Bean 定义继承进行实例化)您可以通过以下Class两种方式之一使用该属性:
- 通常,在容器本身通过反射调用其构造函数直接创建 bean 的情况下,指定要构造的 bean 类,有点等同于带有new运算符的Java 代码。
- 指定包含创建对象的静态工厂方法的实际类,在不太常见的情况下,容器调用静态类上的工厂方法来创建 bean。调用静态工厂方法返回的对象类型可能是同一个类,也可能完全是另一个类。
嵌套类的名称:如果要为嵌套类配置 bean 定义,可以使用嵌套类的二进制名称或源名称。
例如,如果在com.example包中调用了一个SomeThing类,并且SomeThing类有一个名为OtherThing的静态嵌套类,则它们之间可以用美元符号 ($) 或点 (.)分隔。因此bean 定义中的class属性值将是com.example.SomeThing$OtherThing 或com.example.SomeThing.OtherThing。
使用构造函数实例化
当您通过构造函数方法创建 bean 时,所有普通类都可以被 Spring 使用并与 Spring 兼容。也就是说,正在开发的类不需要实现任何特定的接口或以特定的方式进行编码。只需指定 bean 类就足够了。然而,取决于您为该特定bean使用的IoC类型,您可能需要一个默认(空)构造函数。
使用基于 XML 的配置元数据,您可以按如下方式指定 bean 类:
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
使用静态工厂方法实例化
在定义使用静态工厂方法创建的 bean 时,使用class
属性来指定包含静态工厂方法的类和使用factory-method
属性来指定工厂方法的名称。您应该能够调用此方法(带有可选参数,如下所述)并返回一个活动对象,该对象随后被视为通过构造函数创建。这种 bean 定义的一种用途是在遗留代码中调用静态工厂。
以下 bean 定义指定通过调用工厂方法来创建 bean。定义中没有指定返回对象的类型(类),只指定包含工厂方法的类。在这个例子中,createInstance()
方法必须是静态方法。
示例
bean 定义
<bean id="clientService" class="examples.ClientService" factory-method="createInstance"/>
java类
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
使用实例工厂方法实例化
与通过静态工厂方法实例化类似,使用实例工厂方法实例化,是从容器中调用现有 bean 的非静态方法来创建新 bean。要使用此机制,请将class
属性留空,并在factory-bean
属性中指定当前(或父或祖先)容器中 bean 的名称,该容器包含用于创建对象的实例方法。使用factory-method
属性设置工厂方法的名称。
示例
bean 定义
<!-- 工厂bean, 包含创建实例的方法 -->
<bean id="factoryService" class="examples.DefaultFactoryService">
<!-- 为这个工厂bean注入需要的依赖 -->
</bean>
<!-- 通过工厂bean创建的bean -->
<bean id="clientService" factory-bean="factoryService" factory-method="createClientServiceInstance"/>
java类
public class DefaultFactoryService{
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}
一个工厂类也可以包含多个工厂方法,如下例所示:
<bean id="factoryService" class="examples.DefaultFactoryService">
<!-- inject any dependencies required by this locator bean -->
</bean>
<bean id="clientService"
factory-bean="factoryService"
factory-method="createClientServiceInstance"/>
<bean id="accountService"
factory-bean="factoryService"
factory-method="createAccountServiceInstance"/>
public class DefaultFactoryService {
private static ClientService clientService = new ClientServiceImpl();
private static AccountService accountService = new AccountServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
public AccountService createAccountServiceInstance() {
return accountService;
}
}
在 Spring 文档中,“工厂bean”是指在 Spring 容器中配置并通过实例工厂方法或静态工厂方法创建对象的bean 。相比之下,
FactoryBean
(注意大写)是指特定于 Spring 的FactoryBean
实现类。
确定 Bean 的运行时类型
确定特定 bean 的运行时类型并非易事。bean 元数据定义中的指定类只是一个初始类引用,可能与声明的工厂方法结合,或者是可能导致 bean 的不同运行时类型的FactoryBean
类,或者根本没有设置实例级工厂方法(通过指定factory-bean
名称解析)。此外,AOP 代理可能会使用基于接口的代理包装 bean 实例,并限制暴露目标 bean 的实际类型(仅其实现的接口)。
找出特定 bean 的实际运行时类型的推荐方法是BeanFactory.getType
,调用指定的 bean 名称。这将所有上述情况都考虑在内,并返回相同 bean 名称对应的对象类型。
4. 依赖关系
典型的企业应用程序不包含单个对象(或 Spring 用语中的 bean)。即使是最简单的应用程序也有一些对象,它们协同工作以呈现最终用户所看到的连贯应用程序。下一节将解释如何从定义多个独立的 bean 定义到完全实现的应用程序,其中对象协作以实现目标。
4.1. 依赖注入
依赖注入( DI )是一个处理过程, 在对象被构造后或者从工厂返回后, 仅仅通过构造函数参数, 工厂方法的参数或者属性赋值的方式来定义他们的依赖(也就是与其他对象协作). 容器在创建bean后注入他们的依赖. 这个处理过程本质上是bean自己去使用类构造和服务定位模式管理初始化和定位它的依赖项的反转(因此叫控制反转).
使用 DI 原则,代码更清晰,当对象具有依赖关系时,解耦更有效。该对象不查找其依赖项,也不知道依赖项的位置或类。因此,您的类变得更容易测试,特别是当依赖项位于接口或抽象基类上时,这允许在单元测试中使用存根或模拟实现。
DI 存在两种主要的变体:基于构造函数的依赖注入和基于 Setter 的依赖注入
基于构造函数的依赖注入
基于构造函数的 DI 是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个依赖项。和调用带有特定参数的static
工厂方法来构造 bean 几乎是等效的。
构造函数参数解析
构造函数参数解析匹配通过使用参数的类型发生。如果 bean 定义的构造函数参数中不存在潜在的歧义,那么在 bean 定义中定义构造函数参数的顺序就是在实例化 bean 时将这些参数提供给适当的构造函数的顺序。
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
假设ThingTwo
和ThingThree
类不通过继承相关,则不存在潜在的歧义。因此,以下配置工作正常,您不需要在<constructor-arg/>
元素中显式指定构造函数参数索引或类型 。
<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>
当另一个bean被引用时, 类型是已知的,匹配能发生(就像前面例子中的处理过程)。当使用简单类型时,例如 <value>true</value>
,Spring 无法确定值的类型,因此无法在没有帮助的情况下按类型进行匹配。
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;
}
}
构造函数参数类型匹配
上述场景中, 如果使用type
属性给参数指定了类型, 容器就能通过类型匹配. 如下所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
构造函数参数索引
使用index
属性显式指定构造函数参数的索引,如以下示例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
另外, 如果参数有多个简单类型, 可以使用索引解决多个简单类型参数的二义性。
该索引是从 0 开始的。
构造函数参数名称
还可以使用构造函数参数名称进行值消歧,如以下示例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
记住, 要不受限制的使用此功能, 你的代码必须要启用debug标记编译, 这样Spring才能从构造函数查找参数名称. 如果不能或不想启用debug标记, 可以使用@ConstructorProperties
JDK注解显式添加到构造函数的参数上. 看起来如同下面例子:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基于 Setter 的依赖注入
基于 Setter 的 依赖注入是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化bean后,调用 bean 上的setter 方法来完成的。
以下示例显示了一个只能使用纯 setter 注入进行依赖注入的类。这个类是传统的Java。它是一个不依赖于容器特定接口、基类或注释的 POJO。
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
private String remark;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder,String remark) {
this.movieFinder = movieFinder;
this.remark= remark;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
<beans>
<bean id="simpleMovieLister " class="x.y.SimpleMovieLister">
<property name="movieFinder" ref="movieFinder"></property>
<property name="remark" value="备注"></property>
</bean>
<bean id="movieFinder" class="x.y.MovieFinder"/>
</beans>
ApplicationContext
支持构造函数注入和Setter注入。也支持通过构造函数注入部分依赖后再由Setter注入。你使用BeanDefinition
类的形式配置依赖, 这个类与PropertyEditor
实例协作将属性从一种格式转化为另一种格式。尽管如此, 大多Spring用户不会直接使用这些类(在编程方面), 而是使用XML bean
定义, 或者注解注释组件(也就是使用@Component
,@Controller
等),或者用@Configuration
注解的类中使用@Bean
的java代码配置。这些资源将在内部转化为BeanDefinition
实例并用于在IoC容器实例加载。
基于构造函数还是基于 setter 的依赖注入?
因为可以同时混用构造器和Setter注入, 一种比较好的原则是: 强制的依赖使用构造器注入, 可选的依赖则可以使用setter或配置方法。注意: setter方法上使用
@Required
注解将使得对应属性成为必须的依赖。Spring团队提倡构造器注入, 这会让你实现的程序组件是不可变对象并且可以保证依赖不是
null
。更多的好处是, 构造器注入返回给客户端的组件总是完全被初始化的状态。还有个边缘效应, 就是大量的构造函数参数是一种不好的代码味道, 暗示着这个类承载了太多的责任并且需要被合理的重构, 实现关注点分离。Setter注入应该在依赖是可选的前提下优先使用, 这些依赖可以被赋以合理的默认值。 另外, 非null验证必须要在代码使用依赖的所有地方进行。使用setter注入的一个好处是: setter方法使得对象的重配置和重注入更易于控制。通过JMX MBeans管理就是一种setter注入的案例。
使用DI使得特定类更加有意义。 有时候, 当你使用三方类库时, 你并没有源代码, 此时你将手握DI选择权。 例如, 如果三方类没有暴露任何setter方法, 那么使用构造器注入将是唯一途径。
依赖解析过程
容器按照下面阐述的对bean依赖进行解析:
ApplicationContext
被创建,并且使用配置的bean元数据进行初始化。 配置元数据可以是XML、Java 代码或注解。- *对于每个bean,它的依赖以属性、构造函数参数或者静态工厂的参数(如果使用代替普通的构造函数)形式表现。这些依赖将在bean被创建后提供给bean。
- 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。
- 作为值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,例如
int
、long
、String
、boolean
等。
Spring容器在创建后对每个bean的配置进行校验。尽管如此,在bean被创建后bean的属性才会被赋值。单例域的并且设置为预实例化(默认情况)的bean在容器创建后被创建。域的定义参看bean的作用域。除此之外,bean只有在被请求时才创建。一个bean的创建会潜在地导致bean的整个图被创建,也就是bean的依赖,它的依赖的依赖等都被创建和分配.。注意:依赖解析的不匹配可能会后期表现出来,也就是第一次创建受影响的bean时。
循环依赖
如果您主要使用构造函数注入,则可能会创建无法解决的循环依赖场景。例如: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。ApplicationContext
以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您会在创建时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预实例化。
如果没有循环依赖,当一个或多个协作的bean被注入到依赖的bean里面,每个协作bean其实是优先于依赖bean就被完全配置好了。这意味着如果A依赖B,容器会在调用A的setter方法之前完全配置好B。换句话说,这个bean被实例化了(如果它不是预实例化的),它的依赖被设置了,并且他的生命周期函数(如配置的 init
方法 或InitializingBean
回调方法)也被调用了。
依赖注入的例子
以下示例将基于 XML 的配置元数据用于基于 setter 的 依赖注入。Spring XML 配置文件的一小部分指定了一些 bean 定义,如下所示:
<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"/>
相应的ExampleBean
类:
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;
}
}
上面的例子是基于 setter 的依赖注入,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"/>
相应的ExampleBean
类:
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;
}
}
在bean定义的构造函数参数用来通过类ExampleBean
的构造函数参数注入。
现在改变下例子,不用构造函数注入, 而使用静态工厂方法返回对象实例:
<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"/>
相应的ExampleBean
类:
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;
}
}
给静态工厂方法的参数是<constructor-arg/>
元素提供的,就像使用构造函数一样。工厂方法返回的实例类型不需要与包含静态工厂的类的类型一致(尽管在本例中一致)。一个(非静态)实例工厂本质上使用相同方式(除了使用factory-bean
而不是class
属性)。
4.2. 详细依赖和配置
如上一节所述,您可以将 bean 属性和构造函数参数定义为对其他托管 bean(协作者)的引用或作为内联定义的值。为此,Spring的XML配置可以使用<property/>
和<constructor-arg/>
元素包含在bean定义元素中。
纯值数据(原始数据,String等类型)
元素<property/>
的属性value
可以指定为属性或构造参数的纯值数据。Spring的转化服务用来将这些值从String
转化为属性或参数的实际类型.。下例展示了一组这样的配置:
<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>
下例使用p-namespace
展示更简洁的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>
前面的 XML 更简洁。但是,拼写错误是在运行时而不是设计时发现的,除非您在创建 bean 定义时使用支持自动属性完成的 IDE(例如IntelliJ IDEA或Spring Tools for Eclipse)。强烈建议使用此类 IDE 帮助。
您还可以配置一个java.util.Properties
实例,如下所示:
<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>
Spring 容器通过使用 JavaBeans的PropertyEditor
机制将<value/>
元素内部的文本转换为 java.util.Properties
实例。这是一个很好的捷径,并且这是 Spring 团队支持使用嵌套<value/>
元素而不是value
属性样式的少数几个地方之一。
idref
元素
idref
元素仅是一种简单的防错方法,可以将容器中的另一个bean的id(一个字符串,不是引用)传递给属性或构造参数。如下所示:
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
前面的 bean 定义片段与以下片段完全等效(在运行时):
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>
第一种形式优于第二种,因为使用idref
标签能够让容器在部署期间检查引用的bean是不是真的存在。第二种情况下,传递给名为client
的bean的targetName
属性的值不会被校验。仅仅是在client
被实际实例化的时候会发生类型转化(大多数情况下是严重错误)。如果client
bean是个prototype
(原型)bean, 则只有在部署容器很久之后才会发现错误和由此产生的异常。
idref
元素的local
属性不再支持4.0版本的beans XSD,因为它不再提供常规bean
引用的值。当升级到4.0时,请修改现有的idref local
引用,修改为idref bean
。
其中一个<idref/>
元素带值的一个地方(至少在Spring2.0之前的版本中)是在ProxyFactoryBean
定义中AOP拦截器的配置。当你为了防止拼写错拦截器ID而指定拦截器名称时使用<idref/>
元素。
引用其他bean(协作者)
ref
元素是 <constructor-arg/>
或<property/>
内部定义的最终元素。在这里,你将另一个由容器管理的bean(协作者)作为引用的值赋给一个bean的属性。被引用的bean作为引用被赋值给这个bean的属性,并且它是在被赋值前就按需初始化的。(如果这个协作者是个singleton(单例)的话,它已经被容器初始化了)。所有引用最终都是另一个对象的引用。作用域和校验则取决于你是否通过bean
、local
和partent
属性为另一个对象指定ID或名称。
通过<ref/>
元素的bean
属性指定目标bean是最常见的方式,并且允许其在同一个容器或父容器中创建任何bean的引用.。不管是不是配置在XML格式的文件。 bean
属性的值可以是目标bean的ID或者是name中的任一值。下面展示了ref
元素:
<ref bean="someBean"/>
通过parent
属性创建的引用指定目标bean是在当前容器的父容器。其值可能是目标bean的id或name的其中任一值。目标bean必须在当前容器的父容器中。主要使用这个属性的场景是:当你使用了有层次的容器并且在父容器中通过代理proxy包装了一个同名的父bean。下面是一对使用parent
的例子:
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required here -->
</bean>
<!-- 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>
4.0的beans XSD 后
ref
元素的local
属性不再支持, 因此不需要用它为一个正常的bean
引用提供值了。当升级到4.0时请注意修改现有的ref local
到ref bean
。
内部bean
在<property/>
或者<constructor-arg/>
元素内部定义了<bean/>
元素,则称为内部bean。示例:
<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>
内部bean定义不需要指定ID或name,如果指定了,容器也不会使用它们作为bean的标识符。容器也会在创建时忽略它们的scope
标记,, 因为内部bean通常都是匿名的,并且总是跟外部bean一起创建。 一般不可能去单独的访问内部bean,或者将他们注入到除封闭 bean 之外的协作 bean 中。
作为一个极端情况,从一个自定义的scope中获取到销毁回调是可能的,例如对于一个单例bean中作用域为request的内部bean。内部bean的创建是与外部bean的创建是绑定的,但是销毁回调使它特定于request生命周期。这并不是一个普遍的场景,内部bean一般是与包含它的bean有着相同的作用域。
集合
<list/>
,<set/>
,<map/>
和 <props/>
元素分别
对应于Java集合类型(Collection
)的List
,Set
, Map
和 Properties
. 下例展示其用法:
<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>
字典map的键值,或者集set的值,可以是下面元素的任一:
bean | ref | idref | list | set | map | props | value | null
集合合并
Spring容器支持合并集合. 开发人员可以定义一个父的<list/>
,<set/>
,<map/>
和 <props/>
元素,并且子元素的<list/>
,<set/>
,<map/>
和 <props/>
可以继承和覆盖父集合的值. 也就是说, 子集合的值是父集合与子集合元素的合并, 子集合的元素覆盖了父集合中的值.