Chapter 3. 控制反转容器

3.1. 简介

本章将详细深入地探讨Spring框架的控制反转实现(Inversion of Control,IoC)[1]原理。Spring框架所提供的众多功能之所以能成为一个整体正是建立在IoC的基础之上,因此对这一内涵简单、外延丰富的技术我们有必要进行详细的介绍。

org.springframework.beansorg.springframework.context包是Spring IoC容器的基础。BeanFactory提供的高级配置机制,使得管理任何性质的对象成为可能。ApplicationContextBeanFactory的扩展,功能得到了进一步增强,比如更易与Spring AOP集成、消息资源处理(国际化处理)、事件传递及各种不同应用层的context实现(如针对web应用的WebApplicationContext)。

简而言之,BeanFactory提供了配制框架及基本功能,而ApplicationContext则增加了更多支持企业核心内容的功能。ApplicationContext完全由BeanFactory扩展而来,因而BeanFactory所具备的能力和行为也适用于ApplicationContext

本章分为两部份,第一部份讲解BeanFactoryApplicationContext的基本原理,而第二部份则针对ApplicationContext的功能进行讲解。

3.2. 容器和bean的基本原理

在Spring中,那些组成应用的主体(backbone)及由Spring IoC容器所管理的对象被称之为bean。简单地讲,bean就是由Spring容器初始化、装配及被管理的对象,除此之外,bean就没有特别之处了(与应用中的其他对象没有什么区别)。而bean定义以及bean相互间的依赖关系将通过配置元数据来描述。

3.2.1. 容器

org.springframework.beans.factory.BeanFactory是Spring IoC容器的实际代表者,IoC容器负责容纳此前所描述的bean,并对bean进行管理。

在Spring中,BeanFactory是IoC容器的核心接口。它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。

Spring为我们提供了许多易用的BeanFactory实现,XmlBeanFactory就是最常用的一个。该实现将以XML方式描述组成应用的对象以及对象间的依赖关系。XmlBeanFactory类将持有此XML配置元数据,并用它来构建一个完全可配置的系统或应用。

Spring IoC 容器

3.2.1.1. 配置元数据

从上图可以看到,Spring IoC容器将读取配置元数据;并通过它对应用中各个对象进行实例化、配置以及组装。通常情况下我们使用简单直观的XML来作为配置元数据的描述格式。在XML配置元数据中我们可以对那些我们希望通过Spring IoC容器管理的bean进行定义

[Note]Note

到目前为止,基于XML的元数据是最常用到的配置元数据格式。然而,它并不是唯一的描述格式。Spring IoC容器在这一点上是完全开放的。

在本文写作时,Spring支持三种配置元数据格式:XML格式、Java属性文件格式或使用Spring公共API编程实现。由于XML元数据配置格式简单明了,因而本章采用该格式来表达Spring IoC容器的主要理念和特性。

在大多数的应用程序中,并不需要用显式的代码去实例化一个或多个的Spring IoC容器实例。例如,在web应用程序中,我们只需要在web.xml中添加(大约)8 行简单的XML描述符即可(参见Section 3.8.4, “ApplicationContext在WEB应用中的实例化”)。

Spring IoC容器至少包含一个bean定义,但大多数情况下会有多个bean定义。当使用基于XML的配置元数据时,将在顶层的<beans/>元素中配置一个或多个<bean/>元素。

bean定义与应用程序中实际使用的对象一一对应。通常情况下bean的定义包括:服务层对象、数据访问层对象(DAO)、类似Struts Action的表示层对象、Hibernate SessionFactory对象、JMS Queue对象等等。项目的复杂程度将决定bean定义的多寡。

以下是一个基于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-2.0.xsd">
  <bean id="..." class="...">
    <!-- collaborators and configuration for this bean go here -->
  </bean>
  <bean id="..." class="...">
    <!-- collaborators and configuration for this bean go here -->
  </bean>
  <!-- more bean definitions go here... -->
</beans>

3.2.2. 实例化容器

Spring IoC容器的实例化非常简单,如下面的例子:

Resource resource = new FileSystemResource("beans.xml");
BeanFactory factory = new XmlBeanFactory(resource);

... 或...

ClassPathResource resource = new ClassPathResource("beans.xml");
BeanFactory factory = new XmlBeanFactory(resource);

... 或...

ApplicationContext context = new ClassPathXmlApplicationContext(
        new String[] {"applicationContext.xml", "applicationContext-part2.xml"});
// of course, an ApplicationContext is just a BeanFactory
BeanFactory factory = (BeanFactory) context;

3.2.2.1. 组成基于XML配置元数据

将XML配置文件分拆成多个部分是非常有用的。为了加载多个XML文件生成一个ApplicationContext实例,可以将文件路径作为字符串数组传给ApplicationContext构造器。而bean factory将通过调用bean defintion reader从多个文件中读取bean定义。

通常情况下,Spring团队倾向于上述做法,因为这样各个配置并不会查觉到它们与其他配置文件的组合。另外一种方法是使用一个或多个的<import/>元素来从另外一个或多个文件加载bean定义。所有的<import/>元素必须放在<bean/>元素之前以完成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>

在上面的例子中,我们从3个外部文件:services.xmlmessageSource.xmlthemeSource.xml来加载bean定义。这里采用的都是相对路径,因此,此例中的services.xml一定要与导入文件放在同一目录或类路径,而messageSource.xmlthemeSource.xml的文件位置必须放在导入文件所在目录下的resources目录中。正如你所看到的那样,开头的斜杠‘/’实际上可忽略。因此不用斜杠‘/’可能会更好一点。

根据Spring XML配置文件的Schema(或DTD),被导入文件必须是完全有效的XML bean定义文件,且根节点必须为<beans/> 元素。

3.2.3. 多种bean

诚如此前所言,Spring IoC容器将管理一个或多个bean,这些bean将通过配置文件中的bean定义被创建(在XML格式中为<bean/>元素)。

在容器内部,这些bean定义由BeanDefinition 对象来表示,该定义将包含以下信息:

  • 全限定类名:这通常就是已定义bean的实际实现类。如果通过调用static factory方法来实例化bean,而不是使用常规的构造器,那么类名称实际上就是工厂类的类名。

  • bean行为的定义,即创建模式(prototype还是singleton)、自动装配模式、依赖检查模式、初始化以及销毁方法。这些定义将决定bean在容器中的行为

  • 用于创建bean实例的构造器参数及属性值。比如使用bean来定义连接池,可以通过属性或者构造参数指定连接数,以及连接池大小限制等。

  • bean之间的关系,即协作 (或者称依赖)。

上述内容直接被翻译为每个bean定义包含的一组properties。下面的表格列出了部分内容的详细链接:

除了通过bean定义来描述要创建的指定bean的属性之外,某些BeanFactory的实现也允许将那些非BeanFactory创建的、已有的用户对象注册到容器中,比如使用DefaultListableBeanFactoryregisterSingleton(..) 方法。不过大多数应用还是采用元数据定义为主。

3.2.3.1. 命名bean

每个bean都有一个或多个id(或称之为标识符或名称,在术语上可以理解成一回事)。这些id在当前IoC容器中必须唯一。如果一个bean有多个id,那么其他的id在本质上将被认为是别名。

当使用基于XML的配置元数据时,将通过idname属性来指定bean标识符。id属性具有唯一性,而且是一个真正的XML ID属性,因此其他xml元素在引用该id时,可以利用XML解析器的验证功能。通常情况下最好为bean指定一个id。尽管XML规范规定了XML ID命名的有效字符,但是bean标识符的定义不受该限制,因为除了使用指定的XML字符来作为id,还可以为bean指定别名,要实现这一点可以在name属性中使用逗号、冒号或者空格将多个id分隔。

值得注意的是,为一个bean提供一个name并不是必须的,如果没有指定,那么容器将为其生成一个惟一的name。对于不指定name属性的原因我们会在后面介绍(比如内部bean就不需要)。

3.2.3.1.1. bean的别名

在对bean进行定义时,除了使用id属性来指定名称之外,为了提供多个名称,需要通过alias属性来加以指定。而所有的这些名称都指向同一个bean,在某些情况下提供别名非常有用,比如为了让应用的每一个组件能更容易的对公共组件进行引用。

然而,在定义bean时就指定所有的别名并不是总是恰当的。有时我们期望能在当前位置为那些在别处定义的bean引入别名。在XML配置文件中,可用单独的<alias/> 元素来完成bean别名的定义。如:

<alias name="fromName" alias="toName"/>

这里如果在容器中存在名为fromName的bean定义,在增加别名定义之后,也可以用toName来引用。

考虑一个更为具体的例子,组件A在XML配置文件中定义了一个名为componentA-dataSource的DataSource bean。但组件B却想在其XML文件中以componentB-dataSource的名字来引用此bean。而且在主程序MyApp的XML配置文件中,希望以myApp-dataSource的名字来引用此bean。最后容器加载三个XML文件来生成最终的ApplicationContext,在此情形下,可通过在MyApp XML文件中添加下列alias元素来实现:

<alias name="componentA-dataSource" alias="componentB-dataSource"/>
<alias name="componentA-dataSource" alias="myApp-dataSource" />

这样一来,每个组件及主程序就可通过唯一名字来引用同一个数据源而互不干扰。

3.2.3.2. 实例化bean

就Spring IoC容器而言,bean定义基本上描述了创建一个或多个实际bean对象的内容。当需要的时候,容器会从bean定义列表中取得一个指定的bean定义,并根据bean定义里面的配置元数据使用反射机制来创建一个实际的对象。因此这一节将讲解如何告知Spring IoC容器我们将要实例化的对象的类型以及如何实例化对象。

当采用XML描述配置元数据时,将通过<bean/>元素的class属性来指定实例化对象的类型。class 属性 (对应BeanDefinition实例的Class属性)通常是必须的(不过也有两种例外的情形,见Section 3.2.3.2.3, “使用实例工厂方法实例化”Section 3.6, “bean定义的继承”)。class属性主要有两种用途:在大多数情况下,容器将直接通过反射调用指定类的构造器来创建bean(这有点等类似于在Java代码中使用new操作符);在极少数情况下,容器将调用类的静态工厂方法来创建bean实例,class属性将用来指定实际具有静态工厂方法的类(至于调用静态工厂方法创建的对象类型是当前class还是其他的class则无关紧要)。

3.2.3.2.1. 用构造器来实例化

当采用构造器来创建bean实例时,Spring对class并没有特殊的要求,我们通常使用的class都适用。也就是说,被创建的类并不需要实现任何特定的接口,或以特定的方式编码,只要指定bean的class属性即可。不过根据所采用的IoC类型,class可能需要一个默认的空构造器。

此外,IoC容器不仅限于管理JavaBean,它可以管理任意的类。不过大多数使用Spring的人喜欢使用实际的JavaBean(具有默认的(无参)构造器及setter和getter方法),但在容器中使用非bean形式(non-bean style)的类也是可以的。比如遗留系统中的连接池,很显然它与JavaBean规范不符,但Spring也能管理它。

当使用基于XML的元数据配置文件,可以这样来指定bean类:

<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>

给构造函数指定参数以及为bean实例设置属性将在随后的部份中谈及。

3.2.3.2.2. 使用 静态工厂方法实例化

当采用静态工厂方法创建bean时,除了需要指定class属性外,还需要通过factory-method属性来指定创建bean实例的工厂方法。Spring将调用此方法(其可选参数接下来介绍)返回实例对象,就此而言,跟通过普通构造器创建类实例没什么两样。

下面的bean定义展示了如何通过工厂方法来创建bean实例。注意,此定义并未指定返回对象的类型,仅指定该类包含的工厂方法。在此例中, createInstance()必须是一个static方法。

<bean id="exampleBean"
      class="examples.ExampleBean2"
      factory-method="createInstance"/>

给工厂方法指定参数以及为bean实例设置属性将在随后的部份中谈及。

3.2.3.2.3. 使用实例工厂方法实例化

使用静态工厂方法实例化类似,用来进行实例化的实例工厂方法位于另外一个已有的bean中,容器将调用该bean的工厂方法来创建一个新的bean实例

为使用此机制,class属性必须为空,而factory-bean属性必须指定为当前(或其祖先)容器中包含工厂方法的bean的名称,而该工厂bean的工厂方法本身必须通过factory-method属性来设定(参看以下的例子)。

<!-- the factory bean, which contains a method called createInstance() -->
<bean id="myFactoryBean" class="...">
  ...
</bean>
  <!-- the bean to be created via the factory bean -->
<bean id="exampleBean"
      factory-bean="myFactoryBean"
      factory-method="createInstance"/>

虽然设置bean属性的机制仍然在这里被提及,但隐式的做法是由工厂bean自己来管理以及通过依赖注入(DI)来进行配置。

3.2.4. 使用容器

从本质上讲,BeanFactory仅仅只是一个维护bean定义以及相互依赖关系的高级工厂接口。通过BeanFactory我们可以访问bean定义。下面的例子创建了一个bean工厂,此工厂将从xml文件中读取bean定义:

InputStream is = new FileInputStream("beans.xml");
BeanFactory factory = new XmlBeanFactory(is);

基本上就这些了,接着使用getBean(String)方法就可以取得bean的实例;BeanFactory提供的方法极其简单。它仅提供了六种方法供客户代码调用:

  • boolean containsBean(String):如果BeanFactory包含给定名称的bean定义(或bean实例),则返回true

  • Object getBean(String):返回以给定名字注册的bean实例。根据bean的配置情况,如果为singleton模式将返回一个共享的实例,否则将返回一个新建的实例。如果没有找到指定的bean,该方法可能会抛出BeansException异常(实际上将抛出NoSuchBeanDefinitionException异常),在对bean进行实例化和预处理时也可能抛出异常

  • Object getBean(String, Class):返回以给定名称注册的bean实例,并转换为给定class类型的实例,如果转换失败,相应的异常(BeanNotOfRequiredTypeException)将被抛出。上面的getBean(String)方法也适用该规则。

  • Class getType(String name):返回给定名称的bean的Class。如果没有找到指定的bean实例,则抛出NoSuchBeanDefinitionException异常。

  • boolean isSingleton(String):判断给定名称的bean定义(或bean实例)是否为singleton模式(singleton将在bean的作用域中讨论),如果bean没找到,则抛出NoSuchBeanDefinitionException异常。

  • String[] getAliases(String):返回给定bean名称的所有别名。

3.3. 依赖

典型的企业应用不会只由单一的对象(或bean)组成。毫无疑问,即使最简单的系统也需要多个对象一起来满足最终用户的需求。接下来的的内容除了阐述如何单独定义一系列bean外,还将描述如何让这些bean对象一起协同工作来实现一个完整的真实应用。

3.3.1. 注入依赖

依赖注入(DI)背后的基本原理是对象之间的依赖关系(即一起工作的其它对象)只会通过以下几种方式来实现:构造器的参数、工厂方法的参数,或给由构造函数或者工厂方法创建的对象设置属性。因此,容器的工作就是创建bean时注入那些依赖关系。相对于由bean自己来控制其实例化、直接在构造器中指定依赖关系或者类似服务定位器(Service Locator)模式这3种自主控制依赖关系注入的方法来说,控制从根本上发生了倒转,这也正是控制反转(Inversion of Control, IoC) 名字的由来。

应用DI原则后,代码将更加清晰。而且当bean自己不再担心对象之间的依赖关系(以及在何时何地指定这种依赖关系和依赖的实际类是什么)之后,实现更高层次的松耦合将易如反掌。

诚如此前的章节所述,DI主要有两种注入方式,即Setter注入构造器注入

3.3.1.1. Setter注入

通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法,即可实现基于setter的DI。

下面的例子将展示使用setter注入依赖。注意,这个类并没有什么特别之处,它就是普通的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 setMoveFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
    
    // business logic that actually 'uses' the injected MovieFinder is omitted...
}

3.3.1.2. 构造器注入

基于构造器的DI通过调用带参数的构造器来实现,每个参数代表着一个协作者。此外,还可通过给静态工厂方法传参数来构造bean。接下来的介绍将认为给构造器传参与给静态工厂方法传参是类似的。

下面的展示了只能使用构造器参数来注入依赖关系的例子。再次提醒,这个类并没有什么特别之处

public class SimpleMovieLister {
      // the SimpleMovieLister has a dependency on the 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...
}

BeanFactory对于它所管理的bean提供两种注入依赖方式(实际上它也支持同时使用构造器注入和Setter方式注入依赖)。需要注入的依赖将保存在BeanDefinition中,它能根据指定的PropertyEditor实现将属性从一种格式转换成另外一种格式。然而,大部份的Spring用户并不需要直接以编程的方式处理这些类,而是采用XML的方式来进行定义,在内部这些定义将被转换成相应类的实例,并最终得到一个Spring IoC容器实例。

处理bean依赖关系通常按以下步骤进行:

  1. 根据定义bean的配置(文件)创建并初始化BeanFactory实例(大部份的Spring用户使用支持XML格式配置文件的BeanFactoryApplicationContext实现)。

  2. 每个bean的依赖将以属性、构造器参数、或静态工厂方法参数的形式出现。当这些bean被实际创建时,这些依赖也将会提供给该bean。

  3. 每个属性或构造器参数既可以是一个实际的值,也可以是对该容器中另一个bean的引用。

  4. 每个指定的属性或构造器参数值必须能够被转换成属性或构造参数所需的类型。默认情况下,Spring会能够以String类型提供值转换成各种内置类型,比如intlongStringboolean等。

需要强调的一点就是,Spring会在容器被创建时验证容器中每个bean的配置,包括验证那些bean所引用的属性是否指向一个有效的bean(即被引用的bean也在容器中被定义)。然而,在bean被实际创建之前,bean的属性并不会被设置。对于那些singleton类型和被设置为提前实例化的bean(比如ApplicationContext中的singleton bean)而言,bean实例将与容器同时被创建。而另外一些bean则会在需要的时候被创建,伴随着bean被实际创建,作为该bean的依赖bean以及依赖bean的依赖bean(依此类推)也将被创建和分配。

通常情况下,你可以信赖Spring,它会在容器加载时发现配置错误(比如对无效bean的引用以及循环依赖)。Spring会在bean创建的时才去设置属性和依赖关系(只在需要时创建所依赖的其他对象)。Spring容器被正确加载之后,当获取一个bean实例时,如果在创建bean或者设置依赖时出现问题,那么将抛出一个异常。因缺少或设置了一个无效属性而导致抛出一个异常的情况的确是存在的。因为一些配置问题而导致潜在的可见性被延迟,所以在默认情况下,ApplicationContext实现中的bean采用提前实例化的singleton模式。在实际需要之前创建这些bean将带来时间与内存的开销。而这样做的好处就是ApplicationContext被加载的时候可以尽早的发现一些配置的问题。不过用户也可以根据需要采用延迟实例化来替代默认的singleton模式。

最后,我们还要提到的一点就是,当协作bean被注入到依赖bean时,协作bean必须在依赖bean之前完全配置好。例如bean A对bean B存在依赖关系,那么Spring IoC容器在调用bean A的setter方法之前,bean B必须被完全配置,这里所谓完全配置的意思就是bean将被实例化(如果不是采用提前实例化的singleton模式),相关的依赖也将被设置好,而且所有相关的lifecycle方法(如IntializingBean的init方法以及callback方法)也将被调用。

3.3.1.3. 一些例子

首先是一个用XML格式定义的Setter DI例子。相关的XML配置如下:

<bean id="exampleBean" class="examples.ExampleBean">

  <!-- setter injection using the nested <ref/> element -->
  <property name="beanOne"><ref bean="anotherExampleBean"/></property>

  <!-- setter injection using the neater 'ref' attribute -->
  <property name="beanTwo" ref="yetAnotherBean"/>
  <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
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;
    }    
}

正如你所看到的,bean类中的setter方法与xml文件中配置的属性是一一对应的。

接着是构造器注入的例子。以下是xml配置代码以及相对应的java类代码。

<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"/>
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;
    }
}

如你所见,在xml 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"/>
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元素提供,这与使用构造器注入时完全一样。而且,重要的是,工厂方法所返回的实例的类型并不一定要与包含static工厂方法的类类型一致。尽管在此例子中它的确是这样。非静态的实例工厂方法与此相同(除了使用factory-bean属性替代class属性外),因而不在此细述。

3.3.2. 构造器参数的解析

构造器参数将根据类型来进行匹配。如果bean定义中的构造器参数类型明确,那么bean定义中的参数顺序就是对应构造器参数的顺序。考虑以下的类...

package x.y;
  public class Foo {
      public Foo(Bar bar, Baz baz) {
        // ...
    }
}

这里的参数类型非常明确(当然前提是假定类BarBaz在继承层次上并无任何关系)。因此下面的配置将会很好地工作,且无须显式地指定构造器参数索引及其类型。

<beans>
    <bean name="foo" class="x.y.Foo">
        <constructor-arg>
            <bean class="x.y.Bar"/>
        </constructor-arg>
        <constructor-arg>
            <bean class="x.y.Baz"/>
        </constructor-arg>
    </bean>
</beans>

当引用的bean类型已知,则匹配没有问题(如上述的例子)。但是当使用象<value>true<value>这样的简单类型时,Spring将无法决定该值的类型,因而仅仅根据类型是无法进行匹配的。考虑以下将在下面两节使用的类:

package examples;
  public class ExampleBean {
      // No. of years to the 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;
    }
}

3.3.2.1. 构造器参数类型匹配

针对上面的这种情况,我们可以在构造器参数定义中使用type属性来显式的指定参数所对应的简单类型。例如:

<bean id="exampleBean" class="examples.ExampleBean">
  <constructor-arg type="int" value="7500000"/>
  <constructor-arg type="java.lang.String" value="42"/>
</bean>

3.3.2.2. 构造器参数的索引

通过使用index属性可以显式的指定构造器参数出现顺序。例如:

<bean id="exampleBean" class="examples.ExampleBean">
  <constructor-arg index="0" value="7500000"/>
  <constructor-arg index="1" value="42"/>
</bean>

使用index属性除了可以解决多个简单类型构造参数造成的模棱两可的问题之外,还可以用来解决两个构造参数类型相同造成的麻烦。注意:index属性值从0开始

[Tip]Tip

指定构造器参数索引是使用构造器IoC首选的方式。

3.3.3. bean属性及构造器参数详解

正如前面所提到的,bean的属性及构造器参数既可以引用容器中的其他bean,也可以是内联(inline,在spring的XML配置中使用<property/><constructor-arg/>元素定义)bean。

3.3.3.1. 直接量(基本类型、Strings类型等。)

<value/>元素通过字符串来指定属性或构造器参数的值。正如前面所提到的,JavaBean PropertyEditor将用于把字符串从java.lang.String类型转化为实际的属性或参数类型。

<bean id="myDataSource" destroy-method="close"
    class="org.apache.commons.dbcp.BasicDataSource">
  <!-- results in a setDriverClassName(String) call -->
  <property name="driverClassName">
    <value>com.mysql.jdbc.Driver</value>
  </property>
  <property name="url">
    <value>jdbc:mysql://localhost:3306/mydb</value>
  </property>
  <property name="username">
    <value>root</value>
  </property>
</bean>
3.3.3.1.1. 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</value>
    </property>
</bean>

第一种形式比第二种更可取的主要原因是,使用idref标记允许容器在部署时 验证所被引用的bean是否存在。而第二种方式中,传给client bean的targetName属性值并没有被验证。任何的输入错误仅在client bean实际实例化时才会被发现(可能伴随着致命的错误)。如果client bean 是prototype类型的bean,则此输入错误(及由此导致的异常)可能在容器部署很久以后才会被发现。

此外,如果被引用的bean在同一XML文件内,且bean名字就是bean id,那么可以使用local属性,此属性允许XML解析器在解析XML文件时来对引用的bean进行验证。

<property name="targetName">
   <!-- a bean with an id of 'theTargetBean' must exist, else an XML exception will be thrown -->
   <idref local="theTargetBean"/>
</property>

上面的例子与在ProxyFactoryBean bean定义中使用<idref/>元素指定AOP interceptor的相同之处在于:如果使用<idref/>元素指定拦截器名字,可以避免因一时疏忽导致的拦截器ID拼写错误。

3.3.3.2. 引用其它的bean(协作者)

<constructor-arg/><property/>元素内部还可以使用ref元素。该元素用来将bean中指定属性的值设置为对容器中的另外一个bean的引用。如前所述,该引用bean将被作为依赖注入,而且在注入之前会被初始化(如果是singleton bean则已被容器初始化)。尽管都是对另外一个对象的引用,但是通过id/name指向另外一个对象却有三种不同的形式,不同的形式将决定如何处理作用域及验证。

第一种形式也是最常见的形式是通过使用<ref/>标记指定bean属性的目标bean,通过该标签可以引用同一容器或父容器内的任何bean(无论是否在同一XML文件中)。XML 'bean'元素的值既可以是指定bean的id值也可以是其name值。

<ref bean="someBean"/>

第二种形式是使用ref的local属性指定目标bean,它可以利用XML解析器来验证所引用的bean是否存在同一文件中。local属性值必须是目标bean的id属性值。如果在同一配置文件中没有找到引用的bean,XML解析器将抛出一个例外。如果目标bean是在同一文件内,使用local方式就是最好的选择(为了尽早地发现错误)。

<ref local="someBean"/>

第三种方式是通过使用ref的parent属性来引用当前容器的父容器中的bean。parent属性值既可以是目标bean的id值,也可以是name属性值。而且目标bean必须在当前容器的父容器中。使用parent属性的主要用途是为了用某个与父容器中的bean同名的代理来包装父容器中的一个bean(例如,子上下文中的一个bean定义覆盖了他的父bean)。

<!-- in the parent context -->
<bean id="accountService" class="com.foo.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService"  <-- notice that the name of this bean is the same as the name of 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 as here -->
</bean>

('parent'属性的使用并不常见。)

3.3.3.3. 内部bean

所谓的内部bean(inner bean)是指在一个bean的<property/><constructor-arg/>元素中使用<bean/>元素定义的bean。内部bean定义不需要有id或name属性,即使指定id 或 name属性值也将会被容器忽略。

以下是个关于内部bean例子。

<bean id="outer" class="...">
  <!-- instead of using a reference to a target bean, simply define the target inline -->
  <property name="target">
    <bean class="com.mycompany.Person"> <!-- this is the inner bean -->
      <property name="name" value="Fiona Apple"/>
      <property name="age" value="25"/>
    </bean>
  </property>
</bean>

注意:内部bean中的singleton标记及idname属性将被忽略。内部bean总是匿名的且它们总是prototype模式的。同时将内部bean注入到包含该内部bean之外的bean是可能的。

3.3.3.4. 集合

通过<list/><set/><map/><props/>元素可以定义和设置与Java Collection类型对应ListSetMapProperties的值。

<bean id="moreComplexObject" class="example.ComplexObject">
  <!-- results in a setAdminEmails(java.util.Properties) call -->
  <property name="adminEmails">
    <props>
        <prop key="administrator">administrator@somecompany.org</prop>
        <prop key="support">support@somecompany.org</prop>
        <prop key="development">development@somecompany.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>
                <value>yup an entry</value>
            </key>
            <value>just some string</value>
        </entry>
        <entry>
            <key>
                <value>yup a ref</value>
            </key>
            <ref bean="myDataSource" />
        </entry>
    </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的key或value值,或set的value值还可以是以下元素:

bean | ref | idref | list | set | map | props | value | null
3.3.3.4.1. 集合合并

从2.0开始,Spring IoC容器将支持集合的合并。这样我们可以定义parent-style和child-style的<list/><map/><set/><props/>元素,子集合的值从其父集合继承和覆盖而来;也就是说,父子集合元素合并后的值就是子集合中的最终结果,而且子集合中的元素值将覆盖父集全中对应的值。

请注意,关于合并的这部分利用了parent-child bean机制。此内容将在后面介绍,不熟悉父子bean的读者可参见Section 3.6, “bean定义的继承”

用一个例子可能是对此特性的最好描述:

<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
    <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@somecompany.com</prop>
            <prop key="support">support@somecompany.com</prop>
        </props>
    </property>
</bean>
<bean id="child" parent="parent">
    <property name="adminEmails">
        <!-- the merge is specified on the *child* collection definition -->
        <props merge="true">
            <prop key="sales">sales@somecompany.com</prop>
            <prop key="support">support@somecompany.co.uk</prop>
        </props>
    </property>
</bean>
<beans>

在上面的例子中,childbean的adminEmails属性的<props/>元素上使用了merge=true属性。当child bean被容器实际解析及实例化时,其 adminEmails将与父集合的adminEmails属性进行合并。

administrator=administrator@somecompany.com
sales=sales@somecompany.com
support=support@somecompany.co.uk

注意到这里子bean的Properties集合将从父<props/>继承所有属性元素。同时子bean的support值将覆盖父集合的相应值。

对于<list/><map/><set/>集合类型的合并处理都基本类似,在某个方面<list/>元素比较特殊,这涉及到List集合本身的语义学义学,就拿维护一个有序集合中的值来说,父bean的列表内容将排在子bean列表内容的前面。 对于MapSetProperties集合类型没有顺序的概念,因此作为相关的MapSetProperties实现基础的集合类型在容器内部没有排序的语义。

最后需要指出的一点就是,合并功能仅在Spring 2.0(及随后的版本中)可用。不同的集合类型是不能合并(如maplist是不能合并的),否则将会抛出相应的Exceptionmerge属性必须在继承的子bean中定义,而在父bean的集合属性上指定的merge属性将被忽略。

3.3.3.4.2. 强类型集合(仅适用于Java5+)

你若有幸在使用Java5(Tiger),那么你可以使用强类型集合(我自己推荐使用)。比如,声明一个只能包含String类型元素的Collection

假若使用Spring来给bean注入强类型的Collection,那就可以利用Spring的类型转换能,当向强类型Collection中添加元素前,这些元素将被转换。

用一个例子就可以更清楚的说明。考虑以下的类定义,及其相应的(XML)配置...。

public class Foo {
                
    private Map<String, Float> accounts;
    
    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }

}
<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>

foobean的accounts属性被注入之前,通过反射,利用强类型Map<String, Float>的泛型信息,Spring的底层类型转换机制将会把各种value元素值转换为Float类型,因此字符串9.99、2.753.99就会被转换为实际的Float类型。

3.3.3.5. Nulls

<null/>用于处理null值。Spring会把属性的空参数当作空字符串处理。以下的xml片断将email属性设为空字符串

<bean class="ExampleBean">
  <property name="email"><value></value></property>
</bean>

这等同于Java代码: exampleBean.setEmail("")。 而null值则可以使用<null>元素可用来表示。例如:

<bean class="ExampleBean">
  <property name="email"><null/></property>
</bean>

上述的配置等同于Java代码:exampleBean.setEmail(null)

3.3.3.6. XML-based configuration metadata shortcuts

针对常见的value值或bean的引用,Spring提供了简化格式用于替代<value/><ref/>元素。<property/><constructor-arg/><entry/>元素都支持value属性(attribute),它可以用来替代内嵌的<value/>元素。因而,以下的代码:

<property name="myProperty">
  <value>hello</value>
</property>
<constructor-arg>
  <value>hello</value>
</constructor-arg>
<entry key="myKey">
  <value>hello</value>
</entry>

等同于:

<property name="myProperty" value="hello"/>
<constructor-arg value="hello"/>
<entry key="myKey" value="hello"/>

通常情况下,当手工编写配置文件时,你可能会偏向于使用简写形式(Spring的开发团队就是这么做的)。

<property/><constructor-arg/>支持类似的简写属性ref,它可能用来替代整个内嵌的<ref/>元素。因而,以下的代码:

<property name="myProperty">
  <ref bean="myBean">
</property>
<constructor-arg>
  <ref bean="myBean">
</constructor-arg>

等同于:

<property name="myProperty" ref="myBean"/>
<constructor-arg ref="myBean"/>

注意,尽管存在等同于<ref bean="xxx"> 元素的简写形式,但并没有<ref local="xxx">的简写形式,为了对当前xml中bean的引用,你只能使用完整的形式。

最后,map中entry元素的简写形式为key/key-refvalue /value-ref属性,因而,以下的代码:

<entry>
  <key>
    <ref bean="myKeyBean" />
  </key>
  <ref bean="myValueBean" />
</entry>

等同于:

<entry key-ref="myKeyBean" value-ref="myValueBean"/>

再次强调,只有<ref bean="xxx">元素的简写形式,没有<ref local="xxx">的简写形式。

3.3.3.7. 组合属性名称

当设置bean的组合属性时,除了最后一个属性外,只要其他属性值不为null,组合或嵌套属性名是完全合法的。例如,下面bean的定义:

<bean id="foo" class="foo.Bar">
  <property name="fred.bob.sammy" value="123" />
</bean>

foo bean有个fred属性,此属性有个 bob属性,而bob属性又有个sammy属性,最后把sammy属性设置为123。为了让此定义能工作, foofred属性及fredbob属性在bean被构造后都必须非空,否则将抛出NullPointerException异常。

3.3.4. 使用depends-on

多数情况下,一个bean对另一个bean的依赖最简单的做法就是将一个bean设置为另外一个bean的属性。在xml配置文件中最常见的就是使用<ref/>元素。有时候它还有另外一种变体,如果一个bean能感知IoC容器,只要给出它所依赖的id,那么就可以通过编程的方式从容器中取得它所依赖的对象。无论采用哪一种方法,被依赖bean将在依赖bean之前被适当的初始化。

在少数情况下,有时候bean之间的依赖关系并不是那么的直接(例如,当类中的静态块的初始化被时,如数据库驱动的注册)。depends-on属性可以用于当前bean初始化之前显式地强制一个或多个bean被初始化。下面的例子中使用了depends-on属性来指定一个bean的依赖。

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>

<bean id="manager" class="ManagerBean" />

若需要表达对多个bean的依赖,可以在'depends-on'中将指定的多个bean名字用分隔符进行分隔,分隔符可以是逗号、空格及分号等。下面的例子中使用了'depends-on'来表达对多个bean的依赖。

<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" />

3.3.5. 延迟初始化bean

ApplicationContext实现的默认行为就是在启动时将所有singleton bean提前进行实例化。提前实例化意味着作为初始化过程的一部分,ApplicationContext实例会创建并配置所有的singleton bean。通常情况下这是件好事,因为这样在配置中的任何错误就会即刻被发现(否则的话可能要花几个小时甚至几天)。

有时候这种默认处理可能并不是你想要的。如果你不想让一个singleton bean在ApplicationContext实现在初始化时被提前实例化,那么可以将bean设置为延迟实例化。一个延迟初始化bean将告诉IoC 容器是在启动时还是在第一次被用到时实例化。

在XML配置文件中,延迟初始化将通过<bean/>元素中的lazy-init属性来进行控制。例如:

<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true">
    <!-- various properties here... -->
</bean>
  <bean name="not.lazy" class="com.foo.AnotherBean">
    <!-- various properties here... -->
</bean>

ApplicationContext实现加载上述配置时,设置为lazy的bean将不会ApplicationContext启动时提前被实例化,而not.lazy却会被提前实例化。

需要说明的是,如果一个bean被设置为延迟初始化,而另一个非延迟初始化的singleton bean依赖于它,那么当ApplicationContext提前实例化singleton bean时,它必须也确保所有上述singleton 依赖bean也被预先初始化,当然也包括设置为延迟实例化的bean。因此,如果Ioc容器在启动的时候创建了那些设置为延迟实例化的bean的实例,你也不要觉得奇怪,因为那些延迟初始化的bean可能在配置的某个地方被注入到了一个非延迟初始化singleton bean里面。

在容器层次中通过在<beans/>元素上使用'default-lazy-init'属性来控制延迟初始化也是可能的。如下面的配置:

<beans default-lazy-init="true">
    <!-- no beans will be eagerly pre-instantiated... -->
</beans>

3.3.6. 自动装配(autowire)协作者

Spring IoC容器可以自动装配(autowire)相互协作bean之间的关联关系。因此,如果可能的话,可以自动让Spring通过检查BeanFactory中的内容,来替我们指定bean的协作者(其他被依赖的bean)。由于autowire可以针对单个bean进行设置,因此可以让有些bean使用autowire,有些bean不采用。autowire的方便之处在减少或者消除属性或构造器参数的设置,这样可以给我们的配置文件减减肥![2] 在xml配置文件中,autowire一共有五种类型,可以在<bean/>元素中使用autowire属性指定:

Table 3.2. Autowiring modes

模式说明
no

不使用自动装配。必须通过ref元素指定依赖,这是默认设置。由于显式指定协作者可以使配置更灵活、更清晰,因此对于较大的部署配置,推荐采用该设置。而且在某种程度上,它也是系统架构的一种文档形式。

byName

根据属性名自动装配。此选项将检查容器并根据名字查找与属性完全一致的bean,并将其与属性自动装配。例如,在bean定义中将autowire设置为by name,而该bean包含master属性(同时提供setMaster(..)方法),Spring就会查找名为master的bean定义,并用它来装配给master属性。

byType

如果容器中存在一个与指定属性类型相同的bean,那么将与该属性自动装配。如果存在多个该类型的bean,那么将会抛出异常,并指出不能使用byType方式进行自动装配。若没有找到相匹配的bean,则什么事都不发生,属性也不会被设置。如果你不希望这样,那么可以通过设置dependency-check="objects"让Spring抛出异常。

constructor

byType的方式类似,不同之处在于它应用于构造器参数。如果在容器中没有找到与构造器参数类型一致的bean,那么将会抛出异常。

autodetect

通过bean类的自省机制(introspection)来决定是使用constructor还是byType方式进行自动装配。如果发现默认的构造器,那么将使用byType方式。

如果直接使用propertyconstructor-arg注入依赖的话,那么将总是覆盖自动装配。而且目前也不支持简单类型的自动装配,这里所说的简单类型包括基本类型、StringClass以及简单类型的数组(这一点已经被设计,将考虑作为一个功能提供)。自动装配还可以与依赖检查结合使用,这样依赖检查将在自动装配完成之后被执行。

理解自动装配的优缺点是很重要的。其中优点包括:

  • 自动装配能显著减少配置的数量。不过,采用bean模板(见这里)也可以达到同样的目的。

  • 自动装配可以使配置与java代码同步更新。例如,如果你需要给一个java类增加一个依赖,那么该依赖将被自动实现而不需要修改配置。因此强烈推荐在开发过程中采用自动装配,而在系统趋于稳定的时候改为显式装配的方式。

自动装配的一些缺点:

  • 尽管自动装配比显式装配更神奇,但是,正如上面所提到的,Spring会尽量避免在装配不明确的时候进行猜测,因为装配不明确可能出现难以预料的结果,而且Spring所管理的对象之间的关联关系也不再能清晰的进行文档化。

  • 对于那些根据Spring配置文件生成文档的工具来说,自动装配将会使这些工具没法生成依赖信息。

  • 如果采用by type方式自动装配,那么容器中类型与自动装配bean的属性或者构造函数参数类型一致的bean只能有一个,如果配置可能存在多个这样的bean,那么就要考虑采用显式装配了。

尽管使用autowire没有对错之分,但是能在一个项目中保持一定程度的一致性是最好的做法。例如,通常情况下如果没有使用自动装配,那么仅自动装配一个或两个bean定义可能会引起开发者的混淆。

3.3.6.1. 设置Bean使自动装配失效

你也可以针对单个bean设置其是否为被自动装配对象。当采用XML格式配置bean时,<bean/>元素的 autowire-candidate属性可被设为false,这样容器在查找自动装配对象时将不考虑该bean。

对于那些从来就不会被其它bean采用自动装配的方式来注入的bean而言,这是有用的。不过这并不意味着被排除的bean自己就不能使用自动装配来注入其他bean,它是可以的,或者更准确地说,应该是它不会被考虑作为其他bean自动装配的候选者。

3.3.7. 依赖检查

Spring除了能对容器中bean的依赖设置进行检查外。还可以检查bean定义中实际属性值的设置,当然也包括采用自动装配方式设置属性值的检查。

当需要确保bean的所有属性值(或者属性类型)被正确设置的时候,那么这个功能会非常有用。当然,在很多情况下,bean类的某些属性会具有默认值,或者有些属性并不会在所有场景下使用,因此这项功能会存在一定的局限性。就像自动装配一样,依赖检查也可以针对每一个bean进行设置。依赖检查默认为not, 它有几种不同的使用模式,在xml配置文件中,可以在bean定义中为dependency-check属性使用以下几种值:

当需要确保bean的所有属性值(或者属性类型)被正确设置的时候,那么这个功能会非常有用。当然,在很多情况下,bean类会有一些具有默认值的属性,或者有些属性并不会在所有场景下使用,因此这项功能会存在一定的局限性。就像自动装配一样,依赖检查也可以针对每一个bean进行设置。依赖检查默认为not, 它有几种不同的使用模式,在xml配置文件中,可以在bean定义中为dependency-check属性使用以下几种值:

Table 3.3. 依赖检查方式

模式说明
none

没有依赖检查,如果bean的属性没有值的话可以不用设置。

simple

对于原始类型及集合(除协作者外的一切东西)执行依赖检查

object

仅对协作者执行依赖检查

all

对协作者,原始类型及集合执行依赖检查

假若你在使用Java 5(Tiger),可以采用源代码级的注解(annotations)来进行配置,关于这方面的内容可以在Section 25.3.1, “@Required”这一节找到。

3.3.8. 方法注入

在大部分情况下,容器中的bean都是singleton类型的。如果一个singleton bean要引用另外一个singleton bean,或者一个非singleton bean要引用另外一个非singleton bean时,通常情况下将一个bean定义为另一个bean的property值就可以了。不过对于具有不同生命周期的bean来说这样做就会有问题了,比如在调用一个singleton类型bean A的某个方法时,需要引用另一个非singleton(prototype)类型的bean B,对于bean A来说,容器只会创建一次,这样就没法在需要的时候每次让容器为bean A提供一个新的的bean B实例。

上述问题的一个解决办法就是放弃控制反转。通过实现BeanFactoryAware接口(见这里)让bean A能够感知bean 容器,并且在需要的时候通过使用getBean("B")方式(见这里)向容器请求一个新的bean B实例。看下下面这个例子,其中故意使用了这种方法:

// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;

// lots of Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

public class CommandManager implements BeanFactoryAware {

   private BeanFactory beanFactory;

   public Object process(Map commandState) {
      // grab a new instance of the appropriate Command
      Command command = createCommand();
      // set the state on the (hopefully brand new) Command instance
      command.setState(commandState);
      return command.execute();
   }

   // the Command returned here could be an implementation that executes asynchronously, or whatever
   protected Command createCommand() {
      return (Command) this.beanFactory.getBean("command"); // notice the Spring API dependency
   }

   public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
      this.beanFactory = beanFactory;
   }
}

上面的例子显然不是最好的,因为业务代码和Spring Framework产生了耦合。方法注入,作为Spring IoC容器的一种高级特性,可以以一种干净的方法来处理这种情况。

3.3.8.1. Lookup方法注入

Lookup方法注入利用了容器的覆盖受容器管理的bean方法的能力,从而返回指定名字的bean实例。在上述场景中,Lookup方法注入适用于原型bean(尽管它也适用于singleton bean,但在那种情况下直接注入一个实例就够了)。Lookup方法注入的内部机制是Spring利用了CGLIB库在运行时生成二进制代码功能,通过动态创建Lookup方法bean的子类而达到复写Lookup方法的目的。

如果你看下上个代码段中的代码(CommandManager类),Spring容器动态覆盖了createCommand()方法的实现。你的CommandManager类不会有一点对Spring的依赖,在下面这个例子中也是一样的:

package fiona.apple;

// no more Spring imports! 

public class CommandManager {

   public Object process(Object command) {
      // grab a new instance of the appropriate Command interface
      Command command = createCommand();
      // set the state on the (hopefully brand new) Command instance
      command.setState(commandState);
      return command.execute();
   }

    // mmm, but where is the implementation of this method?
   protected abstract CommandHelper createHelper();

}

在包含被注入方法的客户类中(此处是CommandManager),此方法的定义必须按以下形式进行:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

如果方法是抽象的,动态生成的子类会实现该方法。否则,动态生成的子类会覆盖类里的具体方法。让我们来看个例子:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="command" class="fiona.apple.AsyncCommand" scope="prototype">
  <!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
  <lookup-method name="createCommand" bean="command"/>
</bean>

在上面的例子中,标识为commandManager的bean在需要一个新的command bean实例时,会调用createCommand方法。重要的一点是,必须将command部署为原型。当然也可以指定为singleton,如果是这样的话,那么每次将返回相同的command bean实例!

Lookup方法注入既可以结合构造器注入,也可以与setter注入相结合。

请注意,为了让这个动态子类得以正常工作,需要把CGLIB的jar文件放在classpath里。 另外,Spring容器要子类化的类不能是final的,要覆盖的方法也不能是final的。 同样的,要测试一个包含抽象方法的类也稍微有些不同,你需要自己编写它的子类提供该抽象方法的桩实现。 最后,作为方法注入目标的bean不能是序列化的(serialized)。

[Tip]Tip

有兴趣的读者也许已经发现ServiceLocatorFactoryBean (在org.springframework.beans.factory.config包里)的用法和ObjectFactoryCreatingFactoryBean的有些相似, 不同的是它允许你指定自己的lookup接口,不一定非要用Spring的lookup接口,比如ObjectFactory。 要详细了解这种方法请参考ServiceLocatorFactoryBean的Javadocs(它的确减少了对Spring的耦合)。

3.3.8.2. 自定义方法的替代方案

比起Lookup 方法注入来,还有一种很少用到的方法注入形式,该注入能使用bean的另一个方法实现去替换自定义的方法。除非你真的需要该功能,否则可以略过本节。

当使用基于XML配置元数据文件时,可以在bean定义中使用replaced-method元素来达到用另一个方法来取代已有方法的目的。考虑下面的类,我们将覆盖computeValue方法:

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
    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定义:

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
  <!-- arbitrary method replacement -->
  <replaced-method name="computeValue" replacer="replacementComputeValue">
    <arg-type>String</arg-type>
  </replaced-method>
</bean>
  <bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

<replaced-method/>元素内可包含一个或多个<arg-type/>元素,这些元素用来标明被复写的方法签名。只有被复写(override)的方法存在重载(overload)的情况(同名的多个方法变体)才会使用方法签名。为了方便,参数的类型字符串可以采用全限定类名的简写。例如,下面的字符串都表示参数类型为java.lang.String

    java.lang.String
    String
    Str

参数的个数通常足够用来区别每个可能的选择,这个捷径能减少很多键盘输入的工作,它允许你只输入最短的匹配参数类型的字符串。

3.4. bean的作用域

在创建一个bean定义(通常为XML配置文件)时,你可以简单的将其理解为:用以创建由该bean定义所决定的实际对象实例的一张“处方(recipe)”或者模板。就如class一样,根据一张“处方”你可以创建多个对象实例。

你不仅可以控制注入到对象(bean定义)中的各种依赖和配置值,还可以控制该对象的作用域。这样你可以灵活选择所建对象的作用域,而不必在Java Class级定义作用域。Spring Framework支持五种作用域(其中有三种只能用在基于web的Spring ApplicationContext)。

内置支持的作用域分列如下:

Table 3.4. Bean作用域

作用域描述

singleton

在每个Spring IoC容器中一个bean定义对应一个对象实例。

prototype

一个bean定义对应多个对象实例。

request

在一次HTTP请求中,一个bean定义对应一个实例;即每次HTTP请求将会有各自的bean实例,它们依据某个bean定义创建而成。该作用域仅在基于web的Spring ApplicationContext情形下有效。

session

在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。

global session

在一个全局的HTTP Session中,一个bean定义对应一个实例。典型情况下,仅在使用portlet context的时候有效。该作用域仅在基于web的Spring ApplicationContext情形下有效。

3.4.1. Singleton作用域

当一个bean的作用域为singleton, 那么Spring IoC容器中只会存在一个共享的bean实例,并且所有对bean的请求,只要id与该bean定义相匹配,则只会返回bean的同一实例。

换言之,当把一个bean定义设置为singlton作用域时,Spring IoC容器只会创建该bean定义的唯一实例。这个单一实例会被存储到单例缓存(singleton cache)中,并且所有针对该bean的后续请求和引用都将返回被缓存的对象实例。

下图演示了Spring的singleton作用域。

请注意Spring的singleton bean概念与“四人帮”(GoF)模式一书中定义的Singleton模式是完全不同的。经典的GoF Singleton模式中所谓的对象范围是指在每一个ClassLoader指定class创建的实例有且仅有一个。把Spring的singleton作用域描述成一个container对应一个bean实例最为贴切。亦即,假如在单个Spring容器内定义了某个指定class的bean,那么Spring容器将会创建一个且仅有一个由该bean定义指定的类实例。

Singleton作用域是Spring中的缺省作用域。要在XML中将bean定义成singleton,可以这样配置:

<bean id="accountService" class="com.foo.DefaultAccountService"/>

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>

<!-- the following is equivalent, though redundant (and preserved for backward compatibility) -->
<bean id="accountService" class="com.foo.DefaultAccountService" singleton="true"/>

3.4.2. Prototype作用域

Prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。根据经验,对所有有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。

下图演示了Spring的prototype作用域。请注意,典型情况下,DAO不会被配置成prototype,因为一个典型的DAO不会持有任何会话状态,因此应该使用singleton作用域。

要在XML中将bean定义成prototype,可以这样配置:

<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>

<!-- the following is equivalent too (and preserved for backward compatibility) -->
<bean id="accountService" class="com.foo.DefaultAccountService" singleton="false"/>

对于prototype作用域的bean,有一点非常重要,那就是Spring不能对一个prototype bean的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不闻不问了。不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法,而对prototype而言,任何配置好的析构生命周期回调方法都将不会被调用。清除prototype作用域的对象并释放任何prototype bean所持有的昂贵资源,都是客户端代码的职责。(让Spring容器释放被singleton作用域bean占用资源的一种可行方式是,通过使用bean的后置处理器,该处理器持有要被清除的bean的引用。)

谈及prototype作用域的bean时,在某些方面你可以将Spring容器的角色看作是Java new操作符的替代者。任何迟于该时间点的生命周期事宜都得交由客户端来处理。在Section 3.5.1, “Lifecycle接口”一节中会进一步讲述Spring IoC容器中的bean生命周期。

[Note]向后兼容性:在XML中指定生命周期作用域

如果你在bean定义文件中引用'spring-beans.dtd' DTD,要显式说明bean的生命周期作用域你必须使用"singleton"属性(记住singleton生命周期作用域是默认的)。 如果引用的是'spring-beans-2.0.dtd' DTD或者是Spring 2.0 XSD schema,那么需要使用"scope"属性(因为"singleton"属性被删除了,新的DTD和XSD文件使用"scope"属性)。

简单地说,如果你用"singleton"属性那么就必须在那个文件里引用'spring-beans.dtd' DTD。 如果你用"scope"属性那么必须 在那个文件里引用'spring-beans-2.0.dtd' DTD 或'spring-beans-2.0.xsd' XSD。

3.4.3. 其他作用域

其他作用域,即requestsession以及global session仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架)。

[Note]Note

下面介绍的作用域仅仅在使用基于web的Spring ApplicationContext实现(如XmlWebApplicationContext)时有用。如果在普通的Spring IoC容器中,比如像XmlBeanFactoryClassPathXmlApplicationContext,尝试使用这些作用域,你将会得到一个IllegalStateException异常(未知的bean作用域)。

3.4.3.1. 初始化web配置

要使用requestsessionglobal session作用域的bean(即具有web作用域的bean),在开始设置bean定义之前,还要做少量的初始配置。请注意,假如你只想要“常规的”作用域,也就是singleton和prototype,就不需要这一额外的设置。

在目前的情况下,根据你的特定servlet环境,有多种方法来完成这一初始设置。如果你使用的是Servlet 2.4及以上的web容器,那么你仅需要在web应用的XML声明文件web.xml中增加下述ContextListener即可

<web-app>
  ...
  <listener>
    <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
  </listener>
  ...
</web-app>

如果你用的是早期版本的web容器(Servlet 2.4以前),那么你要使用一个javax.servlet.Filter的实现。请看下面的web.xml配置片段:

<web-app>
  ..
  <filter> 
    <filter-name>requestContextFilter</filter-name> 
    <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
  </filter> 
  <filter-mapping> 
    <filter-name>requestContextFilter</filter-name> 
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  ...
</web-app>

RequestContextListenerRequestContextFilter两个类做的都是同样的工作:将HTTP request对象绑定到为该请求提供服务的Thread。这使得具有request和session作用域的bean能够在后面的调用链中被访问到。

3.4.3.2. Request作用域

考虑下面bean定义:

<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>

针对每次HTTP请求,Spring容器会根据loginAction bean定义创建一个全新的LoginAction bean实例,且该loginAction bean实例仅在当前HTTP request内有效,因此可以根据需要放心的更改所建实例的内部状态,而其他请求中根据loginAction bean定义创建的实例,将不会看到这些特定于某个请求的状态变化。当处理请求结束,request作用域的bean实例将被销毁。

3.4.3.3. Session作用域

考虑下面bean定义:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

针对某个HTTP Session,Spring容器会根据userPreferences bean定义创建一个全新的userPreferences bean实例,且该userPreferences bean仅在当前HTTP Session内有效。与request作用域一样,你可以根据需要放心的更改所创建实例的内部状态,而别的HTTP Session中根据userPreferences创建的实例,将不会看到这些特定于某个HTTP Session的状态变化。当HTTP Session最终被废弃的时候,在该HTTP Session作用域内的bean也会被废弃掉。

3.4.3.4. global session作用域

考虑下面bean定义:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>

global session作用域类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义。Portlet规范定义了全局Session的概念,它被所有构成某个portlet web应用的各种不同的portlet所共享。在global session作用域中定义的bean被限定于全局portlet Session的生命周期范围内。

请注意,假如你在编写一个标准的基于Servlet的web应用,并且定义了一个或多个具有global session作用域的bean,系统会使用标准的HTTP Session作用域,并且不会引起任何错误。

3.4.3.5. 作用域bean与依赖

能够在HTTP request或者Session(甚至自定义)作用域中定义bean固然很好,但是Spring IoC容器除了管理对象(bean)的实例化,同时还负责协作者(或者叫依赖)的实例化。如果你打算将一个Http request范围的bean注入到另一个bean中,那么需要注入一个AOP代理来替代被注入的作用域bean。也就是说,你需要注入一个代理对象,该对象具有与被代理对象一样的公共接口,而容器则可以足够智能的从相关作用域中(比如一个HTTP request)获取到真实的目标对象,并把方法调用委派给实际的对象。

[Note]Note

<aop:scoped-proxy/>不能和作用域为singletonprototype的bean一起使用。为singleton bean创建一个scoped proxy将抛出BeanCreationException异常。

让我们看一下将相关作用域bean作为依赖的配置,配置并不复杂(只有一行),但是理解“为何这么做”以及“如何做”是很重要的。

<?xml version="1.0" encoding="UTF-8"?>
<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-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

    <!-- a HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
          
          <!-- this next element effects the proxying of the surrounding bean -->
          <aop:scoped-proxy/>
    </bean>
    
    <!-- a singleton-scoped bean injected with a proxy to the above bean -->
    <bean id="userService" class="com.foo.SimpleUserService">
    
        <!-- a reference to the proxied 'userPreferences' bean -->
        <property name="userPreferences" ref="userPreferences"/>

    </bean>
</beans>

在XML配置文件中,要创建一个作用域bean的代理,只需要在作用域bean定义里插入一个<aop:scoped-proxy/>子元素即可(你可能还需要在classpath里包含CGLIB库,这样容器就能够实现基于class的代理;还可能要使用基于XSD的配置)。上述XML配置展示了“如何做”,现在讨论“为何这么做”。在作用域为requestsession以及globalSession的bean定义里,为什么需要这个<aop:scoped-proxy/>元素呢?下面我们从去掉<aop:scoped-proxy/>元素的XML配置开始说起:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

<bean id="userManager" class="com.foo.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

从上述配置中可以很明显的看到singleton bean userManager被注入了一个指向HTTP Session作用域bean userPreferences的引用。singleton userManager bean会被容器仅实例化一次,并且其依赖(userPreferences bean)也仅被注入一次。这意味着,userManager在理论上只会操作同一个userPreferences对象,即原先被注入的那个bean。而注入一个HTTP Session作用域的bean作为依赖,有违我们的初衷。因为我们想要的只是一个userManager对象,在它进入一个HTTP Session生命周期时,我们希望去使用一个HTTP SessionuserPreferences对象。

当注入某种类型对象时,该对象实现了和UserPreferences类一样的公共接口(即UserPreferences实例)。并且不论我们底层选择了何种作用域机制(HTTP request、Session等等),容器都会足够智能的获取到真正的UserPreferences对象,因此我们需要将该对象的代理注入到userManager bean中, 而userManager bean并不会意识到它所持有的是一个指向UserPreferences引用的代理。在本例中,当UserManager实例调用了一个使用UserPreferences对象的方法时,实际调用的是代理对象的方法。随后代理对象会从HTTP Session获取真正的UserPreferences对象,并将方法调用委派给获取到的实际的UserPreferences对象。

这就是为什么当你将requestsession以及globalSession作用域bean注入到协作对象中时需要如下正确而完整的配置:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
    <aop:scoped-proxy/>
</bean>

<bean id="userManager" class="com.foo.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

3.4.4. 自定义作用域

在Spring 2.0中,Spring的bean作用域机制是可以扩展的。这意味着,你不仅可以使用Spring提供的预定义bean作用域; 还可以定义自己的作用域,甚至重新定义现有的作用域(不提倡这么做,而且你不能覆盖内置的singletonprototype作用域)。

作用域由接口org.springframework.beans.factory.config.Scope定义。要将你自己的自定义作用域集成到Spring容器中,需要实现该接口。它本身非常简单,只有两个方法,分别用于底层存储机制获取和删除对象。自定义作用域可能超出了本参考手册的讨论范围,但你可以参考一下Spring提供的Scope实现,以便于去如何着手编写自己的Scope实现。

在实现一个或多个自定义Scope并测试通过之后,接下来就是如何让Spring容器识别你的新作用域。ConfigurableBeanFactory接口声明了给Spring容器注册新Scope的主要方法。(大部分随Spring一起发布的BeanFactory具体实现类都实现了该接口);该接口的主要方法如下所示:

void registerScope(String scopeName, Scope scope);

registerScope(..)方法的第一个参数是与作用域相关的全局唯一名称;Spring容器中该名称的范例有singletonprototyperegisterScope(..)方法的第二个参数是你打算注册和使用的自定义Scope实现的一个实例。

假设你已经写好了自己的自定义Scope实现,并且已经将其进行了注册:

// note: the ThreadScope class does not exist; I made it up for the sake of this example
Scope customScope = new ThreadScope();
beanFactory.registerScope("thread", scope);

然后你就可以像下面这样创建与自定义Scope的作用域规则相吻合的bean定义了:

<bean id="..." class="..." scope="thread"/>

如果你有自己的自定义Scope实现,你不仅可以采用编程的方式注册自定义作用域,还可以使用BeanFactoryPostProcessor实现:CustomScopeConfigurer类,以声明的方式注册ScopeBeanFactoryPostProcessor接口是扩展Spring IoC容器的基本方法之一,在本章的BeanFactoryPostProcessor中将会介绍。

使用CustomScopeConfigurer,以声明方式注册自定义Scope的方法如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<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-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="thread" value="com.foo.ThreadScope"/>
            </map>
        </property>
    </bean>

    <bean id="bar" class="x.y.Bar" scope="thread">
        <property name="name" value="Rick"/>
        <aop:scoped-proxy/>
    </bean>

    <bean id="foo" class="x.y.Foo">
        <property name="bar" ref="bar"/>
    </bean>

</beans>

CustomScopeConfigurer既允许你指定实际的Class实例作为entry的值,也可以指定实际的Scope实现类实例;详情请参见CustomScopeConfigurer类的JavaDoc。

3.5. 定制bean特性

3.5.1. Lifecycle接口

Spring提供了几个标志接口(marker interface),这些接口用来改变容器中bean的行为;它们包括InitializingBeanDisposableBean。实现这两个接口的bean在初始化和析构时容器会调用前者的afterPropertiesSet()方法,以及后者的destroy()方法。

Spring在内部使用BeanPostProcessor实现来处理它能找到的任何标志接口并调用相应的方法。如果你需要自定义特性或者生命周期行为,你可以实现自己的 BeanPostProcessor。关于这方面更多的内容可以看Section 3.7, “容器扩展点”

下面讲述了几个生命周期标志接口。在附录中会提供相关的示意图来展示Spring如何管理bean,以及生命周期特性如何改变bean的内在特性。

3.5.1.1. 初始化回调

实现org.springframework.beans.factory.InitializingBean接口允许容器在设置好bean的所有必要属性后,执行初始化事宜。InitializingBean接口仅指定了一个方法:

void afterPropertiesSet() throws Exception;

通常,要避免使用InitializingBean接口(而且不鼓励使用该接口,因为这样会将代码和Spring耦合起来)可以在Bean定义中指定一个普通的初始化方法,即在XML配置文件中通过指定init-method属性来完成。如下面的定义所示:

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {
    
    public void init() {
        // do some initialization work
    }
}

(效果)与下面完全一样

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {
    
    public void afterPropertiesSet() {
        // do some initialization work
    }
}

但是没有将代码与Spring耦合在一起。

3.5.1.2. 析构回调

实现org.springframework.beans.factory.DisposableBean接口的bean允许在容器销毁该bean的时候获得一次回调。DisposableBean接口也只规定了一个方法:

void destroy() throws Exception;

通常,要避免使用DisposableBean标志接口(而且不鼓励使用该接口,因为这样会将代码与Spring耦合在一起)可以在bean定义中指定一个普通的析构方法,即在XML配置文件中通过指定destroy-method属性来完成。如下面的定义所示:

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class ExampleBean {

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}

(效果)与下面完全一样

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {

    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}

但是没有将代码与Spring耦合在一起。

3.5.1.2.1. 缺省的初始化和析构方法

如果有人没有采用Spring所指定的InitializingBeanDisposableBean回调接口来编写初始化和析构方法回调,他(以作者的经验)会发现自己正在编写的方法,其名称莫过于init()initialize()dispose()等等。这种生命周期回调方法的名称最好在一个项目范围内标准化,这样团队中的开发人员就可以使用同样的方法名称,并且确保了某种程度的一致性。

有了前面的约定,就可以将Spring容器配置成在每个bean上查找事先指定好的初始化和析构回调方法名称。这样就可以简化bean定义,比如根据约定将初始化回调命名为init(),然后在基于XML的配置中,就可以省略'init-method="init"'配置,而Spring IoC容器将会在bean被创建的时候调用该方法(并且遵循前述的标准生命周期回调规则)。

为了完全弄清如何使用该特性,让我们看一个例子。出于示范的目的,假设一个项目的编码规范中约定所有的初始化回调方法都被命名为init()而析构回调方法被命名为destroy()。遵循此规则写成的类如下所示:

public class DefaultBlogService implements BlogService {

    private BlogDao blogDao;

    public void setBlogDao(BlogDao blogDao) {
        this.blogDao = blogDao;
    }

    // this is (unsurprisingly) the initialization callback method
    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>

注意顶层<beans/>元素'default-init-method'属性的使用。该属性的出现意味着Spring IoC容器会把bean上名为'init'的方法识别为初始化方法回调,并且当bean被创建和装配的时候,如果bean类具有这样的方法,它将会在适当的时候被调用。

类似的,配置析构方法回调是在顶层<beans/>元素上使用'default-destroy-method'属性。

使用该特性可以使你免于在每个bean上指定初始化和析构方法回调的琐碎工作,同时它很好的强化了针对初始化和析构方法回调的命名约定的一致性(一致性是一种应该时常追求的东西)。

最后补充一点,如果实际的回调方法与默认的命名约定不同,那么可以通过在<bean/>元素上使用'init-method''destroy-method'属性指定方法名来覆盖缺省设置。

3.5.1.2.2. 在非web应用中优雅地关闭Spring IoC容器
[Note]Note

在基于web的ApplicationContext实现中已有相应的代码来处理关闭web应用时如何恰当地关闭Spring IoC容器。

如果你正在一个非web应用的环境下使用Spring的IoC容器,例如在桌面富客户端环境下,你想让容器优雅的关闭,并调用singleton bean上的相应析构回调方法,你需要在JVM里注册一个“关闭钩子”(shutdown hook)。这一点非常容易做到,并且将会确保你的Spring IoC容器被恰当关闭,以及所有由单例持有的资源都会被释放(当然,为你的单例配置销毁回调,并正确实现销毁回调方法,依然是你的工作)。

为了注册“关闭钩子”,你只需要简单地调用在AbstractApplicationContext实现中的registerShutdownHook()方法即可。也就是:

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        AbstractApplicationContext ctx
            = new ClassPathXmlApplicationContext(new String []{"beans.xml"});

        // add a shutdown hook for the above context... 
        ctx.registerShutdownHook();

        // app runs here...

        // main method exits, hook is called prior to the app shutting down...
    }
}

3.5.2. 了解自己

3.5.2.1.  BeanFactoryAware

对于实现了org.springframework.beans.factory.BeanFactoryAware接口的类,当它被BeanFactory创建后,它会拥有一个指向创建它的BeanFactory的引用。

public interface BeanFactoryAware {

    void setBeanFactory(BeanFactory beanFactory) throws BeansException;
}

这样bean可以以编程的方式操控创建它们的BeanFactory,当然我们可以将引用的BeanFactory造型(cast)为已知的子类型来获得更多的功能。它主要用于通过编程来取得BeanFactory所管理的其他bean。虽然在有些场景下这个功能很有用,但是一般来说应该尽量避免使用,因为这样将使代码与Spring耦合在一起,而且也有违反转控制的原则(协作者应该作为属性提供给bean)。

BeanFactoryAware等效的另一种选择是使用org.springframework.beans.factory.config.ObjectFactoryCreatingFactoryBean。不过该方法依然没有降低与Spring的耦合,但是它并没有像BeanFactoryAware那样,违反IoC原则。)

ObjectFactoryCreatingFactoryBeanFactoryBean的一个实现,它返回一个指向工厂对象的引用,该对象将执行bean的查找。ObjectFactoryCreatingFactoryBean类实现了BeanFactoryAware接口;被实际注入到客户端bean的是ObjectFactory接口的一个实例。这是Spring提供的一个接口(因而依旧没有完全与Spring解耦),客户端可以使用ObjectFactorygetObject()方法来查找bean(在其背后,ObjectFactory实例只是简单的将调用委派给BeanFactory,让其根据bean的名称执行实际的查找)。你要做的全部事情就是给ObjectFactoryCreatingFactoryBean提供待查找bean的名字。让我们看一个例子:

package x.y;

public class NewsFeed {
    
    private String news;

    public void setNews(String news) {
        this.news = news;
    }

    public String getNews() {
        return this.toString() + ": '" + news + "'";
    }
}
package x.y;

import org.springframework.beans.factory.ObjectFactory;

public class NewsFeedManager {

    private ObjectFactory factory;

    public void setFactory(ObjectFactory factory) {
        this.factory = factory;
    }

    public void printNews() {
        // here is where the lookup is performed; note that there is no
        // need to hardcode the name of the bean that is being looked up...
        NewsFeed news = (NewsFeed) factory.getObject();
        System.out.println(news.getNews());
    }
}

下述是XML配置:

<beans>
    <bean id="newsFeedManager" class="x.y.NewsFeedManager">
        <property name="factory">
            <bean
class="org.springframework.beans.factory.config.ObjectFactoryCreatingFactoryBean">
                <property name="targetBeanName">
                    <idref local="newsFeed" />
                </property>
            </bean>
        </property>
    </bean>
    <bean id="newsFeed" class="x.y.NewsFeed" scope="prototype">
        <property name="news" value="... that's fit to print!" />
    </bean>
</beans>

这里有一个测试用的小程序:在NewsFeedManagerprintNews()方法里,每次针对被注入的ObjectFactory的调用,实际上返回的是一个新的(prototype)newsFeed bean实例。

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.NewsFeedManager;

public class Main {

    public static void main(String[] args) throws Exception {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        NewsFeedManager manager = (NewsFeedManager) ctx.getBean("newsFeedManager");
        manager.printNews();
        manager.printNews();
    }
}

上述程序的执行输出如下所示(当然,返回结果会根据你机器的不同而不同)

x.y.NewsFeed@1292d26: '... that's fit to print!'
x.y.NewsFeed@5329c5: '... that's fit to print!'

3.5.2.2. BeanNameAware

假如bean实现了org.springframework.beans.factory.BeanNameAware接口并被部署在一个BeanFactory中,BeanFactory会通过该接口的setBeanName()方法以告知其被部署时的bean id。在bean属性被设置完成之后,在像InitializingBeanafterPropertiesSet或是自定义init-method这样的初始化回调执行之前,该接口的回调方法会被调用。

3.6. bean定义的继承

在bean定义中包含了大量的配置信息,其中包括容器相关的信息(比如初始化方法、静态工厂方法名等等)以及构造器参数和属性值。子bean定义就是从父bean定义继承配置数据的bean定义。它可以覆盖父bean的一些值,或者添加一些它需要的值。使用父/子bean定义的形式可以节省很多的输入工作。实际上,这就是一种模板形式。

当以编程的方式使用BeanFactory时,子bean定义用ChildBeanDefinition类表示。大多数用户从来不需要以这个方式使用它们,而是以类似XmlBeanFactory中的声明方式去配置bean定义。当使用基于XML的配置元数据时,给'parent'属性指定值,意味着子bean定义的声明。

<bean id="inheritedTestBean" abstract="true"
    class="org.springframework.beans.TestBean">
  <property name="name" value="parent"/>
  <property name="age" value="1"/>
</bean>

<bean id="inheritsWithDifferentClass"
      class="org.springframework.beans.DerivedTestBean"
      parent="inheritedTestBean" init-method="initialize">
    
  <property name="name" value="override"/>
  <!-- the age property value of 1 will be inherited from  parent -->

</bean>

如果子bean定义没有指定class属性,它将使用父bean定义的class属性,当然也可以覆盖它。在后面一种情况中,子bean的class属性值必须同父bean兼容,也就是说它必须接受父bean的属性值。

一个子bean定义可以从父bean继承构造器参数值、属性值以及覆盖父bean的方法,并且可以有选择地增加新的值。如果指定了init-method,destroy-method和/或静态factory-method,它们就会覆盖父bean相应的设置。

剩余的设置将总是从子bean定义处得到:依赖自动装配模式依赖检查singleton作用域延迟初始化

注意在上面的例子中,我们使用abstract属性显式地将父bean定义标记为抽象的。 下面是个父bean定义并没有指定class属性的例子,其中父bean必须显式地标上abstract

<bean id="inheritedTestBeanWithoutClass" abstract="true">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
    parent="inheritedTestBeanWithoutClass" init-method="initialize">
  <property name="name" value="override"/>
  <!-- age will inherit the value of 1 from the parent bean definition-->
</bean>

由于这样的父bean是不完整的,而且还被显式标记为抽象的,因而它无法得到自己的实例。抽象bean定义可作为子bean定义的模板。若要尝试单独使用这样的父bean(比如将它作为其他bean的ref属性而引用,或者直接使用这个父bean的id作为参数调用getBean()方法),将会导致错误。同样地,容器内部的preInstantiateSingletons()方法会完全忽略abstract的bean定义。

[Note]Note

默认情况下,ApplicationContext不是BeanFactory)会预实例化所有singleton的bean。因此很重要的一点是:如果你只想把一个(父)bean定义当作模板使用,而它又指定了class属性,那么你就得将'abstract'属性设置为'true',否则应用上下文将会(试着)预实例化抽象bean。

3.7. 容器扩展点

Spring框架的IoC容器被设计为可扩展的。通常我们并不需要子类化各个BeanFactoryApplicationContext实现类。而通过plugin各种集成接口实现来进行扩展。下面几节专门描述这些不同的集成接口。

3.7.1. 用BeanPostProcessor定制bean

我们关注的第一个扩展点是BeanPostProcessor接口。它定义了几个回调方法,实现该接口可提供自定义(或默认地来覆盖容器)的实例化逻辑、依赖解析逻辑等。如果你想在Spring容器完成bean的实例化、配置和其它的初始化后执行一些自定义逻辑,你可以插入一个或多个的BeanPostProcessor实现。

如果配置了多个BeanPostProcessor,那么可以通过设置'order'属性来控制BeanPostProcessor的执行次序(仅当BeanPostProcessor实现了Ordered接口时,你才可以设置此属性,因此在编写自己的BeanPostProcessor实现时,就得考虑是否需要实现Ordered接口);请参考BeanPostProcessorOrdered接口的JavaDoc以获取更详细的信息。

[Note]Note

BeanPostProcessor可以对bean(或对象)的多个实例进行操作;也就是说,Spring IoC容器会为你实例化bean,然后BeanPostProcessor去处理它。

如果你想修改实际的bean定义,则会用到BeanFactoryPostProcessor(详情见Section 3.7.2, “用BeanFactoryPostProcessor定制配置元数据”)。

BeanPostProcessor的作用域是容器级的,它只和所在容器有关。如果你在容器中定义了BeanPostProcessor,它仅仅对此容器中的bean进行后置处理。BeanPostProcessor将不会对定义在另一个容器中的bean进行后置处理,即使这两个容器都处在同一层次上。

org.springframework.beans.factory.config.BeanPostProcessor接口有两个回调方法可供使用。当一个该接口的实现类被注册(如何使这个注册生效请见下文)为容器的后置处理器(post-processor)后,对于由此容器所创建的每个bean实例在初始化方法(如afterPropertiesSet和任意已声明的init方法)调用前,后置处理器都会从容器中分别获取一个回调。后置处理器可以随意对这个bean实例执行它所期望的动作,包括完全忽略此回调。一个bean后置处理器通常用来检查标志接口,或者做一些诸如将一个bean包装成一个proxy的事情;一些Spring AOP的底层处理也是通过实现bean后置处理器来执行代理包装逻辑。

重要的一点是,BeanFactoryApplicationContext对待bean后置处理器稍有不同。ApplicationContext会自动检测在配置文件中实现了BeanPostProcessor接口的所有bean,并把它们注册为后置处理器,然后在容器创建bean的适当时候调用它。部署一个后置处理器同部署其他的bean并没有什么区别。而使用BeanFactory实现的时候,bean 后置处理器必须通过下面类似的代码显式地去注册:

ConfigurableBeanFactory factory = new XmlBeanFactory(...);
    // now register any needed BeanPostProcessor instances
    MyBeanPostProcessor postProcessor = new MyBeanPostProcessor();
    factory.addBeanPostProcessor(postProcessor);

    // now start using the factory

因为显式注册的步骤不是很方便,这也是为什么在各种Spring应用中首选ApplicationContext的一个原因,特别是在使用BeanPostProcessor时。

[Note]Note

请不要将BeanPostProcessor标记为延迟初始化。如果你这样做,Spring容器将不会注册它们,自定义逻辑无法得到应用。假如你在<beans/>元素的定义中使用了'default-lazy-init'属性,请确信你的各个BeanPostProcessor标记为'lazy-init="false"'

关于如何在ApplicationContext中编写、注册并使用BeanPostProcessor,会在接下的例子中演示。

3.7.1.1. 使用BeanPostProcessor的Hello World示例

第一个实例似乎不太吸引人,但是它适合用来阐述BeanPostProcessor的基本用法。我们所有的工作是编写一个BeanPostProcessor的实现,它仅仅在容器创建每个bean时调用bean的toString()方法并且将结果打印到系统控制台。它是没有很大的用处,但是可以让我们对BeanPostProcessor有一个基本概念。

下面是BeanPostProcessor具体实现类的定义:

package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

    // simply return the instantiated bean as-is
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean; // we could potentially return any object reference here...
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean '" + beanName + "' created : " + bean.toString());
        return bean;
    }
}

这里是相应的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"
       xmlns:lang="http://www.springframework.org/schema/lang"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.0.xsd">

    <lang:groovy id="messenger"
          script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
        <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/> 
    </lang:groovy>
    
    <!-- 
        when the above bean ('messenger') is instantiated, this custom
        BeanPostProcessor implementation will output the fact to the system console
     -->
    <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

注意InstantiationTracingBeanPostProcessor是如此简单,甚至没有名字,由于被定义成一个bean,因而它跟其它的bean没什么两样(上面的配置中也定义了由Groovy脚本支持的bean,Spring2.0动态语言支持的细节请见Chapter 24, 动态语言支持)。

下面是测试代码:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
        Messenger messenger = (Messenger) ctx.getBean("messenger");
        System.out.println(messenger);
    }
}

上面程序执行时的输出将是(或象)下面这样:

Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961

3.7.1.2. RequiredAnnotationBeanPostProcessor示例

在Spring的BeanPostProcessor实现中调用标志接口或使用注解是扩展Spring IoC容器的常用方法。对于注解的用法详见Section 25.3.1, “@Required”,这里没有做深入的说明。通过定制BeanPostProcessor实现,可以使用注解来指定各种JavaBean属性值并在发布的时候被注入相应的bean中。

3.7.2. 用BeanFactoryPostProcessor定制配置元数据

我们将看到的下一个扩展点是org.springframework.beans.factory.config.BeanFactoryPostProcessor。这个接口跟BeanPostProcessor类似,BeanFactoryPostProcessor可以对bean的定义(配置元数据)进行处理。也就是说,Spring IoC容器允许BeanFactoryPostProcessor在容器实际实例化任何其它的bean之前读取配置元数据,并有可能修改它。

如果你愿意,你可以配置多个BeanFactoryPostProcessor。你还能通过设置'order'属性来控制BeanFactoryPostProcessor的执行次序(仅当BeanFactoryPostProcessor实现了Ordered接口时你才可以设置此属性,因此在实现BeanFactoryPostProcessor时,就应当考虑实现Ordered接口);请参考BeanFactoryPostProcessorOrdered接口的JavaDoc以获取更详细的信息。

[Note]Note

如果你想改变实际的bean实例(例如从配置元数据创建的对象),那么你最好使用BeanPostProcessor(见上面Section 3.7.1, “用BeanPostProcessor定制bean”中的描述)

同样地,BeanFactoryPostProcessor的作用域范围是容器级的。它只和你所使用的容器有关。如果你在容器中定义一个BeanFactoryPostProcessor,它仅仅对此容器中的bean进行后置处理。BeanFactoryPostProcessor不会对定义在另一个容器中的bean进行后置处理,即使这两个容器都是在同一层次上。

bean工厂后置处理器可以手工(如果是BeanFactory)或自动(如果是ApplicationContext)地施加某些变化给定义在容器中的配置元数据。Spring自带了许多bean工厂后置处理器,比如下面将提到的PropertyResourceConfigurerPropertyPlaceholderConfigurer以及BeanNameAutoProxyCreator,它们用于对bean进行事务性包装或者使用其他的proxy进行包装。BeanFactoryPostProcessor也能被用来添加自定义属性编辑器。

在一个BeanFactory中,应用BeanFactoryPostProcessor的过程是手工的,如下所示:

XmlBeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
// bring in some property values from a Properties file
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

因为显式注册的步骤不是很方便,这也是为什么在不同的Spring应用中首选ApplicationContext的原因,特别是在使用BeanFactoryPostProcessor时。

ApplicationContext会检测部署在它之上实现了BeanFactoryPostProcessor接口的bean,并在适当的时候会自动调用bean工厂后置处理器。部署一个后置处理器同部属其他的bean并没有什么区别。

[Note]Note

正如BeanPostProcessor的情况一样,请不要将BeanFactoryPostProcessors标记为延迟加载。如果你这样做,Spring容器将不会注册它们,自定义逻辑就无法实现。如果你在<beans/>元素的定义中使用了'default-lazy-init'属性,请确信你的各个BeanFactoryPostProcessor标记为'lazy-init="false"'

3.7.2.1. PropertyPlaceholderConfigurer示例

PropertyPlaceholderConfigurer是个bean工厂后置处理器的实现,可以将BeanFactory定义中的一些属性值放到另一个单独的标准Java Properties文件中。这就允许用户在部署应用时只需要在属性文件中对一些关键属性(例如数据库URL,用户名和密码)进行修改,而不用对主XML定义文件或容器所用文件进行复杂和危险的修改。

考虑下面的XML配置元数据定义,它用占位符定义了DataSource。我们在外部的Properties文件中配置一些相关的属性。在运行时,我们为元数据提供一个PropertyPlaceholderConfigurer,它将会替换dataSource的属性值。

<bean id="dataSource" destroy-method="close"
      class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="jdbc.username"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

实际的值来自于另一个标准Java Properties格式的文件:

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

PropertyPlaceholderConfigurer如果在指定的Properties文件中找不到你想使用的属性,它还会在Java的System类属性中查找。这个行为可以通过设置systemPropertiesMode属性来定制,它有三个值:让配置一直覆盖、让它永不覆盖及让它仅仅在属性文件中找不到该属性时才覆盖。请参考PropertiesPlaceholderConfigurer的JavaDoc以获得更多的信息。

3.7.2.2. PropertyOverrideConfigurer示例

另一个bean工厂后置处理器PropertyOverrideConfigurer类似于PropertyPlaceholderConfigurer。但是与后者相比,前者对于bean属性可以有缺省值或者根本没有值。如果起覆盖作用的Properties文件没有某个bean属性的内容,那么将使用缺省的上下文定义。

bean工厂并不会意识到被覆盖,所以仅仅察看XML定义文件并不能立刻知道覆盖配置是否被使用了。在多个PropertyOverrideConfigurer实例中对一个bean属性定义了不同的值时,最后定义的值将被使用(由于覆盖机制)。

Properties文件的配置应该是如下的格式:

beanName.property=value

一个properties文件可能是下面这样的:

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

这个示例文件可用在这样一个bean容器:包含一个名为dataSource的bean,并且这个bean有driverurl属性。

注意它也支持组合的属性名称,只要路径中每个组件除了最后要被覆盖的属性外全都是非空的(比如通过构造器来初始化),在下例中:

foo.fred.bob.sammy=123

foo bean的fred属性的bob属性的sammy属性被设置为数值123。

3.7.3. 使用FactoryBean定制实例化逻辑

工厂bean需要实现org.springframework.beans.factory.FactoryBean接口。

FactoryBean接口是插入到Spring IoC容器用来定制实例化逻辑的一个接口点。如果你有一些复杂的初始化代码用Java可以更好来表示,而不是用(可能)冗长的XML,那么你就可以创建你自己的FactoryBean,并在那个类中写入复杂的初始化动作,然后把你定制的FactoryBean插入容器中。

FactoryBean接口提供三个方法:

  • Object getObject():返回一个由这个工厂创建的对象实例。这个实例可能被共享(取决于isSingleton()的返回值是singleton或prototype)。

  • boolean isSingleton():如果要让这个FactoryBean创建的对象实例为singleton则返回true,否则返回false。

  • Class getObjectType():返回通过getObject()方法返回的对象类型,如果该类型无法预料则返回null。

在Spring框架中FactoryBean的概念和接口被用于多个地方;在本文写作时,Spring本身提供的FactoryBean接口实现超过了50个。

最后,有时需要向容器请求一个真实的FactoryBean实例本身,而不是它创建的bean。这可以通过在FactoryBean(包括ApplicationContext)调用getBean方法时在bean id前加'&'(没有单引号)来完成。因此对于一个假定id为myBeanFactoryBean,在容器上调用getBean("myBean")将返回FactoryBean创建的bean实例,但是调用getBean("&myBean")将返回FactoryBean本身的实例。

3.8. ApplicationContext

beans包提供了以编程的方式管理和操控bean的基本功能,而context包下的ApplicationContext以一种更加面向框架的方式增强了BeanFactory的功能。多数用户可以采用声明的方式来使用ApplicationContext,甚至不用手动创建它,而通过ContextLoader这样的支持类,把它作为J2EE web应用的一部分自动启动。当然,我们仍然可以采用编程的方式创建一个ApplicationContext。

context包的核心是ApplicationContext接口。它由BeanFactory接口派生而来,因而提供了BeanFactory所有的功能。为了以一种更向面向框架的方式工作以及对上下文进行分层和实现继承,context包还提供了以下的功能:

  • MessageSource, 提供国际化的消息访问

  • 资源访问,如URL和文件

  • 事件传播,实现了ApplicationListener接口的bean

  • 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层

由于ApplicationContext包括了BeanFactory所有的功能,所以通常建议优先采用ApplicationContext。除了一些受限的场合比如在一个Applet中,这时内存的消耗可能很关键,过多的内存占用可能导致反应速度下降。接下来的章节将叙述由ApplicationContextBeanFactory的基础上所添加的那些功能。

3.8.1. 利用MessageSource实现国际化

ApplicationContext接口扩展了MessageSource接口,因而提供了消息处理的功能(i18n或者国际化)。与HierarchicalMessageSource一起使用,它还能够处理嵌套的消息,这些是Spring提供的处理消息的基本接口。让我们快速浏览一下它所定义的方法:

  • String getMessage(String code, Object[] args, String default, Locale loc):用来从MessageSource获取消息的基本方法。如果在指定的locale中没有找到消息,则使用默认的消息。args中的参数将使用标准类库中的MessageFormat来作消息中替换值。

  • String getMessage(String code, Object[] args, Locale loc):本质上和上一个方法相同,其区别在:没有指定默认值,如果没找到消息,会抛出一个NoSuchMessageException异常。

  • String getMessage(MessageSourceResolvable resolvable, Locale locale):上面方法中所使用的属性都封装到一个MessageSourceResolvable实现中,而本方法可以指定MessageSourceResolvable实现。

当一个ApplicationContext被加载时,它会自动在context中查找已定义为MessageSource类型的bean。此bean的名称须为messageSource。如果找到,那么所有对上述方法的调用将被委托给该bean。否则ApplicationContext会在其父类中查找是否含有同名的bean。如果有,就把它作为MessageSource。如果它最终没有找到任何的消息源,一个空的StaticMessageSource将会被实例化,使它能够接受上述方法的调用。

Spring目前提供了两个MessageSource的实现:ResourceBundleMessageSourceStaticMessageSource。它们都继承NestingMessageSource以便能够处理嵌套的消息。StaticMessageSource很少被使用,但能以编程的方式向消息源添加消息。ResourceBundleMessageSource会用得更多一些,为此提供了一下示例:

<beans>
  <bean id="messageSource"
        class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
      <list>
        <value>format</value>
        <value>exceptions</value>
        <value>windows</value>
      </list>
    </property>
  </bean>
</beans>

这段配置假定在你的classpath中有三个资源文件(resource bundle),它们是formatexceptionswindows。通过ResourceBundle,使用JDK中解析消息的标准方式,来处理任何解析消息的请求。出于示例的目的,假定上面的两个资源文件的内容为…

# in 'format.properties'
message=Alligators rock!
# in 'exceptions.properties'
argument.required=The '{0}' argument is required.

下面是测试代码。因为ApplicationContext实现也都实现了MessageSource接口,所以能被转型为MessageSource接口

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", null);
    System.out.println(message);
}

上述程序的输出结果将会是...

Alligators rock!

总而言之,我们在'beans.xml'的文件中(在classpath根目录下)定义了一个messageSource bean,通过它的basenames属性引用多个资源文件;而basenames属性值由list元素所指定的三个值传入,它们以文件的形式存在并被放置在classpath的根目录下(分别为format.propertiesexceptions.propertieswindows.properties)。

再分析个例子,这次我们将着眼于传递参数给查找的消息,这些参数将被转换为字符串并插入到已查找到的消息中的占位符(译注:资源文件中花括号里的数字即为占位符)。

<beans>

    <!-- this MessageSource is being used in a web application -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="baseName" value="WEB-INF/test-messages"/>
    </bean>
    
    <!-- let's inject the above MessageSource into this POJO -->
    <bean id="example" class="com.foo.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", null);
        System.out.println(message);
    }

}

调用execute()方法的输出结果是...

The 'userDao' argument is required.

对于国际化(i18n),Spring中不同的MessageResource实现与JDK标准ResourceBundle中的locale解析规则一样。比如在上面例子中定义的messageSource bean,如果你想解析British (en-GB) locale的消息,那么需要创建format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties三个资源文件。

Locale解析通常由应用程序根据运行环境来指定。出于示例的目的,我们对将要处理的(British)消息手工指定locale参数值。

# in 'exceptions_en_GB.properties'
argument.required=Ebagum lad, the '{0}' argument is required, I say, required.
public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

上述程序运行时的输出结果是...

Ebagum lad, the 'userDao' argument is required, I say, required.

MessageSourceAware接口还能用于获取任何已定义的MessageSource引用。任何实现了MessageSourceAware接口的bean将在创建和配置的时候与MessageSource一同被注入。

3.8.2. 事件

ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口来提供的。如果在上下文中部署一个实现了ApplicationListener接口的bean,那么每当一个ApplicationEvent发布到ApplicationContext时,这个bean就得到通知。实质上,这是标准的Observer设计模式。Spring提供了三个标准事件:

Table 3.5. 内置事件

事件解释
ContextRefreshedEvent

ApplicationContext初始化或刷新时发送的事件。这里的初始化意味着:所有的bean被装载,singleton被预实例化,以及ApplicationContext已就绪可用

ContextClosedEvent

当使用ApplicationContextclose()方法结束上下文时发送的事件。这里的结束意味着:singleton bean 被销毁

RequestHandledEvent

一个与web相关的事件,告诉所有的bean一个HTTP请求已经被响应了(也就是在一个请求结束后会发送该事件)。注意,只有在Spring中使用了DispatcherServlet的web应用才能使用

同样也可以实现自定义的事件。仅仅是简单地调用ApplicationContextpublishEvent()方法,且指定一个实现了ApplicationEvent的自定义事件类实例做参数。事件监听器同步地接收消息,这意味着publishEvent()会被加锁直到所有的监听者都处理完事件(也可以通过ApplicationEventMulticaster实现来使用其它的事件发送策略)。此外,如果使用一个事务上下文,一个监听者接收事件时会在发送者的事务上下文中操作事件。

让我们来看一个例子,首先是XML配置:

<bean id="emailer" class="example.EmailBean">
  <property name="blackList">
    <list>
      <value>black@list.org</value>
      <value>white@list.org</value>
      <value>john@doe.org</value>
    </list>
  </property>
</bean>

<bean id="blackListListener" class="example.BlackListNotifier">
  <property name="notificationAddress" value="spam@list.org"/>
</bean>

下面是实际的类:

public class EmailBean implements ApplicationContextAware {

    private List blackList;
    private ApplicationContext ctx;
    
    public void setBlackList(List blackList) {
        this.blackList = blackList;
    }
    
    public void setApplicationContext(ApplicationContext ctx) {
        this.ctx = ctx;
    }
    
    public void sendEmail(String address, String text) {
        if (blackList.contains(address)) {
            BlackListEvent evt = new BlackListEvent(address, text);
            ctx.publishEvent(evt);
            return;
        }
        // send email...
    }
}
public class BlackListNotifier implement ApplicationListener {

    private String notificationAddress;
    
    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(ApplicationEvent evt) {
        if (evt instanceof BlackListEvent) {
            // notify appropriate person...
        }
    }
}

当然,此例子或许可以用更好的方式来实现(如使用AOP),但它已足以说明基本的事件机制。

3.8.3. 底层资源的访问

为了更好的使用和理解应用上下文,通常用户应当对Spring的Resource有所了解,详见Chapter 4, 资源

应用上下文同时也是个资源加载器(ResourceLoader),能被用来加载多个Resource。一个Resource实质上可以当成一个java.net.URL,可被用来从大多数位置以透明的方式获取底层的资源,包括从classpath、文件系统位置、任何以标准URL描述的位置以及其它一些变种。如果资源位置串是一个没有任何前缀的简单路径,这些资源来自何处取决于实际应用上下文的类型。

部署在应用上下文的bean可能会实现一个特殊的标志接口ResourceLoaderAware,它会在初始化时自动回调将应用上下文本身作为资源加载器传入。

为了让bean能访问静态资源,可以象其它属性一样注入Resource。被注入的Resource属性值可以是简单的路径字符串,ApplicationContext会使用已注册的PropertyEditor,来将字符串转换为实际的Resource对象。

ApplicationContext构造器的路径就是实际的资源串,根据不同的上下文实现,字符串可视为不同的形式(例如:ClassPathXmlApplicationContext会把路径字符串看作一个classpath路径)。然而,它也可以使用特定的前缀来强制地从classpath或URL加载bean定义文件,而不管实际的上下文类型。

3.8.4. ApplicationContext在WEB应用中的实例化

BeanFactory通常以编程的方式被创建不同的是,ApplicationContext能以声明的方式创建,如使用ContextLoader。当然你也可以使用ApplicationContext的实现之一来以编程的方式创建ApplicationContext实例。首先,让我们先分析ContextLoader接口及其实现。

ContextLoader接口有两个实现:ContextLoaderListenerContextLoaderServlet。两者都实现同样的功能,但不同的是,ContextLoaderListener不能在与Servlet 2.2兼容的web容器中使用。根据Servlet 2.4规范, servlet context listener要在web应用程序的servlet context建立后立即执行,并要能够响应第一个请求(在servlet context要关闭时也一样):这样一个servlet context listener是初始化Spring ApplicationContext的理想场所。虽然使用哪个完全取决于你,但是在同等条件下应该首选ContextLoaderListener;对于更多兼容性的信息,请查看ContextLoaderServlet的JavaDoc。

你可以象下面那样使用ContextLoaderListener来注册一个ApplicationContext

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- or use the ContextLoaderServlet instead of the above listener
<servlet>
  <servlet-name>context</servlet-name>
  <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
</servlet>
-->

监听器首先检查contextConfigLocation参数,如果它不存在,它将使用/WEB-INF/applicationContext.xml作为默认值。如果已存在,它将使用分隔符(逗号、冒号或空格)将字符串分解成应用上下文将位置路径。ContextLoaderServletContextLoaderListener一样使用'contextConfigLocation'参数。

3.9. 粘合代码和可怕的singleton

一个应用中的大多数代码最好写成依赖注入(控制反转)的风格,这样代码就和Spring IoC容器无关,它们在被创建时从容器得到自己的依赖,并且完全不知道容器的存在。然而,对于少量需要与其它代码粘合的粘合层代码来说,有时候就需要以一种singleton(或者类似singleton)的方式来访问Spring IoC容器。例如,第三方的代码可能试图(以Class.forName()的方式)直接构造一个新的对象,但无法强制它们从Spring IoC容器中得到这些对象。如果第三方代码构造的对象只是一个小stub或proxy,并且使用singleton方式访问Spring IoC容器来获得真正的对象,那么大多数的代码(由容器产生的对象)仍然可以使用控制反转。因此大多数的代码依然不需要知道容器的存在,或者它如何被访问,并保持与其它代码的解耦,这样所带来的益处是很显然的。EJB也可以使用这种stub/proxy方案代理到由Spring IoC容器产生的普通的Java实现对象。虽然理想情况下Spring IoC容器不需要是singleton,但是如果每个bean使用它自己的non-singleton的Spring IoC容器(当在Spring IoC容器中使用bean时,如Hibernate SessionFactory),对于内存使用或初始化次数都是不切实际。

另一个例子,在一个多层的复杂的J2EE应用中(比如不同的JAR,EJB,以及WAR打包成一个EAR),每一层都有自己的Spring IoC容器定义(有效地组成一个层次结构),如果顶层只有一个web-app(WAR)的话,比较好的做法是简单地创建一个由不同层的XML定义文件组成的组合Spring IoC容器。所有不同的Spring IoC容器实现都可以以这种方式从多个定义文件构造出来。但是,如果在顶层有多个兄弟web-apps,为每一个web-app创建一个Spring IoC容器,而每个ApplicationContext都包含大部分相同的底层的bean定义。因而就会因内存使用,建bean的多个复本会花很长时间初始化(比如Hibernate SessionFactory),以及其它可能产生的副作用而产生问题。作为另一可选的方案,象ContextSingletonBeanFactoryLocatorSingletonBeanFactoryLocator的类可以在需要的时候以有效的singleton方式,加载多层次的(比如一个是另一个的父亲)Spring IoC容器,这些将会作为web应用的Spring IoC容器的父容器。由此底层的bean定义只在需要的时候加载(并且只被加载一次)。

3.9.1. 使用Singleton-helper类

你可以查看SingletonBeanFactoryLocatorContextSingletonBeanFactoryLocator的JavaDoc来获得详细的例子。

正如在EJB那章所提到的,Spring为EJB提供方便使用的基类,通常使用一个non-singleton的BeanFactoryLocator实现,这样在需要时就可以很容易地被SingletonBeanFactoryLocatorContextSingletonBeanFactoryLocator替换。



[1] 参见背景的相关内容