Spring的核心容器, 此部分的参考文档,包含了Spring Framework的所有绝对重点内容, 持续更新中, 欢迎收藏关注。
这节最重要的内容就是Spring Framework的控制反转(IoC)容器。彻底了解IoC容器之后,紧接着会对AOP技术进行全面的讲解。Spring Framework拥有自己的AOP框架,其在概念上很容易理解,并且成功解决了Java企业级编程中80%的AOP需求。
同时也介绍了Spring与AspectJ(最具特色,也是Java企业级应用中最成熟的AOP实现)的集成。
这节内容主要介绍Spring Framework实现IoC的原理。IoC也被叫做依赖注入(DI)。这是一个对象定义他们的依赖的过程。其他对象使用它时,只能通过构造方法参数,传递给工厂方法的参数,或者被设置到对象的属性上并在此对象实例化后被构建或者从一个工厂方法返回。容器在创建bean时注入这些依赖。这个过程是从根本上看是完全相反的,所以叫做控制反转(IoC)。
此处硬生翻译原文,估计会造成误解,原文如下:This chapter covers the Spring Framework implementation of the Inversion of Control (IoC) [1] principle. IoC is also known as dependency injection (DI). It is a process whereby objects define their dependencies, that is, the other objects they work with, only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method. The container then injects those dependencies when it creates the bean. This process is fundamentally the inverse, hence the name Inversion of Control (IoC), of the bean itself controlling the instantiation or location of its dependencies by using direct construction of classes, or a mechanism such as the Service Locator pattern.
org.springframework.beans
和 org.springframework.context
两个包是Spring Framework的IoC容器基础包。BeanFactory
接口提供了高级的配置机制,可以管理任何类型的对象。ApplicationContext
是BeanFactory
的子接口,它在BeanFactory
的基础上增加了更容易与Spring AOP集成的功能,语言资源处理(用于国际化),发布事件,和程序应用层的特定上下文(例如web应用中的WebApplicationContext)。
总之,BeanFactory提供了配置框架和基本功能。ApplicationContext提供更多企业级应用的相关功能。ApplicationContext是BeanFactory完整的超集,专门用于此章节。如果想使用BeanFactory来替代ApplicationContext,可以参考后面专门介绍BeanFactory的章节。
在Spring中,构成你的应用程序的主干对象(译者注:例如发送邮件的对象),是被SpringIoC容器来管理的,它们被称为bean。bean是一个对象,它被Spring IoC容器来实例化,组装和管理。bean只是应用程序中许多对象之一。bean和bean之间的依赖关系则反映在容器使用的配置中。
org.springframework.context.ApplicationContext
接口代表了Spring IoC容器,它负责实例化,组装和配置上面所说的bean。容器通过读取配置来实例化,组装和配置bean。配置的主要形式有XML,Java注解和Java代码,它允许你来定义应用中的对象和他们之间复杂的依赖关系。
ApplicationContext接口的一些实现做到了开箱即用。在单机应用中通常会创建一个ClassPathXmlApplicationContext和FileSystemXmlApplicationContext 的实例。
大多数应用场景中,不需要用户显示的创建一个或多个Spring IoC容器的实例。例如,一个web应用,简单的8行(大约)代码的web.xml文件即可满足需求。(后面章节会给出在web应用中如何方便的创建ApplicationContext实例)。如果你正在使用基于eclipse的Spring Tool Suite,那么这个配置将会很容易创建,只需要为数不多的键盘鼠标操作。
下图为Spring如何工作的抽象视图。应用中的类被配置文件整合起来了,所以在ApplicationContext被创建和初始化后,你已经有一个完全配置和可执行的系统或应用了。
如上图展示的,Spring IoC容器使用了配置;作为一个应用开发者,这些配置可以让你告诉Spring容器去实例化,配置,组装应用程序中的对象。
配置元数据传统上是使用简单,直观的XML文件,这节中,主要用XML配置来说明Spring IoC容器的关键概念和特性。
注意:基于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的配置由至少一个,通常多个Spring容器管理的bean定义组成。基于XML来配置bean,是配置在
元素内,其父元素,也是顶级元素为
。基于Java配置通常用@Bean
来标注方法或用@Configuration
来标注类。
这些bean定义对应于构成应用程序的实际对象。通常你定义服务层对象,数据访问层对象(DAO),展示层对象例如Struts Action
实例,基础底层对象例如Hibernate的SessionFactories
JMS Queues
等等。通常,不会在容器中配置细粒度的domain对象(例如数据库po对象),因为加载这些类通常是DAO和业务逻辑代码的职责。然而,你可以利用Spring集成的AspectJ来在Spring IoC容器之外创建和加载domain对象。这部分内容在后面相关章节会给出介绍。
下面的例子基于XML配置的基本结构:
<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="..." class="...">
bean>
<bean id="..." class="...">
bean>
beans>
属性id
是一个字符串,是某个bean的唯一标识。class
属性用来定义bean的类型,这里需要使用类的完全限定名。id
属性的值可以被协作的bean来引用。关于协作bean的配置,这个例子并没有给出,但是可以参考下面Dependencies章节。
实例化一个Spring IoC容器非常简单。传递给ApplicationContext构造方法的相对路径或绝对路径,实际上是资源的路径,它允许容器从外部各种资源(例如本地文件系统,Java类路径等等)加载配置元数据。
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
学习了Spring IoC容器之后,你可能会想去知道更多有关于Spring的
Resource
,像在Resource章节描述的一样,Spring Resource提供了方便的机制来使用URI规则读取InputStream。尤其是,Resource路径被用来构建应用程序context,下面章节会给出具体描述。
下面的例子展示了service层对象(services.xml)配置文件:
<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="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="itemDao" ref="itemDao"/>
bean>
beans>
下面的例子给出了数据处理层对象的配置:
<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">
bean>
<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
bean>
beans>
上面的例子,service层由PetStoreServiceImpl
和两个JpaAccountDao
、JpaItemDao
类型的数据处理对象(基于JPA标准)组成。property
标签的name
属性的值意为Java Bean属性的名称,ref
属性值则为引用的另一个定义好的bean。id
和ref
的关联,表达了对象之间的关系和相互引用等。对象之间详细的依赖关系介绍,可以参考后面的Dependencies章节。
通过多个XML配置文件来定义bean是非常有必要的。通常,在你的架构中,每一个XML配置文件表示特定的逻辑层或模块。
你可以使用application context构造方法来从所有的XML来加载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>
前面的例子中,外部bean定义来自三个XML文件,services.xml
,messageSource.xml
,和themeSource.xml
。所有的路径都是以下几种,此文件的相对路径,所以services.xml必须是同一目录下,或者是类路径,例如messageSource.xml
和themeSource.xml
。正如你看到的一个主要的斜线被忽略了,这因为路径是相对的,所以最好不要使用斜线。被导入文件的内容,包括最顶级标签
,必须是符合Spring标准的有效XML bean定义。
引用父目录文件的时候,使用相对路径“…/”,这样做是可以的,但是并不推荐这样做。这样做会创建一个相对当前应用程序之外的文件的依赖关系。尤其是,这样引用不推荐用于"classpath:“路径(例如"classpath:…/services.xml”),在运行时解析过程中会选择“最近的”类路径的根目录,然后再寻找它的父目录。classPath配置的改变,可能会导致选择不同的、错误的目录。
你也可以使用绝对路径来代替相对路径:例如“file:C:/config/services.xml” 或 “classpath:/config/services.xml”。然而,你可能意识到,这样做会将你的应用程序的配置,耦合到一个特定的位置上。一般不会直接直接配置这种绝对路径,例如,可以使用"${…}"占位符来获得JVM运行时的系统属性。
import标签是beans命名空间本身提供的特性。在Spring提供的XML名称空间功能里,除了简单的bean定义之外,还有更多的配置特性,例如“context”和“util”名称空间等。
作为更进一步展示配置元数据,定义bean也可以用Spring的Groovybean定义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
指令来导入其他的XMLbean配置文件。
接口ApplicationContext
是一个高级工厂的接口,它能够维护不同bean及其依赖项的注册表。使用方法getBean(String name, Class
可以让你检索到bean的实例。
接口ApplicationContext
可以让你读取bean的定义并访问他们,如下所示:
// 创建ApplicationContext实例,加载bean配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
// 从上述配置文件中配置的bean中,检索你所需要的bean
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// 使用配置好的实例,这就是具体的业务代码了
List<String> userList = service.getUsernameList();
使用groovy配置的时候,看起来与上面非常类型,只不过换成ApplicationContext的其他实现类:
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");
最灵活的方式是使用GenericApplicationContext
,它整合了具有代表性的几种reader,例如为XML准备的XmlBeanDefinitionReader:
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();
或者为groovy准备的GroovyBeanDefinitionReader:
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();
这样reader就可以被整合,匹配到同一个ApplicationContext中来读取多种类型的配置。
然后你可以通过getBean
来检索你的bean的实例了。ApplicationContext
接口也有一些其他的方法来检索bean,但是你的代码中不太可能用到。实际上,你的代码里连getBean()
都不需要调用,所以也就不需要依赖于Spring的API。例如,Spring与web框架的集成,为各种web框架组件(例如controller,JSF)提供了依赖注入,允许您通过配置声明对特定bean的依赖(例如:autowiring注解)。
一个Spring IoC容器管理一个或多个bean。这些bean是根据你提供给容器的配置来被容器创建的,例如在XML配置中,
标签下的配置。
在容器内部,这些bean被表示为BeanDefinition
对象,它包含(以及其他信息)了如下的元数据信息:
一个限制包类名:通常是定义好的bean的实现类。
bean行为配置元素,规定了bean的容器中的行为方式(范围,生命周期回调等等)。
对其他bean的引用;这些引用也被叫做协作者和依赖。
在新创建的对象中设置其他的配置项,例如,管理连接池的bean,你可以设置这个bean的可以使用的连接数,或者最大,最小连接数。
这些元数据转换为组成每个bean定义的一组属性。
下面为具体定义bean的一些具体属性:
Property | 对应的章节名 |
---|---|
class | 实例化bean |
name | 命名bean |
scope | bean应用范围 |
构造方 | 法传参 |
properties | 依赖注入 |
自动模式 | 自动装配依赖 |
懒加载模式 | 懒加载bean |
初始化方法 | 初始化回调 |
销毁方法 | 销毁后回调 |
除了包含创建bean的特定信息以外,ApplicationContext
的实现类还允许用户在容器外创建现有对象。这是通过调用ApplicationContext的getBeanFactory()
方法来实现,这个方法会返回BeanFactory
的实现类DefaultListableBeanFactory
。它通过registerSingleton(..)
和registerBeanDefinition(..)
方法来支持这。但是,一般的应用只通过配置来定义bean。
通过配置和手动创建单例时,需要今早的进行实例化,这是为了让容器在自动装配和其他内省步骤里正确的解析它们。虽然在某种程度上支持现有的元数据和现有的单例实例,但是在运行时(与工厂的实时访问同时)注册新bean并没有得到官方的支持,并且可能导致在bean容器中并发访问异常和/或不一致的状态。
每一个bean都有一个或多个标识。这些bean的标示符在容器内部都必须是唯一的。bean一般只有一个标示符,但是如果需要多个,其他的可以被认为是别名。
基于XML配置中,你使用id
或name
属性来指定bean的标识。id
属性允许你指定一个唯一id。按照惯例它的命名是以英文数字组成的(“myBean”, “fooService”等),但是包含特殊字符也是可以的。如果你想给bean增加其他别名,那么可以使用name
属性,以“,”或“;”分割或空格。作为一个历史记录,在spring3.1之前的版本中,id属性被定义为xsd: id
类型,它可能限制了一些字符。截止到3.1,它被定义成xsd:string
类型。要注意bean的id
属性值在容器中仍要要是唯一的,尽管不再使用XML解析器。
你不需要给bean设置一个id或name。如果没有设置name和id,容器会给这个bean生成一个唯一的名称。然而你想通过名称来引用一个bean,你必须要提供一个名称来使用ref
标签或服务定位来查找。
bean命名约定
该约定是在命名bean时使用标准Java规范,例如字段名。主要是,bean的命名是以小写字母开头,之后使用驼峰是命名。举例说明:"accountManager","accountService","userDao","loginController"等。
bean的命名应该是让你的配置更简单易懂,在使用Spring AOP时采用这里的建议会对你有很大的帮助。
在classPath扫描组件时,Spring会给未命名的bean生成名称,遵循上面说的规则:实质上就是取简单类型并将首字母小写。然而在特殊情况下,类名的前两个字符都是大写的话,Spring会采用原始的命名,不会做改动,具体的逻辑可以参考Spring命名的方法
java.beans.Introspector.decapitalize
。
在bean的定义本身,你可以给bean提供多个名称,即利用id
属性提供一个唯一id或利用name
属性提供一个或多个名称。这些名称都是一个bean的等价名称,这在某些情况下是比较有用的,例如应用程序中的多个组件在引用同一个bean的时候,可以使用每个组件他们特定的bean名称。
然而,在bean的定义处指定别名并不总是足够的。有时候需要为在其他地方定义的bean引入别名。这样的案例经常出现在大规模系统中,每个子系统直接都可能有配置,每个子系统都有自己的一组对象定义。基于XML的配置中,你可以使用
标签来实现这个。
<alias name="fromName" alias="toName"/>
这个例子中,在同意容器内这个bean叫做“fromName”,也可以在用alias命名后被称为“toName”。
例如,子系统A的配置文件可能去通过名称subsystemA-dataSource
来引用一个DataSource
。子系统B的配置想通过名称subsystemB-dataSource
来引用一个DataSource``。当子系统A、B组合到一起时,主系统会用
myApp-dataSource```来命名。如果想让同一对象拥有这三个名字,你可以在主系统中做如下配置
<alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/>
<alias name="subsystemA-dataSource" alias="myApp-dataSource" />
现在,每一个子系统和主系统都可以通过唯一的名称来引用```DataSource``了,并且保证和其他的bean定义没有冲突,其实它们引用的是同一个bean。
基于Java配置
如果你在使用基于Java的配置,可以用```@Bean```注解来设置别名,后面会有基于Java配置的章节,会在那里详细说明。
bean的配置,实质上是为了创建一个或多个对象。容器在需要的时候,会查看bean是如何配置的,然后通过这个bean的配置去实例化一个对象。
如果你是基于XML来配置bean的,你可以在
的class
属性中指定对象的类型(或者说是class)。这个class
属性值,实际是BeanDefinition
实现类的一个属性,通常是必填项。有两种使用Class
属性的方式:
作为代表性的,如果通过容器本身直接实例化bean,指定要构建的bean的类,通过反射调用类的构造方法,这等同于Java的new
操作。
指定的实际类,含有一个静态的工厂方法,调用这个工厂方法后可以创建对象实例,更少的时候,容器在类上调用静态工厂方法去实例化bean。从调用静态工厂方法返回的对象类型可以是相同的类或其他类。
内部类名称
如果你想为静态内部类配置bean定义,你需要使用内部类的另一种名称。
例如,如果你在com.example包下有一个Foo类,Foo有一个静态内部类叫做Bar,那么在定义bean的时候,class属性的值就应该写作:
com.example.Foo$Bar
要注意要用"$"将类和内部类隔开。
当你使用构造方法来创建bean,所有的正常类都可以被Spring来兼容和使用。也就是说,这个类不需要去实现一个指定的接口,也不需要使用指定的某种方式去编写。简单的指明bean的class就足够了。然后,取决于你使用哪种类的IoC容器来实现bean,你可能需要一个默认的(空的)构造方法。
Spring IoC容器几乎可以管理任何你想管理的class。它并不局限于管理真正的javaBean。大部分使用Spring的使用者,更喜欢用默认的(空的)构造方法,或是class属性来构建bean。你也可以在容器中使用有意思的非bean风格(non-bean-style)的class。例如,如果你需要使用一个完全不符合JavaBean规范的从前遗留下来的连接池,对此,Spring也一样可以很好的管理。
基于XML的配置,你可以指定你的bean像下面这样:
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
关于如何向有参构造方法传递参数(如果需要),以及在对象实例化后对象属性如何设置,可以参开依赖注入章节。
当你使用了静态工厂方法来定义一个bean,你使用class
属性来指定的类需要包含静态工厂方法,再利用factory-method
属性来指定这个静态工厂方法的名称。你可以调用此方法并返回一个对象实例。
下面的的bean定义中,bean会被调用静态工厂方法来创建。这个定义中,并没有指定返回的对象类型,只写了含有此静态工厂方法的类名。这个例子中,createInstance()
方法必须是静态方法。
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
关于给工程方法传递参数,工厂方法返回对象后给对象实例属性赋值的详细说明,参考后面章节:依赖和详细配置。
类似于上节中的利用静态工厂方法实例化,这里指的是利用一个已经存在的bean,调用它的非静态的工厂方法来创建bean。使用这种机制,class
属性是允许为空的,在factory-bean
属性中,指定当前容器(或父容器)中包含的bean的名称,这个bean包含调用之后可以创建对象的方法。然后在factory-method
属性中来指定这个方法的名称:
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
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">
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)来管理和配置。参考后面章节:依赖和详细配置。
在Spring文档中,工厂bean指的是在Spring容器中配置好的bean,它将通过一个实例工厂或静态工厂来创建对象。与之相比,
FactoryBean
(注意这个是一个类名)是指Spring中的一个接口,请不要混淆!
一个典型的企业级应用并不是由相互独立的对象组成(按照Spring的说法就是bean)。即使是最简单的应用程序,也是需要有一些对象相互协作才能将整和到一起的应用呈现给终端用户。
依赖注入是对象定义他们依赖的过程,这些依赖指的是与之一起协作的其他对象,只通过构造方法参数,工厂方法的参数或对象属性(调用构造方法或工厂方法后得到的对象)。容器在创建bean之后注入它们的依赖。这个过程是从根本上反转过来了,因此叫做控制反转(IoC),bean自己控制实例化或定位它的依赖。
在使用DI机制时,代码更简洁,当对象被提供给其依赖关系时,解耦更有效。对象并不去寻找它的依赖,也不知道依赖的位置和class。同样的,你的class会更容易的去测试,特别是当依赖是接口或抽象类时,可以用它们的子类或实现类来实现单元测试。
DI主要存在两种方式:基于构造方法的依赖注入,基于setter的依赖注入。
基于构造方法的依赖注入是很成熟的,容器回去调用带有一定数量参数的构造方法,而其中的每一个参数,则代表了一个依赖。调用一个带有特定参数的静态工厂方法与此是几乎等效的,这里讨论的是将参数用于构造函数,并以类似的方式处理静态工厂方法。下面的例子展示了一个class只能通过用构造方法来进行依赖注入。要注意的是这个类没有任何特别的地方,它只是一个POJO,不依赖与容器的特定接口,基类或注解等。
public class SimpleMovieLister {
// SimpleMovieLister有一个依赖是MovieFinder,简单说就是有个MovieFinder类型的属性
private MovieFinder movieFinder;
// 有了这个构造方法,就可以将movieFinder传进来并赋值给属性movieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// 一些其他的业务逻辑,这里忽略. . .
}
处理构造方法参数
处理构造方法参数的时候,要注意匹配好参数的类型。bean定义中,如果构造方法参数没有潜在的歧义,那么在bean定义中,定义构造函数参数的顺序是在bean被实例化时,这些参数被提供给构造函数的顺序。考虑一下下面的类:
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
不存在潜在的歧义,假设Bar
和Baz
没有继承关系。因此下面的配置是没有问题的,你不必在
指定构造方法参数的index
或type
属性。
<beans>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
bean>
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
beans>
当引用另一个bean时,就会知道该类型,并可以自动匹配(就像上面的示例一样)。当使用一个简单类型,例如true
,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>
除了解决多个值的歧义之外,指定索引还可以解决构造函数具有相同类型的两个参数的问题。要注意,index
的值是从0开始的。
构造方法参数名称
你也可以利用构造方法参数的名称来消除歧义:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
bean>
记住,要使这正常使用,你的代码必须使用debug级别来编译,这样Spring才可以查找到构造方法的名称(译者注:关于利用debug来编译,此处是一个重要的细节,如果不了解,在今后的工作中,你可能会遇到一些莫名其妙的问题,详情请点击知识扩展)。如果你不能利用debug级别编译程序(或者说你不想),你可以使用@ConstructorProperties
注解去设置好你的构造方法名称。下面是一个简单的例子:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
容器调用无参构造方法或没有参数的静态工厂方法后,会得到bean的实例,基于setter的依赖注入则是在得到bean的实例后,容器调用bean实例的setter方法来注入依赖属性。
下面的例子展示了只能通过setter方法依赖注入的类。这个类是非常常见的Java类。这是一个和容器特定接口没用依赖的,没用注解也没有基类的POJO。
public class SimpleMovieLister {
// 有一个the MovieFinder的属性,也就是依赖
private MovieFinder movieFinder;
// 有一个setter方法,所以Spring容器可以调用这歌方法来注入一个MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// 其他逻辑则省略了...
}
你应该记得前面内容中介绍的ApplicationContext
,它为它所管理的bean提供了基于构造方法和setter的依赖注入。在通过构造函数方法注入一些依赖项之后,它还支持基于setter方式的依赖注入。你以BeanDefinition
的形式来配置依赖项,可以将其与PropertyEditor
实例结合使用,以将属性从一种格式转换为另一种格式。然而许多Spring的使用者并不会直接用这些类,而是使用XML配置bean定义,带有注解的组件(例如用@Component
,@Controller
注解标注的类),或者带有@Configuration
注解的类中带有@bean
注解的方法。这些形式的配置最终都会被转换到BeanDefinition
实例的内部,并被Spring IoC容器来加载实例。
选择基于构造方法还是setter的依赖注入?
你可以混合使用基于构造方法的和setter的依赖注入,利用构造方法的方式来强制依赖,利用setter的方式来做可选的依赖,这些方式是很不出错的。注意,在setter方法上使用@Required注解来标注,可以让此属性变为必须注入的。
Spring团队通常更赞成使用构造方法的方式依赖注入。因为它支持将应用程序组件以作为不可变对象来实现,并确保所需的依赖项不是null。此外,依靠构造方法注入的对象会在完全初始化后返回。另外,拥有大量的构造方法是一个非常bad的代码,这意味着这个类承载的太多的功能,需要重构了。
基于Setter的依赖注入应该主要用于可选的依赖项,可以用来给对象的一些属性设置默认值。否则,必须在代码使用依赖项的地方进行非空检查。setter注入的一个好处就是setter方法可以让对象在后面可以进行二次配置或重新注入。```JMX```管理bean是利用setter注入的一个非常好的例子。
在处理一个第三方类并且这个类没有源代码时。这个第三方类没有暴露任何setter方法,那么依赖注入的唯一途径就是通过构造方法。
如下是容器解析bean依赖的过程:
ApplicationContext
是通过配置元数据来创建和初始化的,这些元数据描述了所有的bean。配置元信息可以通过XML,Java代码或注解来指定。
对于每个bean,它的依赖用属性,构造方法参数,或者静态工厂方法参数的形式来表达。bean被创建好之后这些依赖会被提供给它。
每一个属性或构造方法参数都是要设置的值的实际定义,或者对容器内另一个bean的引用。
每一个属性或构造方法参数所指定的值,都将被转换为其实际类型的值。默认情况下,Spring可以将以字符串格式提供的值转换为所有内置类型,如int,long,String,boolean等等。
容器在创建的时,Spring容器会验证每一个bean的配置。然而,在实际创建bean之前,bean的属性本身不会被设置。单例的和被设置为首先加载(pre-instantiated)的bean会在容器初始化后被创建。bean的范围在下一章给出详细介绍。除此之外,bean只会在需要它的时候被创建。创建bean的过程可能会引起一些列的bean被创建,例如bean的依赖、其依赖的依赖等等会被一起创建和分配。注意,在后面,依赖之间解析不匹配可能会显现出来,即首先创建有影响的bean时候。
循环依赖
如果你主要使用构造方法的方式注入,有可能造成无法解决的循环依赖。
例如,class A需要通过构造方法注入一个Class B的实例,classB同样需要通过构造方法注入class A的实例。如果你为class A和class B配置bean并且互相注入,Spring IoC容器在运行时会发现这是循环引用,然后抛出异常:BeanCurrentlyInCreationException。
一个解决途径就是去编辑编代码,让这些类可以通过setter注入。作为另一种选择,避免使用构造方法注入而只使用setter注入。换句话说,尽管这并推荐存在循环依赖,但是你可以使用setter来配置循环依赖。
与一般的情况(没有循环依赖)不同,bean A和bean B之间的循环依赖关系迫使其中一个bean在被完全初始化之前注入另一个bean(典型的鸡生蛋,蛋生鸡场景)。
通常你可以信任Spring来做正确的事情。它可以发现配置问题,例如容器加载时发现引用不存在的bean,循环依赖等。bean在被实际创建后,Spring会尽可能晚的设置属性和解决依赖。这意味着Spring容器正常加载后会晚一些抛出异常,也就是说只有当你开始使用这个对象时,创建这个对象或他们的依赖时产生了异常。例如,因为缺失类属性或无效的属性值,bean会抛出一个异常。这可能会导致一些配置问题延迟出现,这就是为什么ApplicationContext
是默认先将实例化单例的bean的。在bean被实际使用之前,需要提前花费一些实际和内存,在创建ApplicationContext时发现配置问题,而不是以后来做这些事儿。你可以修改默认的此行为,以便使单例的bean懒加载,而不会预先实例化。
如果没有循环依赖的存在,当一个或多个协作bean(其他bean的依赖)被注入到一个bean中时,每个协作bean在被注入到bean之前完全被配置。这意味着如果bean A依赖于bean B,Spring IoC容器在调用bean A 上的setter方法前将完全配置好bean B。换句话说,bean被实例化了(如果不是一个预先实例化的单例),他的依赖被设置了,相关的生命周期方法被调用了。
下面的例子是用XML来配置的基于setter的依赖注入:
<bean id="exampleBean" class="examples.ExampleBean">
<property name="beanOne">
<ref bean="anotherExampleBean"/>
property>
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
相关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;
}
}
上面例子中,声明的setter与再XML中指定的类型相匹配。下面的例子使用了基于构造方法的依赖注入(constructor-based):
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg>
<ref bean="anotherExampleBean"/>
constructor-arg>
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
相关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;
}
}
bean定义中指定的构造方法参数,会被ExampleBean
构造方法的参数来使用。
现在来考虑一下这个例子的变体,作为可以替代构造方法的方式,Spring在之前告诉了你,可以使用静态工厂方法来返回一个bean的实例:
<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"/>
相关Java代码:
public class ExampleBean {
// 私有的构造方法
private ExampleBean(...) {
...
}
// 一个静态工厂方法; 此方法的参数可以认为就是xml配置中引用的其他bean
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// 其他操作省略...
return eb;
}
}
静态工厂方法的参数通过
标签来提供,这点与基于构造方法的注入恰好相同。工厂方法返回的class类型不一定要与其所在的类的类型相同,虽然在本例子中恰好是一样的。实例工厂方法基本是以相同的方式来使用(除了使用factory-bean属性来替代class属性),这里不再给出详细说明。
就像前面章节提到的,你可以定义bean属性,构造方法参数作为其他bean的引用,Spring的基于XML配置支持子标签
和
等支持这个功能。
标签
的value
属性值,以可读的字符串形式指定了属性或构造方法参数。Spring的转换服务(即conversion service,后面章节会介绍)就是用来将这些属性或参数的值从字符串转换为它们的实际类型。
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<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>
下面例子使用了更简洁的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配置更加简洁。然而,类似打字错误这样的问题是在运行时被发现而不是在设计时,除非你使用的IDE是像IntelliJ IDEA或Spring Tool Suite(STS)这样的,在你定义bean的时候可以自动填写属性值。这样的IDE支持是强烈推荐的。
你也可以向下面这样配置一个java.util.Properties
实例:
<bean id="mappings"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
value>
property>
bean>
Spring容器使用JavaBeans PropertyEditor
的机制,将
标签内的值转换到java.util.Properties
实例中。这是一个非常好的简写方式,使用嵌套的
标签而不是value
属性,也是Spring团队支持的几个做法之一。
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是否存在。第二个写法中,没有对传递给bean的targetName
属性的值执行验证。只有在bean被实例化的时候才会发现错误。如果这个bean是一个prototype bean,这个错误可能会在容器部署成功后很长时间内才被发现。
标签
idref
上的local
属性,在4.0版本中的xsd就已经不在支持了,因为它不能再为bean提供一个引用值了。当升级至4.0版本时,只要将你项目中的idref local
替换为idref bean
即可。
将
定义在
或
内,这样定义的bean称之为内部bean(inner bean)。
<bean id="outer" class="...">
<property name="target">
<bean class="com.example.Person">
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
bean>
property>
bean>
内部bean并不需要指定id
和name
属性。如果指定了,容器并不会识别这两个标识。容器在创建bean实例的时候也会忽略scope
属性:内部bean应该是匿名的并且总是被伴随着外部bean来创建的。不可能将内部bean注入到除了它的外部bean以外的任何协作bean中,或者用其他的方式来访问这个内部bean。
作为一种不常见的情况,有可能从特定的域接受到销毁的回调函数,例如, request-scoped(请求域)的内部bean包含在单例bean中,创建内部bean实例的过程会绑定到其包含的bean中,但是销毁的回调允许它参与request-scpoe的生命周期。这不是一个常见的场景。一般内部bean也就是和包含他的bean共享作用域。
在
,
,和
标签中,你可以设置属性和参数,分别是Java集合类型的List
,Set
,Map
和Properties
。
<bean id="moreComplexObject" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">[email protected]prop>
<prop key="support">[email protected]prop>
<prop key="development">[email protected]prop>
props>
property>
<property name="someList">
<list>
<value>a list element followed by a referencevalue>
<ref bean="myDataSource" />
list>
property>
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
map>
property>
<property name="someSet">
<set>
<value>just some stringvalue>
<ref bean="myDataSource" />
set>
property>
bean>
map的key/value的值,或者set值,也可以是以下的这些元素:
bean | ref | idref | list | set | map | props | value | null
合并Collection
Spring容器也支持合并Collection。开发者可以在父bean中定义元素
,,
或
,然后有子中也可以定义
,,
或
,子类型的bean可以覆盖和基础父bean定义的集合元素。其实就是,子的值是合并了父子bean的元素的结果。子中的一个值,父中也有此值,那么会覆盖父中的值。
本节只讨论父子bean的机制。对于不熟悉父子bean的读者,可以先去读bean的继承章节。
下面的例子师范了集合的合并(merge):
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">[email protected]prop>
<prop key="support">[email protected]prop>
props>
property>
bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<props merge="true">
<prop key="sales">[email protected]prop>
<prop key="support">[email protected]prop>
props>
property>
bean>
<beans>
注意,子bean的定义中,
标签上设置了merge="true"
。当子bean被容器解析,实例化时,得到的实例有一个adminEmails
属性,这个属性的值就是子bean属性adminEmails
和父bean属性````adminEmails```合并后的结果。
[email protected]
[email protected]
[email protected]
这种合并同样适用于
,,
等集合类型。使用
场景下,有关List
类型的语意,就是仍然保留了有序集合的概念。父中的值优先于子中的值。使用Map
,Set
,Properties
时并没有顺序的概念。因此,无序语义实际指的就是容器内部使用的集合类型Map,Set和Properties等。
Collection merge的局限性
你不能合并不同的集合类型(例如Map
和List
),如果你这么做就会抛出异常。merge
属性必须用在低级别,继承的,子的bean定义中;在父级别中设置````merge```是无效的,并不能得到你想要的结果。
强类型的Collection
Java 5中引入泛型之后,您可以使用强类型集合。就是,你可以声明一个只允许包含String
(只是举一个例子)类型元素的集合。如果你使用Spring去向bean中依赖注入一个强类型集合,那么您可以利用Spring的类型转换支持,这样,强类型集合实例的元素就会在添加到集合之前转换为适当的类型。
public class Foo {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
相关XML配置:
<beans>
<bean id="foo" class="x.y.Foo">
<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>
当foo的accounts属性准备好被注入之后,元素的类型即强类型的Map
就已经能通过反射得到了。因此,Spring的类型转换模块,可以将各种值转换为Float
类型,即上例子中的字符串类型的9.99
,2.75
和3.99
被转换为Float
类型的值。
Spring认为属性的的空参数为空字符串。下面的XML配置中,设置email属性值为空字符串。
<bean class="ExampleBean">
<property name="email" value=""/>
bean>
上面的配置等价于下面的Java代码:
exampleBean.setEmail("");
标签
可以处理null值,如下:
<bean class="ExampleBean">
<property name="email">
<null/>
property>
bean>
上面的配置等同于下面的Java代码:
exampleBean.setEmail(null);
当你定义你的属性或其他协作bean时候,使用p-namespace(xml命名空间)可以让你用
标签的属性来替代
标签。
在XML配置时,Spring支持利用名称空间来扩展配置。这节讨论的bean配置格式仅仅是基于XML配置的。但是p名称空间并不是在xsd中规定的,它值存在于Spring核心部分中。
下面的例子展示了两个XML配置片段,他们是解决的是同一问题:第一个是使用标准XML配置格式,第二个使用的是p-namespace。
<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 name="classic" class="com.example.ExampleBean">
<property name="email" value="[email protected]"/>
bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="[email protected]"/>
beans>
这个例子展示了在bean定义中,p-namespace下有一个email属性。这告诉了Spring这是一个声明属性的行为。像之前提到的,p-namespace并没有在xsd中定义,所以你可以将其设置为属性的名字。
下面的例子仍然包括两个bean的定义,两个都引用了另一个bean:
<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 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>
如你所见,这里例子不光使用了p-namespace,还使用一个特殊格式来声明一个属性。第一个bean定义使用了
来引用名称叫jane的bean,第二个bean定义利用p:spouse-ref="jane"
作为一个属性,也引用了bean jane
,在这里,spouse
是属性名称,后半部分的-ref
表示这里不是直接值,而是对另一个bean的引用。
p-namespace并不如标准XML定义灵活。例如声明引用属性时与以
Ref
结尾的属性冲突。我们建议你慎重选择你的方法,并与团队成员做好沟通,避免同时使用全部三种方式。
和上节的p-namespace类似,c-namespace是在Spring 3.1中引进的,可以用它在行内配置构造方法的参数,可以替代constructor-arg
标签。
让我们来回顾在章节 基于构造方法依赖注入 中的例子,并用c-namespace来重写一下:
<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
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
<constructor-arg value="[email protected]"/>
bean>
<bean id="foo" class="x.y.Foo" c:bar-ref="bar" c:baz-ref="baz" c:email="[email protected]"/>
beans>
使用c-namespace
与p-namespace
的用法类似,同样的,它也需要被声明,虽然并没有在XSD中被规定(但是它在Spring core中可以被识别)。
在一些少见的情景下,例如你无法获取到构造方法参数名(如果字节码文件是在没有调试信息的情况下编译的),这时可以使用参数位置下标:
<bean id="foo" class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"/>
由于XML的语法,下标的符号需要引入"_"作为属性的开头,不能直接以数字开头!(虽然一些IDE是允许的)
在实践中,构造函数解析机制在匹配参数方面是非常有效的,所以除非逼不得已才像上面这么做,我们建议在配置中使用名称表示法来贯穿整个配置。
当设置bean的属性时,你可以使用复合的,嵌套的属性名称,只要每一级的名称都不为null。考虑下面的配置:
<bean id="foo" class="foo.Bar">
<property name="fred.bob.sammy" value="123" />
bean>
beanfoo
有一个属性叫fred
,fred
有一个叫bob
的属性,bob
有一个sammy
的属性,然后最终的属性sammy
的值被设置为123
。为了让它起作用,bean的被构建后,foo
的属性fred
,fred
的属性bob
不能为null
,否则,就会抛出NullPointerException(空指针异常)
。
如果一个bean是另一个bean的依赖,通常意味着一个bean被设置为另一个bean的属性,你通常利用属性就可以搞定。然而,两个bean之间的关系并不那么直接;例如,Java类中一个静态的初始化方法需要被触发。
depends-on
属性可以明确的强制一个或多个bean在其属性值所指定的bean初始化后再进行初始化。可以参考下面的例子,depends-on
属性来指定对一个bean有依赖:
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
如果要依赖多个bean,只需要提供给depends-on
属性多个值即可,利用逗号,空格,分号来分割。
<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" />
bean定义中的
depends-on
属性,可以指定初始化时候的依赖,在单例bean中也可以指定销毁时间的依赖。在此bean本身被销毁之前,被指定的依赖bean首先被销毁,因此,depends-on
也可以控制关闭的顺序。
默认的ApplicationContext
初始化实现是马上加载所有的bean。通常这么做是可取的,因为配置错误,环境错误会立即被发现,而不是过了数小时或数天之后被发现。当不需要这么做时,你可以通过配置bean定义为lazy-init
(懒加载)来阻止bean的预实例化。配置好懒加载的bean,会告诉IoC容器,在需要使用这个bean实例的时候再加载这个bean,而不是容器初始化时立即加载这个bean。
在XML配置中,通过配置
标签中lazy-init
属性来实现懒加载;下面是例子:
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>
容器ApplicationContext
在使用上面配置时,ApplicationContext
不会再启动时马上加载叫lazy
的bean,叫not.lazy
的bean会被马上加载。
然而,当一个懒加载的bean是一个非懒加载的单例bean的依赖时,ApplicationContext
会在启动时立即实例化这个懒加载bean,这是因为容器必须要提供给这个单例bean的依赖。将懒加载的bean注入到其他非懒加载单例bean中。
你也可以利用
标签的default-lazy-init
属性,在容器级别就控制好懒加载:
<beans default-lazy-init="true">
beans>
Spring容器可以自动装配协作bean之间的关系。你可以通过检查ApplicationContext的内容,让Spring自动为你的bean解析协作者(即其他bean)。自动装配有以下优点:
自动装配可以显著减少指定属性或构造方法的场景。
自动装配可以随着你的对象改变而改变。例如,你需要往一个类中增加依赖,那么无需修改配置就可以自动满足这个需求。因此自动装配在开发过程中非常有用,当代码库趋于稳定时,不需要否定显示的装配。(最后一句原文:Thus autowiring can be especially useful during development, without negating the option of switching to explicit wiring when the code base becomes more stable.)
当使用基于XML的配置时,你需要在
标签内指定属性autowire
。自动装配功能有4种模式。你可以为每个bean单独指定自动装配,所以也就可以选择其中一个进行自动装配。
下面是自动装配的4种模式:
模式 | 说明 |
---|---|
no | (默认)不自动装配。bean的引用必须通过ref 属性标签来指定。对于较大型的项目,并不推荐修改此默认配置,因为明确的指定bean可以更易于管理和更可控。在某种意义上,这相当于是记录了系统的结构 |
byName | 根据名称自动装配。Spring自动寻找同名的bean作为需要装配的属性。例如,如果设置了一个bean定义为byName自动装配,并且含有一个master属性(也就是说它有一个setMaster(…)方法) ,Spring寻找到名称为master的bean定义,并设置到其属性中 |
byType | 如果容器中恰好和属性类型相同的bean,那么允许将这个bean自动装配到属性。如果这种bean的数量为多个则会抛出异常,表明你并不适合用词类型的自动装配,如果没有此类型的bean匹配,则不会发生什么异常,属性也就有可能没有被设置 |
constructor | 和bytype类似,但是是用于构造函数参数,如果容器中没有一个和构造函数参数类型一样的bean,则会引发致命异常 |
使用byType 或 constructor 自动装配模式,你可以装配数组和集合类型。这种情况下,容器内所有与预期类型匹配的bean都会被装配至此数据或集合。你可以自动装配强类型的map,如果key类型正好的String
。自动装配的map的value是由和预期类型一样的bean组成,key的值会包含bean的名称。
你可以将自动装配的行为与依赖检查相结合,依赖检查是在自动装配结束后开始执行的。
自动装配在项目中应用时,要么全部使用,不要部分使用。如果一般不使用自动装配,只是在少数的一两个bean定义中使用它,自动装配可能会让开发者产生混淆。
考虑自动装配的局限性和缺点:
property 和 constructor-arg中明确的依赖会覆盖自动装配。你不能自动装配简单类型的属性,如原始类型,String,Class(以及这些简单类型组成的数组)。这个局限是因为就是这么设计的
明确的装配要比自动装配准确。尽管如上面的表所示,Spring小心的避免猜测所导致预期外的结果,但是项目中被Spring管理的对象关系并不会被明确记录。
可能无法从Spring容器生成文档的工具获得使用连接信息。
setter方法或构造函数所指的的类型,容器中可能会存在多个bean匹配上。对于数组,集合或map来说这并不是一个问题。然而对依赖来说只需要一个结果的话,这并不会被有效的解决。如果非唯一值存在,则会抛出致命的异常。
在下面,你可以有以下几个选择:
放弃自动装配,全部使用显示的(常规)装配
将其autowire-candidate
属性设置为false,避免bean定义进行自动装配,如下一节所示。
指派一个单独的bean定义作为主要对象,需要将
标签的属性primary
属性设置为true。
使用基于注解的容器配置,实现更细粒度的配置。
基于每个bean的配置,你可以将一个bean从自动装配中排除。在Spring XML格式中,设置
标签中的属性autowire-candidate
为false;容器会让此bean定义无法进行自动装配(包括注解风格的配置例如@Autowired
)。
属性
autowire-candidate
只对基于类型的自动装配有效。它不对明确的装配例如byName类型的自动装配有效,即使某bean没有被标记为autowire选项,它也会被解析。因此,只要名称匹配,自动装配总会注入一个bean。
你也可以利用表达式来限制自动装配候选者的名称。最顶层标签
的default-autowire-candidates
属性接受一个或多个表达式。例如,限制候选bean的名称是以 Repository 结尾,只需要将表达式写为 *Repository 。要写入多个表达式,则表达式之间用逗号分隔。对于一个bean定义来说,明确指定autowire-candidate
属性的值为true或false总是优先的,对于这个bean定义来说,表达式匹配规则并不生效。
这些技术,对于那些你不希望通过自动装配注入到其他bean的bean十分有用。这并不意味着被排除的bean,本身不能配置自动装配。相反的,而是bean本身不是其他自动装配bean的候选者。
大多数应用场景中,容器中大部分bean都是单例的。当一个单例的bean,需要另一个单例bean协作,或者一个非单例bean需要另一个非单例bean协作,你通常通过定义一个bean作为另一个bean的属性来处理这种依赖关系。当bean的生命周期不同时问题就出现了。假设一个单例bean A需要一个非单例的bean B,也许A的每个方法都调用了。容器只创建bean A一次,因此也就只有一次机会来设置A的属性。当需要时,容器不一定能为bean A提供一个新的bean B的实例。
一个解决办法是放弃一些控制反转。你可以通过实现ApplicationContextAware接口,创建一个bean A aware(此单词不翻译,直译为:知道的,了解的),当bean A需要beanB的时候,容器调用getBean(“B”)方法来获得bean B。下面是相关的例子:
// 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) {
// 创建一个 Command实例
Command command = createCommand();
// 将状态设置为命令实例
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// 注意引用的spring依赖
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
上面的这个例子,在实际应用中是不可取的,因为你的业务代码耦合到了Spring代码中!方法注入是Spring IoC容器的一个高级特性,它能够以一种干净的方式来处理这个例子!
Lookup方法注入,是指容器在其管理的bean上重写方法,将查找结果返回给容器中定义好的bean。查找通常涉及前面所讲的prototype类型的bean。Spring Framework实现这中方法注入主要使用了CGLIB的字节码生成技术去动态生成子类去覆盖方法。
在前面的CommandManager
类中,你可以看见,Spring容器会动态的覆盖createCommand()
方法的实现。CommandManager类不会有任何Spring依赖项,我们可以看其重写的例子:
package fiona.apple;
// 没有任何Spring的依赖!
public abstract class CommandManager {
public Object process(Object commandState) {
// 获取Command接口的新实例
Command command = createCommand();
command.setState(commandState);
// 为新Command实例设置状态
return command.execute();
}
// ok!但是,这个抽象方法在哪里来实现呢?
protected abstract Command createCommand();
}
包含被注入方法的类(本例中的CommandManager),被注入的方法需要满足下面的格式:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是抽象的,会用动态生成子类来实现这个方法。动态生成子类会覆盖那个方法。例如:
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
bean>
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
bean>
不管什么时候,只要commandManager bean调用自己的createCommand()方法它都需要一个myCommand bean的新实例。你必须要注意将myCommand bean定义设置为prototype
,如果实际需求是这样。如果他是单例的,则每次myCommand bean都会返回同一个实例。
作为选择,也可以使用基于注解的配置,你可以通过@Lookup
注解来声明:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
或者,更通俗的,你可以依赖目标bean的类型,将抽象方法的返回类型修改为目标bean的类型:
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract MyCommand createCommand();
}
注意,你会声明这样一个注解来查找一个一般方法的实现,以便让它们能够让Spring的组件去扫描。这种方式不适用于显式的注册或者显式导入bean class。
接受不同scope目标bean的另一个方法是ObjectFactory/ Provider
注入点。具体办法在bean scope章节给出。
感兴趣的读者可以去研究一下ServiceLocatorFactoryBean
(在org.springframework.beans.factory.config
包下面)。
比方法查找注入应用还少的一种方法注入是,可以在管理的bean中用另一个方法实现来替换任意方法。如果你在实际中没有此需求,那么可以完全跳过本节内容。
基于XML的配置中,你可以使用replaced-method
标签去替换已经实现的方法实现。考虑一下下面的类,有一个我们要覆盖的方法:
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
一个实现了org.springframework.beans.factory.support.MethodReplacer
接口的类,定义了一个新方法:
/**
* 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 ...;
}
}
定义原始类的bean,并且指定覆盖方法:
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>Stringarg-type>
replaced-method>
bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
你可以看到,
标签内包含一个或多个
标签,来表示被覆盖的方法签名。只有类中方法是重载的,这个参数声明才是必须的。方便起见,字符串类型的参数可以是完全限定的类型名字的一部分,例如,下面的类型都表示的是java.lang.String
:
java.lang.String
String
Str
因为参数数量,通常足够去彼此区分,这种简写可以节省很多拼写。
当你定义了一个bean,你就创建了一个bean实例化的规则。这是非常重要的,因为这意味着,使用一个类,你可以从一个bean定义来创建多个实例。
你不光可以控制各种各样的的依赖,将配置的值注入到由bean的配置创建的对象,也可以控制由bean定义创建的对象的范围。这种方法非常强大和有效,因为你通过配置文件创建的对象,是可以选择它的范围的,这样避免了你在Java类级别去设置它的范围。bena可以被部署在以下几个范围:Spring支持6个范围,其中4个是只有在web项目中管用的。
下面的范围都是开箱即用的,你也可以定制自己的范围。
范围 | 描述 |
---|---|
singleton | (默认)整个Spring IoC容器中只有一个实例 |
prototype | 每次使用都会创建一个新的实例 |
request | 在一个JTTP请求生命周期内创建一个单实例;就是,每个HTTP请求有它自己的单例的bean,只有在web项目的Spring ApplicationContext的上下文中有效 |
session | 在HTTP session的生命周期内有效。只有在web项目的Spring ApplicationContext的上下文中有效 |
application | 在ServletContext的生命周期内有效。只有在web项目的Spring ApplicationContext的上下文中有效 |
webSocket | 在webSocket的生命周期内有效。 只有在web项目的Spring ApplicationContext的上下文中有效 |
Spring 3.0中,是有线程范围的,但是默认是不注册的。更多信息,请参考SimpleThreadScope。像获得如何注册它和其他的自定义scope,可以查看使用自定义scope
单例的bean,只存在一个被管理的共享实例,对于所有对这个bean实例的请求,例如用id去匹配bean定义,Spring容器都会返回一个特定的bean实例。
换句话说,当你在bean定义中把bean的范围设置成单例的时候,Spring Ioc容器会根据bean的定义只创建一个实例。此单个实例会被存在一个单例bean的缓存中,后面的所有请求和对这个bean的指向,都会返回缓存中的bean实例。
Spring的单例bean概念,不同于Gang of Four (GoF)设计模式书籍中的单例模式。GoF的单例是硬编码对象的范围,对于每个类加载器来说,类的对象有且只有一个实例。Spring的单例范围最好的理解是每一个容器和每一个bean内,有且只有一个实例。这意味着,在一个Spring IoC容器中,你为一个类定义了一个bean定义,Spring容器会为这个bean定义创建一个且只创建一个实例。单例范围是Spring中的默认范围。在xml中创建一个单例的bean,你可以利用下面的方式:
<bean id="accountService" class="com.foo.DefaultAccountService"/>
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>
非单例的,prototype范围的bean,在每次对bean实例的请求都会创建一个新的bean的实例。就是说,bean被注入到另一个bean中或通过getBean()
调用bean的实例。一般来说,使用prototype范围的bean来创建有状态的bean,使用singleton(单例)范围来创建无状态的bean。下面的图表阐明了Spring的prototype范围的bean。比较典型的,一个数据访问对象(DAO)并不会被配置成prototype的bean,因为典型的DAO不具有任何会话状态;对作者来说只是简单的重复使用而已,就像上一节图表中展示的那样。
下面的例子是如何在xml创建prototype的bean:
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>
对比于其他的范围,Spring并不会完整的管理prototype范围bean的生命周期:容器的实例化,配置,其他方式组装对象,和将其交由客户端,整过程并没有对prototype进行进一步记录。因此,尽管初始化生命周期的回调方法不管是什么范围,在所有对象上都会被调用,但在prototype范围情况下,为其配置生命周期的回调并不会被调用。客户端代码必须要清理prototype对象,并释放prototype bean所占用的珍贵的资源。为了让Spring容器释放prototype bean占用的资源,可以使用BeanPostProcessor进行自定义扩展,这里面包含了需要清理的bean的引用。
在某些方面,Spring容器在prototype域中的角色,就相当于Java中new
操作。所有生命周期的操作都需要由客户端自己来处理。
当你使用单例域的bean依赖于prototype bean的时候,要注意,依赖是在实例化的时候才解析。所以如果你将一个peototype域的bean注入到单例域的bean,一个新的prototype bean实例化并且被注入到单例域的bean中。prototype域的实例是提供给单例域的bean的唯一实例。
然而,假设你想单例域的bean在运行时反复获取一个prototype的bean的新实例。你不能将一个prototype的bean依赖注入到单例域的bean中,因为注入只发生一次,是在Spring容器实例化单例域的bean并解析它的依赖时。如果你需要在运行时获得一次或多次prototype bean的实例时,可以参考前面的方法注入章节。
只有你在使用一个web方面的Spring ApplicationContext
(例如XmlWebApplicationContext
)实现时,Request,session,application,和WebSocket作用域才会起作用。如果你将这些作用域用在一个常规的Spring IoC容器中例如ClassPathXmlApplicationContext
,则会抛出一个IllegalStateException
异常,告诉你这是一个未知的bean作用域。
为了支持request
,session
,application
和websocket
等域,在你进行bean定义之前,需要做一些小的配置(这些小的配置在标准域中不需要配置,singleton和 prototype)。
怎么样完成这些初始化步骤,这取决于你的具体Servlet
环境。
如果你使用Spring Web MVC来访问这些域的bean,实际上,就是通过DispatcherServlet
来调用, 根本不需要其他的步骤。
如果你使用Servlet 2.5
容器时,也就是说不使用DispatcherServlet
时(例如使用JSF,Struts等),你需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener
。对于Servlet 3.5+
,可以实现WebApplicationInitializer
接口,或者对于老版本容器可以在web.xml
中做如下的配置:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
listener-class>
listener>
...
web-app>
如果你的listener启动有了问题,可以考虑使用Spring的RequestContextFilter
。这个过滤器的配置依赖与web配置文件,可以参考如下配置:
...
requestContextFilter
org.springframework.web.filter.RequestContextFilter
requestContextFilter
/*
...
其实DispatcherServlet
, RequestContextListener
, 和 RequestContextFilter
,它们都在做一件事,就是将HTTP request对象绑定到处理请求的线程上,这就可以让request或session域加入到请求链中了。
请先看一下下面的XML配置:
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
Spring容器会创建一个beanLoginAction
的实例,这样它就可以处理各个HTTP请求。也就是说,LoginAction
是作用在了Http request级别。你可以随便改变这个实例的内部状态,因为LoginAction
是多实例的,所以实例之间不会受影响;每个请求都会有它自己的实例。当请求结束的时候,实例也会跟着销毁。
可以使用注解的方式来配置请求域,即@RequestScope
,如下所示:
@RequestScope
@Component
public class LoginAction {
// ...
}
看一下下面xml配置的bean:
对于每个HTTP Session
,Spring容器都会创建一个UserPreferences
的实例。也就是说UserPreferences
是作用范围是Session级别的。和上面讲的request作用域类似,你也可以随便改变实例的内部状态而其他的实例则不受影响。当HTTP Session被销毁的时候,这个实例也就随之销毁。
当基于注解来配置的时候,可以使用@SessionScope
来配置:
@SessionScope
@Component
public class UserPreferences {
// ...
}
先看一下下面的xml配置:
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
上面配置的含义是,Spring容器会为整个web应用而创建appPreferences
bean实例,并且只创建一次。也就是说,appPreferences
的作用级别是ServletContext
级别的,是作为ServletContext
的属性来保存的。这与Spring 单例bean有些类似,但是有两点不同:它是对于每个ServletContext
来说的,不是针对ApplicationContext
来说的,还有就是,它是彻底暴露出来的,是作为ServletContext
属性来存储和使用的。
基于注解的配置,可以参考以下内容:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
Spring IoC容器不光实例化你的bean,同时也会实例化你的bean的依赖。如果你想将一个HTTP请求域的bean注入到另一个bean,并且这个bean将会长期存在,那么你可以注入一个aop代理来替代这个bean。也就是说,你需要注入一个代理对象,并且暴露出一样的public接口,也可以从相关域(例如HTTP request域)来检索实际的目标对象,并将该方法委托给实际对象。
使用单例bena的过程里,可以使用aop:scoped-proxy/标签,然后可以引用一个可以序列话的中间代理,从而在反序列化时候的时候重新获得这个单例的bean。
当对一个有具体域范围的bean(例如session域的bean)使用aop:scoped-proxy/ 标签时,共享代理的的每一次调用都会导致重新创建一次目标实例,然后将该实例转发给调用方。
另外,代理并不是从短生命周期(相对而言)bean获取bean的唯一方式。你也可以简单的声明你的注入点(例如构造/setter方法参数或成员变量)来作为ObjectFactory,同时提供getObject()方法来每次检索按需调用–无需保留实例或单独存储实例。
考虑扩展性,你可以声明一个ObjectProvider,额外提供几个访问方法,例如getIfAvailable 和 getIfUnique。
Provider是JSR-330变体,同时为每次检索和尝试来声明Provider和对应的get()。有关JSR-330的相信信息可以参阅这里。
下配置的配置文件很简单,但是足够让你明白“为什么”和“如何”去明白上面讲的是什么意思:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<aop:scoped-proxy/>
bean>
<bean id="userService" class="com.foo.SimpleUserService">
<property name="userPreferences" ref="userPreferences"/>
bean>
beans>
要创建这样一个代理,你需要在有作用域的bean定义中插入一个子元素
,那么为什么需要在request, session 和 custom-scope级别中定义一个
元素?让门来观察一下下面的单例bean,并与上面的例子做对比:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
bean>
如果想深入到容器对bean生命周期的管理, 你可以实现Spring的InitializingBean
和 DisposableBean
两个接口. 容器在初始化时调用afterPropertiesSet()
方法, 在销毁时调用destroy()
方法.
在spring内部是使用BeanPostProcessor
接口去执行它所能发现的所有回调接口或方法. 如果你想定制一些spring没有提供的功能, 那么你可以自己去实现BeanPostProcessor
接口.
除了初始化回调, 销毁回调以外, spring所管理的对象也可以去实现生命周期接口, 这样的话, 这些对象可以随着容器的生命周期来初始化和停止了.
这些生命周期的回调接口被在此章节来说明.
初始化的回调
实现org.springframework.beans.factory.InitializingBean
接口, 让bean在加载完必要的属性之后, 执行自己所需的初始化工作, 这个接口只指定了一个方法:
void afterPropertiesSet() throws Exception;
其实并不一定推荐你去使用这个接口, 因为这相当于和Spring耦合到了一起. 你也可以使用@PostConstruct
注解或指定一个POJO初始化方法. 使用xml配置的时候, 使用init-method
属性去指定没有返回值的, 没有参数的方法. 使用Java配置的话, 使用@bean
注解的initMethod
属性, 如下面的例子:
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
bean的java类:
public class ExampleBean {
public void init() {
// do some initialization work
}
}
实现InitializingBean
:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
java类:
public class AnotherExampleBean implements InitializingBean {
public void afterPropertiesSet() {
// 做一些初始化工作
}
}
销毁的回调
当容器关闭的时候,如果你想让你的bean在这时候执行一些操作,那么可以实现接口org.springframework.beans.factory.DisposableBean
,这个接口只有一个方法:
void destroy() throws Exception;
并不建议以上面这种方式去处理,因为这相当于和spring代码相耦合了。你可以使用@PreDestroy
注解或指定一个普通方法。基于xml的配置,使用
标签下的destroy-method
属性。基于Java的配置,可以使用@Bean
注解的destroyMethod
属性. 如下面得例子所示:
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
ExampleBean得Java代码:
public class ExampleBean {
public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
上面的例子和下面的事一样的, 但是没有和Spring解耦:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
对应的java代码:
public class AnotherExampleBean implements DisposableBean {
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
标签
下的
destroy-method
属性, 可以指定一个值, 然后通过Spring会去自动发现这个bean上的此方法(一些实现了java.lang.AutoCloseable
或java.io.Closeable
接口的累也会因此而匹配上). 这个值也可以设置再default-destroy-method
中,下一节内容会详解介绍.
默认的初始化和销毁方法
当你编写初始化和销毁回调时, 可以不使用使用Spring指定的InitializingBean
和DisposableBean
接口. 通常你应该写类似init(), initialize(), dispose()
的方法, 此时应该尽可能的让这些生命周期方法保持一致, 以便让项目中所有的开发者一目了然.
可以通过配置Spring容器, 让他可以去每个bean中寻找已经命名好的生命周期方法. 这意味着, 作为一个应用开发者, 你可以在你自己的类中定义叫init()
的初始化回调方法, 并且不用在bean的定义中配置init-method="init"
. Spring IoC容器在bean创建时调用此方法. 这种特性会强制执行初始化或回调方法.
可以参考以下例子:
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
// 这是初始化方法
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}
xml:
<beans default-init-method="init">
<bean id="blogService" class="com.foo.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
bean>
beans>
bean的配置中, 顶级标签为beans
, 在这个标签中配置default-init-method="init"
, 就可以让Spring容器创建beans
下所有的bean
时, 都去执行init
方法.
同样的, 在配置销毁回调方法时, 在beans
标签中配置default-destroy-method
属性.
如果有的bean的命名和beans
标签重命名不一致, 可以在bean
标签中配置init-method
和 destroy-method
, 这样就可以覆盖默认值了.
多种方式的结合使用
在Spring2.5 中, 你有三种方式来控制bean的生命周期, 他们分别是: 实现 InitializingBean
和 DisposableBean
回调接口; 自定义init()
和 destroy()
方法; 还有使用@PostConstruct
和 @PreDestroy
注解. 对于某一个bean, 你可以结合上面三种方法, 来控制它的生命周期.
如果对一个bean配置了多种生命周期, 并且每种配置的方法名都不一样, 这些配置会按照下面所讲的顺序来执行. 然而, 如果配置了多个一样的方法名的话, 例如初始化回调的
init()
方法, 如果有像上节所讲的内容一样, 它只会执行一次.
在一个bean上使用多种初始化方式, 调用优先级:
@PostConstruct
注解的方法InitializingBean
接口的afterPropertiesSet()
方法init()
方法对于销毁的回调方法, 他们会同时执行
@PreDestroy
注解的方法DisposableBean
接口的destroy()
方法destroy()
方法启动和停止回调
任何有自己生命周期需求的对象都可以实现接口Lifecycle
接口(例如开始或结束后台进程).
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
任何由Spring管理的对象都可以实现Lifecycle
接口. 当ApplicationContext
在运行时接收到停止或重启的信号时, 它将这些实现了Lifecycle
接口的对象作为一个级联的上下文对象. 它将这些操作委派给了LifecycleProcessor
接口:
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}
需要注意的是, LifecycleProcessor
是Lifecycle
的子接口. 它增加了两个方法来支持contenx的刷新和关闭.
请注意,
org.springframework.context.Lifecycle
接口只是简单的规定了 开始/结束通知, 在context刷新时并不会自动启动. 可以考虑实现org.springframework.context.SmartLifecycle
接口以来替代某个bean的自动启动的精确控制. 同样的, 请注意停止信号并不是稳定的: 在正常停止下, 所有的生命周期bean都会第一时间接收到停止信号, 然而, 在热刷新的时候, 只有销毁方法会被回调, start方法是不会被回调的.
启动和停止的调用顺序是很重要的. 如果两个对象之间是有依赖的, 依赖方会在其依赖启动后启动, 在依赖停止前停止. 但是有时候依赖关系是未知的.你可能只是知道某一对象可能在另一个对象启动之前启动. 在这个场景下, SmartLifecycle
接口定义了另一种方法叫做getPhase()
, 这个方法实际上是在其父接口Phased
中定义的.
public interface Phased {
int getPhase();
}
接口SmartLifecycle
:
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}
启动的时候, 最底层的对象首先启动, 关闭的时候, 顺序则正好相反. 所以实现了SmartLifecycle
接口, 并且在getPhase()
方法中返回Integer.MIN_VALUE
的话, 它就会最先启动并且最后停止. 没有实现SmartLifecycle
接口的话则为"0".
SmartLifecycle
的停止方法会接收一个回调. 实现它的对象, 会在其停止前调用调用run()
. 所以停止就实现了异步操作.
在非web应用中优雅的关闭Spring容器
此章节仅针对非web应用。基于Spring的web容器的关闭已经说得很详细了。
如果你再非web项目中使用了Spring IoC容器,例如桌面程序中,你在其中注册了一个JVM关闭的hook。这样通过调用单例上的销毁方法来实现优雅的关闭,可以释放掉相关的资源。当然,你必须做正确的配置和实现。
想要注册一个停止钩子,你需要调用ConfigurableApplicationContext
的registerShutdownHook()
方法:
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Boot {
public static void main(final String[] args) throws Exception {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// 为上面的 context 增加一个hook
ctx.registerShutdownHook();
// app runs here...
// main method exits, hook is called prior to the app shutting down...
}
}
当ApplicationContext
创建了一个实现了org.springframework.context.ApplicationContextAware
的对象时,此时对象和容器之间就存在某种关联了。
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}