Spring框架5.3.27官方文档(中文翻译)—— 核心技术-IoC容器(一)

533 阅读1小时+

1.IoC容器

最近发现了一个Spring的中文网,文档翻译的很全,故后续不再继续翻译文档。

文章列表

原文:docs.spring.io/spring-fram…

本章涵盖Spring的控制反转(Inversion of Control,IoC)容器。

1.1. 介绍Spring IoC容器和Bean

本章涵盖了Spring框架实现控制反转(IoC)的原理。控制反转(Inversion of Control,IoC),也被叫做依赖注入(Dependency Injection)。在这个过程中,对象只能通过构造函数参数工厂方法的参数或在对象实例被构造从工厂方法返回后在对象实例设置属性来定义它们的依赖关系(即与它们一起工作的其他对象)。然后,容器在创建bean时注入这些依赖项。这个过程基本上是bean本身的逆过程(因此得名为控制反转),通过使用类的直接构造或Service Locator模式等机制来控制其依赖项的实例化或位置。

org.springframework.beansorg.springframework.context包是IoC容器的基础。

  • BeanFactory:接口,提供高级配置机制,用于管理任意对象类型
  • ApplicationContextBeanFactory的子接口,提供如下功能:
    • 更简单的整合Spring AOP特性
    • 消息资源处理(用于国际化)
    • 事件发布
    • 特定的应用层环境,如Web应用中的WebApplicationContext

简而言之,BeanFactory提供了配置框架和基本功能,而ApplicationContext则添加了更多特定于企业的功能。ApplicationContextBeanFactory的一个完整的超集,并且在本章中专门用于描述Spring的IoC容器。有关使用BeanFactory而不是ApplicationContext的更多信息,请参阅有关BeanFactory API的部分。

在Spring中,构成应用程序骨架并由Spring IoC容器管理的对象称为beanbean是由Spring IoC容器实例化、组装和管理的对象。否则,bean只是应用程序中众多对象中的一个。bean以及它们之间的依赖关系反映在容器使用的**配置元数据(configuration metadata)**中。

1.2. 容器概述

org.springframework.context.ApplicationContext接口代表了Spring IoC容器,负责bean的实例化配置组装。容器通过读取配置元数据(configuration metadata),执行实例化配置组装对象。

  • 配置元数据可以由三种方式表示:
    • XML
    • Java注解
    • Java代码

它允许您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系

在独立应用程序(stand-alone application)中,通常创建ClassPathXmlApplicationContextFileSystemXmlApplicationContext的实例。虽然XML一直是定义配置元数据的传统格式,但您可以通过提供少量XML配置来声明地启用对这些附加元数据格式的支持,从而指示容器使用Java注解或代码作为元数据格式。

在大多数应用程序场景中,不需要显式的用户代码来实例化Spring IoC容器的一个或多个实例。例如,在web应用程序场景中,应用程序的web.xml文件中简单的八行(或左右)样板web描述符XML通常就足够了(参见web应用程序的方便的ApplicationContext实例化(Convenient ApplicationContext Instantiation for Web Applications)。如果使用Spring Tools for Eclipse(基于Eclipse的开发环境),只需单击几下鼠标或敲击几下键盘,就可以轻松地创建这个样板配置。

下图显示了Spring工作原理的高层级视图。您的应用程序类配置元数据相结合,这样,在ApplicationContext被创建和初始化之后,您就拥有了一个完全配置好的可执行的系统或应用程序

container-magic.png

1.2.1. 配置元数据

如上图所示,Spring IoC容器使用一种形式的配置元数据。此配置元数据表示作为应用程序开发人员,您告诉Spring容器如何实例化配置组装应用程序中的对象。

传统上,配置元数据以简单直观的XML格式提供,本章大部分内容都使用这种格式来传达Spring IoC容器的关键概念和特性。

基于XML的元数据并不是唯一允许的配置元数据形式。Spring IoC容器本身与实际编写配置元数据的格式完全解耦。现在,许多开发人员为他们的Spring应用程序选择基于Java的配置(Java-based configuration)

有关在Spring容器中使用其他形式的元数据的信息,请参见:

Spring配置由至少一个(通常是多个)bean定义组成,这些bean定义由容器管理。基于XML的配置元数据将这些bean配置为<beans/>元素(顶级元素)内部的<bean>元素。Java配置通常在@Configuration类中使用带有@Bean注解的方法。

这些bean定义对应于构成应用程序的实际对象。通常,您定义服务层(service layer)对象、数据访问(Data Access)对象(DAO)、表示层(presentation)对象(如StrutsAction实例)、基础设施对象(如Hibernate的SessionFactories、JMS的Queues)等等。通常,不需要在容器中配置细粒度的域对象,因为创建和加载域对象通常是DAO层和业务逻辑的责任。但是,您可以使用Spring与AspectJ的集成来配置在IoC容器控制之外创建的对象。参见使用AspectJ在Spring中注入依赖域对象(Using AspectJ to dependency-inject domain objects with Spring)

下面的示例展示了基于XML的配置元数据的基本结构:

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

    <bean id="..." 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>
  • id属性:用于区分Bean定义(Bean Definition)的字符串
  • class属性:定义Bean的类型,使用完全限定类名(the fully qualified classname)

id属性的值指的是协作对象。这个示例中没有显示用于引用协作对象的XML。有关更多信息,请参阅依赖项(Dependencies)

1.2.2. 实例化容器(Instantiating a Container)

提供给ApplicationContext构造函数的位置路径是资源字符串,它允许容器从各种外部资源(如本地文件系统、Java CLASSPATH等)加载配置元数据

ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

在了解了Spring的IoC容器之后,您可能希望更多地了解Spring的资源(Resource)抽象(如参考资料 Resources中所述),它提供了一种方便的机制,用于从URI语法中定义的位置读取InputStream。特别是,资源路径用于构建应用程序上下文,如应用程序上下文和资源路径(Application Contexts and Resource Paths)中所述。

如下为服务层对象(services.xml)配置文件示例:

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

    <!-- services -->

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

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

</beans>

如下为数据访问层对象(daos.xml文件)配置示例:

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

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

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

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

</beans>

在前面的示例中,服务层由PetStoreServiceImpl类和两个类型为JpaAccountDaoJpaItemDao的数据访问对象(基于JPA对象-关系映射标准)组成。属性name元素引用JavaBean属性的名称,ref元素引用另一个bean定义的名称。idref元素之间的这种链接表达了协作对象之间的依赖关系。有关配置对象依赖项的详细信息,请参见依赖项(Dependencies)

组合XML配置元数据

让bean定义跨多个XML文件是很有用的。通常,每个单独的XML配置文件表示体系结构中的一个逻辑层或模块。

您可以使用应用程序上下文构造函数从所有这些XML片段(fragments)加载bean定义。这个构造函数接受多个Resource位置,如前一节所示。或者,使用<import/>元素从另一个或多个文件加载bean定义。下面的例子展示了如何这样做:

在XML配置方式下,可以通过<import/>组合多个配置文件

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

    <bean id="bean1" class="..."/>
    <bean id="bean2" class="..."/>
</beans>

在上面的示例中,外部bean定义是从三个文件加载的:services.xmlmessageSource.xmlthemeSource.xml。所有位置路径都相对于执行导入的定义文件,因此services.xml必须与执行导入的文件位于相同的目录类路径位置,而messageSource.xmlthemeSource.xml必须位于导入文件位置下方的resources位置。如您所见,前导斜杠会被忽略。然而,考虑到这些路径是相对的最好不要使用斜杠。正在导入的文件的内容,包括顶级的<beans/>元素,根据Spring Schema,必须是有效的XML bean定义。

可以(但不推荐)使用".. /"的相对路径来引用父目录中的文件。这样做会创建对当前应用程序外部文件的依赖。特别地,不建议对classpath: URLs(例如,classpath:../services.xml)使用这个引用,因为运行时解析过程会选择“最近的”类路径根(classpath root),然后查看它的父目录。类路径配置更改可能导致选择不同的、不正确的目录。

您始终可以使用完全限定的资源位置,而不是相对路径:例如,file:C:/config/services.xmlclasspath:/config/services.xml。但是,请注意,您正在将应用程序的配置与特定的绝对位置耦合。对于这样的绝对位置,通常最好保留一个间接的位置——例如,通过在运行时根据JVM系统属性解析的“${…}”占位符。

命名空间本身提供了导入指令特性。除了普通bean定义之外,Spring还提供了一系列XML名称空间来提供更多的配置特性——例如,contextutil名称空间。

Groovy Bean定义DSL

作为外部化配置元数据的进一步示例,bean定义也可以在Spring的Groovy bean定义DSL中表示,就像Grails框架所知的那样。通常,这样的配置存在于“.groovy”文件,其结构如下例所示:

beans {
    dataSource(BasicDataSource) {
        driverClassName = "org.hsqldb.jdbcDriver"
        url = "jdbc:hsqldb:mem:grailsDB"
        username = "sa"
        password = ""
        settings = [mynew:"setting"]
    }
    sessionFactory(SessionFactory) {
        dataSource = dataSource
    }
    myService(MyService) {
        nestedBean = { AnotherBean bean ->
            dataSource = dataSource
        }
    }
}

这种配置风格在很大程度上等同于XML bean定义,甚至支持Spring的XML配置名称空间。它还允许通过importBeans指令导入XML bean定义文件。

1.2.3. 使用容器

ApplicationContext是一个高级工厂接口,能够维持不同的Bean及其依赖的注册。通过T getBean(String name, Class<T> requiredType)方法,您可以用于检索你的bean实例。

ApplicationContext允许你读取和访问bean定义,如下面的示例所示:

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

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

// use configured instance
List<String> userList = service.getUsernameList();

在Groovy配置中,引导(bootstrapping)看起来非常相似。它有一个不同的上下文实现类,它支持Groovy(Groovy-aware)(但也理解XML bean定义)。下面的例子展示了Groovy的配置:

ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");

最灵活的变体是GenericApplicationContext与reader委托结合使用——例如,与XML文件的XmlBeanDefinitionReader结合使用,如下例所示:

GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();

你也可以对Groovy文件使用GroovyBeanDefinitionReader,如下面的例子所示:

GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();

您可以在同一个ApplicationContext上混合和匹配这样的读取器委托,从不同的配置源读取bean定义。

然后可以使用getBean来检索bean的实例。ApplicationContext接口有一些其他的方法来检索bean,但理想情况下,应用程序代码不应该使用它们。实际上,您的应用程序代码根本不应该调用getBean()方法,因此根本不依赖于Spring API。例如,Spring与web框架的集成为各种web框架组件(如控制器和JSF管理的bean)提供了依赖注入,允许您通过元数据(如自动装配注解)声明对特定bean的依赖。

1.3. Bean概述

一个Spring IoC容器管理一个或多个Bean。这些Bean由你提供给容器的配置元数据创建。(如:XML文件中<bean/>定义)。

在容器本身内,这些bean定义表示为BeanDefinition对象,其中包含以下元数据(以及其他信息):

  • 完全限定类名(package-qualified class name):正在定义的bean的实际实现类
  • bean行为配置元素,用于表明bean在容器中的行为方式(作用域(scope),生命周期回调(lifecycle callbacks)等)
  • 对bean执行其工作所需的其他bean的引用。这些引用也称为协作者依赖项
  • 要在新创建的对象中设置的其他配置设置—例如,池的大小限制或要在管理连接池的bean中使用的连接数。

此元数据转换为组成每个bean定义的一组属性。下表描述了这些属性:

属性(Property)解释(Explained in…)
Class实例化Bean(Instantiating Beans)
NameBean的命名(Naming Beans)
ScopeBean的作用域(Bean Scopes)
Constructor arguments依赖注入(Dependency Injection)
Properties依赖注入(Dependency Injection)
Autowiring mode自动装配协作者(Autowiring Collaborators)
Lazy initialization mode延迟初始化Bean(Lazy-initialized Beans)
Initialization method初始化回调(Initialization Callbacks)
Destruction method销毁回调(Destruction Callbacks)

除了包含如何创建特定bean的信息的bean定义之外,ApplicationContext实现还允许注册(由用户)在容器外部创建的现有对象。这是通过方法访问ApplicationContextBeanFactory#getBeanFactory()来完成的,该方法返回DefaultListableBeanFactory实现。DefaultListableBeanFactory通过registerSingleton(..)registerBeanDefinition(..)方法支持这种注册。但是,典型的应用程序只使用通过常规bean定义元数据定义的bean。

Bean元数据手动提供的单例实例需要尽早注册,以便容器在自动装配其他自检步骤中正确地推断它们。虽然在某种程度上支持覆盖现有的元数据和现有的单例实例,但在运行时注册新bean(与对工厂的实时访问并发)并不是官方支持的,并且可能导致并发访问异常、bean容器中的不一致状态,或者两者兼而有之。

1.3.1. Bean的命名(Naming Bean)

每个bean都有一个或多个标识符。这些标识符在承载bean的容器中必须是唯一的。一个bean通常只有一个标识符。但是,如果需要多个别名,则可以将额外的名字视为别名。

在基于XML的配置元数据中,可以使用id属性、name属性或两者同时使用来指定bean标识符。

  • id属性允许您精确地指定一个id。通常,这些名称是字母数字('myBean', ' someservice '等),但它们也可以包含特殊字符。如果希望为bean引入其他别名,
  • 还可以在name属性中指定它们,用逗号(,)、分号(;)或空格分隔。

根据历史记录,在Spring 3.1之前的版本中,id属性被定义为xsd:ID类型,这约束了可能的字符。在3.1版本中,它被定义为xsd:string类型。请注意,容器仍然强制执行bean id惟一性,但不再由XML解析器强制执行。

您不需要为bean提供nameid。如果没有显式地提供nameid,容器将为该bean生成唯一的名称。但是,如果您希望通过使用ref元素或Service Locator样式查找来通过名称引用该bean,则必须提供一个名称。不提供名称的动机与使用内部bean(inner beans)自动装配协作器(autowiring collaborators)有关。

Bean命名约定(Conventions)

约定是在命名bean时使用标准的Java约定作为实例字段名。也就是说,bean名称以小写字母开头,然后使用驼峰式大小写。此类名称的示例包括accountManageraccountServiceuserDaologinController等等。

一致地命名bean使您的配置更易于阅读和理解。另外,如果您使用Spring AOP,那么在向一组按名称相关的bean应用通知时,它会有很大帮助。

提示

通过类路径中的组件扫描,Spring按照前面描述的规则为未命名的组件生成bean名:

  • 本质上,采用简单的类名并将其初始字符转换为小写
  • 但是,在特殊情况下,当有多个字符并且第一个和第二个字符都是大写时,将保留原始的大小写。这些规则与java.beans.Introspector.decapitalize (Spring在这里使用的)定义的规则相同。

在Bean定义之外给Bean取别名

在bean定义本身中,可以为Bean提供多个名称,方法是使用由id属性指定的最多一个名称和name属性中任意数量的其他名称的组合。这些名称可以是相同bean的等价别名,并且在某些情况下很有用,例如让应用程序中的每个组件通过使用特定于该组件本身的bean名称来引用公共依赖项。

但是,在实际定义bean的地方指定所有别名并不总是足够的。有时需要为在其他地方定义的bean引入别名。这在大型系统中很常见,其中配置在每个子系统之间被分割,每个子系统都有自己的一组对象定义。在基于XML的配置元数据中,可以使用<alias/>元素来完成此操作。下面的例子展示了如何这样做:

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

在这种情况下,名为fromName的bean(在同一容器中)在使用此别名定义之后也可以被称为toName

例如,子系统A的配置元数据可以通过subsystemA-dataSource的名称引用数据源。子系统B的配置元数据可以通过subsystemB-dataSource的名称引用数据源。当组合使用这两个子系统的主应用程序时,主应用程序通过myApp-dataSource的名称引用数据源。要让三个名字都引用同一个对象,你可以在配置元数据中添加以下别名定义:

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

现在,每个组件和主应用程序都可以通过唯一的名称来引用数据源,并且保证不会与任何其他定义冲突(实际上创建了一个名称空间),但它们引用的是同一个bean。

Java配置(Java-configuration)

如果你使用Java配置,@Bean注解可以用于提供别名。查看使用@Bean注解(Using the @Bean Annotation)

1.3.2. 实例化Bean

bean定义本质上是创建一个或多个对象的方法。容器在被请求时查看指定bean定义,并使用由该bean定义封装的配置元数据来创建(或获取)一个实际对象。

如果你使用基于XML配置的元数据,你在<bean/>class属性指定对象的类型(type or class),该Bean将被实例化。class属性(在内部,它是BeanDefinition实例上的Class属性)通常是强制性的。(对于异常,请参见使用实例工厂方法进行实例化(Instantiation by Using an Instance Factory Method)Bean定义继承(Bean Definition Inheritance))。

你可以用以下两种方式之一来使用Class属性:

  • 通常,在容器本身通过反射调用其构造函数直接创建bean的情况下,指定要构造的bean类,这在某种程度上相当于带有new操作符的Java代码。
  • 指定包含被调用来创建对象的静态(static)工厂方法的实际类,在不太常见的情况下,容器调用类上的静态工厂方法来创建bean。调用静态工厂方法返回的对象类型可以是同一个类,也可以完全是另一个类。

嵌套类名称(Nested class names)

如果您想为嵌套类配置bean定义,您可以使用二进制名称嵌套类的源名称

例如,如果你有一个名为SomeThing的类。这个SomeThing类有一个名为OtherThing静态嵌套类,它们可以用美元符号($)或点(.)分隔。因此,bean定义中的类属性的值将是com.example.SomeThing$OtherThingcom.example.SomeThing.OtherThing

构造器实例化(Instantiation with a Constructor)

当您通过构造函数方法创建bean时,所有普通类都可以被Spring使用并与之兼容。也就是说,正在开发的类不需要实现任何特定的接口,也不需要以特定的方式编码。只需指定bean类就足够了。但是,根据您为特定bean使用的IoC类型,您可能需要一个默认(无参的)构造函数

Spring IoC容器实际上可以管理您希望它管理的任何类。它并不局限于管理真正的JavaBeans。大多数Spring用户更喜欢实际的JavaBeans,只有一个默认的(无参数的)构造函数适当的setter和getter,这些都是按照容器中的属性建模的。您还可以在容器中拥有更多非bean样式的类。例如,如果您需要使用一个完全不符合JavaBean规范的遗留连接池,Spring也可以管理它。

在基于XML配置元数据方式,你可以如下示例所示指定Bean的类:

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

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

有关在构造对象后向构造函数提供参数(如果需要)和设置对象实例属性的机制的详细信息,请参见注入依赖项(Injecting Dependencies)

静态工厂方法实例化(Instantiation with a Static Factory Method)

在定义使用静态工厂方法创建的bean时,请使用class属性指定包含静态工厂方法的类,并使用名为factory-method的属性指定工厂方法本身的名称。您应该能够调用该方法(使用可选参数,稍后将介绍)并返回一个活的对象,随后将其视为通过构造函数创建的对象。这种bean定义的一个用途是在遗留代码中调用静态工厂。

下面的bean定义指定将通过调用工厂方法创建bean。定义没有指定返回对象的类型(类),而是指定包含工厂方法的类。在本例中,createInstance()方法必须是静态(static)方法。下面的例子展示了如何指定一个工厂方法:

<bean id="clientService"
    class="examples.ClientService"
    factory-method="createInstance"/>

下面的例子展示了一个可以使用前面的bean定义的类:

public class ClientService {
    private static ClientService clientService = new ClientService();
    private ClientService() {}

    public static ClientService createInstance() {
        return clientService;
    }
}

有关向工厂方法提供(可选)参数和在对象从工厂返回后设置对象实例属性的机制的详细信息,请参阅详细的依赖项和配置(Dependencies and Configuration in Detail)

实例工厂方法实例化(Instantiation by Using an Instance Factory Method)

与通过静态工厂方法进行实例化类似,使用实例工厂方法进行实例化会从容器中调用现有bean的非静态方法来创建新bean。要使用此机制,请将类属性保留为空,并在factory-bean属性中指定当前(或父或祖先)容器中的bean的名称,该容器包含将被调用以创建对象的实例方法。使用factory-method属性设置工厂方法本身的名称。下面的示例展示了如何配置这样一个bean:

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

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

下面的例子展示了相应的类:

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}

一个工厂类也可以包含多个工厂方法,如下例所示:

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

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

<bean id="accountService"
    factory-bean="serviceLocator"
    factory-method="createAccountServiceInstance"/>

下面的例子展示了相应的类:

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    private static AccountService accountService = new AccountServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }

    public AccountService createAccountServiceInstance() {
        return accountService;
    }
}

这种方法表明,工厂bean本身可以通过依赖项注入(DI)进行管理和配置。请参阅详细的依赖项和配置(Dependencies and Configuration in Detail)

提示

在Spring文档中,“factory bean”指在Spring容器中已经配置好的Bean,通过实例或静态(static)工厂方法创建对象。相比之下,FactoryBean(注意大写)指的是特定于Spring的FactoryBean实现类。

确定Bean的运行时类型(Determining a Bean’s Runtime Type)

确定特定bean的运行时类型非常重要。bean元数据定义中指定的类只是一个初始类引用,可能与声明的工厂方法结合在一起,或者作为一个FactoryBean类,这可能导致bean的不同运行时类型,或者在实例级工厂方法的情况下根本不设置(通过指定的工厂bean名称解析)。另外,AOP代理可以用基于接口的代理来包装bean实例,该代理对目标bean的实际类型(仅是其实现的接口)的暴露有限。

了解特定bean的实际运行时类型的推荐方法是为指定的bean名称使用BeanFactory.getType。这将考虑上述所有情况,并返回BeanFactory所使用的对象类型。getBean调用将返回相同的bean名称。

补充

// BeanFactory
Class<?> getType(String name) throws NoSuchBeanDefinitionException;

ListableBeanFactory还提供了根据类型获取name的方法

//ListableBeanFactory
String[] getBeanNamesForType(Class<?> type)

1.4. 依赖(Dependencies)

典型的企业应用程序不包含单个对象(或Spring术语中的bean)。即使是最简单的应用程序也有几个对象一起工作,以呈现最终用户所看到的一致的‘应用程序。下一节将解释如何从定义许多独立的bean定义转变为对象协作实现目标的完全实现的应用程序。

1.4.1. 依赖注入(Dependency Injection)

依赖注入(Dependency Injection,DI)是一个过程,对象仅通过构造函数参数工厂方法的参数或在对象实例构造从工厂方法返回后在对象实例设置的属性来定义它们的依赖项(即与它们一起工作的其他对象)。然后,容器在创建bean时注入这些依赖项。这个过程基本上是bean本身的逆过程(因此得名为控制反转),通过使用类的直接构造或Service Locator模式来控制其依赖项的实例化或位置。

使用DI原则的代码更清晰,当对象提供依赖关系时,解耦更有效。**对象不查找它的依赖项,也不知道依赖项的位置或类。**因此,您的类变得更容易测试,特别是当依赖项在接口或抽象基类上时,这允许在单元测试中使用存根或模拟实现。

DI的两种方式:

补充

传统创建对象的过程:

  1. 用户:在代码中创建对象
  2. 用户:在代码中创建对象的依赖属性,并设置

Spring IoC容器:

  1. 用户:通过配置的方式指定需要创建的Bean
  2. 用户:通过配置的方式指定当前Bean所需的依赖
  3. Spring IoC容器:根据配置组装Bean。这个Bean的组装过程不再由用户代码控制,而是由Spring容器控制,所以称为控制反转。这样做的好处很明显,Bean之间的依赖关系不再由硬编码实现,而是可以通过配置文件实现,从而实现对象之间的解耦。

构造器方式的依赖注入

基于构造器的DI是通过容器调用一个带有多个参数的构造器来实现的,每个参数代表一个依赖项。调用带有特定参数的静态工厂方法来构造bean几乎是等价的,本讨论类似地处理构造函数和静态工厂方法的参数。下面的例子展示了一个只能通过构造函数注入进行依赖注入的类:

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on a MovieFinder
    private final 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...
}

注意,这个类没有什么特别之处。它是一个POJO,不依赖于容器特定的接口、基类或注解。

构造函数参数解析

构造函数参数解析匹配通过使用参数的类型进行。如果bean定义的构造函数参数中不存在潜在的歧义,那么在bean定义中定义构造函数参数的顺序就是在实例化bean时将这些参数提供给适当构造函数的顺序。考虑下面的类:

package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

假设ThingTwoThingThree类没有继承关系,就不存在潜在的歧义。因此,下面的配置工作得很好,您不需要在<constructor-arg/>元素。

<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg ref="beanTwo"/>
        <constructor-arg ref="beanThree"/>
    </bean>

    <bean id="beanTwo" class="x.y.ThingTwo"/>

    <bean id="beanThree" class="x.y.ThingThree"/>
</beans>

当引用另一个bean时,类型是已知的,并且可以进行匹配(与前面的示例一样)。当使用简单类型(如<value>true</value>)时,Spring无法确定值的类型,因此在没有帮助的情况下无法按类型进行匹配。考虑下面的类:

package examples;

public class ExampleBean {

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

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

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}
构造函数参数类型匹配(Constructor argument type matching)

在上述场景中,如果使用type属性显式指定构造函数实参的类型,则容器可以使用与简单类型的类型匹配,如下例所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>
构造函数参数索引(Constructor argument index)

可以使用index属性显式指定构造函数参数的索引,如下例所示:

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

除了解决多个简单值的歧义之外,指定索引还解决了构造函数具有相同类型的两个参数时的歧义

提示

索引是从0开始的。

构造函数参数名称(Constructor argument name)

你也可以使用构造函数参数名来消除值的歧义,如下面的例子所示:

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

请记住,要使此工作开箱即用,必须在编译代码时启用调试标志,以便Spring可以从构造函数中查找参数名称。如果不能或不想用调试标志编译代码,可以使用JDK的@ConstructorProperties注解来显式地命名构造函数参数。然后,样例类必须如下所示:

package examples;

public class ExampleBean {

    // Fields omitted

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

补充:

.class文件会丢失变量名称数据,所以需要通过调试标识或@ConstructorProperties注解的方式告诉Spring对应的参数名称。

Setter方式的注入

基于setter的DI是由容器

  1. 在调用无参数构造函数无参数静态工厂方法实例化bean之后,
  2. 调用bean上的setter方法来完成的。

下面的示例展示了一个只能通过使用纯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...
}

ApplicationContext为它管理的bean支持基于构造器和基于Setter的DI。在已经通过构造函数方法注入了一些依赖项之后,它还支持基于setter的DI。您以BeanDefinition的形式配置依赖项,并将其与PropertyEditor实例结合使用,将属性从一种格式转换为另一种格式。然而,大多数Spring用户并不直接使用这些类(即以编程方式),

  • 而是使用XML的bean定义、
  • 带注解的组件(即用@Component@Controller等注解的类)
  • 或基于Java的@Configuration类中的@Bean方法。

然后在内部将这些源转换为BeanDefinition的实例,并用于加载整个Spring IoC容器实例。

构造器注入 vs. setter注入(constructor-based vs. setter-based)

由于您可以混合使用基于构造函数和基于setter的DI,因此对于强制依赖项使用构造函数,而对于可选依赖项使用setter方法或配置方法是一条很好的经验法则。注意,在setter方法上使用@Required注解可以使属性成为必须的依赖项;但是,带有编程验证的有参构造函数注入更可取。

Spring团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不为空(null)。此外,构造器注入的组件总是以完全初始化的状态返回给客户端(调用)代码。顺便说一句,大量的构造函数参数是一种不好的代码气味,这意味着类可能有太多的职责,应该重构以更好地解决适当的关注点分离问题。

Setter注入应该主要只用于可选的依赖项,这些依赖项可以在类中被分配合理的默认值。否则,必须在代码使用依赖项的任何地方执行非空检查。setter注入的一个好处是,setter方法使该类的对象可以在以后重新配置重新注入。因此,通过JMX MBeans进行管理是setter注入的一个引人注目的用例

使用对特定类最有意义的DI样式。有时,在处理没有源代码的第三方类时,会为您做出选择。例如,如果第三方类没有公开任何setter方法,那么构造函数注入可能是唯一可用的DI形式。

依赖解析过程(Dependency Resolution Process)

Spring容器按如下方式解决bean的依赖关系:

  • 使用描述所有bean的配置元数据创建并初始化ApplicationContext。配置元数据可以通过XML、Java代码或注解来指定。
  • 对于每个bean,其依赖关系以属性构造函数参数静态工厂方法的参数(如果使用静态工厂方法而不是普通构造函数)的形式表示。在实际创建bean时,将这些依赖项提供给bean。
  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个bean的引用
  • 作为值的每个属性或构造函数参数将从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将以字符串格式提供的值转换为所有内置类型,例如intlongstringboolean等等。

Spring容器在创建容器时验证每个bean的配置。但是,在实际创建bean之前,不会设置bean属性本身。在创建容器时,默认会创建单例作用域(singleton-scoped)并设置为预实例化(pre-instantiated)的bean。作用域在Bean作用域(Bean Scopes)中定义。否则,仅在请求时创建bean。bean的创建可能会导致创建一个bean图,因为bean的依赖项及其依赖项的依赖项(等等)被创建分配。请注意,这些依赖项之间的解析不匹配可能会很晚才出现——也就是说,在受影响bean的第一次创建时出现。

循环依赖(Circular dependencies)

如果主要使用构造函数注入,则可能会创建无法解析的循环依赖场景。

例如:类A通过构造函数注入需要类B的实例,类B通过构造函数注入需要类A的实例。如果将类A和类B配置为相互注入的bean, Spring IoC容器会在运行时检测到这个循环引用,并抛出beancurcurrentlyincreationexception

一个可能的解决方案是编辑一些类的源代码,让它们由setter而不是构造函数来配置。或者,避免构造函数注入,只使用setter注入。换句话说,尽管不推荐这样做,但您可以使用setter注入配置循环依赖项

与典型情况(没有循环依赖关系)不同,bean A和bean B之间的循环依赖关系强制其中一个bean在完全初始化之前注入另一个bean(经典的先有鸡还是先有蛋的场景)。

您通常可以相信Spring会做正确的事情。它在容器加载时检测配置问题,例如对不存在的bean的引用和循环依赖项。Spring尽可能晚地设置属性并解析依赖项,直到bean实际创建完成。这意味着,如果在创建对象或其依赖项之一时出现问题,那么正确加载的Spring容器以后可以在请求对象时生成异常——例如,由于缺少或无效的属性,bean会抛出异常。这可能会延迟一些配置问题的可见性,这就是ApplicationContext实现默认情况预实例化(pre-instantiate)单例bean的原因。在实际需要这些bean之前,需要花费一些前期时间和内存来创建它们,在创建ApplicationContext时(而不是之后)发现配置问题。您仍然可以覆盖此默认行为,以便单例bean惰性地初始化(initialize lazily),而不是马上预实例化。

如果不存在循环依赖项,当一个或多个协作bean被注入依赖bean时,每个协作bean在被注入依赖bean之前会被完全配置。这意味着,如果bean A依赖于bean B, Spring IoC容器将在调用bean A上的setter方法之前完全配置bean B。换句话说,bean被实例化(如果它不是预实例化的单例),它的依赖项被设置并调用相关的生命周期方法(例如配置的init方法(configured init method)InitializingBean回调方法(InitializingBean callback method))。

依赖注入示例

下面的示例将基于XML的配置元数据用于基于setter的DI。Spring XML配置文件的一小部分如下所示指定了一些bean定义:

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

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

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

如下为ExampleBean类:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

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

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

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

在前面的示例中,声明setter来匹配XML文件中指定的属性。下面的例子使用了基于构造函数的DI:

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

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

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

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

如下为ExampleBean类:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

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

在bean定义中指定的构造函数参数用作ExampleBean构造函数的参数。

现在考虑这个例子的一个变体,在这里,Spring被告知调用一个静态工厂方法来返回对象的一个实例,而不是使用构造函数:

<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
    <constructor-arg ref="anotherExampleBean"/>
    <constructor-arg ref="yetAnotherBean"/>
    <constructor-arg value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

下面的例子展示了相应的ExampleBean类:

public class ExampleBean {

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

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

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

静态工厂方法的参数由<constructor-arg/>元素,与实际使用构造函数完全相同。由工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本例中是相同的)。实例(非静态)工厂方法可以以本质上相同的方式使用(除了使用工厂bean属性而不是类属性),因此我们在这里不讨论这些细节。

1.4.2 详细的依赖关系和配置(Dependencies and Configuration in Detail)

如前一节所述,您可以将bean属性和构造函数参数定义为对其他托管bean(协作者)的引用或内联定义的值。Spring基于XML的配置元数据支持其<property/><constructor-arg>用于此目的的元素。

直接值(主要类型,字符串等)

<property/>元素的value属性将属性或构造函数参数指定为人类可读的字符串表示形式。Spring的转换服务(conversion service)用于将这些值从String转换为属性或参数的实际类型。下面的示例显示了设置的各种值:

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

下面的例子使用p命名空间进行更简洁的XML配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close"
        p:driverClassName="com.mysql.jdbc.Driver"
        p:url="jdbc:mysql://localhost:3306/mydb"
        p:username="root"
        p:password="misterkaoli"/>

</beans>

您还可以配置java.util.Properties实例,如下所示:

<bean id="mappings"
    class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">

    <!-- typed as a java.util.Properties -->
    <property name="properties">
        <value>
            jdbc.driver.className=com.mysql.jdbc.Driver
            jdbc.url=jdbc:mysql://localhost:3306/mydb
        </value>
    </property>
</bean>

Spring容器转换<value/>元素放入java.util.Properties实例中,方法是使用JavaBeans的PropertyEditor机制。这是一个很好的快捷方式,也是Spring团队倾向于使用嵌套<value/>元素而不是value属性样式。

idref元素

idref元素只是将容器中另一个bean的id(字符串值,而不是引用)传递给<constructor-arg/><property/>元素的一种**防错误(error-proof)**方法。下面的例子展示了如何使用它:

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

<bean id="theClientBean" class="...">
    <property name="targetName">
        <idref bean="theTargetBean"/>
    </property>
</bean>

前面的bean定义代码段(在运行时)与下面的代码段完全等价:

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

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean"/>
</bean>
  • 第一种形式比第二种形式更可取,因为使用idref标记可以让验容器在部署时证所引用的命名bean是否确实存在

  • 第二个变体中,对传递给客户机bean的targetName属性的值不执行任何验证

只有在实际实例化客户机bean时才会发现错别字(很可能导致致命的结果)。如果客户机bean是一个原型(prototype)bean,那么这个错别字和由此产生的异常可能只有在部署容器很久之后才会被发现。

提示

在4.0 beans XSD中不再支持idref元素上的本地属性,因为它不再通过常规bean引用提供值。升级到4.0模式时,将现有的idref local引用更改为idref bean

一个常见的地方(至少在Spring 2.0之前的版本中)<idref/>元素带来的值是在ProxyFactoryBean bean定义中的AOP拦截器(AOP interceptors)的配置中。使用<idref/>元素可以防止你拼错拦截器ID。

对其他bean的引用(协作者)(References to Other Beans (Collaborators))

ref元素是<constructor-arg/><property/>元素内部的最后的元素。在这里,您将bean的指定属性的值设置为对容器管理的另一个bean(协作者)的引用。被引用的bean是要设置其属性的bean的依赖项,并且在设置属性之前根据需要对其进行初始化。(如果合作者是一个单例bean,它可能已经被容器初始化了。)所有引用最终都是对另一个对象的引用。作用域(Scoping)和验证取决于是否通过bean或父(parent)属性指定其他对象的ID名称

通过bean的属性 <ref/>标记指定目标bean是最通用的形式,且允许在相同容器或父容器中创建对任何bean的引用,而不管它是否在相同的XML文件中。bean属性的值可以与目标bean的id属性相同,也可以与目标bean的name属性中的值之一相同。下面的例子展示了如何使用ref元素:

<ref bean="someBean"/>

通过parent属性指定目标bean将创建对当前容器的父容器中的bean的引用。父(parent)属性的值可以与目标bean的id属性相同,也可以与目标bean的name属性中的值之一相同。目标bean必须位于当前bean的父容器中。您应该主要在以下情况下使用此bean引用变体:您有一个容器层次结构,并且您希望将现有bean包装在具有与父bean相同名称的代理的父容器中。下面的两个清单显示了如何使用parent属性:

<!--父容器-->
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
<!--在子容器的bean需要和父容器的同名-->
<!--class是一个代理类-->
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
</bean>

提示

在4.0 beans XSD中不再支持ref元素上的本地属性,因为它不再通过常规bean引用提供值。升级到4.0模式时,将现有的ref local引用更改为ref bean

内部Bean

<property/><constructor-arg/>元素内部可以有一个<bean/>,用于定义一个内部bean,如下示例:

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

内部bean定义不需要定义的ID或名称。如果指定了该值,则容器不使用该值作为标识符。容器还在创建时忽略作用域(scope)标志,因为内部bean始终是匿名的,并且始终与外部bean一起创建。不可能独立访问内部bean,也不可能将它们注入到协作bean中,而不是注入到封闭bean中。

作为一种极端情况,可以从自定义作用域接收销毁回调——例如,对于包含在单例bean中的请求作用域的内部bean。内部bean实例的创建与它的包含bean绑定在一起,但是销毁回调允许它参与请求范围的生命周期。这种情况并不常见。内部bean通常只是共享其包含bean的作用域。

集合(Collections)

Spring 提供了对ListSetMapProperties的支持,分别是<list/><set/><map/><props/>。如下为如何使用他们的示例:

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

map键或值,或集合值的值也可以是以下任何元素:

bean | ref | idref | list | set | map | props | value | null
集合合并(Collection Merging)

Spring容器还支持合并集合。应用开发者能定义<list/>, <map/>, <set/><props/>元素,并且有<list/>, <map/>, <set/><props/>元素集成并重写父集合。也就是说,子集合的值是合并父集合和子集合元素的结果,子集合元素覆盖父集合中指定的值

关于合并的这一节讨论了父子bean机制。不熟悉父bean和子bean定义的读者可能希望在继续之前阅读相关部分(relevant section)

<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>
    <!--指定parent bean-->
    <bean id="child" parent="parent">     
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <!-- merge用于合并子集合和父集合的元素 -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>

注意在子bean定义的adminEmails属性的<props/>元素中merge=true。当子bean被容器解析并实例化时,结果实例有一个adminEmails Properties集合,其中包含子bean的adminEmails集合与父bean的adminEmails集合合并的结果。下面的清单显示了结果:

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

Properties集合值继承了父<props/>的所有属性元素,并且支持值的子值覆盖父集合中的值。

这种合并行为在<list/><map/><set/>是相似的。在<list/>元素的特定场景中,与List集合类型相关联的语义(即值的有序集合的概念)得到了维护。父级列表的值位于所有子级列表的值之前。对于MapSetProperties集合类型,不存在排序。因此,对于容器内部使用的关联MapSetProperties实现类型底层的集合类型,没有有效的排序语义。

集合合并的局限性(Limitations of Collection Merging)

不能合并不同的集合类型(例如Map和List)。如果您尝试这样做,则会抛出一个适当的Exception。必须在较低的继承子定义上指定merge属性。在父集合定义上指定merge属性是多余的,并且不会导致所需的合并。

强类型集合(Strongly-typed collection)

由于Java对泛型类型的支持,您可以使用强类型集合。也就是说,可以声明一个Collection类型,使其只能包含(例如)String元素。如果您使用Spring将强类型集合依赖注入到bean中,那么您可以利用Spring的类型转换支持,使强类型集合实例的元素在添加到集合之前被转换为适当的类型。下面的Java类和bean定义展示了如何做到这一点:

public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
<beans>
    <bean id="something" class="x.y.SomeClass">
        <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>

something bean的accounts属性准备注入时,关于强类型Map<String, Float>通过反射可用。因此,Spring的类型转换基础结构将各种值元素识别为Float类型,并将字符串值(9.992.753.99)转换为实际的Float类型。

空值和空字符串(Null and Empty String Values)

Spring将属性等空参数视为空字符串。以下基于XML的配置元数据片段将email属性设置为空String值("")。

<!-- 设置"" -->
<bean class="ExampleBean">
    <property name="email" value=""/>
</bean>

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

exampleBean.setEmail("");

<null/>元素处理null值。以下列出了示例:

<!-- 设置null -->
<bean class="ExampleBean">
    <property name="email">
        <null/>
    </property>
</bean>

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

exampleBean.setEmail(null);

带有p命名空间(p-namespace)的XML快捷方式

补充:

p可以理解为property的缩写

p命名空间使得你可以以bean元素的属性的方式描述属性值(而不是<property/>)。

Spring支持带有名称空间(with namespaces)可扩展配置格式,这些名称空间基于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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean name="classic" class="com.example.ExampleBean">
        <property name="email" value="someone@somewhere.com"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="someone@somewhere.com"/>
</beans>

该示例显示了bean定义中名为email的p命名空间中的一个属性。这告诉Spring包含一个属性声明。如前所述,p命名空间没有模式定义,因此可以将属性名称设置为属性名称。

下一个示例包含另外两个bean定义,它们都有对另一个bean的引用:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean name="john-classic" class="com.example.Person">
        <property name="name" value="John Doe"/>
        <property name="spouse" ref="jane"/>
    </bean>

    <bean name="john-modern"
        class="com.example.Person"
        p:name="John Doe"
        p:spouse-ref="jane"/>

    <bean name="jane" class="com.example.Person">
        <property name="name" value="Jane Doe"/>
    </bean>
</beans>

这个示例不仅包括使用p名称空间的属性值,而且还使用特殊格式来声明属性引用。而第一个bean定义使用<property name="spouse" ref="jane"/>创建john-classicjane的引用,第二个bean定义使用p:spouse-ref="jane"作为属性来执行完全相同的操作。在本例中,spouse是属性名,而-ref部分表示这不是一个直接值,而是对另一个bean的引用

提示

p名称空间不如标准XML格式灵活。例如,声明属性引用的格式与以Ref结尾的属性冲突,而标准XML格式则不会。我们建议您仔细选择您的方法,并与您的团队成员进行沟通,以避免生成同时使用所有三种方法的XML文档。

带有c命名(c-namespace)空间的XML快捷方式

补充:

c可以理解为constructor的缩写

与p命名空间相似,c命名空间自Spring 3.1引入,允许内联属性来配置构造函数参数,而不是嵌套的构造函数参数元素。

下面的例子使用c:命名空间来做与基于构造函数的依赖注入(Constructor-based Dependency Injection):相同的事情:

<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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>

    <!-- traditional declaration with optional argument names -->
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg name="thingTwo" ref="beanTwo"/>
        <constructor-arg name="thingThree" ref="beanThree"/>
        <constructor-arg name="email" value="something@somewhere.com"/>
    </bean>

    <!-- c-namespace declaration with argument names -->
    <bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
        c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>

</beans>

c命名空间使用和p命名空间相同的约定(后面的-ref表示bean引用),用名称设置构造函数参数。相似的,它需要在XML文件中声明,即使它没有在XSD模式中定义(它存在于Spring核心中)。

对于构造函数参数名称不可用的极少数情况(通常是字节码编译时没有调试信息),您可以使用回退到参数索引,如下所示:

<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
    c:_2="something@somewhere.com"/>

提示

由于XML语法,索引表示法要求出现前导_,因为XML属性名不能以数字开头(尽管有些IDE允许)。相应的索引符号也可用于<constructor-arg>元素,但不常用,因为在这里,简单的声明顺序通常就足够了。

在实践中,构造函数解析机制在匹配参数方面非常有效,因此,除非您确实需要,我们建议在整个配置中使用名称表示法。

复合属性名称(Compound Property Names)

在设置bean属性时,可以使用复合或嵌套属性名,只要路径的所有组件(最终属性名除外)不为空即可。考虑下面的bean定义:

<bean id="something" class="things.ThingOne">
    <property name="fred.bob.sammy" value="123" />
</bean>

something bean有一个fred属性,fred属性有一个bob属性,bob属性有一个sammy属性,最后一个sammy属性被设置为123。为了使它工作,在bean被构造之后,somethingfred属性和fredbob属性不能为空。否则,抛出NullPointerException

1.4.3. 使用depends-on

如果一个bean是另一个bean的依赖项,这通常意味着一个bean被设置为另一个bean的属性。通常,您可以使用<ref/>元素在基于XML的配置元数据。然而,有时bean之间的依赖关系不太直接。例如,当需要触发类中的静态初始化项时,例如数据库驱动程序注册。depends-on属性可以显式地强制初始化使用该元素的bean之前初始化一个或多个bean。下面的例子使用depends-on属性来表示对单个bean的依赖:

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

要表达对多个bean的依赖,提供一个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" />

提示

depends-on属性既可以指定初始化时依赖项,也可以指定对应的销毁时依赖项(仅在单例(singleton)bean的情况下)。在给定bean本身被销毁之前,首先销毁与给定bean定义依赖关系的依赖bean。因此,依赖还可以控制关机顺序

1.4.4. 延迟初始化Bean(Lazy-initialized Beans)

默认情况下,ApplicationContext实现立即创建和配置所有单例bean,作为初始化过程的一部分。通常,这种预实例化是可取的,因为可以立即发现配置或周围环境中的错误,而不是在几小时甚至几天之后发现。当不需要这种行为时,可以通过将bean定义标记为惰性初始化(lazy-initialized)来防止单例bean的预实例化。惰性初始化bean告诉IoC容器在第一次请求时创建bean实例,而不是在启动时创建。

在XML,通过设置<bean/>lazy-init属性实现延迟初始化:

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>

当前面的配置被ApplicationContext使用时,当ApplicationContext启动时,lazy bean不会被立即预实例化,而not.lazy bean会被立即预实例化。

然而,当惰性初始化bean是未惰性初始化的单例bean的依赖项时ApplicationContext在启动时创建惰性初始化bean,因为它必须满足单例的依赖项。惰性初始化的bean被注入到其他未惰性初始化的单例bean中。

你可以通过<beans/>元素的default-lazy-init属性实现容器级别的延迟初始化,如下:

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

1.4.5. 自动装配协作者(Autowiring Collaborators)

Spring容器可以自动装配协作bean之间的关系。您可以让Spring通过检查ApplicationContext的内容来自动为您的bean解析协作者(其他bean)。自动装配有以下优点

  • 自动装配可以显著减少指定属性构造函数参数的需要。(其他机制,如本章其他地方讨论的bean模板,在这方面也很有价值。)
  • 自动装配可以随着对象的发展而更新配置。例如,如果您需要向类中添加依赖项,则无需修改配置即可自动满足该依赖项。因此,自动装配在开发过程中特别有用,当代码库变得更加稳定时,不排除切换到显式装配的选择。

当使用基于XML配置元数据,可以通过<bean/>autowire属性设置自动装配模式。自动装配功能有四种模式。您可以指定每个bean的自动装配,因此可以选择自动装配哪些bean。四种自动装配模式如下表所示:

ModeExplanation
no(默认)没有自动装配。Bean引用必须由ref元素定义。对于大型部署,不建议更改默认设置,因为显式指定协作者可以提供更好的控制和清晰度。在某种程度上,它记录了系统的结构。
byName属性名自动生成。Spring查找与需要自动装配的属性同名的bean。例如,如果一个bean定义通过名称设置为自动装配,并且它包含一个master属性(也就是说,它有一个setMaster(..)方法),Spring将查找一个名为master的bean定义,并使用它来设置该属性。
byType如果容器中只存在一个属性类型的bean,则允许属性按类型自动装配。如果存在一个以上的bean,则抛出一个致命异常,这表明您不能对该bean使用byType自动装配。如果没有匹配的bean,什么也不会发生(没有设置属性)。
constructor类似于byType,但适用于构造函数参数。如果容器中没有一个构造函数参数类型的bean,则会引发致命错误。

补充

  • byName的方式更精确,尤其是当同一类型的bean有多个的时候,不容易抛出异常。

使用byType或构造函数自动装配模式,您可以装配数组和类型化集合。在这种情况下,将提供容器中与预期类型匹配的所有自动候选项,以满足依赖项。如果期望的键类型是String,则可以自动装配强类型的Map实例。自动装配的Map实例的值由与预期类型匹配的所有bean实例组成,Map实例的键包含相应的bean名称

自动装配的限制和缺点(Limitations and Disadvantages of Autowiring)

自动装配在整个项目中一致使用时效果最好。如果通常不使用自动装配,那么仅使用它来装配一个或两个bean定义可能会使开发人员感到困惑。

考虑自动装配的限制和缺点:

  • propertyconstructor-arg 设置中的显式依赖关系总是覆盖自动装配。您不能自动装配简单的属性,如基本数据类型(primitives)、字符串Classes(以及这些简单属性的数组)。这种限制是设计出来的。
  • 自动装配不如显式装配精确。不过,正如前面的表所指出的,Spring会小心地避免猜测,以免歧义可能产生意想不到的结果。不再显式地记录Spring管理的对象之间的关系。
  • 从Spring容器生成文档的工具可能无法获得装配信息。
  • 容器中的多个bean定义可以与自动装配的setter方法或构造函数参数指定的类型匹配。对于数组集合Map实例,这并不一定是个问题。然而,对于期望单个值的依赖项,这种模糊性不是任意解决的。如果没有唯一的bean定义可用,则抛出异常

在后一种情况下,您有几个选项:

  • 放弃自动装配,使用显示的装配
  • 通过将bean的autowire-candidate属性设置为false来避免自动装配bean定义,如下一节所述。
  • 通过设置一个<bean/>primary属性为true,将一个bean定义指定为主要候选对象
  • 使用基于注解的配置实现更细粒度的控制,如基于注解的容器配置(Annotation-based Container Configuration)中所述。

从自动装配中排除Bean(Excluding a Bean from Autowiring)

在每个bean的基础上,您可以从自动装配中排除一个bean。在Spring的XML格式中,设置<bean/>元素的autowire-candidate属性为false。容器使得该特定bean定义对自动装配基础设施(包括注解样式配置,如@ @Autowired)不可用。

提示

autowire-candidate属性被设计为只影响基于类型的自动装配。它不影响按名称的显式引用,即使指定的bean没有被标记为自动候选,也可以解析显式引用。因此,如果按名称匹配,按名称自动调用仍然会注入一个bean。

您还可以基于对bean名称的模式匹配来限制自动调用候选者。顶层<beans/>元素通过default-autowire-candidates属性接受一个或多个模式。例如,为了将候选状态限制为名称以Repository结尾的任何bean,可以提供*Repository的值。要提供多个模式,请在逗号分隔的列表中定义它们。bean定义的autowire-candidate属性的显式值truefalse总是优先。对于这样的bean,模式匹配规则不适用。

这些技术对于永远不希望通过自动装配将其注入到其他bean中的bean非常有用。这并不意味着不能通过自动装配来配置被排除的bean本身。相反,bean本身并不是自动装配其他bean的候选对象。

1.4.6 方法注入(Method Injection)

在大多数应用程序场景中,容器中的大多数bean都是单例的。当一个单例bean需要与另一个单例bean协作,或者一个非单例bean需要与另一个非单例bean协作时,通常通过将一个bean定义为另一个bean的属性来处理依赖关系。当bean的生命周期不同时,问题就出现了。假设单例bean A需要使用非单例(prototype)bean B,可能是在A上的每个方法调用上。容器只创建单例bean A一次,因此只有一次设置属性的机会。容器不能在每次需要bean B的实例时都为bean A提供新的实例。

一个解决方案是放弃一些控制反转。您可以通过实现ApplicationContextAware接口,并在bean A每次需要时对容器请求(通常是新的)bean B实例进行getBean(“B”)调用,使bean A意识到容器。下面的例子展示了这种方法:

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

前面的情况是不可取的,因为业务代码知道Spring框架并与之耦合。方法注入是Spring IoC容器的一个高级特性,它允许您干净地处理这个用例。

您可以在这篇博客文章中了解更多关于方法注入的动机。

查找方法注入(Lookup Method Injection)

查找方法注入是容器覆盖容器管理bean上的方法并返回容器中另一个命名bean的查找结果的能力。查找通常涉及一个原型(prototype)bean,就像前一节描述的场景一样。Spring框架通过使用来自CGLIB库的字节码生成来动态生成覆盖该方法的子类来实现此方法注入。

提示

  • 要使这个动态子类工作,Spring bean容器子类所包含的类不能是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(); // 注意,这里是抽象方法
}

在包含要注入的方法(本例中是CommandManager)的客户端类中,要注入的方法需要以下形式的签名:

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

如果方法是抽象的,则动态生成的子类实现该方法。否则,动态生成的子类覆盖在原始类中定义的具体方法。考虑下面的例子:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" 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="myCommand"/>
</bean>

标识为commandManager的bean在需要myCommand bean的新实例时调用它自己的createCommand()方法。如果确实需要myCommand bean,则必须谨慎地将其部署为原型。如果它是单例,则每次都返回myCommand bean的相同实例

或者,在基于注解的组件模型中,你可以通过@Lookup注解声明一个查找方法,如下面的例子所示:

public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup("myCommand")
    protected abstract Command createCommand();
}

请注意,通常应该使用具体的存根实现来声明这种带注解的查找方法,以便它们与Spring的组件扫描规则兼容,其中抽象类在默认情况下被忽略。此限制不适用于显式注册或显式导入的bean类。

访问不同作用域目标bean的另一种方法是ObjectFactory/ Provider注入点。请参阅作为依赖的作用域bean(Scoped Beans as Dependencies)

您可能还会发现ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包中)非常有用。

任意方法替换(Arbitrary Method Replacement)

与查找方法注入相比,方法注入的一种不太有用的形式是能够用另一种方法实现替换托管bean中的任意方法。在实际需要此功能之前,您可以跳过本节的其余部分。

对于基于XML的配置元数据,您可以使用已替换方法元素,将已部署bean的现有方法实现替换为另一个。考虑下面的类,它有一个我们想要覆盖的名为computeValue的方法:

public class MyValueCalculator {

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

    // some other methods...
}

一个实现org.springframework.beans.factory.support.MethodReplacer接口的类提供了新的方法定义,如下面的示例所示:

/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
public class ReplacementComputeValue implements MethodReplacer {

    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // get the input value, work with it, and return a computed result
        String input = (String) args[0];
        ...
        return ...;
    }
}

部署原始类并指定方法覆盖的bean定义类似于以下示例:

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

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

你可以使用<replaced-method/>元素一个或多个<arg-type>元素以指示被覆盖的方法的方法签名。只有当方法被重载并且类中存在多个变体时,参数的签名才是必需的。为方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,以下所有匹配java.lang.String

java.lang.String
String
Str

因为参数的数量通常足以区分每种可能的选择,所以这种快捷方式可以节省大量的输入,因为它允许您只键入与参数类型匹配的最短字符串。

1.5 Bean的作用域(Bean Scopes)

当您创建一个bean定义时,您创建了一个配方,用于创建由该bean定义定义的类的实际实例。bean定义是一个配方的想法很重要,因为这意味着,与使用类一样,您可以从一个配方创建许多对象实例

您不仅可以控制要插入到从特定bean定义创建的对象中的各种依赖项配置值,还可以控制从特定bean定义创建的对象的作用域。这种方法功能强大且灵活,因为您可以通过配置选择创建的对象的作用域,而不必在Java类级别上设置对象的作用域。可以将bean定义为部署在多个作用域中的一个。Spring框架支持6个作用域,其中4个只有在使用web感知(web-aware)的ApplicationContext时才可用。您还可以创建自定义作用域(a custom scope)

下表给出支持的作用域:

ScopeDescription
singleton(默认值)将单个bean定义作用于每个Spring IoC容器的单个对象实例。
prototype将单个bean定义限定为任意数量的对象实例。
request将单个bean定义限定在单个HTTP请求的生命周期内。也就是说,每个HTTP请求都有自己的bean实例,该实例是在单个bean定义的后面创建的。只在具有web感知的Spring ApplicationContext上下文中有效。
session将单个bean定义限定在HTTP会话(Session)的生命周期内。只在具有web感知的Spring ApplicationContext上下文中有效。
application将单个bean定义限定ServletContext的生命周期内。只在具有web感知的Spring ApplicationContext上下文中有效。
websocket将单个bean定义限定在WebSocket的生命周期内。只在具有web感知的Spring ApplicationContext上下文中有效。

提示

从Spring 3.0开始,线程作用域是可用的,但默认情况下没有注册。有关更多信息,请参阅SimpleThreadScope的文档。有关如何注册此或任何其他自定义作用域的说明,请参阅使用自定义作用域(Using a Custom Scope)

1.5.1. 单例作用域(The Singleton Scope)

只管理单例bean的一个共享实例,并且对具有一个或多个与该bean定义匹配的ID的bean的所有请求都会导致Spring容器返回一个特定的bean实例

换句话说,当您定义一个bean定义并且它的作用域为单例时,Spring IoC容器只创建由该bean定义定义的对象的一个实例。这个单一实例存储在这种单例bean的缓存中,所有后续的请求和对该命名bean的引用都会返回缓存的对象。下图展示了单例作用域是如何工作的:

singleton.png

Spring的单例bean概念不同于四人帮(Gang of Four, GoF)模式书中定义的单例模式。GoF单例对对象的作用域进行硬编码,使得每个类加载器(ClassLoader)只能创建一个特定类的实例。Spring单例的作用域最好描述为每个容器和每个bean。这意味着,如果您在单个Spring容器中为特定类定义一个bean,那么Spring容器将创建由该bean定义定义的类的一个且仅一个实例单例作用域是Spring的默认作用域。要在XML中将bean定义为单例,您可以像下面的示例那样定义bean:

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

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

1.5.2. 原型(Prototype)作用域

bean部署的非单例原型范围导致每次对特定bean发出请求都创建一个新的bean实例。也就是说,

  • 将bean注入到另一个bean中,
  • 或者通过对容器的getBean()方法调用请求它。

通常,

  • 应该对所有有状态bean使用原型作用域
  • 无状态bean使用单例作用域

补充

可以理解为每次调用getBean()都会产生一个新的bean实例,因为依赖的注入实际上也是通过getBean()获取依赖bean实例的。

下图说明了Spring原型作用域:

prototype.png

(数据访问对象(DAO)通常不配置为原型,因为典型的DAO不保持任何会话状态。对我们来说重用单例图的核心更容易。)

下面的例子将bean定义为XML中的原型:

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

与其他作用域相比,Spring并不管理原型bean的完整生命周期。容器实例化、配置和组装一个原型对象,并将其交给客户端,不再记录该原型实例。因此,尽管初始化生命周期回调方法在所有对象上调用,而不考虑作用域,但在原型的情况下,不会调用配置的销毁生命周期回调客户端代码必须清理原型作用域的对象,并释放原型bean持有的昂贵资源。要让Spring容器释放由原型作用域的bean所持有的资源,请尝试使用自定义bean后置处理器(bean post-processor),该后置处理器包含对需要清理的bean的引用。

在某些方面,Spring容器在原型作用域bean方面的作用是Java new操作符的替代品。此后的所有生命周期管理都必须由客户端处理。(有关Spring容器中bean生命周期的详细信息,请参见生命周期回调(Lifecycle Callbacks)。)

1.5.3. 带有原型bean依赖的单例bean(Singleton Beans with Prototype-bean Dependencies)

当您使用依赖于原型bean的单例作用域bean时,请注意依赖关系是在实例化时解析的。因此,如果您以依赖方式将一个原型作用域的bean注入到单例作用域的bean中,就会实例化一个新的原型bean,然后再将依赖方式注入到单例bean中。原型实例是提供给该单例作用域bean的唯一实例。

补充

  • 单例A依赖原型C,单例B依赖原型C,那么A中的原型C和B中的原型C是两个实例。只不过,依赖仅注入一次,所以A中的C和B中的C不会再发生改变。

但是,假设您希望单例作用域bean在运行时重复获取原型作用域bean的新实例。您不能依赖地将一个原型作用域的bean注入到单例bean中,因为这种注入只发生一次,即在Spring容器实例化单例bean并解析并注入它的依赖项时。如果在运行时不止一次需要一个原型bean的新实例,请参见方法注入(Method Injection)

1.5.4. Request, Session, Application和WebSocket作用域

request, session, applicationwebsocket作用域只有当你使用一个web感知(web-aware)的Spring ApplicationContext实现(比如XmlWebApplicationContext)时才可用。如果您将这些作用域与常规的Spring IoC容器(例如ClassPathXmlApplicationContext)一起使用,则会抛出一个IllegalStateException,它会抱怨未知的bean作用域

补充

在Web服务开发中,使用requestsessionapplication作用域可以简化对Servlet的不同作用域的访问。

初始化Web配置(Initial Web Configuration)

为了在bean上支持requestsessionapplicationwebsocket级别(web-scoped bean)作用域,在定义bean之前需要进行一些小的初始配置。(对于标准作用域:singletonprototype,不需要初始设置。)

如何完成这个初始设置取决于您的特定Servlet环境。

如果您在Spring Web MVC中访问作用域bean(实际上是在Spring DispatcherServlet处理的请求中),则不需要进行特殊设置DispatcherServlet已经公开了所有相关的状态。

如果你使用Servlet 2.5 web容器,请求是在Spring的DispatcherServlet之外处理的(例如,当使用JSF或Struts时),你需要注册org.springframework.web.context.request.RequestContextListener 。对于Servlet 3.0+,这可以通过使用WebApplicationInitializer接口以编程方式完成。或者,对于旧的容器,在你的web应用程序的web.xml文件中添加以下声明:

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

或者,如果监听器设置有问题,可以考虑使用Spring的RequestContextFilter。过滤器映射依赖于周围的web应用程序配置,因此您必须适当地更改它。下面的清单显示了一个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>

DispatcherServletRequestContextListenerRequestContextFilter都做完全相同的事情,即将HTTP请求对象绑定到为该请求提供服务的线程。这使得具有请求和会话作用域的bean在调用链的更下游可用。

Request作用域

考虑以下用于bean定义的XML配置:

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

Spring容器通过为每个HTTP请求使用LoginAction bean定义来创建LoginAction bean的新实例。也就是说,loginAction bean的作用域在HTTP请求级别。您可以随心所欲地更改创建的实例的内部状态,因为从相同loginAction bean定义创建的其他实例看不到这些状态更改。它们是特定于单个请求的当请求完成处理时,将丢弃作用域为该请求的bean

当使用注解驱动的组件Java配置时,可以使用@RequestScope注解将组件分配给请求范围。下面的例子展示了如何这样做:

@RequestScope
@Component
public class LoginAction {
    // ...
}

Session作用域

考虑以下用于bean定义的XML配置:

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

Spring容器通过在单个HTTP会话的生命周期中使用UserPreferences bean定义来创建UserPreferences bean的新实例。换句话说,userPreferences bean在HTTP会话(Session)级别有效地限定了作用域。与请求作用域的bean一样,您可以随心所欲地更改创建的实例的内部状态,要知道其他使用从相同userPreferences bean定义创建的实例的HTTP会话实例看不到这些状态变化,因为它们是特定于单个HTTP会话的。当HTTP会话最终被丢弃时,作用域为该特定HTTP会话的bean也被丢弃

当使用注解驱动的组件或Java配置时,您可以使用@SessionScope注解将组件分配给会话范围。

@SessionScope
@Component
public class UserPreferences {
    // ...
}

Application作用域

考虑以下用于bean定义的XML配置:

<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>

Spring容器通过为整个web应用程序使用一次AppPreferences bean定义来创建AppPreferences bean的新实例。也就是说,appPreferences bean的作用域在ServletContext级别,并作为常规ServletContext属性存储。这有点类似于Spring单例bean,但在两个重要方面有所不同:它是每个ServletContext的单例,而不是每个Spring ApplicationContext(在任何给定的web应用程序中可能有几个),并且它实际上是公开的,因此作为ServletContext属性可见。

当使用注解驱动的组件或Java配置时,您可以使用@ApplicationScope注解将组件分配给应用程序(application)作用域。下面的例子展示了如何这样做:

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}

WebSocket作用域

WebSocket作用域与WebSocket会话的生命周期相关联,并应用于基于WebSocket应用程序的STOMP,参见WebSocket作用域(WebSocket scope)了解更多细节。

将bean限定为依赖项(Scoped Beans as Dependencies)

Spring IoC容器不仅管理对象(bean)的实例化,还管理合作者(或依赖项)的装配。如果您想将一个HTTP请求作用域的bean注入到另一个存在时间较长的作用域的bean中,您可以选择注入AOP代理来代替作用域bean。也就是说,您需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也可以从相关作用域(如HTTP请求)检索实际目标对象,并将方法调用委托给实际对象。

提示

你可能会在单例(singleton)作用域的bean之间使用<aop:scoped-proxy/>,然后引用通过一个可序列化的中间代理,因此能够在反序列化时重新获得目标单例bean。

当声明对于prototype作用域bean使用<aop:scoped-proxy/>,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后将调用转发到该实例。

此外,有作用域的代理并不是以生命周期安全的方式从较短的作用域访问bean的唯一方法。您还可以将注入点(即构造函数或setter参数或autowired字段)声明为ObjectFactory<MyTargetBean>,允许getObject()调用在每次需要时按需检索当前实例,而无需保留实例或单独存储实例。

作为扩展的变体,您可以声明ObjectFactory<MyTargetBean>它提供了几个额外的访问变量,包括getIfAvailablegetIfUnique

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

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.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.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

要创建这样的代理,您可以插入一个<aop:scoped-proxy/>子元素放入一个作用域bean定义中(参见选择要创建的代理类型(Choosing the Type of Proxy to Create)基于XML模式的配置(XML Schema-based configuration))。为什么在requestsession自定义作用域级别定义bean的作用域需要<aop:scoped-proxy/>元素?考虑下面的单例bean定义,并将其与需要为上述作用域定义的定义进行对比(注意,下面的userPreferences bean定义目前是不完整的):

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

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

在前面的示例中,单例bean (userManager)被注入到HTTP会话作用域bean (userPreferences)的引用中。这里的重点是userManager bean是一个单例:每个容器只实例化一次,并且它的依赖项(在本例中只有一个,即userPreferences bean)也只注入一次。这意味着userManager bean只对完全相同的userPreferences对象(即最初注入它的对象)进行操作。

将寿命较短的作用域bean注入寿命较长的作用域bean时,这不是您想要的行为(例如,将HTTP会话作用域协作bean作为依赖项注入到单例bean中)。相反,您需要一个userManager对象,并且在HTTP会话的生命周期内,您需要一个特定于HTTP会话的userPreferences对象。因此,容器创建了一个对象,该对象公开与UserPreferences完全相同的公共接口(理想情况下是一个UserPreferences实例对象),该对象可以从作用域机制(HTTP请求、会话等)中获取真正的UserPreferences对象。容器将这个代理对象注入到userManager bean中,它不知道这个UserPreferences引用是一个代理。在本例中,当UserManager实例调用依赖注入的UserPreferences对象上的方法时,它实际上是调用代理上的方法。然后代理从HTTP会话中获取实际UserPreferences对象,并将方法调用委托给检索到的实际UserPreferences对象。

因此,在将请求和会话作用域的bean注入协作对象时,您需要以下(正确和完整的)配置,如下面的示例所示:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
    <aop:scoped-proxy/> <!-- 代理配置 -->
</bean>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>
选择要创建的代理类型(Choosing the Type of Proxy to Create)

默认情况下,当Spring容器为bean创建代理时,该代理被标记为 <aop:scoped-proxy/> 元素时,将创建一个基于cglib的类代理。

提示

CGLIB代理只拦截公共(public)方法调用!不要在这样的代理上调用非公共方法。它们没有委托给实际作用域的目标对象。

或者,您可以通过将<aop:scoped-proxy/>元素的proxy-target-class属性的值指定false来配置Spring容器,以便为这种作用域bean创建基于标准JDK接口的代理。使用基于JDK接口的代理意味着您不需要在应用程序类路径中添加额外的库来影响此类代理。然而,这也意味着作用域bean的类必须实现至少一个接口,并且所有被注入作用域bean的合作者必须通过它的一个接口引用bean。基于接口的代理示例如下:

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

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

有关选择基于类或基于接口的代理的详细信息,请参见代理机制(Proxying Mechanisms)

1.5.5. 自定义作用域(Custom Scopes)

bean作用域机制是可扩展的。您可以定义自己的作用域,甚至重新定义现有的作用域,尽管后者被认为是不好的做法,并且您不能覆盖内置的单例(singleton)和原型(prototype)作用域。

创建一个自定义作用域

要将自定义作用域集成到Spring容器中,需要实现org.springframework.beans.factory.config.Scope接口,本节将对此进行描述。要了解如何实现自己的作用域,请参阅Spring框架本身提供的Scope实现和Scope javadoc,后者更详细地解释了需要实现的方法。

Scope接口有四个方法,分别从作用域获取对象从作用域移除对象销毁对象

例如,会话作用域实现返回会话作用域bean(如果它不存在,则该方法将bean的新实例绑定到会话以供将来参考后返回该实例)。下面的方法从底层作用域返回对象:

Object get(String name, ObjectFactory<?> objectFactory)

例如,会话作用域实现从底层会话中删除会话作用域bean。应该返回对象,但是如果没有找到具有指定名称的对象,则可以返回null。下面的方法将对象从底层作用域中移除:

Object remove(String name)

下面的方法注册了一个回调函数,当作用域被销毁作用域中指定的对象被销毁时,应该调用这个回调函数:

void registerDestructionCallback(String name, Runnable destructionCallback)

有关销毁回调的更多信息,请参阅javadoc或Spring作用域实现。

下面的方法获取底层作用域的对话标识符

String getConversationId()

这个标识符对于每个作用域是不同的。对于会话作用域的实现,此标识符可以是会话标识符。

使用自定义作用域

在您编写并测试了一个或多个自定义作用域实现之后,您需要让Spring容器意识到您的新作用域。下面的方法是在Spring容器中注册一个新的Scope的中心方法:

void registerScope(String scopeName, Scope scope);

该方法是在ConfigurableBeanFactory接口上声明的,该接口可以通过Spring附带的大多数具体ApplicationContext实现中的BeanFactory属性获得。

registerScope(..)方法的第一个参数是与作用域关联的唯一名称。Spring容器本身中这类名称的例子有单例和原型。registerScope(..)方法的第二个参数是您希望注册和使用的自定义Scope实现的实际实例。

假设您编写了自定义Scope实现,然后注册它,如下面的示例所示。

提示

下一个示例使用SimpleThreadScope,它包含在Spring中,但默认情况下没有注册。对于您自己的自定义Scope实现,说明是相同的。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

然后,您可以创建遵循自定义Scope的作用域规则的bean定义,如下所示:

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

使用自定义作用域实现,您不必局限于对作用域进行编程注册。你也可以通过使用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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://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="thing2" class="x.y.Thing2" scope="thread">
        <property name="name" value="Rick"/>
        <aop:scoped-proxy/>
    </bean>

    <bean id="thing1" class="x.y.Thing1">
        <property name="thing2" ref="thing2"/>
    </bean>

</beans>

提示

当你放置<aop:scoped-proxy/>在一个<bean>对于FactoryBean实现的声明,它的作用域是工厂bean本身,而不是getObject()返回的对象。