1.1 Sring IOC容器和Beans简介
本章介绍了Spring Framework中控制反转(IOC)的实现原理。IOC也被称为依赖注入(DI)。它是一个对象定义依赖关系的过程,即对象只能通过构造函数参数、工厂方法的参数、对象实例在被构造完成后或从工厂方法返回后设置的属性来定义与之工作的其他对象。然后容器在创建bean时注入这些依赖项。常规创建bean依赖关系的过程,通过使用被依赖方的类的直接构造或类似服务定位器模式的机制,用以控制被依赖项的实例化或位置。和常规的创建bean依赖关系的过程比,这个过程(IoC)是完全相反的,因此被称为控制反转(IoC)。
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的章节。
在Spring中,构成应用程序主干并由Spring IoC容器管理的对象被称为bean。bean是由Spring IoC容器实例化、组装并管理的。否则,bean仅仅是应用程序中众多对象之一。Beans包括他们之间的各种依赖,全都由容器所使用的配置元数据表示。
1.2 IoC容器概览
org.springframework.context.ApplicationContext 接口代表了Spring IoC容器,并且负责实例化、配置、组装上一节介绍的beans。容器通过读取配置元数据来获取关于如何实例化、配置、组装对象的指令。这些配置元数据可通过XML、Java注解或者Java代码表示,通过配置元数据,你可以表示组成应用程序的多个对象以及对象之间庞杂的互相依赖关系。
Spring提供了几种ApplicationContext接口的实现,这些实现方式可做到开箱即用。在独立的应用程序中,创建ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext实例是很常见的。尽管XML一直是定义配置元数据的传统格式,但你可以通过提供少量XML配置来声明支持一些额外的元数据格式,例如指示容器使用Java注解或代码作为元数据格式。
在大多数应用程序场景中,开发人员无需显式地使用代码来初始化一个或多个Spring IoC容器实例。例如,在web应用程序场景下,只需在应用程序的web.xml文件中写上大约8行符合web模板的代码就足够了(请参阅web应用程序的快速ApplicationContext实例化)。如果你使用的是基于eclipse的Spring工具套件开发环境,只需要点击鼠标或者敲击键盘就可轻松创建满足条件的配置模板。
下面的图高度抽象了Spring的工作原理,先将应用程序的类与配置元数据相关联,待应用程序上下文被创建和初始化后,你将获得一个完全配置好且可执行的系统或者应用程序。
1.2.1配置源数据
如前图所示,Spring IoC容器使用元数据配置的形式来呈现作为程序开发者该如何告知Spring容器实例化、配置、组装程序中的对象。
配置元数据传统上以简单直观的XML格式提供,这是本章大部分内容用来传达Spring IoC容器的关键概念和特性的方式。
提示:基于xml的元数据不是配置元数据的唯一允许形式。Spring IoC容器本身与实际编写配置元数据的格式完全分离。现在许多开发人员为他们的Spring应用程序选择基于java的配置。
有关使用Spring容器的其他元数据形式的信息,请参阅:
- 基于注释的配置:Spring 2.5引入了对基于注解的配置元数据的支持。
- 基于java的配置:从Spring 3.0开始,Spring JavaConfig项目提供的许多特性成为Spring框架的核心部分。因此,您可以通过使用Java而不是XML文件来定义应用程序类外部的bean。要使用这些新特性,请参阅@Configuration、@Bean、@Import和@DependsOn注解。
Spring配置包含至少一个bean定义,但通常都包含多个bean定义,这些bean定义都由容器进行管理。基于XML的配置元数据展示的beans通过顶级元素<beans/>中的<bean/>元素进行配置。基于Java代码的配置,通常采用被@Configuration注解的类中被@Bean注解的方法的方式来展示beans。
这些bean定义相当于实际的对象,构成了你的应用程序。通常,你会定义业务层对象,数据读写层对象(DAOS),展示层对象(比如Struts Action实例),一些诸如Hibernate Session工厂、JMS队列等等的基础对象。通常不会在容器中定义细粒度的领域对象,因为创建和加载领域对象是数据存取层和业务逻辑层的职责。但是,你可以使用Spring与AspectJ的集成来配置不受IoC容器控制而创建的对象。具体内容请参阅后续章节:Using AspectJ to dependency-inject domain objects with Spring。
下面的例子展示了基于XML的配置元数据的基础结构:
id属性是一个用来标识单个bean定义的字符串,class属性定义了bean的类型并且使用完全限定的类名。id属性的值可以被其他bean引用,这个例子中没有引用其他bean,更多细节请参考后面Dependencies章节。
1.2.2实例化容器
实例化一个Spring IoC容器是非常容易的。将一个或多个位置路径提供给ApplicationContext
的构造方法就可以让容器加载配置元数据,可以从多种外部资源进行获取,例如文件系统、Java的CLASSPATH
等等。
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
提示:学完Spring的IoC容器后,你或许想了解更多关于Spring的抽象资源,就像在Resources章节中描述的一样,它提供了一 种从符合URI语法的位置中读取输入流的便捷机制。特别是在应用上下文和资源路径章节中,资源路径被用来构造应用上 下文。
下面的例子展示了service层对象的配置文件:
<?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
http://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
http://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>
在前例中,service层由PetStoreServiceImpl类和两个类型分别为JpaAccountDao、JpaItemDao(基于JPA对象/关系映射标准)的数据访问对象组成。property标签中的
name元素指代JavaBean属性的名称,ref元素引用了另外一个bean定义的名称。这种id与ref元素之间的链接关系表达出了合作对象之间的依赖。更多关于配置对象的依赖细节,请参考Dependencies章节。
编写基于XML的配置元数据
bean定义可以跨越多个XML文件是非常有用的。通常每个单独的XML配置文件表示一个逻辑层或者是你架构中的一个模块。你可以使用应用上下文的构造器从所有的XML片段加载bean定义。像上面例子中展示的那样,构造器可以接受多个Resource位置。另外,也可以使用一个或多个<import/>元素从其他文件中加载bean定义。例如:
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>
在上面例子中,外部的bean定义通过services.xml、messageSource.xml、themeSource.xml三个文件被加载。所有的位置路径都是相对于定义执行导入的文件,所以services.xml必须存在于导入文件相同的目录下或类路径位置,而messageSource.xml和themeSource.xml必须位于导入文件之下的resources位置。可以看到,这里开头的斜杠(/)被忽略了,因为给定的都是相对路径,因此建议不使用任何斜杠。被导入文件的内容,包括顶级元素<beans/>在内,相对于Spring Schema必须是合法的XML bean定义。
提示: 有可能会存在使用"../"直接引用上级目录的文件的情况,但不建议这样做。这样做会造成对当前应用程序之外的文件的依 赖。特别不推荐在"classpath:"路径中使用"../"这种引用(例如,"classpath:../services.xml"),这种情况下程序在运 行时会先选择导入文件最近的classpath根路径,然后再查找它的上级文件夹。Classpath配置的改变可能会导致程序选 择不同、非正确的目录。
你可能会使用全限定资源路径(例如,file:C:/config/services.xml或者classpath:/config/services.xml)来替代相对 路径。但需要注意,这样做会使你的应用配置同特定的绝对位置耦合在一起。通常更合适的方式是通过间接的方式来使 用绝对路径,例如通过"${…}"占位符,在运行时解析JVM的系统属性。
导入指令是由beans命名空间自身提供的功能。在Spring提供的XML命名空间中,除了可以使用普通bean定义,还可以使用其他配置功能(例如“context”和“util”命名空间)。
Groovy Bean Definition DSL(Domain Specified Language)
作为外部化配置元数据的进一步示例,bean定义也可以在Spring的Groovy bean定义DSL中表示,从Grails框架中可以知道这一点。通常,这样的配置将存在于以".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
}
}
}
这种配置方式相当于XML bean定义,它甚至支持Spring的XML配置命名空间,同时还允许通过使用"importBeans"指令导入XML bean定义的文件。
1.2.3使用容器
ApplicationContext是高级的工厂接口,它能够维护注册其中的不同beans和beans之间的依赖。通过T getBean(String name, Class<T> requiredType)方法,你可以获取beans实例。
ApplicationContext可以让你以如下这种方式读取和获取bean定义:
// 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();
通过Groovy配置进行引导同上面的方式非常相似,只是上下文实现类Groovy-aware(但同样可以理解为XMLbean定义)不同存在差异罢了:
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");
最灵活的变体是GenericApplicationContext和reader delegate相结合,例如XmlBeanDefinitionReader用于读取XML文件:
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();
或者GroovyBeanDefinitionReader
用于读取Groovy文件:
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();
这种reader delegate可以在相同的ApplicationContext上混合和匹配,如果需要,可以从不同的配置源读取bean定义。
然后可以使用getBean来获取你的bean实例。ApplicationContext接口拥有一些其它获取bean的方法,但理想情况下,应用程序代码永远不应该使用它们。事实上,你的应用程序代码中不应存在对getBean()方法的任何调用,即不存在对Spring API的任何依赖。举个例子,Spring与web框架之间的集成,为各种各样web框架组件(如controllers和JSF管理的bean)提供依赖注入,为了不使应用代码与Spring API耦合,你可以通过元数据(比如autowiring注解)声明对特定bean的依赖。
1.3Bean概览
Spring IoC容器管理一个或多个bean,这些bean通过你提供给容器的配置元数据被创建出来,例如,在XML格式中的<bean/>定义。
在容器本身中,这些bean定义以BeanDefinition对象来表示,包含(以及其他信息)如下元数据:
-
包限定类名,通常是定义的bean的实际实现类。
-
Bean行为配置元素,表示bean在容器内的行为 (作用域,生命周期回调,等)。
-
Bean实现功能时对其它bean的引用,这些引用被称作协作者或依赖关系。
-
在新创建对象中设置其它配置项,例如,bean所管理连接池使用的连接的数量,或连接池的规模限制。
由元数据翻译来的属性集构成了每一个bean定义。
Property | Explained in… |
---|---|
class(类) | Instantiating beans(实例化bean) |
name(名称) | Naming beans(命名bean) |
scope(作用域) | Bean scopes(bean作用域) |
constructor arguments(构造参数) | Dependency Injection(依赖注入) |
properties(属性) | Dependency Injection(依赖注入) |
autowiring mode(自动装配方式) | Autowiring collaborators(自动装配协作者) |
lazy-initialization mode(延迟初始化方式) | Lazy-initialized beans(延迟初始化的bean) |
initialization method(初始方法) | Initialization callbacks(初始化回调) |
destruction method(销毁方法) | Destruction callbacks(销毁回调) |
除了包含如何创建特定bean的信息的bean定义之外,ApplicationContext的实现还允许用户注册在容器之外创建的存在的对象。这是通过访问ApplicationContext的BeanFactory中getBeanFactory()方法完成的,该方法返回BeanFactory实现类DefaultListableBeanFactory。DefaultListableBeanFactory通过registerSingleton(..)和registerBeanDefinition(..)方法来支持前面提及的外部注册的方式。然而,典型的应用程序只使用通过元数据bean定义定义的bean。
提示: Bean元数据和手动提供的单实例需要被尽可能早地注册,以便容器在进行自动装配和其他内省步骤期间更好地关联 它们。尽管覆盖已存在的元数据和单实例在某种程度上是被支持的,在运行时注册新的bean(与实时访问工厂同时)并 未得到官方支持,并且可能导致bean容器的并发访问异常和/或状态不一致。
1.3.1bean命名
每个bean都有一个或多个标识符。这些标识符能在容器中唯一地表示bean,一个bean通常只会有一个标识符,但如果需要的数量不止一个,其他的标识符可被视为别名。
在基于XML的配置元数据中,你可以使用id和/或name属性来指定bean标识符。id属性允许你精确地指定一个id。通常这些名字是字母数字(如‘myBean’,‘fooService’,等等),但是也可能包含特殊字符。如果想要为bean引入其它别名,你可以在name属性中指定它们,以逗号(,)、分号(;)或者空格隔开即可。作为历史记录,在Spring 3.1版本之前,id属性被定义为 xsd:ID类型,它约束了可能的字符。从3.1开始,它被定义为 xsd:string类型。需要注意,bean id唯一性仍是被强制执行的,但不再由XML解析器执行而改由容器执行。
如果没有id或名称明确地被提供,容器会为bean生成一个唯一的名称,你不再需要为bean提供名称或id。但是,如果你需要通过名称引用bean,例如通过使用ref元素或者Service Locator检索方式,则必须提供名称。不提供名称的动机与使用内部bean和自动装配合作者有关。
Bean命名习惯
在为bean命名时采用标准的Java实例属性命名规范,即bean名称以小写的字母开始,然后是驼峰命名风格。这类名字的例子(没有引号)‘accountManager’,‘accountService’,‘userDao’,‘loginController’,等等。
一致的bean命名风格使得你的配置易读和你易于理解,并且如果使用Spring AOP的话,当你应用切面到一堆与名称相关的bean时,这将很有帮助。
提示:在类路径中的组件扫描中,Spring为未命名的组件生成bean名称,遵从上面的规则:本质上,使用类名并将首字母转为 小写。然而,在(不寻常的)特殊情况下,当有一个以上的字符,并且第一和第二字符都是大写时,原始的大小写保留。这 同java.beans.Introspector.decapitalize(Spring在此处使用它)中定义的你规则一致。
在bean定义之外为bean起别名
在bean定义自身中, 你可以为bean提供不止一个名称,通过使用(id属性中指定的最多一个名称和name属性中任意数量的名称之间的)组合来实现。这些名称相当于bean的别名,它们对于有些场景很有用,例如允许应用程序中的每个组件,通过使用特指该组件本身的bean名称引用一个公共的依赖项。
然而,在bean被实际定义处指定所有别名通常是不够的。有时会有这种需求产生,在bean被定义的其它位置引入别名。这种场景在大型系统中非常常见,在这样系统中配置配置拆分到各个子系统中,每个子系统有它自己的对象定义集合。在基于XML的配置元数据中,你可以使用<alias/>元素来实现上述需求。
<alias name="fromName" alias="toName"/>
此处,在同一个容器中,命名为fromName的bean,在使用别名定义后,也可以被称为toName。
举个例子,子系统A的配置元数据可能通过subsystemA-dataSource名称来引用DataSource(数据源),而子系统B则通过subsystemB-dataSource名称引用DataSource(数据源)。当整合使用这两个子系统的主应用程序时,使用myApp-dataSource名称来引用DataSource(数据源)。要使三个名称都引用你添加到MyApp配置元数据中的同一对象,请使用以下别名定义:
<alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/>
<alias name="subsystemA-dataSource" alias="myApp-dataSource" />
现在,每个组件和主应用程序都可通过唯一的名称引用到数据源,并且保证不会和任何其他定义发生冲突(有效地创建命名空间),而引用到同一bean。
如果你正在使用Java配置,可以通过使用@Bean注解提供别名,更多细节请参考Using the @Bean annotation。
1.3.2. 实例化beans
bean定义本质上是一个用于创建一个或多个对象的配方。容器被询问时会查看特定命名对应的配方(bean definition),然后使用由该bean定义封装的配置元数据来创建(或获取)实际的对象。
如果你使用基于XML的配置元数据,在<bean/>元素的class属性中,你可以指定需要被实例化的bean的类型或(类)。这个class属性,是BeanDefinition实例中的一个内部成员变量,通常是强制性的。(异常情况,请参考Instantiation using an instance factory method and Bean definition inheritance——使用实例工厂方法和bean定义继承的实例化。)你可以通过以下两种方式中的任意一种来使用Class属性:
- 通常,在容器自己通过反射的方式调用bean的构造方法直接创建bean的情况下指定要构造的bean类,某种程度上等同于使用Java代码的new操作。
- 指定包含静态工厂方法的实际类,在不太常见的情况下,容器调用类的静态工厂方法创建bean。通过调用静态的工厂方法返回的对象类型可能和调用类相同,也有可能是一个完全不同的类。
内部类名
如果要为静态内部类配置bean定义,则必须使用内部类的二进制名称。例如,在com.example包下有一个命名为Foo的类,并且Foo类有一个静态的内部类Bar,'class'属性的值将会是:com.example.Foo$Bar。注意在名字中使用$字符将嵌套类名同外部类名分隔开。
通过构造器实例化
当你使用构造器的方式创建bean时,所有普通类都可用,并且与Spring兼容。也就是说,正在开发的类无需实现任何指定的接口或者采用特定的编码风格。仅仅指定bean类就足矣。但是,根据对指定bean所使用IoC的类型,你或许需要一个默认(空)的构造方法。
Spring IoC容器几乎能够管理任何你想交由管理的类,不仅仅局限于管理标准的JavaBean。大多数Spring使用者喜欢只含有一个默认(无参)构造函数,以及根据容器中的属性生成恰当的set、get方法。当然,你也可以在容器中使用非bean形式(non-bean-style)的类。例如,你需要使用遗留系统中的连接池,显然它完全不遵循JavaBean规范,Spring仍旧能够很好的管理它。
在基于XML的配置元数据中,你可以采用如下方式指定bean类:
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
想要了解为构造函数指定参数(有必要的话)的实现机制以及为构造好的对象设置对象实例属性,请参考Injecting Dependencies章节。
通过静态工厂方法实例化
当定义采用静态工厂方法创建的bean时,使用class属性指定包含静态工厂方法的类和名为factory-method的属性来指定工厂方法本身的名称。Spring将调用此方法(其他可选参数稍后介绍)返回实例对象,这个对象随后会像使用通过构造函数创建的对象一样被使用。这种bean定义的一个用处类似于在传统代码中调用静态工厂。
下面的bean定义指定了通过调用工厂方法创建的bean。定义并未指明返回对象的类型(类),仅仅指定了包含工厂方法的类。在这个例子中,createInstance()方法必须是一个静态方法。在例中将展示如何指定一个工厂方法:
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
接下来的例子将会展示配合上例中的bean定义所使用的类:
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
想知道关于在工厂方法中应用(可选)参数和给从工厂中返回的对象设置对象实例属性机制的更多细节,请参考Dependencies and Configuration in Detail章节。
通过实例工厂方法实例化
和使用静态工厂方法实例化类似,采用实例工厂方法实例化调用存在于容器中的bean身上的非静态方法以创建新的bean。采用这种机制实例化,需要将class属性置之不理,并在factory-bean属性中,指定bean名称,这个名称属于当前(或父级或祖级)容器中存在的bean,而且该bean包含一个可以通过调用它以创建对象的实例方法。使用factory-method属性设置工厂方法本身的名称。下面的例子展示了如何配置一个这样的bean:
<!-- 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"/>
下面的例子展示了与之相关的Java类:
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}
一个工厂类也可以持有不止一个工厂方法,就像下面例子中展示的一样:
<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"/>
下面的示例代码是与上例相关的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;
}
}
这种方式显示了工厂bean本身也能够通过依赖注入(DI)被管理和配置。参考Dependencies and Configuration in Detail章节。
在Spring文档中,“factory bean”是指在Spring容器中配置的能够通过实例或者静态工厂方法创建对象的bean。相比较而言,FactoryBean(注意大小写)是指Spring特有的 |
1.4依赖
一个典型的企业应用程序不只是由单个对象组成(或者Spring中使用的bean)。即便是最简答的应用程序也会拥有不止一个对象,这些对象相互协作为终端用户呈现一个条理分明的应用程序。接下来的这一节会阐述,如何定义多个独立于完全实现的应用程序的bean定义,这些bean定义在程序中协同完成某个目标。
1.4.1依赖注入
依赖注入(DI)是一个过程(一道工序),在这个过程中,对象通过构造器的参数、工厂方法的参数、被设置到(已创建的或工厂方法返回的)对象实例上的属性,来定义它的依赖(也就是说,它需要使用的其他对象)。然后容器在创建bean的时候注入这些依赖关系。通常情况下,bean自己采用类的直接构建或服务定位器模式来控制它依赖对象的实例化或位置,依赖注入却不同,它是前面过程根本上地反转(也因此得名,控制反转)。
使用依赖注入使得代码看起来更加清爽,并且为对象提供依赖关系时解耦也会显得更为有效。对象不用查找它的依赖对象并且无需知道依赖对象的位置或类。这会直接导致,你的类变得更加容易测试,尤其是对接口或抽象基类的依赖,这允许你在单元测试中stub或mock实现。
DI主要有两种实现方式:基于构造函数的依赖注入和基于set方法的依赖注入。
基于构造器的依赖注入
基于构造器的DI由容器调用多参数构造方法完成,每个参数表示一个依赖项。使用具体的参数调用静态工厂方法以构建bean近乎等效,本文将类似地对待构造函数和静态工厂方法的参数。下面的例子展示了一个仅能采用构造器方式进行依赖注入的类:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private 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...
}
注意,关于上面的类并无什么特别之处。它就是一个不依赖于容器特定接口、基类或注解的普通Java对象。
构造器参数解析
构造器参数解析时使用参数的类型进行匹配。在bean定义的构造参数中如果没有潜在歧义,在bean定义中被定义的构造器参数的顺序,即是在实例化bean时对应构造器中那些参数的顺序。看下面这个类:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
假设ThingTwo和ThingThree两个类之间没有继承关系,不存在潜在的歧义。那么,下面的配置能够很好的运转,你无需在<constructor-arg/>元素中明确地指明构造器参数的索引或者类型。
<beans>
<bean id="thingOne" class="x.y.ThingOne">
<constructor-arg ref="thingTwo"/>
<constructor-arg ref="thingThree"/>
</bean>
<bean id="thingTwo" class="x.y.ThingTwo"/>
<bean id="thingThree" class="x.y.ThingThree"/>
</beans>
当另一个bean被引用时,类型是已知的,并且匹配也没问题(就像前例中一样)。当使用简单类型时,例如<value>true</value>,Spring无法决定值的类型,在没有其它帮助的情形下也就无法根据类型进行匹配。看下面的类:
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private int years;
// The Answer to Life, the Universe, and Everything
private 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>
需要记住一点,为使这一工作开箱即用,代码必须在启用调试标记的情况下进行编译,以使Spring能够从构造器中查找参数名。
如果你不能或不想在调试标记状态下编译代码,你可以使用JDK注解@ConstructorProperties指出你构造器参数的名称。必须像下面的样例一样:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基于set方法的依赖注入
基于set方法的依赖注入,由容器在调用无参构造或者无参静态工厂方法实例化bena之后,再调用beans上的set方法来完成。接下来的例子将展示只能使用纯set注入的类。该类是普通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...
}
应用上下文对于它所管理的beans,支持构造注入和set注入。它还支持在一些依赖通过构造器方式被注入过后再使用set注入的方式。以BeanDefinition的格式配置依赖项,并通过与PropertyEditor实例配合使用,将属性从一种形式转换为另外一种形式。然而,大部分Spring使用者在代码层不会直接用到这些类,而是使用XML bean定义、含有注解的组件(例如,通过@Component、@Controller等注解的类)或者基于Java代码含有@Configuration注解的类中被@Bean注解的方法。这些源文件随后会在内部被转换为BeanDefinition的实例并被用于加载整个Spring IoC容器实例。
使用构造注入还是set注入?
因为能够混合使用构造注入和set注入,所以使用构造注入处理强制依赖关系和set注入或者配置方法处理可选依赖关系,是一条很好的经验法则。需要记住在set方法上使用@Required注解能够将属性成为必须的依赖项。
Spring团队一般提倡构造注入,因为它能让应用程序组件实现为不可变对象,并保证所需的依赖项非空。此外构造注入的组件通常以一种完全初始化的状态被返还给客户端(调用方)代码。作为补充说明,大量的构造器参数是不友好的编码风格,这意味着类可能有太多的职责,这样的类应当被重构使得它所关注点更为简单清晰。
set注入主要用于可选依赖项,这些依赖项在类中可以被分配合理的默认值。否则,在代码使用依赖项的任何地方都必须执行非空检查。set注入的好处之一是,set方法使类的对象在后面的某个时刻能重新配置或重新注入。因此,通过JMX MBeans进行管理是一个非常显著的set注入例子。
对特定类使用最为合理的DI方式。有时候,在处理没有源码的第三方类时,后面介绍的内容将为你提供选择。例如,如果第三方类没有暴露任何set方法,那么构造注入将会成为唯一的注入方式。
依赖解析过程
容器按照如下步骤执行bean依赖解析:
-
使用描述所有beans的配置元数据,创建并初始化应用上下文(ApplicationContext)。配置元数据可以通过XML、Java代码或者注解来指定。
-
对于每个bean,它的依赖项通过属性、构造参数、静态工厂方法参数来表示(如果你使用DI而不是标准的构造函数)。当bean被实际创建的时候,这些依赖将会被提供给它。
-
每个属性或者构造器参数既可以是一个实际的值,也可以是容器中另一个bean的引用。
-
每个作为值的属性或构造器参数,都是从它的指定格式转换为其实际类型。默认情况下,Spring会将提供的字符串格式的值转换为所有内置的类型,例如int、long、String、boolean等等。
Spring容器被创建时,由其自己校验每个bean的配置。然而,直到实际创建bean时,才会设置bean属性本身。作用域为单例的beans,会提前到创建容器时被创建。作用域在Bean scopes章节中有介绍。除此之外,bean仅会在被请求时才创建。bean创建可能会导致一系列的bean被创建,因为bean的依赖和它的依赖的依赖(等等)被创建和分配。需要注意,这些依赖之间的解析不匹配可能会滞后出现,即在第一次创建受影响的bean时。
循环依赖
如果你主要使用构造注入,有可能会出现创建不可解析的循环依赖的场景。举个例子:类A在构造注入过程中需要一个类B的实例,而类B在构造注入过程中需要一个类A的实例。如果将A、B两个类配置为互相注入的beans,Spring IoC容器在运行时会检测到这个循环引用,并抛出异常(BeanCurrentlyInCreationException)。
一个可能的解决方法是修改类的源代码,将构造器注入改为setter注入。或者,避免构造器注入而仅使用set注入。换句话说,你可以使用set注入配置循环依赖项,尽管不推荐这样做。
与典型的场景不同(无循环依赖),bean A和bean B之间的循环依赖,导致其中一个bean在未完全初始化之前被注入到另一个bean(一个经典的场景,先有鸡还是先有蛋)。
通常来讲,你可以相信Spring会做正确的事情。它会在容器的加载周期检测配置问题,例如引用不存在的beans和循环依赖。在在实际创建bean时,Spring会尽可能迟地设置属性和解析依赖。这意味着,当你真正向容器请求所需对象时,已经正确加载的容器会创建你所请求的对象或者对象的依赖,如果创建过程中出现问题,即便Spring容器已经被正确加载仍旧会产生异常。举个例子,bean因为丢失的或非法的属性而抛出异常。这种潜在的配置问题延迟暴露的风险,就是为什么应用上下文会以提前实例化单例beans作为默认实现。在实际使用beans之前耗费时间和内存创建它们,带来的好处是让你在应用上下文创建bean的时候即可发现配置问题,而不用等到真正请求它时才发现问题。你仍然能够覆盖这种默认的行为,以便单例beans能够被延迟初始化而不是提前实例化。
如果不存在循环依赖,当一个或多个协作bean被注入依赖bean时,每个协作bean在被注入到依赖bean之前会被完全配置。这意味着,如果bean A依赖于bean B,Spring IoC容器会在调用bean A的setter方法之前完全配置好bean B。换句话说,实例化bean时(如果该bean不是提前实例化的但实例),它的依赖项会被提前设置,并且与之相关的生命周期方法(例如配置好的init方法或initializingBean回调方法)也都会被调用。
依赖注入的例子
下面的例子为基于setter方法的依赖注入使用基于XML的配置元数据。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"/>
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方法被声明用以匹配在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 属性),所以此处不再讨论那些细节。
1.4.2 依赖关系和详细配置
如前一节所述,您可以将bean属性和构造函数参数定义为对其他托管bean(合作者)的引用,或者定义为内联的值。Spring基于XML的配置元数据支持其<property/>和<constructor-arg/>元素内的子元素类型。
直接值(基本类型,字符串,等等)
<property/>元素的value属性将属性或构造函数参数指定为人类可读的字符串表示形式。Spring的转换服务用于将这些值从字符串转换为属性或参数的实际类型。以下示例显示正在设置的各种值:
<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="masterkaoli"/>
</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
http://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="masterkaoli"/>
</beans>
前面的XML更加简洁。但是,除非使用支持创建bean定义时自动完成属性的IDE(例如Intellij IDEA或Spring工具套件),否则会在运行时发现拼写错误。强烈建议提供此类IDE协助。
您还可以配置一个java.util.Properties实例,如下所示:
<bean id="mappings"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<!-- 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(字符串值-而不是引用)传递给<constructor-arg/>或<property/>元素。下面的示例演示如何使用它:
<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实际存在。在第二个变体中,不会对传递给客户机bean的targetname属性的值执行任何验证。只有当客户机bean实际被实例化时,才会发现拼写错误(最有可能是致命的结果)。如果客户机bean是原型bean,则只有在部署容器很长时间之后,才能发现此拼写错误和由此产生的异常。
4.0 beans xsd不再支持idref元素的本地属性,因为它不再提供常规bean引用的值。在升级到4.0模式时,更改对idref bean的现有idref本地引用。
<idref/>元素带来值的一个常见位置(至少在Spring2.0之前的版本中)是在proxyFactoryBean定义中AOP拦截器的配置中。指定拦截器名称时使用<idref/>元素可防止您拼写错误拦截器ID。
对其他bean(合作者)的引用
ref元素是<constructor arg/>或<property/>定义元素内的最后一个元素。在这里,您将bean的指定属性的值设置为对容器管理的另一个bean(合作者)的引用。引用的bean是要设置其属性的bean的依赖项,在设置该属性之前,根据需要对其进行初始化。(如果合作者是单例bean,那么它可能已经由容器初始化。)所有引用最终都是对另一个对象的引用。作用域和验证取决于您是否通过bean、本地或父属性指定另一个对象的ID或名称。
通过<ref/>标记的bean属性指定目标bean是最一般的形式,允许在同一容器或父容器中创建对任何bean的引用,而不管它是否在同一XML文件中。bean属性的值可以与目标bean的id属性相同,也可以与目标bean的name属性中的一个值相同。以下示例显示如何使用ref元素:
<ref bean="someBean"/>