Spring官方文档翻译(7章)

三、核心技术

这部分的文档覆盖了spring完整的技术。

在这些技术中最重要的要属Spring的控制反转(IoC)容器了,紧随其后的是全面覆盖的面向切面编程(AOP)技术。Spring有它自己的AOP框架,它很容易理解,而且成功解决了Java企业编程中80%的AOP需求。

Spring也集成了AspectsJ(目前在Java领域使用最丰富最成熟的AOP实现 )。

7. IoC容器

7.1 Spring的IoC容器和bean的简介

本章主要介绍Spring关于控制反转的实现。控制反转(IoC)又被称作依赖注入(DI)。它是一个对象定义其依赖的过程,它的依赖也就是与它一起合作的其它对象,这个过程只能通过构造方法参数、工厂方法参数、或者被构造或从工厂方法返回后通过settter方法设置其属性来实现。然后容器在创建bean时注入这些依赖关系。这个过程本质上是反过来的,由bean本身控制实例化,或者直接通过类的结构或Service定位器模式定位它自己的依赖,因此得其名曰控制反转。

org.springframework.beansorg.springframework.context两个包是Spring IoC容器的基础。BeanFactory接口提供了一种先进的管理任何类型对象的配置机制。ApplicationContextBeanFactory的子接口,它使得与Spring AOP特性集成更简单,添加了消息资源处理(用于国际化)、事件发布,还添加了应用层特定的上下文,比如用于web应用的WebApplicationContext

简而言之,BeanFactory提供了配置框架和基础功能,ApplicationContext在此基础上添加了更多的企业级特定功能。ApplicationContextBeanFactory的完整超集,且仅用于本章描述Spring的IoC容器。更多使用BeanFactory代替ApplicationContext的信息请参考7.16 BeanFactory“

在Spring中,形成应用主干且被Spring IoC容器管理的对象称为beans。一个bean就是被Spring IoC容器实例化、装配和管理的对象。简单来说,一个bean就是你的应用中众多对象中一个。Beans和它们之间的依赖被容器中配置的元数据反射。

7.2 容器概述

org.springframework.context.ApplicationContext代表了Spring的IoC容器,并且负责实例化、配置和装配前面提及的bean。容器通过读取配置元数据获取指令来决定哪些对象将被实例化、配置和装配。配置元数据反映在XML、Java注解或者Java代码中。它允许你表达组合成应用的对象及它们之间丰富的依赖关系。

Spring提供了几个可以直接使用的ApplicationContext接口的实现。在独立的应用中通常创建ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext的实例。XML是定义配置元数据的传统形式,也可以通过添加一小段XML配置让容器声明式地支持Java注解或Java代码配置元数据的形式。

在大部分应用场景中,显式的代码不需要实例化一个或多个Spring IoC容器。例如,在web应用中,在web.xml中引用简单的八行(大约)样板XML描述符足够了(参考7.15.4 web应用中方便地实例化ApplicationContext](#context-create))。如果使用Spring工具套件Eclipse开发环境,样板配置只需要简单地点击几次鼠标或键盘就可以被创建。

下面的图表展示了Spring是怎么工作的。应用程序的类文件和配置元数据是绑定在一起的,所以在ApplicationContext被创建和初始化后,你将拥有完整的已配置好的和可执行的系统或应用。
图7.1 Spring的IoC容器
这里写图片描述

7.2.1 配置元数据

如前述图表所述,Spring IoC容器是消费配置元数据的一种形式,配置元数据代表开发者告诉Spring容器怎么去实例化、配置和装配应用中的对象。

配置元数据习惯上使用简单直观的XML形式,本章大部分地方也使用这种形式来传达Spring IoC容器的关键概念和特性。

  • 基于XML的元数据不是配置元数据的唯一方式,Spring IoC容器本身是与实际写配置元数据的形式完全解耦的,现在许多开发者选择基于Java的配置形式来配置Spring应用程序。

关于使用其它形式配置元数据,参考:

  • 基于注解的配置,Spring 2.5引入了基于注解配置元数据的形式。
  • 基于Java的配置,从Spring 3.0开始,Spring JavaConfig项目提供的许多特性成为了Spring框架核心的一部分。因引,可以在应用程序类的外部通过Java定义bean而不是在XML文件中。使用这些新特性,请参考@Configuration, @Bean, @Import@DepensOn注解。

Spring配置包含了至少一个必须容器管理的bean定义。基于XML的配置元数据显示为被顶级<beans/>元素包含的<bean/>元素。Java配置典型地使用@Bean注解@Configuration类中方法。

这些bean定义与组成应用的实际对象通信。典型地,可以定义service层对象、数据访问对象(DAO)、诸如Struts Action实例的视图层对象、基础对象如hibernate SessionFactories、JMS Queues,等等,也不需要在容器中定义细粒度的领域对象,因为通常由DAO层和业务逻辑来创建和加载领域对象。然而,可以使用与AspectJ的集成来配置在IoC容器控制之外创建的对象。参考使用AspectJ依赖注入领域对象

下面展示了XML配置的基础结构:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

id属性是一个字符串,它唯一标识一个bean的定义,class属性定义了bean的类型并且需要使用全路径。id属性的值指向了合作的对象。XML指定合作者XML并没有显示在上述例子中,具体可参考依赖

7.2.2 实例化容器

实例一个Spring IoC容器很简单,提供给ApplicationContext构造方法的一个或多个路径是实际的资源字符串,这使得容器可以从不同的外部文件加载配置元数据,比如本地的文件系统,Java的CLASSPATH,等等。

ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});
 
 
  • 1
  • 1
  • 在学习Spring IoC容器之后,你可能想更多地了解Spring的Resource概念,它提供了一种很便利的机制以URI语法的形式读取输入流,参考7 资源。特别地,Resource路径可被用于构造应用上下文,请参考7.7 应用上下文与资源路径

下面的例子展示了service层的对象配置文件(services.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- services -->

    <bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
        <property name="accountDao" ref="accountDao"/>
        <property name="itemDao" ref="itemDao"/>
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for services go here -->

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

下面的例子展示了数据访问对象daos.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="accountDao"
        class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for data access objects go here -->

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在前面的例子中,service层包含类PetStoreServiceImpl和两个数据访问对象JpaAccountDaoJpaItemDao(基于JPA 对象关系映射标准)。property元素的name属性指向了JavaBean属性的名字,ref属性指向另一个bean定义的名字。idref之间的链接表达了两个合作者间的依赖关系。详细的配置和对象依赖请参考依赖

组合基于XML的配置元数据

跨越多个XML文件配置bean定义是很有用的,通常一个独立的XML配置文件代表架构中的一个逻辑层或模块。

可以使用ApplicationContext的构造方法加载所有这些XML片段的bean定义。构造方法传入多个Resource路径即可,就像前面章节介绍的那样。替代方案,也可以使用一个或多个<import/>元素从其它文件加载bean定义。例如:

<beans>
    <import resource="services.xml"/>
    <import resource="resources/messageSource.xml"/>
    <import resource="/resources/themeSource.xml"/>

    <bean id="bean1" class="..."/>
    <bean id="bean2" class="..."/>
</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面的例子中,外部的bean定义从三个文件中加载进来:services.xml, messageSource.xmlthemeSource.xml。所有的路径对于当前文件都是相对路径,所以services.xml必须在同一个目录或当前路径所在的classpath下,而messageSource.xmlthemeSource.xml则必须在resources路径下,resources必须当前文件目录的下一级。如你所见,开头的斜杠被忽略了,即使给了路径也是相对的,不使用开头的斜杠格式还更好点。被导入文件的内容,包括顶级的<beans/>元素,必须是Spring Schema验证通过的XML bean定义。

  • 可以使用“../”路径的形式引用父目录的文件,但这是不推荐的。这样做会在当前应用之外的文件上创建一个依赖。尤其是这种引用不推荐放在“classpath:”URL中(例如,“classpath:../services.xml”),在运行时会选择最近的classpath的根目录,然后再在其父目录中寻找。classpath配置改变可能会导致选择了不同的错误的目录。
    可以总是使用全路径定位资源代替相对路径,例如“file:C:/config/services.xml”或“classpath:/config/services.xml”。然而,注意这样会把应用程序的配置与特定的绝对路径耦合了。一般地,可以间接配置那个绝对路径,例如,通过JVM系统属性“${…}”占位符在运行时加载。
7.2.3 使用容器

ApplicationContext接口是一个先进的工厂,用于维护不同的bean及其依赖关系的注册。使用T getBean(String name, Class requiredType)方法可以获取bean实例。

ApplicationContext允许你可以像下面这样读取bean定义并访问它们:

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});

// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// use configured instance
List<String> userList = service.getUsernameList();
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

使用getBean()获取bean实例。ApplicationContext接口还有几个其它方法用于获取bean,但是理想状态下应用程序代码应该从来都不会用到它们。应用程序代码的解应该不会调用getBean()方法,而且不应该依赖于Spring的API。例如,Spring的web框架集成为不同的web框架类提供了依赖注入,比如controller和JSF管理的bean。

7.3 Bean概述

Spring IoC容器管理了至少一个bean,这些bean通过提供给容器的配置元数据来创建,例如,XML形式的<bean/>定义。

在容器本身内部,这些bean定义表示为BeanDefinition对象,它包含以下元数据:

  • 包限定的类名:被定义bean的实际实现类。
  • bean行为的配置元素,在容器中bean以哪种状态表现(scope,lifecycle,callback,等等)。
  • 需要一起工作的其它对象的引用,这些引用又被称作合作者(collaborator)或依赖。
  • 设置到新创建对象中的其它配置,例如,在bean中使用的连接数,用于管理连接池或池的大小。

元数据转化成了一系列的属性,组成了每个bean的定义。
表 7.1. bean定义

属性参考
class7.3.2 实例化bean
name7.3.1 命名bean
scope7.5 bean的作用域
构造方法的参数7.4.1 依赖注入
属性7.4.1 依赖注入
自动装配的模式7.4.5 自动装配合作者
延迟初始化的模式7.4.4 延迟初始化bean
初始化的模式初始化回调
销毁方法销毁回调

除了包含创建指定bean的信息的bean定义外,ApplicationContext实现也允许注册用户在容器外创建的已存在的对象。可以通过ApplicationContextgetBeanFactory()方法获取其BeanFactory的实现DefaultListableBeanFactoryDefaultListableBeanFactory通过registerSingleton(…)registerBeanDefinition(..)方法支持这样的注册。然而,典型的应用都是通过元数据定义bean。

  • bean元数据和手动提供的单例实例需要尽早注册,为了容器在自动装配时正确推断它们和其它内省的步骤。在一定程度上支持重写已存在的元数据和已存在的单例实例,然而在运行时注册新bean(并发访问工厂)并不正式地支持,并且可能在容器中导致并发访问异常和/或不一致的状态。
7.3.1 命名bean

每一个bean都有一个或多个标识符。这些标识符在托管bean的容器中必须唯一。一个bean通常只有一个标识符,但如果需要更多标识符,可以通过别名来实现。

在XML中,可以使用id和/或name属性来指定bean的标识符。id属性允许显式地指定一个id。按照约定,这些名字是字母数字的(’myBean’, ‘fooService’, 等),但是也可以包含一些特殊字符。如果你想引入其它别名,也可以在name属性中指定,用逗号(,)、分号(;)或空格分割。作为历史记录,Spring 3.1之前的版本,id属性被定义为xsd:ID,它会限制一些可能的字符。3.1开始,它被定义为xsd:string类型。注意id的唯一性是被容器强制执行的,而不再是XML解析器。

给bean指定一个名字或id并不是必需的。如果没有显式地指定名字或id,容器会为那个bean生成一个唯一的名字。但是,如果要根据名字引用那个bean,比如通过ref元素或Service定位器查找,那么必须指定一个名字。不提供名字一般用于内部bean自动装配合作者

  • bean命名的约定
    命名bean时采用标准Java对字段的命名约定。bean名字以小写字母开头,然后是驼峰式。例如,(不带引号)‘accountManager’, ‘accountService’, ‘userDao’, ‘loginController’, 等等。
    按统一的规则命名bean更易于阅读和理解,而且,如果使用Spring AOP,当通过名字应用advice到一系列bean上将会非常有帮助。
在bean定义外给bean起别名

在bean的定义中,可以为其提供多个名字,通过与id属性指定的名字或name属性里的其它名字组合起来。这些名字对同一个bean是等价的,在有些情况下很有用,比如,允许应用中的每个组件通过其本身指定的名字引用共同的依赖。

在bean定义的地方指定所有的别名并不足够。有时候希望在其它地方为bean指定一个别名。这在大型系统中很常见,它们的配置被很多子系统分割,每个子系统有它自己的一系列bean定义。在XML配置中,可以使用<alias/>元素指定别名。

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

在上述这个案例中,同一个容器中这个bean叫fromName,在使用了别名定义也可以叫toName

例如,子系统A中的配置元数据通过subsystemA-datasource引用一个数据源,子系统B中的配置元数据通过subsystemB-datasource引用这个数据源,使用这两个子系统的主应用通过myApp-datasource引用这个数据源。为了使用这三个名字引用相同的对象,可以在MyApp配置中添加如下的别名定义:

<alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/>
<alias name="subsystemA-dataSource" alias="myApp-dataSource" />
 
 
  • 1
  • 2
  • 1
  • 2

现在每个组件和主应用都可以通过不同的名字引用这个数据源了,并且可以保证不会与任何其它的定义冲突(有效地创建了一个命名空间),它们还引用了同一个bean。

7.3.2 实例化bean

一个bean的定义本质上就是创建一个或多个对象的食谱。容器查看这个食谱并使用被bean定义封装的配置元数据来创建(或获取)实际的对象。

如果使用XML配置,在<bean/>元素的class属性中指定被实例化的对象的类型(或类)即可。class属性通常是必需的,它实际是BeanDefinition实例的Class属性(例外,参考使用实例的工厂方法实例化7.7 Bean定义继承)。有两种使用Class属性的方式:

  • 典型地,容器直接通过反射调用bean的构造方法来创建一个bean,这个Java代码中使用new操作符是等价的。
  • 容器通过调用工厂类中的static工厂方法来创建一个bean。通过调用static工厂方法返回的对象类型可能是同一个类,也可能是完全不同的另一个类。
    • 内部类名字。如果为一个static嵌套类配置bean定义,必须使用嵌套类的二进制名字。
      例如,如果在com.example包中有一个类叫Foo,并且Foo类中有一个static嵌套类叫Bar,那么bean定义中的class属性的值应该是com.example.Foo$Bar
      注意使用$字符分割嵌套类和外部类。
使用构造方法实例化

用构造方法创建bean的方式,在Spring中所有正常的类都可以使用并兼容。也就是说,被开发的类不需要实现任何特定的接口或以特定的形式编码。仅仅指定bean类就足够了。但是,依靠什么样的类型来指定bean,你可能需要默认的空构造方法。

Spring的IoC窗口几乎可以管理所有的类,并不仅限于真的JavaBean。大部分用户更喜欢带有默认(无参)构造方法和适当setter/getter方法的JavaBean。你也可以拥有不同的非bean风格的类。例如,如果你需要使用不是JavaBean格式的连接池,Spring一样可以管理它。

在XML中,可以像下面这样指定bean类:

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

<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

关于如何为构造方法提供参数(如果需要的话)及在对象被构造之后为其设置属性,请参考注入依赖

使用静态工厂方法实例化

当使用静态工厂方法创建一个bean时,需要使用class属性指定那个包含静态工厂方法的类,并使用factory-method属性指定工厂方法的名字。应该可以调用这个方法(带参数的稍后讨论)并返回一个有效的对象,之后它就像用构造方法创建的对象一样对待。一种这样的bean定义的使用方法是在代码调用静态工厂。

下面的bean定义指定了通过调用工厂方法创建这个bean。这个定义没有指定返回类型,仅仅指定了包含工厂方法的类。在这个例子中,createInstance()方法必须是静态的。

<bean id="clientService"
    class="examples.ClientService"
    factory-method="createInstance"/>
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
public class ClientService {
    private static ClientService clientService = new ClientService();
    private ClientService() {}

    public static ClientService createInstance() {
        return clientService;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

关于如何为工厂方法提供参数及在对象从工厂返回之后为其设置属性,请参考依赖与配置详解

使用实例的工厂方法实例化(非静态)

与使用静态工厂方法实例化类似,使用实例的工厂方法实例化是调用一个已存在的bean的非静态方法来创建一个新的bean。为了使用这种机制,请把class属性置空,在factory-bean属性中指定被调用的用来创建对象的包含工厂方法的那个bean的名字,并factory-method属性中设置工厂方法的名字。

<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();
    private DefaultServiceLocator() {}

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

一个工厂类可以拥有多个工厂方法,如下:

<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

<bean id="accountService"
    factory-bean="serviceLocator"
    factory-method="createAccountServiceInstance"/>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();
    private static AccountService accountService = new AccountServiceImpl();

    private DefaultServiceLocator() {}

    public ClientService createClientServiceInstance() {
        return clientService;
    }

    public AccountService createAccountServiceInstance() {
        return accountService;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

工厂bean本身也可以通过依赖注入管理和配置,参考依赖与配置详解

  • 在Spring的文档中,工厂bean引用Spring容器配置的通过实例或静态工厂方法创建对象的bean。相比之下,FactoryBean引用的是Spring特定的FactoryBean

7.4 依赖

一个典型的企业级应用包含了不止一个对象(或者用Spring的说法叫bean)。即使是最简单的应用也需要几个对象一起工作以展现给最终用户看到的连贯应用。下一节介绍怎么样定义一系列独立的bean使它们相互合作实现一个共同的目标。

7.4.1 依赖注入

依赖注入是一个对象定义其依赖的过程,它的依赖也就是与它一起合作的其它对象,这个过程只能通过构造方法参数、工厂方法参数、或者被构造或从工厂方法返回后通过setter方法设置其属性来实现。然后容器在创建这个bean时注入这些依赖。这个过程本质上是反过来的,由bean本身控制实例化,或者直接通过类的结构或Service定位器模式定位它自己的依赖,因此得其名曰控制反转。

使用依赖注入原则代码更干净,并且当对象提供了它们的依赖时解耦更有效。对象不查找它的依赖,且不知道依赖的位置或类。比如,类变得更容易测试,尤其是当依赖是基于接口或抽象基类时,这允许在单元测试时模拟实现。

依赖注入有两种主要的方式,基于构造方法的依赖注入基于setter方法的依赖注入

基于构造方法的依赖注入

基于构造方法的依赖注入,由容器调用带有参数的构造方法来完成,每个参数代表一个依赖。调用带有特定参数的静态工厂方法创建bean几乎是一样的,这里把构造方法的参数与静态工厂方法的参数同等对待。下面的例子展示了一个只能通过构造方法注入依赖的类。注意,这个类并没有什么特殊的地方,它仅仅只是一个没有依赖容器的特定接口、基类或注解的POJO。

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on a MovieFinder
    private MovieFinder movieFinder;

    // a constructor so that the Spring container can inject a MovieFinder
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
构造方法参数的解决

构造方法参数的解决匹配时使用参数的类型。如果没有潜在的歧义存在于bean定义的构造方法参数中,那么在bean实例化时bean定义中参数的顺序与构造方法中的顺序保持一致。例如,下面这个类:

package x.y;

public class Foo {

    public Foo(Bar bar, Baz baz) {
        // ...
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

没有潜在的歧义,假设BarBaz类没有继承关系。然后下面这样配置就可以了,不需要在<construct-arg/>中显式地指定参数的索引或类型。

<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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

当另一个bean被引用时,类型是已知的并且匹配可能出现(像上例一样)。
当使用简单类型时,比如<value>true</value>,Spring无法判断值的类型,所以没有类型就没办法匹配。例如,下面这个类:

package examples;

public class ExampleBean {

    // Number of years to calculate the Ultimate Answer
    private int years;

    // The Answer to Life, the Universe, and Everything
    private String ultimateAnswer;

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在上述场景中,如果使用type属性指定了参数的类型则容器会使用类型匹配。例如:
(译者注:如果不指定类型,spring分不清把7500000赋给year还是42赋给year,因为constructor-arg并不是按它们出现的顺序与参数匹配)

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

使用index属性显式地指定参数的顺序。例如:

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

除了解决多个简单值的起义,使用索引还可以解决构造方法中存在多个相同类型的参数的问题。注意索引从0开始。

也可以使用参数的名字来消除起义:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

注意,为了使其可以立即使用,代码必须开启调试标记来编译,这样Spring才能从构造方法中找到参数的名字。如果没有开启调试标记(或不想)编译,可以使用JDK的注解@ConstructorProperties显式地指定参数的名字。类看起来不得不像下面这样:

package examples;

public class ExampleBean {

    // Fields omitted

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
基于setter方法的依赖注入

基于setter方法的依赖注入,由容器在调用无参构造方法或无参静态工厂方法之后调用setter方法来实例化bean。

下面的例子展示了一个只能通过纯净的setter方法注入依赖的类。这个类符合Java的约定,它是一个没有依赖容器的特定接口、基类或注解的POJO。

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on the MovieFinder
    private MovieFinder movieFinder;

    // a setter method so that the Spring container can inject a MovieFinder
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

ApplicationContext对它管理的bean支持基于构造方法和基于setter方法的依赖注入,也支持在使用构造方法注入依赖之后再使用setter方法注入依赖。以BeanDefinition的形式配置依赖,可以与PropertyEditor实例一起把属性从一种形式转化为另一种形式。但是,大多数Spring用户不直接(编程式地)使用这些类,而是使用XML bean定义、注解的组件(例如,以@Component@Controller注解的类)或基于Java的@Configuration类的@Bean方法。这些资源然后都被内部转化为了BeanDefinition的实例,并用于加载完整的Spring IoC容器的实例。

  • 基于构造方法或基于setter方法的依赖注入?
    因为可以混合使用基于构造方法和基于setter方法的依赖注入,所以使用构造方法注入强制依赖并使用setter方法或配置方法注入可选依赖是一个不错的原则。注意,在setter方法上使用@Required注解可以使属性变成必需的依赖。
    Spring团队一般倡导使用构造方法注入,因为它会使应用程序的组件实现为不可变的对象,并保证必需的依赖不为null。另外,构造方法注入的组件总是以完全初始化的状态返回给客户端(调用)代码。注意,大量的构造方法参数是代码的坏味道,这意味着类可能有很多责任,应该被重构为合适的部分以实现关注点的分离。
    setter方法应该仅仅只用于可选依赖,这些可选依赖应该在类中被赋值合理的默认值。否则,在使用这项依赖的任何地方都要做非null检查。setter方法注入的好处之一是可以使那个类的对象稍后重新配置或重新注入。使用JMX MBeans的管理是使用setter注入很好的案例。
    特定的类选择最合适的依赖注入方式。有时你并没有第三方类的源码,你就需要选择使用哪种方式。例如,如果一个第三方类没有暴露任何setter方法,那么只能选择构造方法注入了。
依赖注入的过程

容器按如下方式处理依赖注入:

  • ApplicationContext被创建并初始化描述所有bean的配置元数据。配置元数据可以是XML、Java代码或注解。
  • 对于每一个bean,它的依赖以属性、构造方法参数或静态工厂方法参数的形式表示。这些依赖在bean实际创建时被提供给它。
  • 每一个属性或构造方法参数都是将被设置的值的实际定义,或容器中另一个bean的引用。
  • 每一个值类型的属性或构造方法参数都会从特定的形式转化为它的实际类型。默认地,Spring可以把字符串形式的值转化为所有的内置类型,比如int, long, String, boolean,等等。

Spring容器在创建时会验证每个bean的配置。但是,bean的属性本身直到bean实际被创建时才会设置。单例作用域的或被设置为预先实例化(默认)的bean会在容器创建时被创建。作用域的定义请参考7.5 bean的作用域。否则,bean只在它被请求的时候才会被创建。创建一个bean会潜在地引起一系列的bean被创建,因为bean的依赖及其依赖的依赖(等等)会被创建并赋值。注意,那些不匹配的依赖可能稍后创建,比如,受影响的bean的首次创建(译者注:这句可能翻译的不到位,有兴趣的自己翻译下,原文为 Note that resolution mismatches among those dependencies may show up late, i.e. on first creation of the affected bean)。

  • 循环依赖
    如果你主要使用构造方法注入,很有可能创建一个无法解决的循环依赖场景。
    例如,类A使用构造方法注入时需要类B的一个实例,类B使用构造方法注入时需要类A的一个实例。如果为类A和B配置bean互相注入,Spring IoC容器会在运行时检测出循环引用,并抛出异常BeanCurrentlyInCreationException
    一种解决方法是把一些类配置为使用setter方法注入而不是构造方法注入。作为替代方案,避免构造方法注入,而只使用setter方法注入。换句话说,尽管不推荐,但是可以通过setter方法注入配置循环依赖。
    不像典型的案例(没有循环依赖),A和B之间的循环依赖使得一个bean在它本身完全初始化之前被注入了另一个bean(经典的先有鸡/先有蛋问题)。

你通常可以相信Spring做正确的事。它会在容器加载时检测配置问题,比如引用不存在的bean和循环依赖。Spring尽可能晚地设置属性和解决依赖,在bean实际被创建之后。这意味着Spring正确加载了,之后如果在创建对象或它的依赖时出现问题那么Spring又会产生异常。例如,因为一个缺少或失效的属性导致bean抛出了异常。这潜在地会推迟一些配置问题的可视性,这就是为什么ApplicationContext的实现类默认预先实例化单例bean。在bean实际需要之前会花费一些时间和内存成本,因此在ApplicationContext创建时会发现配置问题,而不是之后。你也可以重写默认的行为让单例bean延迟初始化,而不是预先实例化。

如果不存在循环依赖,当一个或多个合作的bean被注入依赖的bean时,每个合作的bean都将在注入依赖的bean之前被完全实例化。这意味着如果A依赖于B,那么在调用A的setter方法注入B之前会完全实例化B。换句话说,bean被实例化(如果不是预先实例化的单例),它的依赖被设置,然后相关的生命周期方法被调用(比如,配置的初始化方法初始化bean的回调方法)。

依赖注入的示例

下面的例子使用XML配置setter方法注入。XML的部分配置如下:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

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

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
public class ExampleBean {

    private AnotherBean beanOne;
    private YetAnotherBean beanTwo;
    private int i;

    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setIntegerProperty(int i) {
        this.i = i;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

上面的例子中,XML中setter方法与属性匹配,下面的例子使用构造方法注入:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- constructor injection using the nested ref element -->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>

    <!-- constructor injection using the neater ref attribute -->
    <constructor-arg ref="yetAnotherBean"/>

    <constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
public class ExampleBean {

    private AnotherBean beanOne;
    private YetAnotherBean beanTwo;
    private int i;

    public ExampleBean(
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        this.beanOne = anotherBean;
        this.beanTwo = yetAnotherBean;
        this.i = i;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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"/>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
public class ExampleBean {

    // a private constructor
    private ExampleBean(...) {
        ...
    }

    // a static factory method; the arguments to this method can be
    // considered the dependencies of the bean that is returned,
    // regardless of how those arguments are actually used.
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

        ExampleBean eb = new ExampleBean (...);
        // some other operations...
        return eb;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

通过<constructor-arg/>元素为静态工厂方法提供参数,与实际使用构造方法时一样。工厂方法返回的类型不必与包含工厂方法的类的类型一致,即使在这个例子中是一样的。实例(非静态)工厂方法完全一样的使用方法(除了使用factory-bean代替class属性),所以这里就不再赘述了。

7.4.2 依赖与配置详解

如前所述,可以定义bean的属性和构造方法的参数引用其它的bean(合作者),或设置值。在XML配置中,可以使用<property/>或<constructor-arg/>的属性达到这个目的。

直接设置值(原始类型,String,等等)

<property/>的value属性可以为属性或构造方法参数指定人类可读的字符串形式的值。Spring的转换器service会把这些值转换成属性或参数的实际类型。

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <!-- results in a setDriverClassName(String) call -->
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
    <property name="username" value="root"/>
    <property name="password" value="masterkaoli"/>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

下面的例子使用p命名空间可以更简洁的表示:

<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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

上述XML更简洁了,但是错误只能在运行时被发现,而不是设计的时候,除非使用类似IntelliJ IDEA或Spring工具套件(STS)的开发工具,它们可以在创建bean定义时自动完成属性的拼写。强烈推荐使用这类IDE。

也可以像下面这样配置java.util.Properties:

<bean id="mappings"
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

    <!-- typed as a java.util.Properties -->
    <property name="properties">
        <value>
            jdbc.driver.className=com.mysql.jdbc.Driver
            jdbc.url=jdbc:mysql://localhost:3306/mydb
        </value>
    </property>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Spring容器会使用JavaBean的PropertyEditor机制把<value/>中的内容转换成java.util.Properties实例。这样更简短,Spring团队也更喜欢使用内置的<value/>元素代替value属性。

idref元素

idref元素是一种很简单的错误检查方式,可以把容器中另一个bean的id(字符串值-不是引用)传递给<constructor-arg/>或<property/>元素。

<bean id="theTargetBean" class="..."/>

<bean id="theClientBean" class="...">
    <property name="targetName">
        <idref bean="theTargetBean" />
    </property>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上面的片段在运行时与下面的片段完成一样:

<bean id="theTargetBean" class="..." />

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean" />
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

第一种形式比第二种形式更好,因为使用idref标签时容器会在部署时验证引用的命名的bean是否实际存在。第二种形式不会对client bean的targetName属性的值执行任何验证。错误只能在client bean实际实例化时才能发现(可能导致致命的结果)。如果client bean是一个prototype类型的bean。那么错误很可能会在容器启动很久之后才能发现。

  • idref元素的local属性在4.0版本的xsd中不再支持了,因为它不提供对普通bean的引用。在升级的4.0的schema时只要把已存在的idref local改成idref bean即可。(译者注:idref local只能引用同一个文件中的bean,所以不建议使用了,改成idref bean即可。)
引用其它bean(合作者)

ref是<constructor-arg/>或<property/>中的最后一个元素。可以使用它让一个bean的指定属性引用另一个bean(合作者)。被引用的bean就是这个bean的一个依赖,并且会在此属性设置前被初始化(如果此合作者是单例的,它可能已经被初始化了)。所有的引用最终都是对另一个对象的引用。作用域和检验由通过bean, local或parent属性指定的另一个对象的id或name决定。

通过bean的<ref/>标签指定目标bean是最常用的形式,并且可以引用同一个或父容器中的任何bean,不论是否在同一个XML文件中。引用的值可能是目标bean的id属性,或name属性中的一个(译者注:因为name属性可以指定多个name)。

<ref bean="someBean"/>
 
 
  • 1
  • 1

通过parent属性可以指定对当前容器的父容器中的bean的引用。parent属性的值可能是目标bean的id属性或name属性中的一个,而且目标bean必须在当前容器的父容器中。这种引用形式主要出现在有容器继承并且想把父容器中存在的bean包装成同样名字的代理用于子容器的时候。

<!-- in the parent context -->
<bean id="accountService" class="com.foo.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • ref元素的local属性在4.0版本的xsd中不再支持了,因为它不提供对普通bean的引用。在升级的4.0的schema时只要把已存在的ref local改成ref bean即可。(译者注:ref local只能引用同一个文件中的bean,所以不建议使用了,改成ref bean即可。)
内部bean

在<property/>或<constructor-arg/>元素内部定义的bean就是所谓的内部bean。

<bean id="outer" class="...">
    <!-- instead of using a reference to a target bean, simply define the target bean inline -->
    <property name="target">
        <bean class="com.example.Person"> <!-- this is the inner bean -->
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

内部bean不需要定义id或name,即使指定了,容器也不会使用它作为标识符。容器在创建内部bean时也会忽略其作用域标志,内部bean总是匿名的且总是随着外部bean一起创建。不可能把内部bean注入到除封闭bean以外的合作bean,也不可能单独访问它们。

有一种边界情况,可以从自定义作用域中接收销毁方法的回调,例如,对于包含在一个单例bean中的request作用域的内部bean,它的创建依赖于包含它的bean,但是销毁方法的回调允许它参与到request作用域的生命周期中。这并不是一种常见的情况,内部bean一般共享包含它的bean的作用域。

集合

在<list/>, <set/>, <map/>和<props/>元素中,可以分别设置Java集合类型List, Set, Map和Properties的属性和参数。

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

map的键值或set的值也可以是以下任何元素:

bean | ref | idref | list | set | map | props | value | null
 
 
  • 1
  • 1
集合的合并

Spring容器同样支持集合的合并。开发者可以定义一个父类型的<list/>, <set/>, <map/>或<props/>元素,然后让子类型继承它,也可以重写父集合里的值。也就是说,子集合的值是父集合与子集合值合并的结果,且子集合的元素重写了父集合中同样的元素。

这节主要讨论父子集合的合并机制,读者如果对父子bean的定义不太熟悉,可以先读相关章节再继续。

下面的例子展示了集合的合并:

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">administrator@example.com</prop>
                <prop key="support">support@example.com</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

注意子bean的adminEmails属性上的props元素的merge=true。当child被实例化时,它拥有一个adminEmails Properties集合,这个集合包含了父子两个集合adminEmails合并的结果。

administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

子集合Properties的值集继承了所有父集合的元素,并且子集合中的support值重写了父集合的值。

<list/>, <map/>和 <set/>集合的合并行为也是类似的。在<list/>元素合并的时候,因为List集合维护的是有序的值,所以父集合的值在子集合值的前面。在Map, Set和Properties中的值则不存在顺序。没有顺序的集合类型更有效,因此容器内部更倾向于使用Map, Set和Properties。

集合合并的局限性

不能合并不同的集合类型(比如,Map和List),如果试图这么做将会抛出异常。merge属性必须指定在子集合的定义上,在父集合上指定merge属性将是多余的且不会合并集合。

强类型集合

Java5引入了泛型类型,所以可以使用强类型的集合。也就是说,例如,可以声明一个只能包含String元素的集合类型。如果使用Spring把强类型的集合注入到一个bean,可以利用Spring的类型转换以便元素在被添加到集合之前转换成合适的类型。

public class Foo {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
<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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

当foo的accounts属性准备注入的时候,会通过反射获得强类型Map<String, Float>的泛型信息。然后Spring的类型转换机制识别到value的类型为Float,并把String类型的值9.99, 2.75和3.99转换成实际的Float类型。

null和空字符串值

Spring把对属性的空参数作为空字符串。下面的XML片段把email属性的值设置成了空字符串值(”“)。

<bean class="ExampleBean">
    <property name="email" value=""/>
</bean>
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

上面的例子与下面的Java代码是等价的:

exampleBean.setEmail("")
 
 
  • 1
  • 1

<null/>元素处理null值。例如:

<bean class="ExampleBean">
    <property name="email">
        <null/>
    </property>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

上面的配置与下面的Java代码等价:

exampleBean.setEmail(null)
 
 
  • 1
  • 1
使用p命名空间简写XML

p命名空间使你可以使用bean元素的属性,而不是内置的<property/>元素,来描述属性值和合作的bean。

Spring支持使用命名空间扩展配置,这是基于XML的schema定义的。本章讨论的bean的配置形式是定义在XML schema文档中的。然而,p命名空间不是定义在XSD文件中的,它存在于Spring的核心包中。

下面例子中的两段XML是一样的结果,第一段使用标准的XML形式,第二段使用p命名空间形式。

<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="foo@bar.com"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="foo@bar.com"/>
</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这个例子展示了p命名空间中一个叫做email的属性。这会告诉Spring包含了一个属性声明。如前所述,p命名空间没有schema定义,所以可以把xml的属性名设置成java的属性名。

下面的例子包含两个引用了其它对象的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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

如你所见,此例不仅使用p命名空间定义属性值,还使用特定的形式声明了属性的引用。第一个bean定义使用<property name=”spouse” ref=”jane”/>创建了从john到jane的引用,第二个bean定义使用p:spouse-ref=”jane”作为属性做了完全相同的事。这个例子中spouse是属性名,凭借-ref表明这不是直接的值而是对另一个对象的引用。

  • p命名空间并没有标准的XML形式灵活。例如,属性如果以Ref结尾则会与属性的引用冲突,标准的XML形式就不会有这样的问题。我们推荐仔细地选择自己的方式,并和团队成员交流,以避免XML文档中同时出现三种不同的方式。
使用c命名空间简写XML

使用p命名空间简写XML类似,c命名空间是Spring3.1新引入的,允许使用内联属性配置构造方法的参数,而不用嵌套constructor-arg元素。

让我们一起回顾下基于构造方法的依赖注入这节,使用c: 命名空间如下:

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

    <!-- traditional declaration -->
    <bean id="foo" class="x.y.Foo">
        <constructor-arg ref="bar"/>
        <constructor-arg ref="baz"/>
        <constructor-arg value="foo@bar.com"/>
    </bean>

    <!-- c-namespace declaration -->
    <bean id="foo" class="x.y.Foo" c:bar-ref="bar" c:baz-ref="baz" c:email="foo@bar.com"/>

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

c: 命名空间与p: 命名空间使用一样的转换(后面的-ref用于bean引用),通过名字设置构造方法的参数。同样地,c命名空间也没有定义在XSD文件中(但是存在于Spring核心包中)。

对于极少数的情况,不能获得构造方法参数名时(通常不使用调试信息编译字节码),可以退而求其次使用参数的索引。

<!-- c-namespace index declaration -->
<bean id="foo" class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"/>
 
 
  • 1
  • 2
  • 1
  • 2
  • 由于XML语法,索引符号需要以 _ 开头作为XML属性名,而不能以数字开头(即使在一些IDE中允许)。

经过实践,构造方法的解决机制在匹配参数方面很有效率,所以除非真的需要,我们推荐在所有配置的地方都使用名字符号。

合成属性名

可以使用合成或嵌套的属性名设置bean的属性,只要路径中除了最后的属性值的所有的组件都不为null,考虑使用如下bean定义:

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

foo有一个属性叫fred,fred有一个属性叫bob,bob有一个属性叫sammy,并且最后的sammy属性的值被设置为123。其中,foo的fred属性和fred的bob属性在bean构造后必须不为null,否则将会抛出空指针异常。

7.4.3 使用depends-on

如果一个bean是另一个bean的依赖,那通常意味着这个bean被设置为另一个bean的属性。典型地,在XML配置中使用<ref/>元素来完成这件事。但是,有时bean之间的依赖并不是直接的,例如,一个静态的初始化器需要被触发,比如在数据库驱动注册时。depends-on属性可以明确地强制一个或多个bean在使用它(们)的bean初始化之前被初始化。

下面的例子使用depends-on属性表示对一个单例bean的依赖:

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
 
 
  • 1
  • 2
  • 1
  • 2

要表示对多个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" />
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

depends-on属性能够同时指定初始化时依赖和通信销毁时依赖,这只能运用在单例bean的情况中。依赖的bean在给定的bean销毁之前被销毁。因此,depends-on也能够控制关闭的顺序。(译者注,初始化的时候依赖的对象先初始化才能注入,销毁时需要依赖的对象先销毁才能解绑)

7.4.4 延迟初始化的bean

默认地,ApplicationContext的实现创建和配置所有单例bean作为初始化过程的一部分。通常这种预初始化是令人满意的,因为配置或环境中的错误可以立即被发现,相反则可能需要数小时甚至数天才能发现错误。如果不需要预初始化,可以把bean定义为延迟初始化来阻止预初始化。延迟初始化的bean会告诉IoC容器在第一次请求到这个bean时才初始化,而不是启动的时候。

在XML中,这种行为是通过<bean/>元素的lazy-init属性控制的,例如:

<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>
 
 
  • 1
  • 2
  • 1
  • 2

当ApplicationContext使用上面的配置启动时时,名为lazy的bean并不急着预初始化,而not.lazy则会预初始化。

但是,当一个延迟初始化的bean是另一个非延迟初始化的单例bean的依赖时,ApplicationContext会在启动时创建这个延迟初始化的bean,因为它必须用来满足那个单例的依赖。这个延迟初始化的bean被注入到非延迟初始化的单例bean中。

也可以在<beans/>元素上设置其default-lazy-init属性以便在容器级别控制延迟初始化。

<beans default-lazy-init="true">
    <!-- no beans will be pre-instantiated... -->
</beans>
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
7.4.5 自动装配合作者

Spring容器可以相互合作的bean间自动装配其关系。你可让让Spring通过检查ApplicationContext的内容自动为你解决bean之间的依赖。自动装配有以下优点:

  • 自动装配将极大地减少指定属性或构造方法参数的需要(在这点上,其它机制比如本章其它小节讲解的bean模板也是有价值的)。
  • 自动装配可以更新配置当你的对象进化时。例如,如果你需要为一个类添加一个依赖,那么不需要修改配置就可以自动满足。因此,自动装配在开发期间非常有用,但不否定在代码库变得更稳定的时候切换到显式的装配。

使用XML配置时,可以为带有autowire属性的bean定义指定自动装配的模式。自动装配的功能有四种模式。可以为每个自动装配的bean指定一种模式。
表7.2 自动装配的模式

模式解释
no默认地没有自动自动装配。bean的引用必须通过ref元素定义。对于大型部署,不推荐更改默认设置,因为显式地指定合作者能够更好地控制且更清晰。在一定程度上,这也记录了系统的结构。
byName按属性名称自动装配。Spring为待装配的属性寻找同名的bean。例如,如果一个bean被设置为按属性名称自动装配,且它包含一个属性叫master(亦即,它有setMaster(…)方法),Spring会找到一个名叫master的bean并把它设置到这个属性中。
byType按属性的类型自动装配,如果这个属性的类型在容器中只存在一个bean。如果多于一个,则会抛出异常,这意味着不能为那个bean使用按类型装配。如果一个都没有,则什么事都不会发生,这个属性不会被装配。
constructor与按类型装配类似,只不过用于构造方法的参数。如果这个构造方法的参数类型在容器中不存在明确的一个bean,将会抛出异常。

使用按类型装配或构造方法自动装配模式,可以装配数组和集合类型。在这个情况下,容器中所有匹配期望类型的候选者都将被提供用来满足此依赖。你可以自动装配强类型的Map如果其键的类型是String。自动装配Map的值将包含所有匹配期望类型的bean实例,并且Map的键将包含对应的bean的名字。

可以联合使用自动装配行为和依赖检查,其中依赖检查会在自动装配完成后执行。

自动装配的局限性和缺点

如果一个项目一直使用自动装配,它会运行得很好。如果只是用在一两个bean上,则会出现混乱。

自动装配有如下局限性和缺点:

  • propertyconstructor-arg上显式设置的依赖总是覆盖自动装配。而且,不能自动装配所谓的简单属性,如基本类型、StringClasses(也包括简单属性的数组)。这是设计层面的局限性。
  • 自动装配没有显式装配精确。如上表所述,尽管Spring很小心地避免了模棱两可的猜测,但其管理的对象之间的关系并不会被明确地记录。
  • 从Spring容器生成文档的工具可能找不到装配信息。
  • 容器中的多个bean定义可能会匹配setter方法或构造方法参数指定的类型。对于数组、集合或Map,这未必是个问题。但是,对于那些需要单个值的依赖,这种歧义并不能随意解决。如果没有各自不同的bean定义,将会抛出异常。

在上述场景中,有如下几种选择:

  • 放弃自动装配,改为显式地装配。
  • 设置bean的autowire-candidate属性为false以避免自动装配,这会在下一章节讲解。
  • 设置<bean/>标签的primary属性为true,从而为其指定一个单独的bean作为主要的候选者。
  • 使用基于注解的配置实现更细粒度的控制,参考7.9 基于注解的容器配置
避免bean自动装配

在单个bean上,可以避免自动装配。在xml形式中,设置<bean>元素的autowire-candidate属性为false,容器就不会让这个bean进入自动装配的架构中(包括注解形式的配置,比如@Autowired)。

也可以通过定义bean名称的匹配模式避免自动装配。顶级元素<beans>的属性default-autowire-candidates可以接受一个或多个模式。例如,为了限制以Repository结尾的bean自动装配,提供一个值*Repository即可。如果需要提供多个模式,请以逗号分隔。bean定义上的autowire-candidate属性优先级要更高一点,对于这类bean,匹配模式将不起作用。

这项技术对于那些你不想它们被自动注入到其它bean的bean非常有用。这并不意味着它自己不能使用自动装配,而是,它不是其它bean自动装配的候选者。

7.4.6 方法注入

在大部分应用场景中,容器中的大多数bean都是单例的。当一个单例bean需要与另一个单例bean合作,或者一个非单例bean需要与另外一个非单例bean合作的时候,仅仅定义一个bean作为另一个的属性就能解决它们的依赖关系。如果bean的生命周期不一致就会出现问题。假如,一个单例bean A 需要使用一个非单例bean B,也许在每次调用A的方法时都需要。容器仅仅创建这个单例bean A 一次,所以只有一次机会设置其属性。容器不能在 A 每次调用的时候都提供一个新的 B 的实例。

一种解决方法是放弃控制反转。你可以通过实现ApplicationContextAware接口使 A 能连接到容器,并在每次 A 需要 B 的实例的时候调用容器的getBean(“B”)方法取得新的 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) {
        // grab a new instance of the appropriate Command
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    protected Command createCommand() {
        // notice the Spring API dependency!
        return this.applicationContext.getBean("command", Command.class);
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

上面这种方式并不推荐使用,因为业务代码跟spring耦合了。方法注入,从某种角度来看是spring IoC容器的高级特性,允许以一种干净的方式处理这种用例。

  • 可以在这篇博客找到更多关于方法注入的信息。
查找方法注入

查找方法注入是容器的一项能力,它可以重写容器管理的bean的方法,并返回另一个bean的查找结果。查找往往涉及到前面描述的那种原型(prototype)bean。spring利用CGLIB库动态地生成字节码子类,从而重写方法以实现查找方法注入。

  • 为了能使动态的子类有效,被继承的类不能是final,且被重写的方法也不能是final
  • 单元测试一个具有抽象方法的类时,需要手动继承此类并重写其抽象方法。
  • 组件扫描的具体方法也需要具体类。
  • 一项关键限制是查找方法不能使用工厂方法和配置类中的@Bean方法,因为容器不会在运行时创建一个子类及其实例。
  • 最后,方法注入的目标对象不能被序列化。

查看前面关于CommandManager类的代码片段,可以发现spring容器会动态生成createCommand()方法的实现。CommandManager不会有任何的spring依赖,如下所示:

package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

    public Object process(Object commandState) {
        // 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();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

客户端类包含了将被注入的方法(本例中的CommandManager),被注入的方法需要如下签名:

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

如果方法是抽象的,动态生成的子类会实现这个方法。另外,动态生成的子类也会重写原类中的具体方法。例如:

<!-- 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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

commandManager这个bean会在任何它需要command实例的时候调用其createCommand()方法。如果实际需要,则必须把command声明为原型(prototype)。如果被声明为单例(singleton),则每次返回的都是同一个command实例。

  • 有兴趣的读者可能会发现ServiceLocatorFactoryBean(位于包org.springframework.beans.factory.config中)就是这么用的。ServiceLocatorFactoryBean中用法与其它工具类一样,ObjectFactoryCreatingFactoryBean,但是它允许你使用自己的查找接口而不是spring指定的查找接口。请参考附加信息中这些类的javadoc文档。
任意的方法替换

方法注入的一种很少使用的形式,它是使用另外的方法实现替换目标bean中的任意方法。读者可以跳过本章的剩余部分直到这项功能真正需要的时候再回来看。

以xml形式为例,可以使用replaced-method元素指定用另一种实现替换bean中已存在的方法。如下例,我们希望重写其computeValue方法:

public class MyValueCalculator {

    public String computeValue(String input) {
        // some real code...
    }

    // some other methods...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

一个实现了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 ...;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

原类的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"/>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

可以使用一个或多个<arg-type/>元素指明原方法的参数签名。参数的签名仅仅当原方法有多个重载方法时才是必要的。为了方便,string类型的参数也可以是其全路径的子串,例如,以下方式均可匹配java.lang.String

java.lang.String
String
Str
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

因为参数的个数往往就能够区分每一种可能了,这种简写方法可以少打几个字,所以你可以只输入最短的字符就能够匹配参数类型。

7.5 bean的作用域

当创建一个bean定义时,就给了一份创建那个类的实例的食谱。bean定义是一份食谱,这个想法很重要,因为那意味着,可以根据一份食谱创建很多实例。

不仅可以控制bean实例的各种各样的依赖关系和配置值,还可以控制这些实例的作用域(scope)。这种方式很强大且是弹性的,因为可以通过配置选择实例的作用域,而不需要在类级别控制其作用域。bean可以被部署成多个作用域之一,spring支持很多作用域,对于web应用可以使用5种。

下面的作用域都是可以直接使用的,也可以创建自定义作用域。

表 7.3. bean的作用域

作用域描述
singleton(单例)默认值,每个spring的IoC容器中只保留一份bean定义对应的实例。
prototype(原型)一份bean定义对应多个实例。
request(请求)依赖于Http请求的生命周期,也就是说,每个Http请求都有它自己实例。这只在web应用上下文中有效。
session(会话)依赖于Http会话的生命周期。这只在web应用上下文中有效。
globalSession(全局会话)依赖于全局Http会话的生命周期。典型地仅当使用在Portlet上下文中有效。这只在web应用上下文中有效。
application(应用)依赖于ServletContext的生命周期。这只在web应用上下文中有效。
websocket依赖于WebSocket的生命周期。这只在web应用上下文中有效。

- 从Spring 3.0开始,还有一个thread作用域,但默认并没有被注册。更多信息请参考SimpleThreadScope的文档。关于怎么注册这个作用域及任何自定义作用域,请参考使用自定义作用域

7.5.1 单例作用域

一个单例bean仅共享同一个实例,并且所有的请求都只返回同一个实例。

换句话说,当把一个bean定义为单例时,spring容器严格地只创建那个bean定义的一个实例。这个单独的实例被存储在单例bean的缓存中,并且所有后来的请求及引用都会返回这个缓存的对象。

这里写图片描述

spring中单例bean的概念不同于GoF设计模式中关于单例模式的定义。GoF的单例模式硬编码了对象的作用域,所以一个特定的类有且只有一个实例在一个类加载器中被创建。spring单例的作用域被更好地描述 为每个容器及每个bean。这意味着如果在一个spring容器中定义了一个特定类的bean,那么spring容器会且仅会创建一个那个bean定义对应的类的实例。单例作用域是spring默认的作用域。在xml形式中,按如下方式定义一个单例bean:

<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"/>
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
7.5.2 原型作用域

非单例的原型作用域的bean将会导致每次请求时都创建一个新的实例。也就是说,在这个bean被注入到另一个bean时,或通过容器的getBean()方法请求它时都会创建一个新的实例。通常来说,对于所有有状态的bean使用原型作用域,对于无状态的bean使用单例作用域。

下面的图表描绘了spring的原型作用域。一个DAO对象无需配置成原型的,因为它不具有状态,这里仅仅是作者为了重用单例作用域那节的图表。

这里写图片描述

在xml形式中,按如下方式定义一个原型bean:

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

与其它作用域相比,spring不会管理一个原型bean的完整生命周期:容器实例化、配置、装配原型对象,然后把它交给客户端,没有更多原型实例的记录了。因此,尽管初始化的回调方法在所有对象上被调用,但是对于原型对象,销毁的回调方法并未被调用。客户端代码必须自己清除原型作用域的对象并释放其占用的资源。为了让spring容器释放原型对象占用的资源,试着使用自定义的bean后置处理器,它拥有将要被清除的bean的引用。

在某些方面,spring容器关于原型对象的角色就相当于Java中的new操作。从传递点之后所有的生命周期都有客户端自己处理(关于spring容器中bean的生命周期的详细信息,请参考7.6.1 生命周期回调)。

7.5.3 依赖于原型bean的单例bean

当使用依赖于原型bean的单例bean的时候,注意依赖关系是在实例化的时候被解决的。因此,如果依赖注入一个原型bean到单例bean,一个新的原型bean会被实例化,然后注入到单例bean中。这个原型实例是唯一一个提供给这个单例bean的实例。

但是,假设需要在运行时单例bean每次都获得一个新的原型bean,没有办法做到注入一个新的原型bean到单例bean,因为注入仅仅发生一次,当容器实例化单例bean的时候已经解决了它的依赖的注入。如果需要在运行时获取新的原型实例,请参考7.4.6 方法注入

7.5.4 请求、会话、全局会话、应用及WebSocket作用域

请求、会话、全局会话、应用及WebSocket作用域仅仅当使用在web上下文中才有效(比如XmlWebApplicationContext)。如果在其它spring容器中使用这些作用域,比如ClassPathXmlApplicationContext,将会抛出IllegalStateException,因为未知的作用域。

初始化web配置

为了支持请求、会话、全局会话、应用及WebSocket作用域,一些小小的初始化配置是必须的(对于单例和原型作用域,初始化配置并不是必须的)。

怎么完成这个初始化配置依赖于特定的Servlet环境。

如果使用Spring Web MVC,使用Spring的DispatcherServletDispatcherPortlet处理请求,那么就需要什么配置了,因为DispatcherServletDispatcherPortlet已经帮你配置好了。

如果使用Servlet 2.5 Web容器,不使用Spring的DispatcherServlet处理请求(例如,使用JSF或Struts时),则需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener。对于Servlet 3.0+,这项配置可以通过WebApplicationInitializer接口编程式处理。对于更老的容器,只能在web.xml文件中添加如下声明了:

<web-app>
    ...
    <listener>
        <listener-class>
            org.springframework.web.context.request.RequestContextListener
        </listener-class>
    </listener>
    ...
</web-app>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果使用上述配置,考虑使用Spring的RequestContextFilter。过滤器映射依赖于其web应用配置,所以应该合适地调整之。

<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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

DispatcherServlet, RequestContextListenerRequestContextFilter其实是做了同一件事,也就是绑定Http请求对象到服务于那个请求的线程。这样才能使得请求或会话作用域的bean能够更深入到调用链中。

请求作用域

考虑如下xml形式的bean定义:

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

spring容器为每次Http请求创建了一个新的LoginAction的实例。也就是说,loginAction的作用域是Http请求级别的。可以尽情地改变这个实例的内部状态,因为从同一份bean定义创建的其它实例不会发现这些状态的改变,它们只与特定的请求相关。当请求处理完成了,它绑定的bean就会被丢弃。

当使用注解驱动的组件或Java配置时,@RequestScope注解可以被用来赋值给一个组件的请求作用域。

@RequestScope
@Component
public class LoginAction {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
会话作用域

考虑如下xml形式的bean定义:

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

spring容器为一个单独的Htpp会话创建一个新的UserPreferences实例。也就是说userPreferences的作用域是Http会话级别的。与请求作用域的bean一样,可以尽情地改变这个实例的内部状态,因为从同一份bean定义创建的其它实例不会发现这些状态的改变,它们只与特定的Http会话相关。当这个Http会话被丢弃的时候,它绑定的bean也就被丢弃了。

当使用注解驱动的组件或Java配置时,@SessionScope 注解可以被用来赋值给一个组件的会话作用域。

@SessionScope
@Component
public class UserPreferences {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
全局会话作用域

考虑下面的定义:

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

全局会话作用域与标准的Http会话作用域类似,不过它只用在基于portlet的web应用上下文中。portlet规范中定义了全局会话的概念,那就是共享了同一个portlet应用中所有的组件的全局的会话。定义为全局会话作用域的bean的生命周期与全局的portlet会话相关。

如果在标准的Servlet应用中定义一个或多个拥有全局会话作用域的bean,那么标准的Http会话作用域将被使用,并不会报错。

应用作用域

考虑如下xml形式的bean定义:

<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
 
 
  • 1
  • 1

对于一个完整的web应用spring容器会创建一个新的AppPreferences实例。也就是说,appPreferences的作用域是ServletContext级别的,作为一个ServletContext普通属性被存储。这与spring的单例bean很类似,但有两种非常重要的区别:它在每个ServletContext中是单例,而不是spring的“ApplicationContext”(ApplicationContext在任何给定的web应用中可能存在多个)。另外,它实际上是作为ServletContext的属性被暴露并可见。

当使用注解驱动的组件或Java配置时,@ApplicationScope 注解可以被用来赋值给一个组件的应用作用域。

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
有作用域的bean作为依赖项

spring容器不仅管理对象(bean)的实例化,也连接合作者(依赖)。例如,如果注入一个Http请求作用域的bean到另一个更长周期的bean,可能会在原bean的位置选择使用AOP代理。也就是说,需要注入一个代理对象,这个代理对象暴露了与原对象相同的公共接口,但是它能从相关的作用域获取真实的目标对象,并以委托的方式调用真实对象的方法。

  • 你可能也会在单例bean之间使用<aop:scoped-proxy/>,通过序列化的中间代理使用这个引用,然后可以重新获得反序列化的目标单例bean。
    当在原型对象上使用<aop:scoped-proxy/>时,在共享的代理对象上每次的方法调用都会创建一个新的目标实例。
    另外,有作用域的代理并不是唯一一种生命周期安全的访问更短作用域的bean的方式。也可以简单地声明注入点(构造方法、setter方法的参数或自动装配的字段)作为ObjectFactory<MyTargetBean>,在每次需要的时候调用getObject()获取当前实例——这种方式不用占有实例或分开存储它。
    这种方式在JSR-330中的变种叫作Provider,使用Provider<MyTargetBean>声明并在每次需要时调用get()方法。更多关于JSR-330的详细信息请点击这里

下面的配置中仅仅只有一行,便更重要地是要理解“为什么”和“怎么样”。

<?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.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
        <!-- instructs the container to proxy 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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

为了创建那么个代理,在bean定义中插入子元素<aop:scoped-proxy/>即可(参考选择代理的类型41 基于xml的配置)。为什么请求、会话、全局会话及自定义级别的作用域的bean需要<aop:scoped-proxy/>元素呢?让我们检验下面的单例bean的定义,并与前面的定义做对比(下面的userPreferences的定义目前来说是不完整的)。

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

<bean id="userManager" class="com.foo.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

在这个例子中,单例userManager被注入了一个Http会话作用域的对象userPreferences。这里的显著点是userManager是一个单例:它将在每个容器中严格地实例化一次,并且它的依赖(此例中仅仅userPreferences一个)也只会注入一次。这就意味着userManager将只会操作同一个userPreferences对象,也就是一开始注入的那个。

这并不是我们把短周期的bean注入长周期的bean想要的结果,例如,注入一个Http会话bean到一个单例bean中。不如说,我们只需要一个userManager对象,但是需要与特定会话关联的userPreferences对象。这样,容器创建一个与UserPreferences类相同的公共接口的对象(理想状态是UserPreferences的一个实例),它可以从作用域机制(Http请求、会话等)中获取真实的UserPreferences对象。然后把这个代理对象注入到userManager中,且userManager不会意识到这是一个代理。在这个案例中,当UserManager实例调用依赖注入的UserPreferences对象的方法时,实际上是调用的这个代理的方法。这个代理然后从Http会话中获取真实的UserPreferences对象,并在这个真实的UserPreferences对象上委托方法调用。

因此,当注入请求、会话和全局会话作用域的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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
选择代理的类型

默认地,当spring容器创建一个标记了<aop:scoped-proxy/>元素的bean的代理时,会创建一个基于CGLIB的代理。

  • CGLIB代理只能拦截公共方法调用!不要在那个代理上调用非公共方法,那样不会委托给真实的目标对象。

替代方案,可以配置spring容器创建标准的JDK基于接口的代理,通过指定<aop:scoped-proxy/>元素的proxy-target-class属性的值为false。使用JDK基于接口的代理意味着在classpath上不需要额外的库了。但是,这也意味着原类必须实现至少一个接口,并且注入了这个bean的所有合作者都必须通过这个bean的一个接口引用它。

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.foo.DefaultUserPreferences" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.foo.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

更多关于选择基于类的代理还是选择基于接口的代理的详细信息,请参考[11.6 代理机制](#proxying mechanisms)。

自定义作用域

作用域机制是可以扩展的,我们可以定义自己的作用域,甚至可以重新定义已存在的作用域,不过后者被认为是坏的实践,并且不能重写内置的单例和原型作用域。

创建自定义作用域

为了让自定义的作用域集成到spring容器中,需要实现org.springframework.beans.factory.config.Scope接口,这个接口会在接下来讲到。关于怎么实现自己的作用域,请参考spring自己提供的关于Scope的实现和Scopejavadocs,这里更详细地解释了需要实现的方法。

Scope接口有四个方法,从作用域中获取对象、移除对象和允许它们被销毁。

下面的方法用来从作用域中返回对象。例如,会话作用域的实现返回会话作用域的bean(如果不存在,则会返回一个新的实例,并绑定到会话中,为了将来使用)。

Object get(String name, ObjectFactory objectFactory)
 
 
  • 1
  • 1

下面的方法用来从作用域中移除对象。例如,会话作用域的实现从会话中移除会话作用域的bean。对象应该被返回,但是如果指定名字的对象未找到可以返回null。

Object remove(String name)
 
 
  • 1
  • 1

下面的方法用来注册作用域在销毁时或其中对象被销毁时应该执行的回调。参考javadocspring的作用域的一个实现获取关于销毁回调的更多信息。

void registerDestructionCallback(String name, Runnable destructionCallback)
 
 
  • 1
  • 1

下面的方法用来从作用域中获取会话(conversation,非session)标识。每个作用域的标识都不一样。对于会话作用域的实现,这个标识可以是会话id(session id)。

String getConversationId()
 
 
  • 1
  • 1
使用自定义作用域

在写完或测试完一个或多个自定义作用域的实现后,需要让spring容器识别到新的作用域。下面的方法是注册一个新的作用域到spring容器的核心方法。

void registerScope(String scopeName, Scope scope);
 
 
  • 1
  • 1

这个方法定义在ConfigurableBeanFactory接口中,它允许大部分ApplicationContext的具体实现,这些实现通过BeanFactory属性与Spring联系在一起。

registerScope(..)方法的第一个参数是唯一与一个作用域关联的名字,例如,spring容器自己的singletoprototype。它的第二个参数是具体的自定义的Scope的实现的一个实例,也就是希望注册并使用的。

假设你写了自定义的Scope实现,并像下面这样注册了它。

  • 下面的例子使用了已经包含在spring中但默认未注册的SimpleThreadScope。这种方式同样适用于自定义的Scope实现。
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
 
 
  • 1
  • 2
  • 1
  • 2

然后就可以按照作用域的规则使用自定义的作用域了:

<bean id="..." class="..." scope="thread">
 
 
  • 1
  • 1

使用自定义的Scope实现,并不一定要编程式地注册,也可以像下面这样声明,使用CustomScopeConfigurer类:

<?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.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="thread">
                    <bean class="org.springframework.context.support.SimpleThreadScope"/>
                </entry>
            </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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 当放置<aop:scoped-proxy/>在一个FactoryBean实现中时,这个bean本身就有作用域了,它并不是从getObject()返回的对象。

7.6 bean的特性

7.6.1 生命周期回调

为了与容器中bean的生命周期管理交互,可以实现Spring的InitializingBeanDisposableBean接口。容器会在初始化和销毁bean时调用前者的afterPropertiesSet()和后者destroy()方法执行相应的动作。

  • 在现代spring应用中,JSR-250的@PostConstruct@PreDestroy注解通常被认为是接收生命周期回调更好的实践。使用这些注解意味着bean不会与spring的接口耦合。更详细的信息请参考7.9.8 @PostConstruct和@PreDestroy
    如果不想使用JSR-250的注解,但又不想不与spring耦合,可以在bean定义时使用init-method和destroy-method指定其初始化和销毁方法。

spring内部使用BeanPostProcessor的实例处理它能找到的任何回调接口并调用合适的方法。如果需要定制一些spring未提供的又可以立即使用的功能或生命周期行为,可以直接实现自己的BeanPostProcessor 。更多信息请参考7.8 容器扩展点

除了初始化和销毁回调,spring管理的对象也可以实现Lifecycle接口,从而参与到容器本身的生命周期的启动和关闭中。

生命周期回调接口将在下面描述。

初始化回调

org.springframework.beans.factory.InitializingBean接口允许bean的所有必要属性都被容器设置好后执行初始化操作。InitializingBean接口只有一个方法:

void afterPropertiesSet() throws Exception;
 
 
  • 1
  • 1

其实并不建议使用InitializingBean 接口因为没有必要把代码与spring耦合起来。可以使用@PostConstruct注解或在bean定义中指定初始化方法作为替代方案。在xml配置中,可以使用init-method属性指定一个无参的方法。在Java配置中,可以使用@BeaninitMethod属性,参考接收生命周期回调。例如,下面这种方式不会与spring耦合:

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
 
 
  • 1
  • 1
public class ExampleBean {

    public void init() {
        // do some initialization work
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上面这种方式严格来说与下面的方式是一样的:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
 
 
  • 1
  • 1
public class AnotherExampleBean implements InitializingBean {

    public void afterPropertiesSet() {
        // do some initialization work
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
销毁回调

实现了org.springframework.beans.factory.DisposableBean接口的bean允许在包含这的容器被销毁时获取一个回调。DisposableBean接口只有一个方法:

void destroy() throws Exception;
 
 
  • 1
  • 1

其实并不建议使用DisposableBean回调接口,因为没必要把代码与spring耦合。可以使用@PreDestroy注解或在bean定义中指定销毁方法作为替代方案。在xml配置中,可以使用<bean/>元素的destroy-method属性。在Java配置中,可以使用@BeandestroyMethod属性,参考接收生命周期回调。例如,下面这种方式不会与spring耦合:

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

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

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上面这种方式严格来说与下面是一样的:

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

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

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • <bean>元素的destroy-method属性可以被赋予一个特殊的值“inferred”,它可以引导spring自动检测类中的公共closeshutdown方法(任何实现了java.lang.AutoCloseablejava.io.Closeable接口的类会匹配)。这个特殊的值“inferred”也可以设置在 <beans>元素的default-destroy-method属性上,这样会使所有的bean都执行这种行为(参考默认的初始化和销毁方法)。注意,这在Java配置中是默认行为。
默认的初始化和销毁方法

当不使用spring指定的InitializingBeanDisposableBean回调接口编写初始化和销毁回调方法时,我们一般会把这些方法命名为init(), initialize(), dispose()等等。理想状态下,在整个项目中这些方法的名字都是标准化的,以便所有的开发者都使用相同的名字并保证一致性。

可以配置spring容器在每个bean上自动寻找初始化和销毁的回调方法。这意味着,开发者可以直接使用init()作为初始化方法而不用为每一个bean配置init-method=”init”了。容器会在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.");
        }
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
<beans default-init-method="init">

    <bean id="blogService" class="com.foo.DefaultBlogService">
        <property name="blogDao" ref="blogDao" />
    </bean>

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

顶级元素<beans/>上的default-init-method属性会使得spring容器自动识别bean中的init方法作为初始化回调方法。当bean创建并装配后,如果它有这个方法就会在合适的时候被调用。

同样地,可以配置顶级元素<beans/>上的default-destroy-method属性声明销毁的回调方法。

当bean存在与上述约定不一样的回调方法名称时,可以使用<bean/>元素本身的init-methoddestroy-method代替默认的方法。

spring容器保证在bean的所有依赖都注入完毕时立马调用初始化回调方法。因此,初始化方法是调用在原生(非原生即代理)的bean引用上,这意味着AOP的拦截器还没开始生效。目标bean首先被完全创建,然后AOP代理及其拦截器链才被应用。如果目标bean与代理是分开定义的,我们甚至可以绕过代理直接与原生bean进行交互。因此,在初始化方法上应用拦截器是矛盾的,因为,这样做会把目标bean的生命周期与它的代理或拦截器耦合在一起,在直接使用原生bean时就会有很奇怪的语法。

组合使用生命周期机制

从spring 2.5 开始,有三种方式可以控制bean的生命周期行为:InitializingBeanDisposableBean回调接口;自定义的init()destroy()方法;@PostConstruct@PreDestroy注解。可以混合使用这些方式来控制给定的bean。

  • 如果一个bean上配置了多种方式,并且每一种方式都配置为不同的方法名称,那么每一个配置的方法将按下面的顺序执行。但是,如果配置的相同的方法名称,那么只会执行一次。

同一个bean配置了多种方式,对于初始化方法将按以下顺序调用:

  • @PostConstruct注解的方法
  • InitializingBean接口定义的afterPropertiesSet()方法
  • 自定义的init()方法

销毁方法也是同样的顺序:

  • @PreDestroy注解的方法
  • DisposableBean接口定义的destroy()方法
  • 自定义的destroy()方法
启动和停止回调

Lifecycle接口为任何有自己生命周期需求的对象定义了一些基本的方法(例如,启动和停止一些后台进程)。

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

所有spring管理的对象都可以实现这个接口。然后,当ApplicationContext接收到启动和停止的信号时,例如,运行时的stop/restart场景,它会级联调用此上下文内所有的Lifecycle实现。它是通过委托LifecycleProcessor实现的:

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注意,LifecycleProcessor本身扩展了Lifecycle接口,同时添加了两个另外的方法用于在上下文刷新和关闭时做出响应。

  • 注意,org.springframework.context.Lifecycle接口只能显式地接收启动和停止的通知,而不会在因上下文刷新导致自动启动时接收到通知。考虑使用org.springframework.context.SmartLifecycle接口实现更细粒度的控制,它可以检测到自动启动(当然,也包括启动阶段)。同样地,请注意停止通知并不保证在销毁之前来临:正常停止时,所有的Lifecycle bean将会在销毁回调前首先接收到停止通知,在上下文生命周期内的热刷新或强制刷新,仅仅销毁方法会被调用。

启动和停止的调用顺序可能是很重要的。如果两个对象存在依赖关系,那么依赖方将在其依赖启动之后启动,并在其依赖停止之前停止。但是,有些时候依赖并不是直接的。你可能只知道一些特定类型的对象将在另外一些类型的对象之前启动。在这种情形中,SmartLifecycle接口有另外一种选择,它的父接口Phased中定义了一个getPhase()方法。

public interface Phased {

    int getPhase();

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

启动时,低相位(phase)的对象先启动;停止时,顺序反过来。因此,实现了SmartLifecycle接口并且getPahse()返回Integer.MIN_VALUE的对象将先启动后停止。反过来,带有Integer.MAX_VALUE相位的对象将会后启动先停止(可能是因为它依赖于其它进程运行)。对于那些正常的未实现SmartLifecycle接口的Lifecycle对象,它们的相位默认值为0。因此,负相位的对象将在这些正常对象启动之前启动,停止之后停止,反之亦然。

可以发现SmartLifecycle中的stop方法可以接受一个回调。任何实现必须在其停止线程完成以后调用回调(Runnable)的run()方法。在必须的时候可以异步停止,因为LifecycleProcessor接口默认的实现DefaultLifecycleProcessor会等到超时直到每个相位的对象都调用了那个回调。默认每个相位的超时时间是30秒。可以在上下文中定义一个名叫“lifecycleProcessor”的bean重写默认的生命周期处理器。如果仅仅只修改超时时间,那么下面的定义就足够了:

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- timeout value in milliseconds -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

如前所述,LifecycleProcessor接口也定义了上下文刷新(onRefresh())和关闭(onClose())的方法。后者会简单地驱动关闭进程即使stop()方法被显式地调用了,但是它会发生在上下文关闭的时候。另一方面,刷新方法会带来SmartLifecycle bean的另一个功能。在上下文被刷新时(在所有对象都被实例化和初始化之后),这个方法会被调用,并且那时默认的生命周期处理器会检查每个SmartLifecycle对象的isAutoStartup()方法返回的布尔值。如果为true,那么这个对象会在那时被启动而不会等待上下文显式地调用或它自己的start()方法(不像上下文的刷新,标准的上下文实现并不会自动启动)。相位的值及依赖关系将会按与上面描述相同的方式检查启动顺序。

在非web应用中优雅地关闭spring的IoC容器
  • 这节只针对非web应用。spring中基于web的ApplicationContext的实现已经可以优雅地关闭容器了当相关的web应用停止时。

如果在非web应用环境中使用spring的IoC容器,例如,在富客户端桌面环境中,注册一个JVM的关闭钩子(shutdown hook)。这样做可以保证优雅地关闭并调用相关单例bean的销毁方法从而使得所有的资源都被释放。当然,你必须要正确地配置并实现这些销毁方法。

为了注册一个关闭钩子,调用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(
                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...

    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
ApplicationContextAware和BeanNameAware

ApplicationContext创建了一个实现了org.springframework.context.ApplicationContextAware接口的对象实例时,这个实例就拥有了ApplicationContext的引用。

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

因此,bean可以编程式地操作创建了它的ApplicationContext,可以通过ApplicationContext接口,也可以把它强制转换成它的子类,比如ConfigurableApplicationContext,这样可以暴露更多的功能。一种使用方式是提取其它bean。有时这个能力很有用,但是,通常来说应该避免这样操作,因为这会使得代码与spring耦合,并且不符合控制反转的原则,而控制反转中合作者是以属性的方式提供的。ApplicationContext还提供了一些诸如访问文件资源、发布应用事件、访问消息源的方法。关于附加功能的描述请参考7.15 ApplicationContext的附加功能

从spring 2.5 开始,自动装配就是另外一种获取ApplicationContext引用的方式。传统的constructorbyType装配模式(参考7.4.5 自动装配合作者)能够分别为构造方法参数或setter方法参数提供一个ApplicationContext类型的依赖。另外,也可以使用注解方式自动装配字段或方法参数。如果这样做了,ApplicationContext就会被注入到一个字段、构造方法参数或者其它需要ApplicationContext类型的方法参数,当然这些字段或参数得带有@Autowired注解。更多信息请参考7.9.2 @Autowired

ApplicationContext创建了一个实现了org.springframework.beans.factory.BeanNameAware接口的类时,这个类就拥有了一个定义在它关联的对象定义中的名字的引用。

public interface BeanNameAware {

    void setBeanName(string name) throws BeansException;

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

这个方法会在操作了正常的属性之后但是在初始化方法之前运行,不管是InitializingBeanafterPropertiesSet()方法还是自定义的init()方法

7.6.3 其它Aware接口

除了上面讨论的ApplicationContextAwareBeanNameAware接口,spring还提供了一系列的Aware接口使得bean可以从容器获取需要的资源(依赖)。大部分重要的Aware接口汇总在下面了——通常,它们的名字已经可以很好地解释它们的依赖类型了。

表7.4 Aware接口

名称注入的依赖描述
ApplicationContextAware声明ApplicationContext7.6.2 ApplicationContextAware和BeanNameAware
ApplicationEventPublisherAware封闭的ApplicationContext的事件发布者7.15 ApplicatinContext的附加功能
BeanClassLoaderAware用于加载bean的类加载器7.3.2 实例化bean
BeanFactoryAware声明BeanFactory7.6.2 ApplicationContextAware和BeanNameAware
BeanNameAware定义的bean的名字7.6.2 ApplicationContextAware和BeanNameAware
BootstrapContextAware容器运行于其中的资源适配器BootstrapContext。一般只用于JCA所在的上下文中。32 JCA CCI
LoadTimeWeaverAware加载时处理类定义的织入者11.8.4 加载时使用AspectJ织入
MessageSourceAware配置的用于解决消息的策略(用于支持参数化及国际化)7.15 ApplicatinContext的附加功能
NotificationPublisherAwarespring中JMX通知发布者31.7 通知
PortletConfigAware当前容器运行于其中的PortletConfig。仅用于web上下文中。25 Portlet MVC 框架
PortletContextAware当前容器运行于其中的PortletContext。仅用于web上下文中。25 Portlet MVC 框架
ResourceLoaderAware配置的用于低级别访问资源的加载器8 资源
ServletConfigAware当前的容器运行于其中的ServletConfig。仅用于web上下文中。22 Web MVC 框架
ServletContextAware当前的容器运行于其中的ServletContext。仅用于web上下文中。22 Web MVC 框架

再次注意,使用这些接口将使你的代码与spring的API耦合,并且不会遵循控制反转的原则。因此,他们被推荐用于需要编程式访问容器的基础设施bean。

7.7 bean定义的继承

bean的定义中可以包含大量的配置信息,包括构造方法参数、属性值以及容器指定的信息,比如初始化方法、静态工厂方法名等等。子bean定义可以从父定义中继承配置信息。子定义可以重写这些值,也可以在需要的时候添加新的。使用父子定义可以少码几个字,这是模板的一种形式。

如果编程式地使用ApplicationContext,子定义可以使用ChildBeanDefinition类。不过大部分用户并不这样使用,而是以类似ClassPathXmlApplicationContext的形式声明式地配置。在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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

如果没有指定子定义的class,则会使用父定义的class,不过也可以重写它。后者必须保证子定义中的class与父定义中的class兼容,即它可以接受父定义中的属性值。

子定义可以从父定义继承作用域、构造方法参数值、属性值以及方法,也可以为它们添加新值。任何在子定义中定义的作用域、初始化方法、销毁方法或者静态工厂方法都会覆盖父定义中的配置。

其余的配置总是取决于子定义:依赖、自动装配模式、依赖检查、单例、延迟初始化。

上面的例子中在父定义中显式地使用了abstract属性。如果父定义不指定类,那么必须显式地标记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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

父bean不能被实例化,因为它本身并不完整,并且被显式地标记为abstract了。当一个bean被定义为abstract,则它只能用做定义子bean的模板。如果试着使用abstract的父bean,比如通过另一个bean引用它或调用getBean()方法获取它,都会报错。同样,容器内部的preInstantiateSingletons()方法也会忽略定义为abstract的bean。

  • ApplicationContext默认预实例化所有的单例。因此,如果有一个仅用于模板的bean,并且指定了类,请保证把abstract属性设置为true,否则容器会尝试预实例化它。

7.8 容器扩展点

通常,开发者不需要自己实现ApplicationContext。然而,Spring提供了一些特殊的集成接口,实现这些接口可以扩展Spring。下面几个章节将介绍这些集成接口。

7.8.1 使用BeanPostProcessor自定义bean

可以实现BeanPostProcessor接口定义的回调方法,从而提供自己的实例化逻辑、依赖注入逻辑等。如果想要在spring容器完成了实例化、配置及初始化bean前后实现自定义的逻辑,那么可以插入一个或多个BeanPostProcessor的实现。

可以配置多重BeanPostProcessor实例,并且可以设置它们的order属性控制它们的执行顺序,不过只有当这些实例也实现了Ordered接口才可以。更多信息请参考BeanPostProcessorOrdered接口的javadoc,也可以参考下面关于BeanPostProcessor的编程式注册的说明。

  • BeanPostProcessor操作对象实例,也就是说IoC容器实例化bean实例后BeanPostProcessor才工作。
    BeanPostProcessor是每个容器作用域的,即使使用容器继承也不行。声明在一个容器内的BeanPostProcessor只能处理那个容器内的bean。换句话说,一个容器内的bean不会被另一个容器的BeanPostProcessor处理,即使两个容器处于同一继承体系中。
    为了改变实际的bean定义,需要使用BeanFactoryPostProcessor,参考7.8.2 使用BeanFactoryPostProcessor自定义配置元数据

org.springframework.beans.factory.config.BeanPostProcessor接口包含两个回调方法。如果容器中一个类被注册为后处理器,那么对于容器创建的每一个bean实例,在它们初始化前后都会获得这个后处理器的一个回调。这个后处理器可以对bean实例采取任何行动,也可以完全忽略回调。一个bean后处理器一般用于检查回调接口或用代理包装bean。一些Spring的AOP基础类为了提供代理包装的逻辑都被实现为后处理器。

(译者注:此处要理解实例化(instantiation)和初始化(initialization)的区别,bean是先实例化再初始化的,后(置)处理器是针对实例化来说的,它是在bean实例化后起作用,但是它内部的两个方法分别在bean初始化前后执行拦截的作用)

ApplicationContext会自动检测所有实现了BeanPostProcessor接口的所有bean,接着把它们注册为后处理器,以便在其它bean创建的时候调用它们。后处理器bean可以像容器中其它bean一样部署。

注意,当使用@Bean工厂方法配置一个BeanPostProcessor实现类时,这个工厂方法的返回值类型必须是这个实现类本身或至少得是org.springframework.beans.factory.config.BeanPostProcessor接口,清晰地表明这个bean是后处理器的本性。否则,ApplicationContext在完全创建它之前不能通过类型检测到它。因为BeanPostProcessor需要早点被实例化以便应用到其它bean的初始化中,所以早期的类型检测是必要的。

  • 虽然我们推荐使用ApplicationContext自动检测BeanPostProcessor,但是也可以使用ConfigurableBeanFactoryaddBeanPostProcessor方法编程式地注册。这在注册之前处理条件逻辑或者在同一继承体系上下文之间复制后处理器很有用。注意,编程式注册的BeanPostProcessor并不遵从Ordered接口的顺序,那是因为它们注册的顺序就是它们执行的顺序。同样地,编程式注册的BeanPostProcessor总是在通过自动检测注册的BeanPostProcessor之前执行。

  • 实现了BeanPostProcessor接口的类是很特殊的,并且会被容器特殊对待。所有BeanPostProcessor和它们直接引用的bean会在启动时实例化,且作为ApplicationContext启动的特殊部分。然后,所有的BeanPostProcessor按顺序注册并应用到所有后来的bean上。因为AOP的自动代理也被实现为BeanPostProcessor本身,所以无论是BeanPostProcessor还是它们直接引用的bean都有获得自动代理的资格,且不会有切面织入它们。对于这些bean,你应该会看到一条信息日志消息:“Bean foo is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)”。注意,如果有bean通过自动装配或@Resource(可能会变成自动装配)被装配到了BeanPostProcessor,Spring在通过类型匹配查找依赖时可能会访问非预期的bean,从而使它们具有自动代理或其它后处理的资格。例如,如果通过不带name属性的@Resource注解在一个字段或setter方法上声明了一个依赖,并且这个字段或setter方法的名字并不直接与一个bean的名字通信,然后Spring将通过类型去匹配它们。

下面的例子展示了如何在ApplicationContext中编写、注册并使用BeanPostProcessor

例子,BeanPostProcessor风格的Hello World

第一个例子展示了基本用法,它显示了一个自定义的BeanPostProcessor实现在bean在被创建时调用它们的toString()方法并把结果打印到控制台。

请看下面自定义的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;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
<?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.xsd
        http://www.springframework.org/schema/lang
        http://www.springframework.org/schema/lang/spring-lang.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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

从上面可以看到,InstantiationTracingBeanPostProcessor是如此简单的定义。它甚至不需要一个名字,而且因为它是一个bean,它也可以像其它bean一样被依赖注入。(上面的例子也展示了如何通过Groovy脚本语言定义一个bean。关于Spring的动态语言支持的详细描述请参考35 动态语言支持。)

下面简单展示了Java应用如何执行前面的代码和配置:

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

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面应用的输出看起来像下面这样

Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
 
 
  • 1
  • 2
  • 1
  • 2
例子:RequiredAnnotationBeanPostProcessor

使用回调接口、注解或自定义的BeanPostProcessor实现类是一种扩展Spring容器的常用方式。Spring的RequiredAnnotationBeanPostProcessor是其中一个例子——它可以保证bean中被注解@Required标记的属性能被依赖注入一个值。

7.8.2 使用BeanFactoryPostProcessor自定义配置元数据

接下来我们看的一个扩展点是org.springframework.beans.factory.config.BeanFactoryPostProcessor。这个接口的语法与BeanPostProcessor类型,主要区别是:BeanFactoryPostProcessor操作的是配置元数据,也就是说,Spring容器允许BeanFactoryPostProcessor读取配置元数据并可能在容器实例化这些bean之前改变它们,当然,BeanFactoryPostProcessor除外。

可以配置多重BeanFactoryPostProcessor,也可以通过设置它们的order属性控制它们的执行顺序,当然,只有实现了Ordered接口才能设置顺序。更多BeanFactoryPostProcessorOrdered接口的详细信息请参考javadoc。

  • 如果想改变实际的bean实例,使用BeanPostProcessor即可。尽管在技术上使用BeanFactoryPostProcessor也是可以的,但是这样做会导致不成熟的bean实例化,违背了标准的容器生命周期,并可能导致负面影响,比如绕过bean的后置处理。
    同样地,BeanFactoryPostProcessor也是每个容器作用域的,即使在容器继承体系中也是如此。在一个容器中定义的BeanFactoryPostProcessor,只能作用于这个容器的bean定义,不能处理另一个容器的bean定义,即使两个容器羽毛球同一个继承的体系也不可以。

ApplicationContext中定义的BeanFactoryPostProcessor是自动执行的,以便作用于这个容器中定义的元数据。Spring包含很多预先定义好的BeanFactoryPostProcessor,比如PropertyOverrideConfigurePropertyPlaceholderConfigure。也可以使用自定义的BeanFactoryPostProcessor,比如,用于注册自定义的属性编辑器。

ApplicationContext会自动检测所有实现了BeanFactoryPostProcessor接口的bean,并在适当的时候使用这些bean作为bean工厂后置处理器。可以像部署其它bean一样部署这些后置处理器。

  • BeanPostProcessor一样,一般不会配置BeanFactoryPostProcessor延迟初始化。如果没有别的bean引用一个Bean(Factory)PostProcessor,那这个后置处理器不会被实例化。因此,设置延迟初始化会被忽略,并且Bean(Factory)PostProcessor会自动初始化,即使在<beans/>元素设置default-lazy-inittrue也没用。
例子:类名替代PropertyPlaceholderConfigurer

如果使用标准的Java Properties形式从外部文件中加载属性值,那么需要使用PropertyPlaceholderConfigurer。这样做可以定制环境相关的属性值,比如数据库URL和密码,而不需要冒很大的风险去修改容器中XML的定义。

查看下面的XML配置片段,配置DataSource时使用了占位符。这个例子展示了如何从外部文件配置属性。在运行时,PropertyPlaceholderConfigurer会替代其中的一些属性。这些被替代的占位符以${property-name}的形式定义,这与Ant/log4j/JSP EL的风格是一致的。

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations" value="classpath:com/foo/jdbc.properties"/>
</bean>

<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>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

实际的值是以标准Java Properties的形式存在于另一个文件中的:

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

因此,在运行时字符串${jdbc.username}会被替换为值’sa’,其它的占位符也是按一样的规则替换。PropertyPlaceholderConfigurer会检查一个bean定义的大部分属性。另外,占位符的前缀和后缀是可以自定义的。

Spring 2.5中引入的context命名空间中有专门配置占位符的元素。一个或多个位置可以使用逗号分割开。

<context:property-placeholder location="classpath:com/foo/jdbc.properties"/>
 
 
  • 1
  • 1

PropertyPlaceholderConfigurer不仅查找我们指定的Properties文件中的属性值。默认地,如果在指定的文件中没有找到它还会检查Java的系统属性。设置它的systemPropertiesMode属性可以改变它的行为,包含以下三个值:

  • never(0):从不检查系统属性。
  • fallback(1):如果指定文件中没有找到才检查系统属性。这是默认值。
  • override(2):在尝试查找指定的文件前先检查系统属性。这允许系统属性覆盖其它的属性来源。

更多信息请参考PropertyPlaceholderConfigurer的javadoc。

  • 可以使用PropertyPlaceholderConfigurer去替换类名,这在运行时指定实现类有时候会很有用。例如:

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <value>classpath:com/foo/strategy.properties</value>
    </property>
    <property name="properties">
        <value>custom.strategy.class=com.foo.DefaultStrategy</value>
    </property>
    </bean>
    
    <bean id="serviceStrategy" class="${custom.strategy.class}"/>
    
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果在运行时这个类不能被解析为正确的类,那么这个bean会创建失败,对于非延迟初始化的bean,这种失败发生在ApplicationContextpreInstantiateSingletons()期间。

PropertyOverrideConfigurer

PropertyOverrideConfigurer,另外一个bean工厂后置处理器,与PropertyPlaceholderConfigurer类似,但又不像后者,原始的bean定义的属性可以有默认值或没有值。如果重写的Properties文件没有合适的值,那就使用默认值。

注意,bean定义是不会意识到被重写的,所以从XML定义是看不出重写配置器被使用的。如果有多个PropertyOverrideConfigurer实例定义了同一个属性的不同的值,那么最后一个会起作用,因为重写机制的存在。

属性文件按下面的形式编写:

beanName.property=value
 
 
  • 1
  • 1

例如:

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb
 
 
  • 1
  • 2
  • 1
  • 2

这个例子适用于包含了一个dataSource的bean的定义,且它包含driverurl属性。

也支持复合的属性名,只要除了最后一个属性之前的组件不为空即可。例如:

foo.fred.bob.sammy=123
 
 
  • 1
  • 1

它表示foo有一个fred属性,fred有一个bob属性,bob有一个属性sammy,这个sammy属性会被设置成123

  • 注意,重写的值总是字面值,它们不会被翻译成bean的引用,这种转换也适用于XML定义中指定的bean引用的原始值。

Spring 2.5中引入的context命名空间中有专门配置属性重写的元素。

<context:property-override location="classpath:override.properties"/>
 
 
  • 1
  • 1

(译者注:关于BeanPostProcessorBeanFactoryPostProcessor的使用请参考本人另一篇博文:http://blog.csdn.net/tangtong1/article/details/51916679

7.8.3 使用FactoryBean自定义实例化逻辑

为对象实现org.springframework.beans.factory.FactoryBean接口,这些对象是他们自己的工厂。

FactoryBean接口是Spring窗口实例化逻辑的插入点。如果有一个非常复杂的初始化代码,它用Java能比XML更好地表述,那么就可以创建一个自己的FactoryBean,编写复杂的初始化逻辑在那个类中,并插入到窗口中。

FactoryBean接口提供了三个方法:

  • Object getObject():返回这个工厂创建的一个实例。这个实例可以共享,由这个工厂单例还是原型决定。
  • boolean isSingleton():如果返回的是单例则返回true,否则返回false
  • Class getObjectType():返回getObject()方法返回的对象的类型,如果事先不知道类型那就返回null

FactoryBean的概念和接口在Spring框架中被大量使用,Spring自己至少使用了50个FactoryBean的实现。

当需要从容器获得实际的FactoryBean的实例时,调用ApplicationContextgetBean()方法时在bean的id前面加上&符号即可。所以对于一个id为myBeanFactoryBean,调用getBean(“myBean”)会返回FactoryBean创建的实例,然而,调用getBean(“&myBean”)则会返回FactoryBean的实例本身。

(译者注:关于FactoryBean的使用请参考本人另一篇博文:http://blog.csdn.net/tangtong1/article/details/51920069

7.9 基于注解的容器配置

注解形式比XML形式更好吗?

注解形式的引入引起了一个话题,它是否比XML形式更好。简单的回答是视情况而定。详细的回答是每一种方式都有它的优缺点,通常由开发者决定哪种方式更适合他们。由于他们定义的方式,注解提供了更多的上下文声明,导致了更短更简明的配置。然而,XML形式装配组件不会涉及到它们的源码或者重新编译它们。一些开发者更喜欢亲近源码,但另一些则认为注解类不是POJO,且配置很分散,难以控制。

不管做出什么选择,Spring都支持两种风格且可以混用它们。另外,通过Java配置的方式,Spring可以让注解变得非侵入式,不会触碰到目标组件的源码。而且,所有的配置方式Spring Tool Suite都支持。
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

一种XML形式的替代方案是使用基于注解的配置,它依赖于字节码元数据,用于装配组件并可取代尖括号式的声明。不同于使用XML描述一个bean,开发者需要把配置移动到组件类本身,并给相关的类、方法及字段声明加上注解。像例子:RequiredAnnotationBeanPostProcessor中提及的一样,联合使用BeanPostProcessor和注解是扩展Spring容器的一种常用方法。例如,Spring 2.0引入了@Required注解,它强制属性必须获取一个值。Spring 2.5遵循同样的方式驱动依赖注入。本质上,@Autowired注解提供了相同的能力,如7.4.5 自动装配合作者中描述的一样,不过它提供了更细粒度的控制和更广泛的适用性。Spring 2.5也支持JSR-250的注解,比如@PostConstruct@PreDestroy。Spring 3.0支持JSR-330的注解,它们位于javax.inject包下,比如@Inject@Named。更详细的信息可以在相关章节中找到。

  • 注解形式的注入在XML形式之前执行,因此,如果同时使用了两者,则XML形式的注入会覆盖注解形式的注入。

可以一个一个地注册这些bean,也可以隐式地注册它们,使用下面的配置即可(注意,请包含context命名空间)。

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(隐式注册的后置处理器包括AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor以及前面提到的RequiredAnnotationBeanPostProcessor。)

  • <context:annotation-config/>只能寻找它所定义的上下文中的注解。这意味着,如果在一个WebApplicationContext中为一个DispatcherServlete配置了<context:annotation-config/>,那么它只会检测controller中的@Autowired,而不会检测service。更多信息请参考22.2 DispatcherServlet
7.9.1 @Required
  • JSR-330的@Inject注解可以替换下面例子中Spring的@Autowired注解。更多详情请点这里

可以在构造方法上使用@Autowired

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 从Spring 4.3开始,如果目标bean只有一个构造方法,则@Autowired的构造方法不再是必要的。如果有多个构造方法,那么至少一个必须被注解以便告诉容器使用哪个。

也可以在setter方法上使用@Autowired

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

也可以应用在具有任意名字和多个参数的方法上:

public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

也可以应用在字段上,甚至可以与构造方法上混用:

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

也可以从ApplicationContext中提供特定类型的所有bean,只要添加这个注解在一个那种类型的数组字段或方法上即可:

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

同样适用于集合类型:

public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 如果需要数组或list中的元素按顺序排列的话,可以让这些bean实现org.springframework.core.Ordered接口或使用@Order注解或标准的@Priority注解。

甚至Map也可以被自动装配,只要key的类型是String就可以。Map的value将包含所有的特定类型的bean,并且key会包含这些bean的名字。

public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

默认地,如果没有候选的bean则自动装配会失败。这种默认的行为表示被注解的方法、构造方法及字段必须(required)有相应的依赖。也可按下面的方法改变这种行为。

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired(required=false)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 每个类只能有一个构造方法被标记为required,但可以有多个非必须的构造方法被注解。这种情况下,每个构造方法都会考虑候选者,且Spring使用最贪婪的那个构造方法,它的依赖都能被满足,并且有最多个的参数。
    @Autowiredrequired属性比@Required注解更推荐使用。这个required属性表示如果不能被自动装配那么这个属性是非必须的、会被忽略的。另一方面,@Required则更强势,它要求这个属性必须被设置不管容器以什么样的方式支持它。如果没有值被注入,就会抛出异常。

也可以把@Autowired用在那些著名的可解析的依赖的接口上:BeanFactory, ApplicationContext, Environment, ResourceLoader, ApplicationEventPublisher, 以及MessageSource。这些接口和它们扩展的接口,比如ConfigurableApplicationContextResourcePatternResolver,会自动解析,不需要特殊设置。

public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

@Autowired, @Inject, @Resource@Value注解都是被Spring的BeanPostProcessor处理的,这反过来意味着我们不能使用自己的BeanPostProcessorBeanFactoryPostProcessor类型来处理这些注解。这些类型必须通过XML或使用Spring的@Bean方法显式地装配。

7.9.3 使用@Primary微调基于注解的自动装配

因为基于类型的自动装配可能会导致多个候选者,所以对这种过程通常需要更多的控制。一种方式是使用Spring的@Primary注解。它表示如果存在多个候选者且另一个bean只需要一个特定类型的bean依赖时,就使用标记了@Primary注解的那个依赖。如果只有一个候选者那就直接使用那个候选者即可。

我们假设下面的配置,定义firstMovieCatalog作为主要的(primary)MovieCatalog

@Configuration
public class MovieConfiguration {

    @Bean
    @Primary
    public MovieCatalog firstMovieCatalog() { ... }

    @Bean
    public MovieCatalog secondMovieCatalog() { ... }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

使用这种配置,下面的MovieRecommender将装配firstMovieCatalog

public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

通信的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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog" primary="true">
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
7.9.4 使用限定符微调基于注解的自动装配

当一个类型有几个实例时使用@Primary是一种有效的方式。当需要对选择过程做更多的控制时,那就需要用到Spring的@Qualifier注解了。为指定的参数绑定一个限定的值,可以缩小类型匹配的范围,使用这种方式,这个值可以是一个很普通的描述性的值:

public class MovieRecommender {

    @Autowired
    @Qualifier("main")
    private MovieCatalog movieCatalog;

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

@Qualifier注解还能用在构造方法参数或方法参数上:

public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(@Qualifier("main")MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

通信的bean定义如下所示。带有限定符“main”的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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="main"/>

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="action"/>

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

bean的名字被认为是默认的限定符,它是一种后备的匹配。因此,可以定义一个bean的id为“main”,而不是内嵌qualifier元素,这会有一样的匹配结果。然而,尽管可以使用这种转换通过名字引用指定的bean,但是@Autowired默认是使用带有限定符的类型驱动的注入的。这意味着,带有bean名字的限定符总是会缩小类型匹配的语义,它们从语义上无法表达引用的是一个独立的bean的id。好的限定符是“main”、“EMEA”或者“persistent”等,它们表达了指定的组件是不同于bean的id的,它们可能会在匿名的bean定义中自动形成,就像前面的例子一样。

限定符也可以用于集合类型上,如前所述,比如Set<MovieCatalog>。在这种情况下,所有声明了指定限定符的bean都将被注入到集合中。这表示限定符并不是独立的,它们甚至可以有限定规则。例如,可以使用相同的限定符“action”定义多个MovieCatalog,所有的这些都会被注入到带有@Qualifier(“action”)注解的Set<MovieCatalog>中。

  • 如果想要表达是通过名字注入的,不要使用@Autowired,甚至不要通过@Qualifier的值引用一个bean的名字。可以使用JSR-250中的@Resource注解,它从语义上表达了通过bean的独立的名字引用它们,这样匹配过程跟声明的类型就没有关系了。不如说@Autowired有不同的语义:在通过类型选择到了候选的bean后,指定了的限定符就会被认为在这些已选择的bean中,例如,“account”限定符只会匹配那些拥有相同限定符标签的bean。
    对于那些定义为集合、Map或者数组类型的bean,@Resource也是一种很好的解决方案,直接通过名字引用指定的集合或数组。不过从4.3开始,集合、Map和数组类型也可以通过@Autowired类型匹配引用了,只要元素的类型被保存在@Bean的返回类型签名中或者集合继承体系中。在这种情况下,限定符可以用于选择相同类型的集合,就像前面描述的一样。从4.3开始,@Autowired也可以自引用注入,比如,引用当前正在注入的bean。注意,自引用只是一种后备选项,还是优先使用正常的依赖注入其它的bean。在那种情况下,自引用不参与到正常的候选者选择中,并且从来都不是主要的,相反,它们总是有最低的优先级。特别地,使用自引用作为最后的手段,例如,通过bean的事务代理调用同一个实例的其它方法:在这种情况下考虑把受影响的方法提取到一个分离的委托中。作为替代方案,@Resource可以通过独立的名字获取到当前bean的一个代理。@Autowired也可以通过限定符应用到参数级别上,比如字段、构造方法或多个参数的方法。相反地,@Resource只能应用在字段或只有一个参数的setter方法上。结论是,如果注入的目标是构造方法或多参数的方法就使用带限定符的@Autowired吧。

也可创建自定义的限定符注解,只要定义一个注解并在其定义上添加@Qualifier即可:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

    String value();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然后就可以在字段或参数上使用自定义的限定符了:

public class MovieRecommender {

    @Autowired
    @Genre("Action")
    private MovieCatalog actionCatalog;
    private MovieCatalog comedyCatalog;

    @Autowired
    public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
        this.comedyCatalog = comedyCatalog;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

接着,提供候选的bean定义的信息。可以添加<qualifier/>标签作为<bean/>标签的子标签,并指定其typevalue去匹配自定义的限定符注解。类型通过这个注解的全路径匹配,当然,如果没有风险的话也可以使用其类名。两种方式如下所示:

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="Genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="example.Genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

7.10 类路径扫描及管理的组件中,我们将会看到一种在XML中配置限定符元数据的替代方案。特别地,请参考7.10.8 通过注解提供注解元数据

在一些情况下,使用没有值的注解可能就足够了。这非常有用当注解提供了一种更通用的目的,并且可以运用到不同的依赖上。例如,当没有网络时可能会需要一种offline的类别。第一步定义这个简单的注解:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然后,添加这个注解到将被自动装配的字段或属性上:

public class MovieRecommender {

    @Autowired
    @Offline
    private MovieCatalog offlineCatalog;

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后,这个bean定义就只需要限定符类型:

<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline"/>
    <!-- inject any dependencies required by this bean -->
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

也可以自定义限定符注解,使它们带有命名的属性或者替代简单的value属性。如果多个属性值被指定在一个将被装配的字段或参数上,那么bean的定义必须匹配所有的属性值。例如,请看下面的注解定义:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

    String genre();

    Format format();

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这种情况下Format是一个枚举:

public enum Format {
    VHS, DVD, BLURAY
}
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

被装配的字段使用这个自定义的限定符注解,它包含两个属性:genreformat

public class MovieRecommender {

    @Autowired
    @MovieQualifier(format=Format.VHS, genre="Action")
    private MovieCatalog actionVhsCatalog;

    @Autowired
    @MovieQualifier(format=Format.VHS, genre="Comedy")
    private MovieCatalog comedyVhsCatalog;

    @Autowired
    @MovieQualifier(format=Format.DVD, genre="Action")
    private MovieCatalog actionDvdCatalog;

    @Autowired
    @MovieQualifier(format=Format.BLURAY, genre="Comedy")
    private MovieCatalog comedyBluRayCatalog;

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

最后,这些bean的定义需要包含这些限定符。这个例子也展示了bean的meta属性可以使用<qualifier/>子元素代替。如果可以,优先使用<qualifier/>及其属性,但是,如果滑限定符自动装配机制会使用<meta/>标签提供的值,就像下面例子中最后两个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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Action"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Comedy"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="DVD"/>
        <meta key="genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="BLURAY"/>
        <meta key="genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
7.9.5 使用泛型作为自动装配限定符

除了@Qualifier注解,也可以使用Java的泛型类型作为一种显式的限定。例如,假设有如下配置:

@Configuration
public class MyConfiguration {

    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

假设,上述bean实现了泛型接口,例如Store<String>Store<Integer>,可以使用@Autowired装配Store接口,并且泛型会作为一种限定:

@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

泛型限定符也可以用于自动装配的List、Map或数组:

// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
7.9.6 CustomAutowireConfigurer

CustomAutowireConfigurer是一个BeanFactoryPostProcessor,它可以注册自定义的限定符注解类型,甚至它们没有被Spring的@Qualifier注解所注解。

<bean id="customAutowireConfigurer"
        class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
    <property name="customQualifierTypes">
        <set>
            <value>example.CustomQualifier</value>
        </set>
    </property>
</bean>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

AutowireCandidateResolver通过以下方式决定了自动装配的候选者:

  • 每个bean定义的auto-candidate
  • <beans/>元素上定义的default-autowire-candidates模式
  • 使用了@Qualifier注解或任何在CustomAutowireConfigurer中注册的自定义注解

当多个bean被限定为候选者时,主要决定因素如下:如果这些候选者中有一个bean定义上明确地设置了primay属性为true,那么它将被选择。

7.9.7 @Resource

Spring也支持使用JSR-250的@Resource注解在字段和setter方法上进行注入。这在Java EE 5和6中是一种通用的模式,例如,在JSF 1.2管理的bean或JAX-WS 2.0终端。Spring也同样支持这种模式来管理对象。

@Resource拥有一个name属性,默认地,Spring会把这个name属性的值解释为将要注入的bean的名字。换句话说,它按照名字的语法进行注入,如下例所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder")
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果没有提供名字,默认的名字从字段名或setter方法名派生而来。对于一个字段,它会取字段的名字,对于stter方法,它会取bean的属性名。所以,下面的方法会使用名字为“movieFinder”的bean注入到setter方法中。

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 这个注解提供的名字会被ApplicationContextCommonAnnotationBeanPostProcessor解析为一个bean的名字。如果显式地配置了Spring的SimpleJndiBeanFactory,这个名字也可以通过JNDI进行解析。但是,我们建议你使用默认的行为,简单地使用Spring的JNDI查找能力来间接寻址。

如果@Resource没有显式地指定名字,与@Autowired类似,它会寻找主要的(primary)类型匹配如果没有找到默认的名字,并解决众所周知的依赖:BeanFactory, ApplicationContext, ResourceLoader, ApplicationEventPublisher, MessageSource接口。

因此,下面的例子中customerPreferenceDao字段首先会寻找名字为customerPreferenceDao的bean,然后才会寻找接口CustomerPreferenceDao的主要的类型匹配。同样地,“context”字段会寻找ApplicationContext类型的已知的可解析的依赖。

public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
7.9.8 @PostConstruct和@PreDestroy

CommonAnnotationBeanPostProcessor不仅能够识别到@Resource注解,还能识别到JSR-250的生命周期注解。这是在Spring 2.5引入的,这项支持为初始化回调及销毁回调又提供了一种替代方案。CommonAnnotationBeanPostProcessor是在ApplicationContext中注册的,因此带有这些注解的方法会与Spring自身的生命周期接口方法或显式声明的回调方法在同样调用。下面的例子中,缓存会在初始化的时候设置并在销毁时清除。

public class CachingMovieLister {

    @PostConstruct
    public void populateMovieCache() {
        // populates the movie cache upon initialization...
    }

    @PreDestroy
    public void clearMovieCache() {
        // clears the movie cache upon destruction...
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

关于组合使用不同的生命周期机制的详细信息,请参考组合使用生命周期机制

7.10 类路径扫描及管理的组件

本章的大部分例子都将采用XML的形式配置元数据。上一节(7.9 基于注解的容器配置)描述了怎么在源码级别提供配置。即便如此,基本的配置还是得通过XML来配置,注解仅仅用于驱动依赖注入。本节提供了一种隐式地通过扫描类路径检测候选组件的方式。候选组件是那些符合相应过滤规则并与容器通信的类。这种方式可以让我们不再通过XML的形式执行bean的注册,而是采用注解(比如@Component)、AspectJ表达式或自定义的过滤规则来选择哪些类将被注册到容器中。

  • 从Spring 3.0开始,许多由Spring的Java配置项目提供的功能都成为了Spring的核心部分。这使得我们可以通过Java而不是XML形式定义bean。参考@Configuration, @Bean, @Import@DependsOn注解,来看一看怎么使用这些新功能。
7.10.1 @Component及其扩展注解

@Repository注解是一种用于标识存储类(也被称为数据访问对象或者DAO)的标记。异常的自动翻译是这个标记的用法之一,参考20.2.2 异常翻译

Spring提供了一些扩展注解:@Component, @Service@Controller@Component可用于管理任何Spring的组件。@Repository, @Service@Controller@Component用于指定用例的特殊形式,比如,在持久层、服务层和表现层。使用@Service@Controller能够让你的类更易于被合适的工具处理或与切面(aspect)关联。比如,这些注解可以使目标组件成为切入点。当然,@Repository, @Service@Controller也能携带更多的语义。因此,如果你还在考虑使用@Component还是@Service用于注解service层,那么就选@Service吧,它更清晰。同样地,如前面所述,@Repository还能够用于在持久层标记自动异常翻译。

7.10.2 元注解

Spring提供了很多注解可用于元注解。元注解即一种可用于别的注解之上的注解。例如,@Service就是一种被元注解@Component注解的注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // Spring will see this and treat @Service in the same way as @Component
public @interface Service {

    // ....
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

元注解也可以组合起来形成组合注解。例如,@RestController注解是一种@Controller@ResponseBody组合的注解。

另外,组合注解也可以重新定义来自元注解的属性。这在只想暴露元注解的部分属性值的时候非常有用。例如,Spring的@SessionScope注解把它的作用域硬编码为session,但是仍然允许自定义proxyMode

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {

    /**
     * Alias for {@link Scope#proxyMode}.
     * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
     */
    @AliasFor(annotation = Scope.class)
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

@SessionScope然后就可以使用了,而且不需要提供proxyMode,如下:

@Service
@SessionScope
public class SessionScopedService {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

或者重写proxyMode的值,如下:

@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

更多信息请参考Spring注解编程模型

7.10.3 自动检测类并注册bean定义

Spring能够自动检测被注解的类,并把它们注册到ApplicationContext中。例如,下面的两个会被自动检测到:

@Service
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
@Repository
public class JpaMovieFinder implements MovieFinder {
    // implementation elided for clarity
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

为了能够自动检测到这些类并注册它们,需要为@Configuration类添加@ComponentScan注解,并设置它的basePackage属性为这两个类所在的父包(替代方案,也可以使用逗号、分号、空格分割这两个类所在的包)。

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 上面的配置也可以简单地使用这个注解的value属性,例如:ComponentScan(“org.example”)

也可以使用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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.example"/>

</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 使用<context:component-scan>隐式地允许<context:annotation-config>的功能。因此,使用<context:component-scan>时一般就不需要再包含<context:annotation-config>元素了。

  • 类路径扫描的包必须保证这些包出现在classpath中。当使用Ant构建JAR包时,请确定不要激活仅仅使用文件的开关。同样地,类路径目录可能在某些环境下基于安全考虑不允许暴露,基于JDK 1.7.0_45及更高版本的app(需要在清单中设置信任库,参考http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

另外,使用component-scan元素时默认也启用了AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor。这意味着这两个组件被自动检测到了且不需要在XML中配置任何元数据。

  • 可以使用annotation-config元素并设置其属性为false来禁用AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor
7.10.4 使用过滤器自定义扫描

默认地,只有使用注解@Component, @Repository, @Service, @Controller或自定义注解注解的类才能被检测为候选组件。然而,我们可以使用自定义的过滤器修改并扩展这种行为。添加这些过滤器到@ComponentScan注解的includeFiltersexcludeFilters参数即可(或component-scan元素的子元素include-filterexclude-filter)。每个过滤器元素都需要typeexpression属性。下表描述了相关的选项:
表7.5. 过滤器类型

过滤器类型表达式例子描述
annotation(默认)org.example.SomeAnnotation目标组件类级别的注解
assignableorg.example.SomeClass目标组件继承或实现的类或接口
aspectjorg.example..*Service+用于匹配目标组件的AspecJ类型表达式
regexorg.example.Default.*用于匹配目标组件类名的正则表达式
customorg.example.MyTypeFilterorg.springframework.core.type.TypeFilter接口的自定义实现

下面的例子展示了如何忽略掉所有的@Repository注解,并使用带有“stub”的Repository代替:

@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

或者使用XML形式配置:

<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex"
                expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation"
                expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 也可以设置这个注解的useDefaultFilters=false或为<component-scan/>元素提供属性use-default-filters=”false”忽略掉默认的过滤器。这将不会自动检测带有@Component, @Repository, @Service, @Controller@Configuration注解的类。
7.10.5 在组件内部定义bean元数据

Spring的组件也可以为容器贡献bean的定义元数据,只要在@Component注解的类内部使用@Bean注解即可。下面是一个简单的例子:

@Component
public class FactoryMethodComponent {

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    public void doWork() {
        // Component method implementation omitted
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这个类是Spring的一个组件,它包含一个应用相关的方法doWork()。但是,它也通过方法publicInstance()贡献了一个bean定义。@Bean注解标识了这个工厂方法和其它的bean属性,比如@Qualifier注解的限定符。其它可用于此处的方法级别注解还有@Scope, @Lazy及自定义注解等。

  • 除了扮演组件初始化的角色,@Lazy注解还可以放置在被@Autowired@Inject标记的注入点。在这种情况下,它会使得注入使用延迟代理。

自动装配的字段和方法也可以像前面讨论的一样被支持,也可以支持@Bean方法的自动装配:

@Component
public class FactoryMethodComponent {

    private static int i;

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    // use of a custom qualifier and autowiring of method parameters

    @Bean
    protected TestBean protectedInstance(
            @Qualifier("public") TestBean spouse,
            @Value("#{privateInstance.age}") String country) {
        TestBean tb = new TestBean("protectedInstance", 1);
        tb.setSpouse(spouse);
        tb.setCountry(country);
        return tb;
    }

    @Bean
    private TestBean privateInstance() {
        return new TestBean("privateInstance", i++);
    }

    @Bean
    @RequestScope
    public TestBean requestScopedInstance() {
        return new TestBean("requestScopedInstance", 3);
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

上面的例子使用了另一个叫作privateInstance的bean的Age属性自动装配了String类型的参数country。Spring的表达式语言使用#{}的记法定义了这个属性的值。对于@Value注解,提前配置的表达式解析器会在需要解析表达式文本的时候寻找bean的名字。

在Spring组件内部的@Bean方法的处理不同于使用@Configuration注解的类内部的@Bean方法。不同之处是@Component类不会使用CGLIB拦截调用的方法和字段从而进行增强。CGLIB代理在调用@Configuration类中的@Bean方法时会创建对合作对象的引用,这种方法的调用不会通过正常的Java语法调用,而是通过容器以便提供生命周期管理,甚至在通过编程地方式调用@Bean方法时也会形成对其它bean的引用。相反,调用普通的@Component类中的@Bean方法只会形成标准的Java语法调用,不会有特殊的CGLIB处理过程及其它的限制条件。

  • 你可能会定义@Bean方法为静态的,这样就不用创建包含它的类的实例了。这在定义后置处理器bean时会形成特殊的情况,比如BeanFactoryPostProcessorBeanPostProcessor,因为这类bean会在容器的生命周期前期被初始化,而不会触发其它部分的配置。
    注意,对静态@Bean方法的调用永远不会被容器拦截,即使在@Configuration类内部。这是由于技术上的瓶颈:CGLIB的子类代理只会重写非静态方法。因此,对另一个@Bean方法的直接调用只有标准的Java语法,只会从工厂方法本身直接返回一个独立的实例。
    由于Java语言的可见性,@Bean方法并不一定会对容器中的bean有效。你可能很随意的在非@Configuration类中定义或定义为静态方法。然而,在@Configuration类中的正常的@Bean方法都需要被重写的,因此,它们不应该定义为privatefinal
    @Bean方法也可以在父类中被发现,同样适用于Java 8中接口的默认方法。这使得组建复杂的配置时能具有更好的灵活性,甚至可能通过Java 8的默认方法实现多重继承,这在Spring 4.2开始支持。
    最后,注意一个类中可能会存在相同bean的多个@Bean方法,这会在运行时选择合适的工厂方法。使用的算法时选择“最贪婪”的构造方法,一些场景可能会按如下方法选择相应的工厂方法:满足最多依赖的会被选择,这与使用@Autowired时选择多个构造方法时类似。
7.10.6 命名自动检测的组件

当一个组件被扫描过程自动检测到时,它的名字由BeanNameGenerator定义的策略生成。默认地,Spring的扩展注解(@Component, @Repository, @Service@Controller)都包含一个value属性,这个value值会提供一个名字以便通信。

如果这样的注解没有明确地提供value值,或者另外一些检测到的组件(比如自定义过滤器扫描到的组件),那么默认生成器会返回一个首字母小写的短路径的类名。比如,下面两个组件,它们的名字分别为myMovieListermovieFinderImpl

@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
  • 如果不想遵循默认的名字生成策略,也可以提供自定义的策略。首先,需要实现BeanNameGenerator接口,并且要包含一个无参构造方法。然后,配置扫描器时为其指定这个自定义生成器的全路径:

    @Configuration
    @ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
    public class AppConfig {
        ...
    }
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 1
    • 2
    • 3
    • 4
    • 5
    <beans>
        <context:component-scan base-package="org.example"
            name-generator="org.example.MyNameGenerator" />
    </beans>
       
       
    • 1
    • 2
    • 3
    • 4
    • 1
    • 2
    • 3
    • 4

一般地,当其它的组件可能会明确地引用这个组件时为其注解提供一个名字是个很好地方式。另外,当容器装配时自动生成的名字足够用了。

7.10.7 为自动检测的组件提供作用域

一般Spring管理的组件的作用域默认为singleton。但是,有时可能会需要不同的作用域,这时可以通过@Scope注解来声明:

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

更详细的信息请参考7.5.4 Request, session, global session, application和WebSocket作用域

  • 也可以自定义策略处理作用域而不是依靠这种注解的方法,实现ScopeMetadataResolver接口,并包含一个默认的无参构造方法,然后在配置扫描器的时候提供其全路径即可。

    @Configuration
    @ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
    public class AppConfig {
    ...
    }


    <beans>
    <context:component-scan base-package="org.example"
    scope-resolver="org.example.MyScopeResolver" />
    </beans>

当使用非单例作用域时,有必要为作用域内的对象生成代理。原因如有作用域的bean作为依赖项中描述。因此,component-scan元素需要指明scoped-proxy属性。有三种可选值:无,接口和目标类。例如,下面的配置将使用标准的JDK动态代理:

@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
    ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
<beans>
    <context:component-scan base-package="org.example"
        scoped-proxy="interfaces" />
</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
7.10.8 使用注解提供限定符

@Qualifier注解在7.9.4 使用限定符微调基于注解的自动装配中被讨论过。那节的例子中展示了如何使用@Qualifier注解,并展示了如何使用自定义的限定符注解提供更细粒度的控制。那些例子都是基于XML形式的,使用qualifiermeta子元素为bean提供限定符。同样地,也可以在类级别提供注解达到同样的效果。下面的三个例子展示了用法:

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 与大部分注解一样,请记住注解元数据是绑定到类定义本身的,然而XML形式允许为相同类型提供多个bean并绑定不同的限定符,因为XML的元数据是绑定到每个实例的而不是每个类。

7.11 使用JSR 330标准注解

从Spring 3.0开始,Spring开始支持JSR-330的标准注解用于依赖注入。这些注解与Spring自带的注解一样被扫描。仅仅只需要引入相关的jar包即可。

  • 如果使用Maven,javax.inject也可以在标准Maven仓库中找到,添加如下配置到pom.xml即可。

    <dependency>
        <groupId>javax.inject</groupId>
        <artifactId>javax.inject</artifactId>
        <version>1</version>
    </dependency>
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 1
    • 2
    • 3
    • 4
    • 5
7.11.1 使用@Inject和@Named依赖注入

可以像下面这样使用@javax.inject.Inject代替@Autowired

import javax.inject.Inject;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    public void listMovies() {
        this.movieFinder.findMovies(...);
        ...
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

@Autowired一样,可以在字段级别、方法级别或构造参数级别使用@Inject。另外,也可以定义注入点为Provider,以便按需访问短作用域的bean或通过调用Provider.get()延迟访问其它的bean。上面例子的一种变体如下:

import javax.inject.Inject;
import javax.inject.Provider;

public class SimpleMovieLister {

    private Provider<MovieFinder> movieFinder;

    public void listMovies() {
        this.movieFinder.get().findMovies(...);
        ...
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

如果你喜欢为依赖添加一个限定符,也可以像下面这样使用@Named注解:

import javax.inject.Inject;
import javax.inject.Named;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
7.11.2 @Named:与@Component注解等价

可以像下面这样使用@javax.inject.Named代替@Component

import javax.inject.Inject;
import javax.inject.Named;

@Named("movieListener")
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

通常使用@Component都不指定名字,同样地@Named也可以这么用:

import javax.inject.Inject;
import javax.inject.Named;

@Named
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

使用@Named时,也可以像使用Spring注解一样使用组件扫描:

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • @Component不同的是,JSR-330的@Named注解不能组合成其它的注解,因此,如果需要构建自定义的注解,请使用Spring的注解。
7.11.3 JSR-330标准注解的局限性

使用标准注解时,应该要了解以下不支持的特性:

表 7.6. Spring组件模型与JSR-330变种的对比

Springjavax.inject.*javax.inject的局限性
@Autowired@Inject@Inject没有require属性,可以使用Java 8的Optional代替。
@Component@NamedJSR-330没有提供组合模型,仅仅只是一种标识组件的方式
@Scope(“singleton”)@SingletonJSR-330默认的作用域类似于Spring的prototype。然而,为了与Spring一般的配置的默认值保持一致,JSR-330配置的bean在Spring中默认为singleton。为了使用singleton以外的作用域,必须使用Spring的@Scope注解。javax.inject也提供了一个@Scope注解,不过这仅仅被用于创建自己的注解。
@Qualifier@Qualifier/@Namedjavax.inject.Qualifier仅使用创建自定义的限定符。可以通过javax.inject.Named创建与Spring中@Qualifier一样的限定符
@Value-
@Required-
@Lazy-
ObjectFactoryProviderjavax.inject.Provider是对Spring的ObjectFactory的直接替代,仅仅使用简短的get()方法即可。它也可以与Spring的@Autowired或无注解的构造方法和setter方法一起使用。

7.12 基于Java的容器配置

7.12.1 基本概念:@Bean和@Configuration

Spring中基于Java的配置的核心内容是@Configuration注解的类和@Bean注解的方法。

@Bean注解表示一个方法将会实例化、配置并初始化一个对象,且这个对象会被Spring容器管理。这就像在XML中<beans/>元素中<bean/>元素一样。@Bean注解可用于任何Spring的@Component注解的类中,但大部分都只用于@Configuration注解的类中。

注解了@Configuration的类表示这个类的目的就是作为bean定义的地方。另外,@Configuration类内部的bean可以调用本类中定义的其它bean作为依赖。最简单的配置大致如下:

@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面的AppConfig类与下面的XML形式是等价的:

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
全量的@Configuration和简化的@Bean模式?
当@Bean方法不定义在@Configuration的类中时,它们会被一种简化的模式处理。例如,定义在@Component类或普通类中的@Bean方法。

不同于全量的@Configuration模式,简化的@Bean方法不能轻易地使用别的依赖。通常在简化械下一个@Bean方法不会调用另一个@Bean方法。

推荐在@Configuration类中使用@Bean方法,从而保证全量模式总是起作用。这样可以防止同一个@Bean方法被无意中调用多次,并减少一些狡猾的bug。
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

@Bean@Configuration注解将会在下面的章节中详细讨论。首先,我们来看看基于Java配置以不同的方式创建Spring的容器。

7.12.2 使用AnnotationConfigApplicationContext实例化Spring容器

下面的章节介绍Spring 3.0中引入的AnnotationConfigApplicationContext。这个ApplicationContext的实现不仅可以把@Configuration类作为输入,同样普通的@Component类和使用JSR-330注解的类也可以作为输入。

当使用@Configuration类作为输入时,这个类本身及其下面的所有@Bean方法都会被注册为bean。

@Component和JSR-330类作为输入时,它们会被注册为bean,并且假设在必要的时候使用了@Autowired@Inject

简单的构造方法

与使用ClassPathXmlApplicationContext注入XML文件一样,可以使用AnnotationConfigApplicationContext注入@Configuration类。这样就完全不用在Spring容器中使用XML了:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

如前面所述,AnnotationConfigApplicationContext不限于只注入@Configuration类,任何@Component或JSR-330注解的类都能被提供给这个构造方法。例如:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

上面假设了MyServiceImpl, Dependency1, Dependency2使用了Spring的依赖注入注解比如@Autowired

使用register(Class
public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
使用scan(String…)扫描组件

为了扫描组件,只要像下面这样配置@Configuration类即可:

@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig  {
    ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 有经验的用户可能更熟悉使用等价的XML形式配置:

    <beans>
    <context:component-scan base-package="com.acme"/>
    </beans>

上面的例子中,com.acme包会被扫描,只要是使用了@Component注解的类,都会被注册进容器中。同样地,AnnotationConfigApplicationContext也暴露了scan(String…)方法用于扫描组件:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 请记住@Configuration类是被@Component元注解注解的类,所以它们也会被扫描到。上面的例子中,假设AppConfig定义在com.acme包中(或更深的包中),调用scan()时它也会被扫描到,并且它里面配置的所有@Bean方法会在refresh()的时候被注册到容器中。
使用AnnotationConfigWebApplicationContext支持web应用

一个WebApplicationContextAnnotationConfigApplicationContext的变种是AnnotationConfigWebApplicationContext。这个实现可以用于配置Spring的ContextLoaderListener的servlet监听器、Spring MVC的DispatcherServlet等。下面是一个典型的配置Spring MVC web应用的片段。注意包含contextClass的context-param和init-param的用法:

<web-app>
    <!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
        instead of the default XmlWebApplicationContext -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <!-- Configuration locations must consist of one or more comma- or space-delimited
        fully-qualified @Configuration classes. Fully-qualified packages may also be
        specified for component-scanning -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.AppConfig</param-value>
    </context-param>

    <!-- Bootstrap the root application context as usual using ContextLoaderListener -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Declare a Spring MVC DispatcherServlet as usual -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
            instead of the default XmlWebApplicationContext -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!-- Again, config locations must consist of one or more comma- or space-delimited
            and fully-qualified @Configuration classes -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.acme.web.MvcConfig</param-value>
        </init-param>
    </servlet>

    <!-- map all requests for /app/* to the dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
7.12.3 使用@Bean注解

@Bean是方法级别的注解,它与XML中的<bean/>类似,同样地,也支持<bean/>的一些属性,比如,init-method, destroy-method, autowring和name。

可以在@Configuration@Component注解的类中使用@Bean注解。

声明bean

只要在方法上简单的加上@Bean注解就可以定义一个bean了,这样就在ApplicationContext中注册了一个类型为方法返回值的bean。默认地,bean的名字为方法的名称,如下所示:

@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面的方式与下面的XML形式等价:

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

两种方式都定义了一个名字为transferService的bean,且绑定了TransferServiceImpl的实例:

transferService -> com.acme.TransferServiceImpl
 
 
  • 1
  • 1
Bean之间的依赖

@Bean注解的方法可以有任意个参数用于描述这个bean的依赖关系。比如,如果TransferService需要一个AccountRepository,我们可以通过方法参数实现这种依赖注入。

@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这种机制与基于构造方法的依赖注入是完全相同的,更多详细内容可以查看相关章节

接收生命周期回调

任何使用@Bean定义的类都有正常的生命周期回调,并且可以使用@PostConstruct@PreDestroy注解,更多详细信息请参考JSR-250注解

正常的生命周期回调被完美支持, 如果一个bean实现了InitializingBean, DisposableBean或者Lifecycle,它们的方法将被容器依次调用。

同样地,也支持*Aware系列的接口,比如BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware等等。

@Bean注解也支持任意的初始化及销毁的回调方法,这与XML的init-method和destroy-method是非常类似的。

public class Foo {
    public void init() {
        // initialization logic
    }
}

public class Bar {
    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public Foo foo() {
        return new Foo();
    }

    @Bean(destroyMethod = "cleanup")
    public Bar bar() {
        return new Bar();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 默认地,使用Java配置的方式如果一个bean包含了公共的close或shutdown方法,它们将会被自动地包含在销毁的回调中。如果有公共的close或shutdown方法,但是我们并不想使用它们,那么只要加上@Bean(destroyMethod=”“)就可以屏蔽掉默认的推测了。
    我们可以使用这种特性在通过JNDI获得的资源上,并且这些资源是外部应用管理的。特别地,保证一定要在DataSource上使用它,因为DataSource在Java EE的应用服务器上是有问题的。

    @Bean(destroyMethod="")
    public DataSource dataSource() throws NamingException {
    return (DataSource) jndiTemplate.lookup("MyDS");
    }

    同样地,使用@Bean方法,可以很容易地选择编程式地JNDI查找:使用Spring的JndiTemplate/JndiLocatorDelegate帮助类或直接使用JNDI的InitialContext,但是不要使用JndiObjectFactoryBean变种,因为它会强制你去声明一个返回类型作为FactoryBean的类型代替实际的目标类型,这会使得交叉引用变得很困难。

当然,上面例子中的Foo,也可以直接在构造期间直接调用init()方法:

@Configuration
public class AppConfig {
    @Bean
    public Foo foo() {
        Foo foo = new Foo();
        foo.init();
        return foo;
    }

    // ...

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 在Java中可以用你喜欢的方式直接操作对象,而不需要总是依赖容器的生命周期!
指定bean的作用域
使用@Scope注解

可以使用任何标准的方式为@Bean注解的bean指定一个作用域,这些方式请参考Bean作用域章节。

默认地作用域为singleton,但是可以使用@Scope注解重写:

@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
@Scope和scoped-proxy

Spring提供了一种简便地方式声明bean的作用域,它被称为scoped proxy。最简单地方式是创建那么一个代理,使用XML配置的形式则使用<aop:scoped-proxy/>元素。在Java中使用@Scope注解配置bean的方式提供了与XML代理模式属性同样的功能。默认是没有代理的(ScopedProxyMode.No),但是可以指定ScopedProxyMode.TARGET_CLASS或ScopedProxyMode.INTERFACES。

如果把xml形式改写为Java形式,看起来如下:

// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
    return new UserPreferences();
}

@Bean
public Service userService() {
    UserService service = new SimpleUserService();
    // a reference to the proxied userPreferences bean
    service.setUserPreferences(userPreferences());
    return service;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
自定义bean的名称

默认地,使用@Bean默认的方法名为其bean的名字,然而这项功能可以使用name属性重写:

@Configuration
public class AppConfig {

    @Bean(name = "myFoo")
    public Foo foo() {
        return new Foo();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
bean的别名

正如7.3.1 命名bean中所讨论地一样,有时候可以想要给同一个bean多个名字,亦即别名,@Bean注解的name属性就可以达到这样的目的, 你可以为其提供一个String的数组。

@Configuration
public class AppConfig {

    @Bean(name = { "dataSource", "subsystemA-dataSource", "subsystemB-dataSource" })
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
Bean描述

有时可能需要为一个bean提供更详细的描述。这对于监控bean很有用。

可以使用@Description注解为其添加一段描述:

@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Foo foo() {
        return new Foo();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
使用@Configuration注解

@Configuration是类级别的注解,这表示一个对象是bean定义的来源。@Configuration注解的类里面使用@Bean注解的方法声明bean。对其中的@Bean方法的调用也能实现内部bean的依赖。参考7.12.1 基础概念:@Bean和@Configuration

注入内部依赖

当@Bean的方法对另外一个有依赖时,简单地调用另外一个@Bean注解的方法即可表达这种依赖:

@Configuration
public class AppConfig {

    @Bean
    public Foo foo() {
        return new Foo(bar());
    }

    @Bean
    public Bar bar() {
        return new Bar();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在上例中,foo这个bean通过构造函数注入接收了bar的引用。

  • 这种方式仅仅适用于在@Configuration内部定义的@Bean方法。在普通的@Component类中不能声明内部依赖。
查找方法注入

正如之前提到的,查找方法注入是一项很少使用到的先进的技术。它对于一个单例bean依赖另一个原型bean很有用。在Java中使用了一种很自然的方法来实现了这种模式。

public abstract class CommandManager {
    public Object process(Object commandState) {
        // 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();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

使用Java配置,我们可以创建一个CommandManager的子类,实现其createCommand()方法,这样就可以让它查找到新的原型command对象。

@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
    AsyncCommand command = new AsyncCommand();
    // inject dependencies here as required
    return command;
}

@Bean
public CommandManager commandManager() {
    // return new anonymous implementation of CommandManager with command() overridden
    // to return a new prototype Command object
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
更多关于Java配置内部工作的信息

下面的例子显示了@Bean注解的方法被调用了两次:

@Configuration
public class AppConfig {

    @Bean
    public ClientService clientService1() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientService clientService2() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientDao clientDao() {
        return new ClientDaoImpl();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

clientDao()方法在clientService1()和clientService2()中分别被调用了一次。由于这个方法创建了一个ClientDaoImpl的实例并返回了它,但是你可能希望有两个实例(每个service一个)。这种定义可能是有问题的:在Spring中,实例化的bean默认是单例的。这就是神奇的地方:所有的@Configuration类都会在启动的时候被CGLIB子类化。在子类中,所有的子类方法都会在调用父类的方法之前检查有没有缓存的bean,如果没有再创建一个新的实例。注意,从Spring3.2开始,classpath不再需要包含CGLIB了因为CGLIB相关的类已经被打包在org.springframework.cglib中了,并且可以直接使用。

  • 这种行为可以会根据bean的作用域而变化,我们这里只讨论单例。
  • 实际上还会有一些限制,因为CGLIB是在启动的时候动态地添加这些特性,所以配置的类不能是final的。不过,从4.3开始,任何构造方法都允许放置在配置类中,包含@Autowired或一个非默认的构造方法用于默认注入即可。如果想避免任何CGLIB带来的限制,考虑在非@Configuration类中使用@Bean方法,比如普通的@Component类。这样在@Bean方法之中跨方法调用就不会被拦截了,所以这样只能依赖于构造方法或方法组织的注入了。
7.12.5 组合的Java配置
使用@Import注解

与XML中使用<import/>一样用于模块化配置,@Import注解允许从另一个配置类中加载@Bean定义。

@Configuration
public class ConfigA {

     @Bean
    public A a() {
        return new A();
    }

}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

现在,我们不需要同时指定ConfigA.class和ConfigB.class了,只要明确地指定ConfigB即可:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // now both beans A and B will be available...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这种方式简化了容器的实例化,只要处理一个类就行了,而不需要开发者记住大量的@Configuration类。

  • 从Spring 4.2开始,@Import也可以支持对普通组件类的引用了,与AnnotationConfigApplicationContext.register()方法类似。这在避免组件扫描的时候很有用,使用少量的配置类作为切入点用于明确定义所有的组件类。
在导入的@Bean定义上注入依赖

上面的例子可以很好地工作,但是太简单了。在大部分场景下,bean都会依赖另一个配置类中的bean。使用XML时,这没有什么问题,因为不需要编译,只要简单地声明ref=”someBean”即可,并信任Spring可以很好地处理它。当然,使用配置类时,Java编译器会有一些限制,那就是必须符合Java的语法。

幸运地,解决这个问题也很简单。正如我们之前讨论的,@Bean方法可以有任意的参数用于描述其依赖。让我们考虑一下实际的场景,有几个配置类,并且每个都依赖于其它的类:

@Configuration
public class ServiceConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }

}

@Configuration
public class RepositoryConfig {

    @Bean
    public AccountRepository accountRepository(DataSource dataSource) {
        return new JdbcAccountRepository(dataSource);
    }

}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }

}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

也可以有其它的方法来实现相同的结果。请记住@Configuration类也是容器中的一个bean:这意味着它们可以像其它bean一样使用@Autowired和@Value注解。

  • 请确定你都是以最简单的方式注入的依赖。@Configuration类会在上下文初始化的早期被处理,所以它的依赖会在更早期被初始化。如果可能的话,请像上面这样使用参数化注入。

同样地,对于通过@Bean声明的BeanPostProcessor和BeanFactoryPostProcessor请谨慎对待。它们通常被声明为静态的@Bean方法,不会触发包含它们的配置类。另外,@Autowired和@Value在配置类本身上是不起作用的,因为它们太早被实例化了。

@Configuration
public class ServiceConfig {

    @Autowired
    private AccountRepository accountRepository;

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl(accountRepository);
    }

}

@Configuration
public class RepositoryConfig {

    private final DataSource dataSource;

    @Autowired
    public RepositoryConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }

}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • @Configuration类中的构造方法注入只在Spring 4.3以后才支持。另外请注意,如果目标bean只有一个构造方法也可以不指定@Autowried,在上例中,RepositoryConfig构造方法上的@Autowired是非必要的。

在上面的场景中,@Autowired可以很好地工作,并提供希望的结果,但是被装配的bean的定义声明是模糊不清的。例如,当一个开发者查看ServiceConfig类时,你怎么知道@Autowired AccountRepository在哪定义的呢?它在代码中并不清楚,并且这可以很微小。记住Spring工具套件提供了一些工具,可以画出所有东西是怎么装配起来的——这可以是你需要的。同样地,你的IDE也可以很容易地找出所有的定义和AccountRepository类型引用的地方,并可以快速地找出@Bean方法定义的地方。

在某些情况下,这种含糊是不被接受的,并且你希望可以在IDE中直接从一个@Configuration类到另一个,可以考虑装配配置类本身:

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // navigate 'through' the config class to the @Bean method!
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在上面的场景下,AccountRepository的定义就很明确了。然而,ServiceConfig与RepositoryConfig耦合了;这是一种折衷的方法。这种耦合某种程度上可以通过接口或抽象解决,如下:

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

@Configuration
public interface RepositoryConfig {

    @Bean
    AccountRepository accountRepository();

}

@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(...);
    }

}

@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return DataSource
    }

}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

现在ServiceConfig就与具体的DefaultRepositoryConfig松耦合了,并且内置的IDE工具也可以生效:对于 开发者可以很容易地获得RepositoryConfig实现类的继承体系。使用这种方式,操纵@Configuration类和它们的依赖与基于接口的代码没有区别。

有条件地包含@Configuration类或@Bean方法

有时候有条件地包含或不包含一个@Configuration类或@Bean方法很有用,这基于特定的系统状态。一种通用的方法是使用@Profile注解去激活bean,仅当指定的配置文件包含在了Spring的环境中才有效(参考7.13.1 bean定义配置文件)。

@Profile注解实际是实现了一个更灵活的注解@Conditional。@Condition注解表明一个@Bean被注册之前会先询问@Condition。

Condition接口的实现只要简单地提供matches(…)方法,并返回true或false即可。例如,下面是一个实际的Condition实现用于@Profile:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() != null) {
        // Read the @Profile annotation attributes
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
    }
    return true;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

更多信息请参考@Conditional的javadoc。

绑定Java与XML配置

Spring的@Configuration类并不能100%地替代XML配置。一些情况下使用XML的命名空间仍然是最理想的方式来配置容器。在某些场景下,XML是很方便或必要的,你可以选择以XML为主,比如ClassPathXmlApplicationContext,或者以Java为主使用AnnotationConfigApplicationContext并在需要的时候使用@ImportResource注解导入XML配置。

XML为主使用@Configuration类

更受人喜爱的方法是从XML启动容器并包含@Configuration类。例如,在大型的已存在的系统中,以前是使用XML配置的,所以很容易地创建@Configuration类,并包含他们到XML文件中,下面我们会讲解以XML为主的案例。

记住@Configuration类仅仅用于bean的定义。在这个例子中,我们创建了一个叫做AppConfig的配置类,并把它包含到system-test-config.xml中。因为<context:annotation-config/>打开了,所以容器能够识别到@Configuration注解并处理其中的@Bean方法。

@Configuration
public class AppConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

    @Bean
    public TransferService transferService() {
        return new TransferService(accountRepository());
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

system-test-config.xml:

<beans>
    <!-- enable processing of annotations such as @Autowired and @Configuration -->
    <context:annotation-config/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="com.acme.AppConfig"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

jdbc.properties:

jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 在上面的system-test-config.xml中,AppConfig的<bean/>并没有声明一个id。当然如果声明了也是可以授受的,但是对于没有被其它bean引用的bean并有必要声明id,并且它也没可能从容器获取一个明确的名字。同样地,DataSource也不需要一个明确的id,因为它是通过类型装配的。

因为@Configuration是被元注解@Component注解的,所以@Configuration注解的类也可以被自动扫描。同样使用上面的例子,我们可以重新定义system-test-config.xml来使用组件扫描。注意,这种情况下,我们就没必要明确地声明<context:annotation-config/>了,因为<context:component-scan/&tl;已经包含了同样的功能。

system-test-config.xml:

<beans>
    <!-- picks up and registers AppConfig as a bean definition -->
    <context:component-scan base-package="com.acme"/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
以@Configuration类为主使用@ImportResource引入XML

在一些应用中,@Configuration类是主要的配置方式,也需要使用一些XML配置。在这些场景下,简单地使用@ImportResource并按需要定义XML文件即可。这种方式可以以Java为主,并保持少量的XML配置。

@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource(url, username, password);
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

properties-config.xml

<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

7.13 环境抽象层

~~~~~~~~~~~未完待续~~~~~~~~~~~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值