Spring 核心技术 - IoC容器

Spring 学习指南大全
Spring 核心技术

官方文档版本 Version 5.2.22.RELEASE

文章目录

IoC容器

Inversion of Control :IoC容器

IoC容器,也就是我们常说的控制反转容器,将我们创建 Bean 的过程交由 Spring 的 IoC 容器去创建,从而达到系统代码之间的解耦等。

Spring IoC 容器和 Beans 介绍

本章涵盖了控制反转 (IoC) 原则的 Spring 框架实现。IoC 也称为依赖注入(DI)。在这个过程中,对象可通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖关系(即它们使用的其他对象)。然后,容器在创建bean 时注入这些依赖项。这个过程基本上是 bean 本身的逆过程(因此得名,控制反转),通过使用类的直接构造或服务定位器模式之类的机制来控制其依赖项的实例化或位置。

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

BeanFactory 接口提供了一种高级配置机制,能够管理任何类型的对象。

ApplicationContextBeanFactory 的子接口。它扩展了:

  • 更容易与 Spring 的 AOP 特性集成
  • 消息资源处理(用于国际化)
  • 事件发布
  • 特定于应用程序层的上下文,如用于 web 应用程序的 WebApplicationContext

简而言之,BeanFactory 提供了配置框架和基本功能,ApplicationContext 添加了更多特定于企业的功能。ApplicationContext 是BeanFactory 的完整超集,在本章中专门用于描述 Spring 的 IoC 容器。有关使用 BeanFactory 而不是 ApplicationContext 的更多信息,请参见后续 BeanFactory 文档。

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

也就是说,使用 Spring 框架的 IoC 容器后,我们程序中的 Bean 全由 Spring IoC 容器去实例化、组装和管理。以前通过 new 关键字去创建对象,并且对象里面依赖的对象也是通过 new 的方式进行创建,现在全部交由 Spring IoC 容器去管理,我们只需要进行配置 Bean 之间的依赖关系即可。IoC 是通过读取配置元数据来确定依赖关系的。

Bean 就是 Java 中的对象

Factory 工厂,BeanFactory 对象工厂

Application 应用程序,Context 上下文

ApplicationContext 应用程序上下文

BeanFactory 相当于 Spring IoC 容器的总接口,ApplicationContext 是为 web 环境提供的 IoC 的扩展接口

容器概述

Container :容器

org.springframework.context.ApplicationContext 接口代表 Spring IoC 容器,负责实例化、配置和组装 beans。容器通过读取配置元数据来获得关于实例化、配置和组装什么对象的指令。配置元数据可以用 XML、Java 注解或 Java 代码表示。它让您能够表达组成应用程序中的对象以及这些对象之间丰富的相互依赖关系。

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

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

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

在这里插入图片描述

配置元数据

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

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

基于 XML 的元数据不是唯一允许的配置元数据形式。Spring IoC 容器本身与实际编写配置元数据的格式完全分离。如今,许多开发人员为他们的 Spring 应用程序选择基于 Java 配置 bean 的方式来配置元数据。

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

  • 基于注释的配置:Spring 2.5 引入了对基于注解的配置元数据的支持。

  • 基于 Java 配置类:从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多特性成为了 Spring 框架核心的一部分。因此,您可以通过使用 Java 而不是 XML 文件来定义应用程序类外部的 beans。要使用这些新特性,请参阅 @Configuration@Bean@Import@DependsOn注解。

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

这些 bean definitions (bean 定义)用于组成应用程序的实际对象。通常,您定义 service 服务层对象、数据访问对象(DAO)、表示对象(如Struts Action实例)、基础结构对象(如Hibernate SessionFactories、JMS Queues(队列)等)。通常,不在容器中配置细粒度域对象,因为创建和加载域对象通常是 DAO 和业务逻辑的责任。但是,您可以使用 Spring 与 AspectJ 的集成来配置在 IoC 容器控制之外创建的对象。请参见使用 AspectJ 向 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="...">  
        <!-- bean的其它依赖 -->
    </bean>

    <bean id="..." class="...">
        <!-- bean的其它依赖 -->
    </bean>

    <!-- 更多的bean配置 -->

</beans>
  • id 属性是标识单个 bean definition (bean definition :bean 定义)的字符串。
  • class 属性定义了 bean 的类型,并使用全路径类名。

id 属性的值还可以引用依赖对象。这个例子中没有显示引用依赖对象的 XML。请参阅依赖关系了解详情。

实例化容器

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

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

这里实例化了通过读取 XML 配置元数据的 IoC 容器,并指定了读取的 XML 元数据配置。

在了解了 Spring 的 IoC 容器之后,您可能想了解更多关于 Spring 的 Resource 资源抽象(如第二章资源中所述),它提供了一种从 URI 语法中定义的位置读取 InputStream 的便利机制。特别是,资源路径用于构建应用程序上下文,如第二章中应用程序上下文和资源路径中所述。

以下示例显示了服务层对象(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"/>
        <!-- 此处还可以配置更多的依赖关系 -->
    </bean>

    <!-- 更多 services 服务定义 -->

</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">
        <!-- 此处可配置其依赖关系 -->
    </bean>

    <bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
        <!-- 此处可配置其依赖关系 -->
    </bean>

    <!-- 更多数据访问层对象的定义 -->

</beans>

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

当我们在应用系统中创建 PetStoreServiceImpl 实例时,IoC 就会通过此处配置的依赖关系,把 PetStoreServiceImpl 依赖的对象注入到创建的实例中,也就是 DI 依赖注入。

编写基于 XML 的配置元数据

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

您可以使用应用程序上下文构造函数从所有这些 XML 片段中加载 bean 定义。这个构造函数接受多个资源位置,如前一节所示。或者,使用一次或多次出现的 < import/> 元素从另一个或多个文件中加载 bean 定义。以下示例显示了如何实现这一点:

<beans>
    <import resource="services.xml"/>	<!-- 通过 import 导入其它配置元数据 -->
    <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 资源位置。如您所见,前导斜杠会被忽略。然而,考虑到这些路径是相对的,最好不要使用斜杠。根据 Spring 模式,被导入文件的内容,包括顶级的 < beans/>元素,必须是有效的 XML bean 定义。

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

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

namespace 名称空间本身提供了导入指令特性。除了普通 bean 定义之外,Spring 提供的一些 XML namespace 名称空间还提供了更多的配置特性,例如 context 和 util (namespace 名称空间)。

Groovy Bean 定义 DSL

Groovy 和 Kotlin 都是在 JVM 的基础上创建的语言,为了简化 Java 的开发

作为外部化配置元数据的另一个例子,bean definitions(定义) 也可以用 Spring 的 Groovy Bean Definition 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 定义文件。

使用容器

ApplicationContext 是高级工厂的接口,能够维护不同 beans 及其依赖项的注册表。通过使用方法 T getBean(String name,Class<T> requiredType),来可以检索获取 Bean 的实例。

ApplicationContext 允许您读取 bean 定义并访问它们,如下例所示:

// 创建容器并指定的配置元数据
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// 从容器中检索配置的实例
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// 使用配置的实例
List<String> userList = service.getUsernameList();

对于 Groovy 配置,引导看起来非常相似。它有一个不同的上下文实现类,支持 Groovy (但也理解 XML bean 定义)。以下示例显示了Groovy 配置:

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

最灵活的变体是结合读取器委托的 GenericApplicationContext,例如,结合 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 接口有一些其他方法来检索 beans,但是,理想情况下,您的应用程序代码不应该使用它们。实际上,您的应用程序代码应该根本没有对 getBean() 方法的调用,因此完全不依赖于 Spring APIs。例如,Spring 与 web 框架的集成为各种 web 框架组件(如控制器和 JSF 管理的 bean)提供了依赖注入,允许您通过元数据(如自动连接注解)声明对特定 bean 的依赖。

Bean 概述

Spring IoC 容器管理一个或多个 beans。这些 bean 是使用您提供给容器的配置元数据创建的(例如,以XML <bean/>定义的形式)。

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

  • 包限定的类名:通常是定义的 bean 的实际实现类。
  • Bean 行为配置元素,声明 bean 在容器中的行为方式(范围、生命周期回调等)。
  • 对 bean 完成其工作所需的其他 bean 的引用。这些引用也称为协作者或依赖者。
  • 要在新创建的对象中设置的其他配置设置,例如,池的大小限制或在管理连接池的 bean 中使用的连接数。

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

bean definition

bean definition 属性解说于文档
Class实例化Beans
Name命名Beans
Scope(范围、区域)Bean 范围
Constructor arguments(构造函数参数)依赖注入
Properties(属性)依赖注入
Autowiring mode(自动装配模型)自动装配协作者
Lazy initialization mode(延迟初始化模式)延迟初始化的beans
Initialization method(初始化方法)初始化回调
Destruction method(销毁方法)销毁回调

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

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

命名 Beans

每个 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 提供名称或 id。如果没有显式提供名称或 id,容器将为该 bean 生成一个唯一的名称。但是,如果您想通过名称引用该 bean,通过使用 ref 元素或 Service Locator (服务定位器)样式查找,您必须提供一个名称。不提供名称的动机与使用内部 beans 和自动装配协作者有关。

Bean命名约定

约定是在命名 beans 时对实例字段名称使用标准的 Java 约定。也就是说,bean 名称以小写字母开始,并从那里开始区分大小写。此类名称的示例包括 accountManageraccountServiceuserDaologinController等。

一致地命名 beans 使您的配置更容易阅读和理解。此外,如果您使用 Spring AOP,它在将“切面建议”应用到一组按名称相关的 beans时会有很大帮助。

通过在类路径中扫描组件,Spring 按照前面描述的规则为未命名的组件生成 bean 名称:本质上,采用简单的类名,并将其初始字符转换为小写。但是,在(不常见的)特殊情况下,当有多个字符并且第一个和第二个字符都是大写时,原始的大小写会被保留。这些规则与 java.beans.Introspector.decapitalize (Spring在这里使用的)定义的规则相同。

在 Bean Definition (定义) 之外别名化 Bean

在 bean definition (定义)本身中,通过使用 id 属性指定的最多一个名称和 name 属性中任意数量的其他名称的组合,可以为 bean 提供多个名称。这些名称可以是同一个 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-configuration java配置

如果使用 Java configuration,可以使用 @Bean 注解来提供别名。有关详细信息,请参见后续文章 使用@Bean注解。

实例化 Beans

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

如果使用基于 XML 的配置元数据,可以在 <bean/> 元素的 class 属性中指定要实例化的对象的类型(或类)。这个 class 属性(在内部,它是 BeanDefinition 实例上的一个 Class 属性)通常是必需的。(有关异常,请参见后续文章 通过使用实例工厂方法和 Bean 定义继承进行实例化。)您可以通过两种方式之一使用 Class 属性:

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

嵌套类名

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

例如,如果在 com.example 包中有一个名为 SomeThing 的类,而这个 SomeThing 类有一个名为 OtherThing 的静态嵌套类,则可以用美元符号($)或点(.)。因此,bean 定义中 class 属性的值应该是 com.example.SomeThing$OtherThingcom.example.SomeThing.OtherThing

用构造函数实例化

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

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

使用基于 XML 的配置元数据,您可以按如下方式指定 bean 类:

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

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

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

用静态工厂方法实例化

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

以下 bean definition (定义) 指定通过调用工厂方法来创建 bean。definition (定义) 没有指定返回对象的类型(类),只指定了包含工厂方法的类。在此示例中,createInstance() 方法必须是静态方法。以下示例显示了如何指定工厂方法:

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

下面的示例显示了一个可以使用前面的 bean definition (定义)的类:

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

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

有关向工厂方法提供(可选)参数以及在对象从工厂返回后设置对象实例属性的机制的详细信息,请参见后续文章-详细的依赖关系和配置。

使用实例工厂方法进行实例化

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

<!-- 工厂bean,它包含一个名为 createInstance() 的方法 -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- 注入该 bean 所需的任何依赖项 -->
</bean>

<!-- 通过工厂 bean 创建的 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">
    <!-- 注入该 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)来管理和配置。请详细查看后续文章-依赖项和配置。

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

确定 Bean 的运行时类型

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

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

Dependencies 依赖

典型的企业应用程序不包含单个对象(或者 Spring 术语中的 bean)。即使是最简单的应用程序也有几个对象,它们一起工作来呈现最终用户所看到的一致的应用程序。下一节将解释如何从定义大量独立的 bean definitions (定义)发展到一个完全实现的应用程序,在这个应用程序中,对象之间可以协作来实现一个目标。

依赖注入

Dependency injection (DI):依赖注入

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

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

DI 有两种主要的变体:基于构造函数的依赖注入和基于 Setter (设置器)的依赖注入。

基于构造函数的依赖注入

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

public class SimpleMovieLister {

    // SimpleMovieLister 依赖于 MovieFinder
    private final MovieFinder movieFinder;

    // 构造函数,以便 Spring 容器可以注入 MovieFinder
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // 省略了使用注入的 MovieFinder 的业务逻辑...
}

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

构造函数参数解析

通过使用参数的类型,可以进行构造函数参数解析匹配。如果 bean definition(定义)的构造函数参数中不存在潜在的模糊性,则构造函数参数在 bean definition(定义)中的定义顺序就是实例化 bean 时这些参数提供给相应构造函数的顺序。考虑下面的类:

package x.y;

public class ThingOne {

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

假设 ThingTwo 和 ThingThree 类没有继承关系,则不存在潜在的歧义。因此,以下配置工作正常,并且您不需要在 <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 {

    // 计算最终答案的年数
    private final int years;

    // 生命、宇宙和一切的答案
    private final String ultimateAnswer;

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

构造函数参数类型匹配

在前面的方案中,如果通过使用 type 属性显式指定构造函数参数的类型,容器可以使用简单类型的类型匹配,如下面的示例所示:

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

构造函数参数索引

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

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

除了解决多个简单值的模糊性之外,指定索引还可以解决构造函数有两个相同类型的参数时的模糊性。

该索引从0开始。

构造函数参数名称

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

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

请记住,要使这一工作开箱即用,您的代码必须在启用 debug flag (调试标志) 的情况下编译,以便 Spring 可以从构造函数中查找参数名。如果不能或不想用 debug flag (调试标志) 编译代码,可以使用 @ConstructorProperties JDK 注解显式命名构造函数参数。示例类必须如下所示:

package examples;

public class ExampleBean {

    // 省略的字段

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}
基于 Setter 的依赖注入

基于 Setter 的 DI 是在调用无参数构造函数或无参数静态工厂方法实例化 bean 之后,通过容器调用 bean 上的 setter 方法来完成的。

下面的示例显示了一个只能通过使用纯 setter 注入进行依赖注入的类。这个类是传统的 Java。它是一个不依赖于特定于容器的接口、基类或注释的 POJO。

public class SimpleMovieLister {

    // SimpleMovieLister 依赖于 MovieFinder
    private MovieFinder movieFinder;

    // setter 方法,以便 Spring 容器可以注入 MovieFinder
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // 省略了使用注入的 MovieFinder 的业务逻辑...

}

ApplicationContext 为其管理的 beans 支持基于构造函数和基于 setter 的 DI。在通过构造函数方法注入了一些依赖项之后,它还支持基于 setter 的DI。以 BeanDefinition 的形式配置依赖项,与 PropertyEditor 实例结合使用,将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类(即以编程方式),而是使用 XML bean definitions(定义)、带注解的组件(即用 @Component@Controller 等注释的类),或者基于 Java 的 @Configuration 类中的 @Bean 方法。然后,这些源在内部被转换成 BeanDefinition 的实例,并用于加载整个 Spring IoC 容器实例。

基于构造函数还是基于 setter 的DI?

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

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

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

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

依赖关系解析过程

容器执行 bean 依赖关系解析,如下所示:

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

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

循环依赖

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

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

一个可能的解决方案是编辑一些类的源代码,由 setters 而不是 constructors 来配置。或者,避免构造函数注入,只使用 setter 注入。换句话说,虽然不推荐,但是可以用 setter 注入来配置循环依赖。

与典型情况(没有循环依赖)不同,bean A 和 bean B 之间的循环依赖迫使其中一个 bean 在自身完全初始化之前被注入到另一个bean 中(典型的先有鸡还是先有蛋的情况)。

对于循环依赖的更多细节,留在 Spring 进阶-源码分析篇讲解

你通常可以相信 Spring 会做正确的事情。它在容器加载时检测配置问题,比如引用不存在的 beans 和循环依赖。Spring 尽可能晚地设置属性和解析依赖关系,当 bean 被实际创建时。这意味着,如果在创建一个对象或它的一个依赖项时出现问题,那么当您请求该对象时,已经正确加载的 Spring 容器可能会在以后生成一个异常——例如,bean 由于缺少或无效的属性而抛出一个异常。一些配置问题的这种潜在延迟可见性是 ApplicationContext 实现默认预实例化单例 beans 的原因。以在实际需要之前创建这些 beans 的一些前期时间和内存为代价,您会在创建 ApplicationContext 时发现配置问题,而不是以后。您仍然可以覆盖这种默认行为,以便 singleton (单体) beans 延迟初始化,而不是急切地预实例化。

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

依赖注入示例

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

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- 使用嵌套 ref 元素的 setter 注入 -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

    <!-- 使用整洁的 ref 属性的 setter 注入 -->
    <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;
	
    // 通过 Set 方法进行注入
    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

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

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

在前面的示例中,setters 被声明为与 XML 文件中指定的属性相匹配。下面的示例使用基于构造函数的 DI:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- 使用嵌套 ref 元素的构造函数注入 -->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>

    <!-- 使用简洁的 ref 属性的构造函数注入 -->
    <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 definition (定义)中指定的构造函数参数用作 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 {

    // 私有构造函数
    private ExampleBean(...) {
        ...
    }

    // 静态工厂方法;此方法的参数可以是
    // 考虑到返回的bean的依赖性,
    // 不管这些参数实际上是如何使用的。
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

        ExampleBean eb = new ExampleBean (...);
        // 一些其他操作...
        return eb;
    }
}

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

依赖关系和配置细节

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

直接值(原始值,字符串等等)

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

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <!-- 导致 setDriverClassName(String) 调用 -->
    <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-namespace 进行更简洁的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    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>

注意在使用时,需要引入对应 XML 的约束。如:xmlns:p=“http://www.springframework.org/schema/p”

前面的 XML 更加简洁。但是,错别字是在运行时发现的,而不是在设计时才发现,除非您在创建 bean definitions(定义)时使用支持自动属性完成的IDE (如 IntelliJ IDEA 或 Spring Tools for Eclipse )。强烈推荐这种 IDE 帮助。

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

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

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

Spring 容器通过使用 JavaBeans 的 PropertyEditor 机制将 <value/> 元素中的文本转换成 java.util.Properties 实例。这是一个很好的捷径,也是 Spring 团队喜欢使用嵌套的 <value/> 元素而不是 value 属性样式的少数地方之一。

idref 元素

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

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

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

前面的 bean definition(定义) 片段完全等同于(在运行时)下面的片段:

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

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean"/>
</bean>

第一种形式优于第二种,因为使用 idref 标记可以让容器在部署时验证被引用的命名 bean 是否确实存在。在第二个变体中,对传递给客户机 bean 的 targetName 属性的值不执行任何验证。只有在实际实例化客户端 bean 时,才会发现拼写错误(很可能导致致命的结果)。如果客户机 bean 是一个原型 bean,那么这种输入错误和由此产生的异常可能只有在部署容器之后很久才会被发现。

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

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

对其他 Beans (协作者)的引用

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

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

<ref bean="someBean"/>

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

<!-- 在父上下文中 -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- 根据需要在此插入依赖关系 -->
</bean>
<!-- 在子(后代)上下文中 -->
<bean id="accountService" 
    class="org.springframework.aop.framework.ProxyFactoryBean"><!-- bean名称与父bean相同 -->
    <property name="target">
        <ref parent="accountService"/> <!-- 注意我们是如何引用父 bean 的 -->
    </property>
    <!-- 根据需要在此处插入其他配置和依赖项 -->
</bean>

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

内部 Beans

<property/><constructor-arg/> 元素中的 <bean/> 元素定义了一个内部 bean,如下例所示:

<bean id="outer" class="...">
    <!-- 不使用对目标 bean 的引用,只需内联定义目标 bean -->
    <property name="target">
        <bean class="com.example.Person"> <!-- 这是一个内部 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 集合

<list/><set/><map/><props/> 元素分别设置 Java Collections (集合) 类型 list、set、mapproperties 的属性和参数。以下示例显示了如何使用它们:

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- 导致 setAdminEmails(java.util.Properties) 调用 -->
    <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>
    <!-- 导致 setSomeList(java.util.List) 调用 -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- 导致 setSomeMap(java.util.Map) 调用 -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- 导致 setSomeSet(java.util.Set) 调用 -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

映射键或值或设置值的值也可以是以下任何元素:

bean | ref | idref | list | set | map | props | value | null

集合合并

Spring 容器还支持合并集合。应用程序开发人员可以定义父 <list/>、<map/>、<set/><props/> 元素,并让子 <list/>、<map/>、<set/><props/> 元素继承和重写父集合中的值。也就是说,子集合的值是父集合和子集合的元素合并的结果,子集合的元素重写父集合中指定的值。

关于合并的这一节讨论父子 bean 机制。不熟悉父 bean 和子 bean definitions (定义) 的读者可能希望在继续阅读相关部分,在后续文章中会介绍。

下面的示例演示集合合并:

<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">
            <!-- 合并在子集合定义中指定 -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>

请注意在子 bean definition(定义)的 adminEmails 属性的 <props/> 元素上使用了 merge = true 属性。当容器解析并实例化子 bean时,生成的实例具有一个 adminEmails 属性集合,该集合包含子 bean 的 adminEmails 集合与父 bean 的 adminEmails 集合合并的结果。以下清单显示了结果:

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

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

这种合并行为同样适用于 <list/>、<map/><set/> 集合类型。在 <list/> 元素的特定情况下,维护与列表集合类型相关联的语义(即值的有序集合的概念)。父列表的值在所有子列表的值之前。对于 Map、Set 和 Properties 集合类型,不存在排序。因此,对于作为容器内部使用的关联 Map、Set 和 Properties 实现类型基础的集合类型,排序语义是无效的。

集合合并的局限性

您不能合并不同的集合类型(如 Map 和 List)。如果您尝试这样做,将会引发适当的异常。必须在继承的下级子定义中指定 merge 属性。在父集合定义上指定合并属性是多余的,并且不会导致所需的合并。

强类型集合

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

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.99、2.75和3.99) 转换为实际的 Float 类型。

Null 和空字符串值

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

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

前面的示例相当于下面的 Java 代码:

exampleBean.setEmail("");

<null/> 元素处理空值。以下清单显示了一个示例:

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

上述配置相当于以下 Java 代码:

exampleBean.setEmail(null);
带有 p 命名空间的 XML 快捷方式

p-namespace(命名空间) 允许您使用 bean 元素的属性(而不是嵌套的 <property/> 元素)来描述协作 bean 的属性值,或者同时描述两者。

Spring 支持带有名称空间的可扩展配置格式,这是基于 XML 模式定义的。本章中讨论的 beans 配置格式是在 XML 模式文档中定义的。然而,p-namespace 没有在 XSD 文件中定义,只存在于 Spring 的核心中。

以下示例显示了两个 XML 片段(第一个使用标准 XML 格式,第二个使用 p-namespace ),它们解析出相同的结果:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        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-namespace 没有模式定义,所以您可以将属性的名称设置为 bean 的属性名称。

下一个示例包括另外两个 bean definitions (定义),它们都引用了另一个 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"/><!-- 引用了jane -->
    </bean>

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

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

这个示例不仅包括使用 p-namespace 的属性值,还使用特殊的格式来声明属性引用。第一个 bean 定义使用 <property name= "spouse" ref="jane"/> 来创建从 bean john 到 bean jane 的引用,而第二个 bean 定义使用 p:spouse-ref="jane" 作为属性来做完全相同的事情。在这种情况下,spouse 是属性名,而 -ref 部分表明这不是一个直接的值,而是对另一个 bean 的引用。

p-namespace 不如标准 XML 格式灵活。例如,声明属性引用的格式与以 Ref 结尾的属性冲突,而标准 XML 格式则不会。我们建议您仔细选择您的方法,并将其传达给您的团队成员,以避免生成同时使用所有三种方法的 XML 文档。

带有 c 命名空间的 XML 快捷方式

与 p-namespace 的 XML 快捷方式类似,Spring 3.1 中引入的 c-namespace 允许内联属性来配置构造函数参数,而不是嵌套的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
        https://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 注意 C 命名空间依赖 -->

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

    <!-- 具有可选参数名称的传统声明 -->
    <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声明 -->
    <bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
        c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>

</beans>

c: 名称空间使用与 p:one (bean 引用的 -ref )相同的约定,通过名称设置构造函数参数。类似地,它需要在 XML 文件中声明,即使它没有在 XSD 模式中定义(它存在于 Spring 核心中)。

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

<!-- c命名空间索引声明 -->
<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> 元素也有相应的索引符号,但不常用,因为简单的声明顺序通常就足够了。

在实践中,构造函数解析机制在匹配参数方面非常有效,所以除非您真的需要,否则我们建议在整个配置中使用名称符号。

复合属性名称

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

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

something bean 有一个 fred 属性,该属性有一个 bob 属性,该属性有一个 sammy 属性,最后一个 sammy 属性被设置为值 123。为了实现这一点,在构造 bean 之后,某物的 fred 属性和 fred 的 bob 属性不能为 null 。否则,将引发 NullPointerException。

使用 depends-on (依赖于)

如果一个 bean 是另一个 bean 的依赖项,这通常意味着一个 bean 被设置为另一个 bean 的属性。通常,您可以使用基于 XML 的配置元数据中的 <ref/> 元素来实现这一点。然而,有时 beans 之间的依赖关系不那么直接。一个例子是当一个类中的静态初始化器需要被触发时,比如数据库驱动注册。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 属性既可以指定初始化时的依赖关系,也可以指定相应的销毁时的依赖关系(仅限于单例 beans 的情况)。在销毁给定 bean 本身之前,首先销毁与给定 bean 定义依赖关系的依赖 bean。因此,依赖项也可以控制关闭顺序。

延迟初始化 beans

默认情况下,ApplicationContext 实现急切地创建和配置所有的 singleton(单体) beans,作为初始化过程的一部分。一般来说,这种预实例化是可取的,因为配置或周围环境中的错误会立即被发现,而不是几个小时甚至几天之后。当不希望出现这种行为时,可以通过将bean 定义标记为 lazy(延迟)初始化来防止单例 bean 的预实例化。lazy(延迟)初始化的 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 使用时,lazy(延迟) bean 不会在 ApplicationContext 启动时被预先实例化,而 not.lazy bean 会被预先实例化。

然而,当 lazy(延迟)初始化的 bean 是未 lazy(延迟) 初始化的单例 bean 的依赖项时,ApplicationContext 在启动时会创建 lazy(延迟)初始化的 bean,因为它必须满足单例的依赖项。lazy(延迟) 初始化的 bean 会被注入到其他没有 lazy(延迟)初始化的单例 bean 中。

还可以通过使用 <beans/> 元素上的 default-lazy-init 属性在容器级别控制 lazy(延迟)初始化,如下例所示:

<beans default-lazy-init="true">
    <!-- 不会预实例化任何beans... -->
</beans>
自动装配协作者

Autowiring Collaborators : 自动装配 / 自动连接 / 自动配置 协作者

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

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

当使用基于 XML 的配置元数据时(请参见依赖注入),可以使用 <bean/> 元素的 autowire 属性为 bean 定义指定自动装配模式。自动装配功能有四种模式。您可以为每个 bean 指定自动连接,从而可以选择要自动连接的 bean。下表描述了四种自动装配模式:

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

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

自动装配的局限性和缺点

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

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

  • 属性和构造函数参数设置中的显式依赖项总是覆盖自动连接。您不能自动连接简单的属性,如普通类型、字符串和类(以及此类简单属性的数组)。这种限制是有意设计的。
  • 自动配置不如显式配置精确。尽管如此,正如前面的表格中所提到的,Spring 小心地避免了猜测,以防可能会产生意想不到的结果的歧义。Spring 管理的对象之间的关系不再被明确地记录。
  • 从 Spring 容器生成文档的工具可能无法获得连接信息。
  • 容器中的多个 bean definitions (定义)可能与 setter 方法或构造函数参数指定的类型相匹配,从而被自动绑定。对于数组、集合或Map 实例,这不一定是问题。然而,对于期望单个值的依赖项,这种不确定性不是任意解决的。如果没有唯一的 bean definitions(定义)可用,则会引发异常。

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

  • 放弃自动配置,支持显式配置。
  • 通过将 bean definition (定义)的 autowire-candidate 属性设置为 false 来避免 definition (定义)的自动连接,如下一节所述。
  • 通过将其 <bean/> 元素的 primary 属性设置为 true,将 single (单个) bean definition (定义)指定为主要候选。
  • 实现基于注解的配置中可用的更细粒度的控制,如后续-基于注解的容器配置中所述。
从自动连接中排除 Bean

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

autowire-candidate (自动关联候选) 属性旨在仅影响基于类型的自动关联。它不影响按名称的显式引用,即使指定的 bean 没有被标记为 autowire 候选,这些引用也会被解析。因此,如果名称匹配,按名称自动连接仍然会注入一个 bean。

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

这些技术对于那些不想通过自动连接注入到其他 bean 中的 bean 非常有用。这并不意味着被排除的 bean 本身不能通过使用自动连接来配置。更确切地说,bean 本身不是自动连接其他 bean 的候选对象。

方法注入

Method Injection : 方法注入

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

一个解决方案是放弃一些控制反转。您可以通过实现 ApplicationContextAware 接口使 bean A 知道容器,每次 bean A 需要时,通过对容器进行 getBean("B ") 调用来请求(通常是新的) bean B 实例。以下示例显示了这种方法:

// 使用有状态命令样式的类来执行某些处理的类
package fiona.apple;

// Spring-API导入
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CommandManager implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public Object process(Map commandState) {
        // 获取适当命令的新实例
        Command command = createCommand();
        // 在(希望是全新的)命令实例上设置状态
        command.setState(commandState);
        return command.execute();
    }

    protected Command createCommand() {
        // 注意 Spring API的依赖性!
        return this.applicationContext.getBean("command", Command.class);
    }

    public void setApplicationContext(
            ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

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

你可以在这篇博客中读到更多关于方法注入的动机。

查找方法注入

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

  • 为了使这个动态子类化工作,Spring bean 容器子类化的类不能是 final 最终的,并且要被覆盖的方法也不能是 final 最终的。
  • 对一个有抽象方法的类进行单元测试需要你自己子类化这个类,并提供抽象方法的存根实现。
  • 组件扫描也需要具体的方法,这需要具体的类来完成。
  • 另一个关键限制是,查找方法不能与工厂方法一起工作,尤其是不能与配置类中的 @Bean 方法一起工作,因为在这种情况下,容器不负责创建实例,因此不能动态创建运行时生成的子类。

对于前面代码片段中的 CommandManager 类,Spring 容器动态覆盖 createCommand() 方法的实现。CommandManager 类没有任何Spring 依赖项,如修改后的示例所示:

package fiona.apple;

// 没有 Spring imports

public abstract class CommandManager {

    public Object process(Object commandState) {
        // 获取相应命令接口的新实例,这里是原型模式的简单实现
        Command command = createCommand();
        // 在(希望是全新的)命令实例上设置状态
        command.setState(commandState);
        return command.execute();
    }

    // 行...但是这种方法的实现在哪里呢?
    protected abstract Command createCommand();
}

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

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

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

<!-- 作为原型部署的有状态bean(非单例),原型模式 -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- 根据需要在此注入依赖项 -->
</bean>

<!-- commandProcessor 使用 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();
}

或者,更习惯地说,您可以依赖目标 bean 根据 lookup 方法声明的返回类型进行解析:

public abstract class CommandManager {

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

    @Lookup
    protected abstract Command createCommand();
}

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

访问不同作用域的目标 beans 的另一种方式是 ObjectFactory/ Provider 注入点。将作用域 Beans 视为依赖项。

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

任意方法替换

与查找方法注入相比,方法注入的一个不太有用的形式是用另一个方法实现替换受管 bean 中的任意方法的能力。您可以安全地跳过本节的其余部分,直到您真正需要这个功能。

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

public class MyValueCalculator {

    public String computeValue(String input) {
        // 一些真正的代码...
    }

    // 其他一些方法...
}

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

/**
 * 旨在用于覆盖现有的 computeValue(String)
 * MyValueCalculator 中的实现
 */
public class ReplacementComputeValue implements MethodReplacer {

    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // 获取输入值,对其进行处理,并返回计算结果
        String input = (String) args[0];
        ...
        return ...;
    }
}

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

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
    <!-- 任意方法替换 -->
    <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

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

Bean Scopes(作用域)

Scopes : 作用域、范围

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

您不仅可以控制要插入到从特定 bean definition(定义)创建的对象中的各种依赖关系和配置值,还可以控制从特定 bean definition(定义)创建的对象的范围。这种方法既强大又灵活,因为您可以通过配置选择所创建对象的范围,而不必在 Java 类级别上限定对象的范围。可以将 Beans 定义为部署在多个作用域之一中。Spring 框架支持六个作用域,其中四个只有在使用 web 感知的 ApplicationContext 时才可用。您也可以创建自定义范围。

下表描述了支持的范围:

范围描述
singleton:单例(默认)对于每个 Spring IoC 容器,将单个 bean definition(定义)限定为 single (单个)对象实例。
prototype:原型将单个 bean definition(定义)的范围限定为任意数量的对象实例。相当于多例模式或者设计模式中的原型模式,每次都会返回新的对象,
request:请求将单个 bean definition(定义)的范围限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例,该实例是在单个 bean definition(定义)的基础上创建的。仅在 web 关注的 Spring ApplicationContext 的上下文中有效。
session:会话将单个 bean definition(定义)的范围限定为 HTTP 会话的生命周期。仅在 web 关注的 Spring ApplicationContext 的上下文中有效。
application:应用程序将单个 bean definition(定义)的范围限定为 ServletContext 的生命周期。仅在 web 关注的 Spring ApplicationContext 的上下文中有效。
websocket:网络通信将单个 bean definition(定义)的范围限定为 WebSocket 的生命周期。仅在 web 关注的 Spring ApplicationContext 的上下文中有效。

从 Spring 3.0 开始,线程范围是可用的,但是默认情况下没有注册。有关更多信息,请参见 SimpleThreadScope 的文档。有关如何注册此范围或任何其他自定义范围的说明,请参见后续使用自定义范围。

单例范围

只管理一个 singleton bean 的共享实例,所有对具有与 bean definition(定义)匹配的 ID 的 bean 的请求都会导致 Spring 容器返回一个特定的 bean 实例。

换句话说,当您定义一个 bean definition(定义)并且它的作用域是 singleton 时,Spring IoC 容器会创建由该 bean definition(定义)的对象的一个实例。这个实例存储在这种 single(单独)bean 的缓存中,所有后续的请求和对该命名 bean 的引用都返回缓存的对象。下图显示了单例范围的工作方式:

在这里插入图片描述

Spring 的 singleton bean 概念不同于 【四人帮(Gof)模式书】 中定义的 singleton 模式。GoF singleton 对一个对象的范围进行了硬编码,使得每个类装入器只创建一个特定类的实例。Spring singleton 的范围最好描述为每个容器和每个 bean。这意味着,如果您在一个 Spring容器中为一个特定的类定义了一个 bean,那么 Spring 容器将创建该 bean definition(定义)所定义的类的一个且仅有一个实例。singleton 作用域是 Spring 的默认作用域。要在 XML 中将 bean 定义为单例,可以定义一个 bean,如下例所示:

四人帮(Gof)模式书:经典设计模式书

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

<!-- 下面是等效的,尽管是多余的(默认情况下是单例范围) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

Spring 默认就是单例模式,所以可以不用指定 scope。

原型范围

Prototype Scope:原型范围,也可以称为多例模式

bean 部署的非单例原型范围导致每次对特定 bean 发出请求时都会创建一个新的 bean 实例。也就是说,bean 被注入到另一个 bean 中,或者您通过对容器的 getBean() 方法调用来请求它。通常,应该对所有有状态 bean 使用 prototype 作用域,对无状态 bean 使用singleton 作用域。

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

在这里插入图片描述

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

以下示例将 bean 定义为 XML 中的原型:

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

与其他作用域不同,Spring 不管理原型 bean 的完整生命周期。容器实例化、配置和组装一个原型对象,并把它交给客户机,而没有该原型实例的进一步记录。因此,尽管初始化生命周期回调方法在所有对象上都被调用,而不考虑范围,但是在原型的情况下,配置的销毁生命周期回调不会被调用。客户端代码必须清理原型范围的对象,并释放原型 beans 拥有的昂贵资源。要让 Spring 容器释放由原型范围的bean 持有的资源,可以尝试使用一个定制的 bean post-processor(后处理器),它持有一个对需要清理的 bean 的引用。

post-processor(后处理器) 会在后续的-容器扩展点介绍

在某些方面,Spring 容器在原型范围的 bean 方面的角色是 Java new 操作符的替代品。超过这一点的所有生命周期管理都必须由客户处理。(有关 Spring 容器中 bean 的生命周期的详细信息,请参见后续-生命周期回调。)

具有原型 bean 依赖性的单例 bean

当您在原型 bean 上使用具有依赖关系的单例作用域 bean 时,请注意依赖关系是在实例化时解析的。因此,如果将原型范围的 bean 依赖注入到单例范围的 bean 中,新的原型 bean 将被实例化,然后依赖注入到单例 bean 中。原型实例是唯一一个提供给单例范围 bean 的实例。

也就是说,单例 bean 只会注入一次 原型 bean

但是,假设您希望单例作用域 bean 在运行时重复获取原型作用域 bean 的新实例。您不能将原型范围的 bean 依赖注入到您的单例 bean 中,因为这种注入只发生一次,当 Spring 容器实例化单例 bean 并解析和注入其依赖项时。如果在运行时多次需要原型 bean 的新实例,请参见前面-依赖注入的-方法注入。

Request、Session、Application、和WebSocket 作用域

仅当使用 web 感知的 Spring ApplicationContext 实现(如 XmlWebApplicationContext )时,Request(请求)、Session(会话)、Application(应用程序)和 websocket 范围才可用。如果您将这些作用域与常规的 Spring IoC 容器(如 ClassPathXmlApplicationContext )一起使用,则会抛出一个 IllegalStateException,它会报告一个未知的 bean 作用域。

初始Web配置

为了在Request(请求)、Session(会话)、Application(应用程序)和 websocket 级别支持 bean 的作用域( web 作用域 bean),在定义 bean 之前需要进行一些小的初始配置。(标准作用域:singleton(单例)和 prototype (原型)不需要这种初始设置。)

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

如果您在 Spring Web MVC 中访问作用域 beans,实际上,在由 Spring DispatcherServle t处理的请求中,不需要特殊的设置。DispatcherServlet 已经公开了所有相关的状态。

如果您使用 Servlet 2.5 web 容器,在 Spring 的 DispatcherServlet 之外处理请求(例如,当使用 JSF 或 Struts 时),您需要注册 org.springframework.web.context.request.RequestContextListener','ServletRequestListener。对于 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>

DispatcherServlet、RequestContextListener 和 RequestContextFilter 都做完全相同的事情,即将 HTTP 请求对象绑定到服务于该请求的线程。这使得 Request(请求)范围和 Session(会话)范围的 beans 在调用链的更下游可用。

请求范围

考虑以下 bean definition(定义)的 XML 配置:

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

Spring 容器通过为每个 HTTP 请求使用 loginAction bean definition(定义)来创建 LoginAction bean 的新实例。也就是说,loginAction bean 的作用域是 HTTP 请求级别。您可以根据需要任意更改创建的实例的内部状态,因为从同一 loginAction bean definition(定义)创建的其他实例看不到这些状态更改。它们是特定于个人要求的。当请求完成处理时,该请求范围内的 bean 将被丢弃。

当使用注解驱动的组件或 Java 配置时,可以使用 @RequestScope 注解将组件分配给请求范围。以下示例显示了如何实现这一点:

@RequestScope
@Component
public class LoginAction {
    // ...
}
会话范围

考虑以下 bean definition(定义)的 XML 配置:

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

Spring 容器通过在单个 HTTP 会话的生命周期内使用 UserPreferences bean definition(定义)来创建 userPreferences bean 的新实例。换句话说,userPreferences bean 的作用域实际上在 HTTP 会话级别。与请求范围的 bean 一样,您可以根据需要任意更改所创建的实例的内部状态,因为其他 HTTP 会话实例也在使用从同一个 userPreferences bean definition(定义)创建的实例,它们看不到这些状态更改,因为它们是特定于单个 HTTP 会话的。当 HTTP 会话最终被丢弃时,作用于该特定 HTTP 会话的 bean 也被丢弃。

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

@SessionScope
@Component
public class UserPreferences {
    // ...
}
应用程序范围

考虑以下 bean definition(定义)的 XML 配置:

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

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

当使用注解驱动的组件或 Java 配置时,可以使用 @ApplicationScope 注解将组件分配给应用程序范围。以下示例显示了如何实现这一点:

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}
具有作用域的 bean 作为依赖项

Spring IoC 容器不仅管理对象 (beans) 的实例化,还管理协作者(或依赖者)的连接。例如,如果您想要将一个 HTTP 请求范围的 bean 注入到另一个更长寿命范围的 bean 中,您可以选择注入一个 AOP 代理来代替该范围的 bean。也就是说,您需要注入一个代理对象,它公开与作用域对象相同的公共接口,但也可以从相关的作用域(比如 HTTP 请求)中检索真正的目标对象,并将方法调用委托给真正的对象。

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

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

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

作为扩展变量,您可以声明 ObjectProvider<MyTargetBean>,它提供了几个附加的访问变量,包括 getIfAvailable 和 getIfUnique。

这种方法的 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">

    <!-- 作为代理公开的 HTTP 会话范围的 bean -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- 指示容器代理周围的bean -->
        <aop:scoped-proxy/> <!-- 定义代理的行。 -->
    </bean>

    <!-- 一个单独作用域的bean,注入了上述bean的代理 -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- 对代理 userPreferences bean 的引用 -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

要创建这样的代理,需要将一个子 <aop:scoped-proxy/> 元素插入到一个作用域 bean definition (定义)中(请参见后续【选择要创建的代理类型】和【基于XML模式的配置】)。为什么作用域在 request(请求)、session(会话)和自定义作用域级别的 beans definition (定义)需要 <aop:scoped-proxy/>元素?考虑下面的 singleton bean definition(定义),并将其与您需要为上述范围定义的内容进行对比(注意,下面的 userPreferences bean definition(定义) 是不完整的):

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

<bean id="userManager" class="com.something.UserManager">
    <!-- 将一个 session 会话作用域的 bean 注入到单例 bean 中 -->
    <property name="userPreferences" ref="userPreferences"/>
</bean>

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

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

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

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

选择要创建的代理类型

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

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

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

<!-- DefaultUserPreferences 实现 UserPreferences 接口 -->
<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>

有关选择基于类或基于接口的代理的更多详细信息,请参见后续【代理机制】。

自定义作用域

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

创建自定义范围

要将您的自定义范围集成到 Spring 容器中,您需要实现 org.springframework.beans.factory.config.Scope 接口,这将在本节中描述。要了解如何实现自己的作用域,请参阅 Spring 框架本身提供的作用域实现和 Scope(作用域) javadoc,其中详细解释了需要实现的方法。

Scope 接口有四种方法可以从作用域中获取对象,从作用域中移除对象,以及销毁对象。

例如,session(会话) 范围实现返回会话范围的 bean(如果它不存在,该方法在将 bean 绑定到会话以供将来引用后,返回 bean 的新实例)。下面的方法从基础范围返回对象:

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

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

Object remove(String name)

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

void registerDestructionCallback(String name, Runnable destructionCallback)

有关销毁回调的更多信息,请参见 javadoc 或 Spring scope 实现。

下面的方法获取基础范围的会话标识符:

String getConversationId()

这个标识符对于每个作用域都是不同的。对于 session (会话)范围的实现,该标识符可以是 session (会话)标识符。

如果要指定要作用域,就是要在基础作用域里,创建自定义的作用域并通过标识符来区分,如 session 作用域的标识符就是 session

使用自定义范围

在编写和测试一个或多个自定义范围实现之后,您需要让 Spring 容器知道您的新范围。下面的方法是向 Spring 容器注册新作用域的主要方法:

void registerScope(String scopeName, Scope scope);

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

registerScope 的第一个参数(…)方法是与范围相关联的唯一名称。Spring 容器中这种名称的例子有 singleton 和 prototype。registerScope 的第二个参数(…)方法是您希望注册和使用的自定义范围实现的实际实例。

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

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

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);	// 注册名称为 thread 的作用域

然后,您可以创建遵循自定义范围的范围规则的 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/> 放在 FactoryBean 实现的 <Bean> 声明中时,作用域是工厂 bean 本身,而不是从 getObject() 返回的对象。

FactoryBean :实现了此类的类,可以被称为工厂类,在被注入时,返回的对象是通过调用其 getObject() 方法返回

自定义 Bean 的性质

Spring 框架提供了许多接口,可以用来定制 bean 的性质。本节将它们分组如下:

  • Lifecycle Callbacks:生命周期回调
  • ApplicationContextAware 和 BeanNameAware
  • 其它 Aware 接口
生命周期回调

为了与容器对 bean 生命周期的管理进行交互,可以实现 Spring InitializingBean 和 DisposableBean 接口。容器为前者调用 afterPropertiesSet(),为后者调用 destroy(),让 bean 在初始化和销毁 bean 时执行某些操作。

JSR-250 @PostConstruct 和 @PreDestroy 注解通常被认为是现代 Spring 应用程序中接收生命周期回调的最佳实践。使用这些注释意味着您的 beans 没有耦合到特定于 Spring 的接口。有关详细信息,请参见后续【使用 @PostConstruct 和 @PreDestroy】。

如果您不希望使用 JSR-250 注释,但是仍然希望移除耦合,那么可以考虑 init-method 和 destroy-method bean definition (定义)元数据。

在内部,Spring 框架使用 BeanPostProcessor 实现来处理它可以找到的任何回调接口,并调用适当的方法。如果您需要 Spring 默认不提供的定制特性或其他生命周期行为,您可以自己实现一个 BeanPostProcessor。有关更多信息,请参见后续【容器扩展点】。

除了初始化和销毁回调之外,Spring 管理的对象还可以实现 Lifecycle (生命周期)接口,以便这些对象可以参与启动和关闭过程,这是由容器自身的生命周期驱动的。

本节描述了生命周期回调接口。

初始化回调

Initialization Callbacks : 初始化回调

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

void afterPropertiesSet() throws Exception;

我们建议您不要使用 InitializingBean 接口,因为它不必要地将代码耦合到 Spring。或者,我们建议使用 @PostConstruct 注解或指定一个 POJO 初始化方法。对于基于 XML 的配置元数据,可以使用 init-method 属性指定具有 void 无参数签名的方法的名称。对于 Java 配置类,可以使用 @Bean 的 initMethod 属性。请参见后续【接收生命周期回调】。考虑下面的例子:

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

    public void init() {
        // 做一些初始化工作
    }
}

前面的示例与下面的示例(包含两个清单)具有几乎完全相同的效果:

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

    @Override
    public void afterPropertiesSet() {
        // 做一些初始化工作
    }
}

然而,前面两个例子中的第一个没有将代码耦合到 Spring。

销毁回调

Destruction Callbacks : 销毁回调

实现 org.springframework.beans.factory.DisposableBean 接口可以让 bean 在包含它的容器被销毁时获得回调。DisposableBean 接口指定了一个方法:

void destroy() throws Exception;

我们建议您不要使用 DisposableBean 回调接口,因为它不必要地将代码耦合到 Spring。或者,我们建议使用 @PreDestroy 注解或指定bean definitions(定义)支持的通用方法。对于基于 XML 的配置元数据,您可以在 <bean/> 上使用 destroy-method 属性。对于 Java 配置,可以使用 @Bean 的 destroyMethod 属性。请参见接收生命周期回调。考虑以下定义:

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

    public void cleanup() {
        // 做一些销毁工作(比如释放池连接)
    }
}

前面的定义与下面的定义具有几乎完全相同的效果:

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

    @Override
    public void destroy() {
        // 做一些销毁工作(比如释放池连接)
    }
}

然而,前面两个定义中的第一个没有将代码耦合到 Spring。

您可以为 <bean> 元素的 destroy-method 属性分配一个特殊的( inferred(推断的))值,该值指示 Spring 自动检测特定 bean 类上的公共关闭或关闭方法。(因此,任何实现 java.lang.AutoCloseablejava.io.Closeable 的类都将匹配。)还可以在 <beans> 元素的 default-destroy-method 属性上设置这个特殊( inferred (推断))值,以便将此行为应用于整个 bean 集(请参见【默认初始化和销毁方法】)。注意,这是 Java configuration(配置)的默认行为。

默认初始化和销毁方法

当您编写不使用特定于 Spring 的 InitializingBean 和 DisposableBean 回调接口的初始化和销毁方法回调时,您通常会使用 init()、initialize()、dispose() 等名称来编写方法。理想情况下,这种生命周期回调方法的名称在整个项目中是标准化的,以便所有开发人员使用相同的方法名称并确保一致性。

您可以配置 Spring 容器来“寻找”每个 bean 上的命名初始化和销毁回调方法名。这意味着作为应用程序开发人员,您可以编写应用程序类并使用名为 init() 的初始化回调,而不必为每个 bean 定义配置 init-method="init " 属性。当创建 bean 时,Spring IoC 容器调用该方法(根据前面描述的标准生命周期回调契约)。该特性还为初始化和销毁方法回调强制实施一致的命名约定。

假设您的初始化回调方法命名为 init(),销毁回调方法命名为 destroy()。然后,您的类类似于下面示例中的类:

public class DefaultBlogService implements BlogService {

    private BlogDao blogDao;

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

    // 这是(不足为奇的)初始化回调方法
    public void init() {
        if (this.blogDao == null) {
            throw new IllegalStateException("必须设置[blogDao]属性。");
        }
    }
}

然后,您可以在 bean 中使用该类,如下所示:

<beans default-init-method="init">

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

</beans>

顶级 <beans/> 元素属性中 default-init-method 属性的存在导致 Spring IoC 容器将 bean 类中名为 init 的方法识别为初始化方法回调。当创建和组装 bean 时,如果 bean 类有这样的方法,它将在适当的时候被调用。

通过在顶级 <beans/> 元素上使用 default-destroy-method 属性,可以类似地配置 destroy 方法回调(即在 XML 中)。

如果现有的 bean 类已经有了不符合约定命名的回调方法,您可以通过使用 <bean/> 本身的 init-method 和 destroy-method 属性指定(即以 XML 格式)方法名来覆盖默认值。

Spring 容器保证在为 bean 提供所有依赖项后,立即调用配置的初始化回调。因此,初始化回调是在原始 bean 引用上调用的,这意味着AOP 拦截器等还没有应用于 bean。首先完全创建一个目标 bean,然后应用一个 AOP 代理(例如)及其拦截器链。如果目标 bean 和代理是分开定义的,那么您的代码甚至可以绕过代理,与原始的目标 bean 进行交互。因此,将拦截器应用于 init 方法是不一致的,因为这样做会将目标 bean 的生命周期耦合到它的代理或拦截器,并在您的代码直接与原始目标 bean 交互时留下奇怪的语义。

组合生命周期机制

从 Spring 2.5 开始,您有三种选择来控制 bean 的生命周期行为:

  • InitializingBean 和 DisposableBean 回调接口
  • 自定义 init() 和 destroy() 方法
  • @PostConstruct 和 @PreDestroy 注解。您可以结合这些机制来控制给定的 bean。

如果为一个 bean 配置了多个生命周期机制,并且每个机制都配置了不同的方法名,那么每个配置的方法都将按照本说明后面列出的顺序运行。但是,如果为多个生命周期机制配置了相同的方法名(例如,初始化方法的 init(),则该方法将运行一次,如前一节所述。

为同一个 bean 配置的具有不同初始化方法的多个生命周期机制的调用如下:

  1. 用 @PostConstruct 注解的方法
  2. 由 InitializingBean 回调接口定义的 afterPropertiesSet()
  3. 自定义配置的 init() 方法

销毁方法的调用顺序相同:

  1. 用 @PreDestroy 注解的方法
  2. DisposableBean 回调接口定义的 destroy()
  3. 自定义配置的 destroy() 方法
启动和关闭回调

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

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

任何 Spring 管理的对象都可以实现 Lifecycle(生命周期)接口。然后,当 ApplicationContext 本身接收到开始和停止信号时(例如,对于运行时的停止/重启场景),它会将这些调用级联到该上下文中定义的所有Lifecycle(生命周期)实现。它通过委托给 LifecycleProcessor(生命周期处理器)来实现这一点,如下面的清单所示:

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

注意,LifecycleProcessor 本身是 Lifecycle(生命周期)接口的扩展。它还添加了另外两个方法,用于对被刷新和关闭的上下文做出反应。

请注意,常规的 org.springframework.context.Lifecycle 接口是显式启动和停止通知的普通契约,并不意味着在上下文刷新时自动启动。为了对特定 bean 的自动启动进行细粒度控制(包括启动阶段),可以考虑实现 org.springframework.context.SmartLifecycle

另外,请注意,停止通知不能保证在销毁前发出。在常规关闭时,所有 Lifecycle(生命周期) beans 首先会收到一个停止通知,然后才会传播常规销毁回调。但是,在上下文的生存期内进行热刷新或停止刷新尝试时,只调用 destroy 方法。

启动和关闭调用的顺序可能很重要。如果任何两个对象之间存在“依赖于”关系,则依赖方在其依赖之后开始,在其依赖之前停止。然而,有时直接依赖关系是未知的。您可能只知道某种类型的对象应该在另一种类型的对象之前开始。在这些情况下,SmartLifecycle 接口定义了另一个选项,即在其超接口 Phased 上定义的 getPhase() 方法。以下清单显示了 Phased 接口的定义:

public interface Phased {

    int getPhase();
}

以下列表显示了 SmartLifecycle 接口的定义:

public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);
}

启动时,相位最低的对象先启动。停止时,遵循相反的顺序。因此,实现 SmartLifecycle 并且其 getPhase() 方法返回 Integer.MIN_VALUE 将是第一个开始和最后一个停止的。在光谱的另一端,相位值为 Integer.MAX_VALUE 表示该对象应该最后启动并首先停止(可能是因为它依赖于其他正在运行的进程)。在考虑阶段值时,了解任何未实施 SmartLifecycle 的“正常” Lifecycle(生命周期) 对象的默认阶段为0也很重要。因此,任何负相位值都表示对象应该在这些标准组件之前开始(并在它们之后停止)。对于任何正相位值,情况相反。

如果 生命周期类 A 的数值为 0,生命周期类 B 数值为 1,则 类 A 应该在 类 B 之前启动,在停止时,必须在类 B 停止之后停止。相反,类 B 必须在类 A 之后启动,在停止时,必须在类 A 之前停止。

SmartLifecycle 定义的 stop 方法接受回调。在实现的关闭过程完成后,任何实现都必须调用回调的 run() 方法。这使得在必要时可以异步关闭,因为 LifecycleProcessor 接口的默认实现 DefaultLifecycleProcessor 会等待每个阶段中的对象组达到其超时值,以调用该回调。默认的每阶段超时是30秒。您可以通过在上下文中定义一个名为 lifecycleProcessor 的 bean 来覆盖默认的生命周期处理器实例。如果您只想修改超时,定义以下内容就足够了:

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- 以毫秒为单位的超时值 -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

如前所述,LifecycleProcessor 接口还为上下文的刷新和关闭定义了回调方法。后者驱动关闭过程,就像显式调用了 stop() 一样,但它发生在上下文关闭时。另一方面,“刷新”回调启用了 SmartLifecycle beans 的另一个特性。当上下文被刷新时(在所有对象被实例化和初始化之后),回调被调用。此时,默认的生命周期处理器会检查每个 SmartLifecycle 对象的 isAutoStartup() 方法返回的布尔值。如果为 true,则该对象在该点启动,而不是等待上下文的或其自己的 start() 方法的显式调用(与上下文刷新不同,对于标准上下文实现,上下文启动不会自动发生)。如前所述,phase (阶段值) 和任何“依赖”关系决定了启动顺序。

在非 Web 应用程序中优雅地关闭 Spring IoC 容器

本节仅适用于非 web 应用程序。Spring 基于 web 的 ApplicationContext 实现已经有了代码,可以在相关 web 应用程序关闭时优雅地关闭 Spring IoC 容器。

如果您在非 web 应用程序环境中使用 Spring 的 IoC 容器(例如,在富客户端桌面环境中),请向 JVM 注册一个关闭挂钩。这样做可以确保正常关机,并在您的单例 beans 上调用相关的销毁方法,以便释放所有资源。您仍然必须正确配置和实现这些销毁回调。

若要注册关闭挂钩,请调用在 ConfigurableApplicationContext 接口上声明的 registerShutdownHook() 方法,如下例所示:

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

public final class Boot {

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

        // 为上述上下文添加一个关闭挂钩...
        ctx.registerShutdownHook();

        // 应用程序在此运行...

        // main方法退出,在应用程序关闭之前调用hook...
    }
}
ApplicationContextAware 和 BeanNameAware

当 ApplicationContext 创建实现 org.springframework.context.ApplicationContextAware 接口的对象实例时,将为该实例提供对该 ApplicationContext 的引用。下面的列表显示了 ApplicationContextAware 接口的定义:

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

因此,beans 可以通过 ApplicationContext 接口或通过将引用转换为该接口的已知子类(如 ConfigurableApplicationContext,它公开了附加功能),以编程方式操作创建它们的 ApplicationContext。一种用途是编程检索其他 beans。有时这种能力是有用的。但是,一般来说,您应该避免它,因为它将代码耦合到 Spring,并且不遵循控制风格的反转,在控制风格中,协作者作为属性提供给 beans。ApplicationContext 的其他方法提供对文件资源、发布应用程序事件和访问 MessageSource 的访问。ApplicationContext 的附加功能中描述了这些附加功能。

自动装配是获取对 ApplicationContext 的引用的另一种方法。传统的构造函数和 byType 自动装配模式(如自动装配协作器中所述)可以分别为构造函数参数或 setter 方法参数提供 ApplicationContext 类型的依赖关系。要获得更大的灵活性,包括自动关联字段和多参数方法的能力,请使用基于注解的自动关联功能。如果这样做,ApplicationContext 将自动连接到需要 ApplicationContext 类型的字段、构造函数参数或方法参数中(如果所涉及的字段、构造函数或方法带有 @Autowired 注解)。有关更多信息,请参见后续【使用 @Autowired】。

当 ApplicationContext 创建一个实现 org.springframework.beans.factory.BeanNameAware 接口的类时,将为该类提供一个对其关联对象定义中定义的名称的引用。以下清单显示了 BeanNameAware 接口的定义:

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

在填充普通 bean 属性之后,但在初始化回调(如 InitializingBean.afterPropertiesSet() 或自定义 init-method )之前,调用回调。

也就是说,此方法在 bean 初始化填充完属性后,就会调用 setBeanName 方法。然后再调用初始化回调

其它 Aware 接口

除了 ApplicationContextAware 和 BeanNameAware (前面讨论过)之外,Spring 还提供了广泛的感知回调接口,让 beans 向容器表明它们需要某种基础设施依赖。一般来说,名称表示依赖类型。下表总结了最重要的 Aware 接口:

也就是说 实现了 Aware 的类,通常会有不同功能的回调

名称注入依赖解释于
ApplicationContextAware声明(注入)ApplicationContextApplicationContextAware 和 BeanNameAware
ApplicationEventPublisherAware封闭 ApplicationContext 的事件发布者。后续【应用程序上下文的附加功能】
BeanClassLoaderAware用于加载bean类的类加载器。前面文章【实例化Beans】
BeanFactoryAware声明 BeanFactory。后续 BeanFactory 描述
BeanNameAware声明 bean 的名称ApplicationContextAware 和 BeanNameAware
BootstrapContextAware容器在其中运行的资源适配器 BootstrapContext。通常仅在JCA感知的 ApplicationContext 实例中可用。非核心篇章 【JCA CCI】篇
LoadTimeWeaverAware已定义的 weaver 用于在加载时处理类定义。后续【在 Spring 框架中用AspectJ 进行加载时编织】
MessageSourceAware解析消息的配置策略(支持参数化和国际化)。后续【应用程序上下文的附加功能】
NotificationPublisherAwareSpring JMX 通知发布者。后续 JMX 【通知】篇
ResourceLoaderAware为资源的低级访问配置加载程序。后续【资源】篇
ServletConfigAware容器运行所在的当前 ServletConfig。仅在 web 感知的 Spring ApplicationContext 中有效。后续 【Spring MVC】 篇
ServletContextAware容器运行所在的当前 ServletContext。仅在 web 感知的 Spring ApplicationContext 中有效。后续 【Spring MVC】 篇

再次注意,使用这些接口将您的代码绑定到 Spring API,并且不遵循控制风格的反转。因此,我们建议将它们用于需要对容器进行编程访问的基础设施 beans。

Bean Definition 继承

bean definition(定义)可以包含许多配置信息,包括构造函数参数、属性值和特定于容器的信息,如初始化方法、静态工厂方法名等。子bean definition(定义)从父 definition(定义)继承配置数据。子 definition(定义)可以根据需要覆盖一些值或添加其他值。使用父bean 和子 bean definition(定义)可以节省大量的输入。实际上,这是一种模板形式。

如果以编程方式使用 ApplicationContext 接口,则子 bean definition(定义)由 ChildBeanDefinition 类表示。大多数用户不在这个级别上使用它们。相反,它们在一个类中声明性地配置 bean definition(定义),比如 ClassPathXmlApplicationContext。当使用基于 XML 的配置元数据时,可以通过使用 parent 属性来指示子 bean definition(定义),将 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"/>
    <!-- 年龄属性值1将从父代继承 -->
</bean>

如果没有指定,子 bean definition(定义)使用父 definition(定义)中的 bean 类,但也可以覆盖它。在后一种情况下,子 bean 类必须与父类兼容(也就是说,它必须接受父类的属性值)。

子 bean definition(定义)继承父 bean 的作用域、构造函数参数值、属性值和方法覆盖,并可以选择添加新值。您指定的任何作用域、初始化方法、销毁方法或静态工厂方法设置都会重写相应的父设置。

其余的设置总是取自子 definition(定义):依赖、自动配置模式、依赖性检查、单例和延迟初始化。

前面的示例通过使用 abstract 属性将父 bean definition(定义)显式标记为抽象。如果父定义没有指定类,则需要将父 bean definition(定义)显式标记为抽象,如下例所示:

<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将从父bean定义中继承值1 -->
</bean>

父 bean 不能自行实例化,因为它是不完整的,并且它也被显式标记为抽象的。当一个 definition(定义)是抽象的时,它只能作为一个纯模板 bean definition(定义)使用,作为子 definition(定义)的父 definition(定义)。通过将抽象父 Bean 作为另一个 bean 的 ref 属性引用,或者使用父 bean ID 进行显式 getBean() 调用,试图单独使用这样的抽象父 bean 会返回错误。类似地,容器的内部preInstantiateSingletons() 方法忽略被定义为抽象的 bean definition(定义)。

默认情况下,ApplicationContext 预实例化所有 singletons (单例)。因此,如果您有一个(父) bean definition(定义),并且该 definition(定义)指定了一个类,那么必须确保将 abstract 属性设置为 true,否则应用程序上下文将实际(尝试)预实例化抽象 bean,这一点很重要(至少对于 singleton beans 是如此)。

容器扩展点

通常,应用程序开发人员不需要划分 ApplicationContext 实现类的子类。相反,Spring IoC 容器可以通过插入特殊集成接口的实现来扩展。接下来的几节将描述这些集成接口。

使用 BeanPostProcessor 定制 Beans

BeanPostProcessor 接口定义了回调方法,您可以实现这些方法来提供自己的(或覆盖容器的默认)实例化逻辑、依赖关系解析逻辑等等。如果您想在 Spring 容器完成实例化、配置和初始化 bean 之后实现一些定制逻辑,您可以插入一个或多个定制 BeanPostProcessor 实现。

您可以配置多个 BeanPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanPostProcessor 实例的运行顺序。只有当BeanPostProcessor 实现了 Ordered 接口时,才能设置此属性。如果您编写自己的 BeanPostProcessor,也应该考虑实现 Ordered 接口。有关更多详细信息,请参见 BeanPostProcessor 和 Ordered 接口的 javadoc。另请参见下方关于【以编程方式注册 BeanPostProcessor 实例】的说明。

BeanPostProcessor 实例对 bean(或对象) 实例进行操作。也就是说,Spring IoC 容器实例化一个 bean 实例,然后BeanPostProcessor 实例完成它们的工作。

BeanPostProcessor 实例的范围是每个容器。只有当您使用容器层次结构时,这才是相关的。如果在一个容器中定义了一个BeanPostProcessor,那么它只对该容器中的 beans 进行后处理。换句话说,在一个容器中定义的 beans 不会被在另一个容器中定义的 BeanPostProcessor 进行后处理,即使这两个容器属于同一个层次结构。

要更改实际的 bean definition(定义)(即定义bean的蓝图),您需要使用一个 BeanFactoryPostProcessor,如后续文章【使用BeanFactoryPostProcessor 自定义配置元数据】中所述。

org.springframework.beans.factory.config.BeanPostProcessor 接口正好由两个回调方法组成。当这样的类注册为容器的后处理器时,对于容器创建的每个 Bean 实例,在调用容器初始化方法(如 InitializingBean.afterPropertiesSet() 或任何声明的 init 方法)之前和任何 bean 初始化回调之后,后处理器都会从容器获得回调。后处理器可以对 bean 实例进行任何操作,包括完全忽略回调。bean 后处理器通常检查回调接口,或者它可以用代理包装 bean。为了提供代理包装逻辑,一些 Spring AOP 基础设施类被实现为 bean 后处理器。

ApplicationContext 自动检测实现 BeanPostProcessor 接口的配置元数据中定义的任何 beans。ApplicationContext 将这些 bean 注册为后处理器,以便稍后在创建 bean 时可以调用它们。Bean 后处理器可以像任何其他 bean 一样部署在容器中。

请注意,当通过在配置类上使用 @Bean 工厂方法来声明 BeanPostProcessor 时,工厂方法的返回类型应该是实现类本身,或者至少是 org.springframework.beans.factory.config.BeanPostProcessor 接口,清楚地表明该 Bean 的后处理器性质。否则,ApplicationContext 无法在完全创建它之前按类型自动检测它。因为 BeanPostProcessor 需要尽早实例化,以便应用于上下文中其他 beans 的初始化,所以这种早期类型检测非常关键。

以编程方式注册 BeanPostProcessor 实例

虽然推荐的注册 BeanPostProcessor 的方法是通过 ApplicationContext 自动检测(如前所述),但是您可以通过使用addBeanPostProcessor 方法以编程方式针对 ConfigurableBeanFactory 注册它们。当您需要在注册之前评估条件逻辑时,或者甚至是在层次结构中跨上下文复制 BeanPostProcessor 时,这可能会很有用。但是,请注意,以编程方式添加的BeanPostProcessor 实例不考虑有序接口。在这里,注册的顺序决定了执行的顺序。还要注意,以编程方式注册的BeanPostProcessor 实例总是在那些通过自动检测注册的实例之前被处理,而不管任何显式排序。

BeanPostProcessor 实例和 AOP 自动代理

实现 BeanPostProcessor 接口的类是特殊的,被容器区别对待。作为 ApplicationContext 的特殊启动阶段的一部分,所有BeanPostProcessor 实例和它们直接引用的 beans 都在启动时实例化。接下来,所有的 BeanPostProcessor 实例都以排序的方式注册,并应用于容器中的所有其他 beans。因为 AOP 自动代理是作为 BeanPostProcessor 本身实现的,所以无论是BeanPostProcessor 实例还是它们直接引用的 beans 都不适合自动代理,因此,它们织入方面。

对于任何这样的 bean,您应该看到一条信息性日志消息:

Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)

Bean someBean 没有资格被所有的 BeanPostProcessor 接口处理(例如:没有资格自动代理)

如果您通过使用自动装配或 @Resource (这可能会退回到自动装配)将 bean 装配到您的 BeanPostProcessor 中,那么 Spring 在搜索类型匹配依赖候选项时可能会访问意外的 bean,从而使它们不适合自动代理或其他类型的 bean 后处理。例如,如果您有一个用 @Resource 注解的依赖项,其中字段或 setter 名称不直接对应于 bean 的声明名称,并且没有使用 name 属性,那么Spring 将访问其他 bean,以便按类型匹配它们。

以下示例显示了如何在 ApplicationContext 中编写、注册和使用 BeanPostProcessor 实例。

示例:Hello World,BeanPostProcessor 样式

第一个例子说明了基本用法。该示例显示了一个自定义的 BeanPostProcessor 实现,该实现在容器创建每个 bean 时调用该 bean 的toString() 方法,并将结果字符串打印到系统控制台。

下面的列表显示了自定义 BeanPostProcessor 实现类的定义:

package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

    // 简单地按原样返回实例化的bean
    public Object postProcessBeforeInitialization(Object bean, String beanName) {// 之前
        return bean; // 我们有可能在这里返回任何对象引用...
    }

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

以下 beans 元素使用 InstantiationTracingBeanPostProcessor:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang
        https://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="菲奥娜·艾波真是太梦幻了。"/>
    </lang:groovy><!-- 这里使用了 groovy 动态语言,其实和 java 差不多,只是为了简化 java 开发语句 -->

    <!--
    	当上述bean (messenger)被实例化时,这个自定义 BeanPostProcessor 实现将把事实输出到系统控制台
    -->
    <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

请注意 InstantiationTracingBeanPostProcessor 仅仅是如何定义的。它甚至没有名称,因为它是一个 bean,所以可以像对任何其他 bean一样进行依赖注入。(前面的配置还定义了一个由 Groovy 脚本支持的 bean。Spring 动态语言支持在后续标题为【动态语言支持】的章节中有详细介绍。)

Groovy 文件,也会被 JVM 解析为 class 文件

以下 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 = ctx.getBean("messenger", Messenger.class);
        System.out.println(messenger);
    }

}

上述应用程序的输出如下所示:

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

将回调接口或注解与定制的 BeanPostProcessor 实现结合使用是扩展 Spring IoC 容器的一种常见方式。一个例子是 Spring 的AutowiredAnnotationBeanPostProcessor 是一个 BeanPostProcessor 实现,附带了 Spring 发行版和 autowires 注解字段、setter 方法和任意配置方法。

使用 BeanFactoryPostProcessor 自定义配置元数据

我们要看的下一个扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。也就是说,Spring IoC 容器让BeanFactoryPostProcessor 读取配置元数据,并在容器实例化除了 BeanFactoryPostProcessor 实例之外的任何 beans 之前潜在地更改它。

您可以配置多个 BeanFactoryPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanFactoryPostProcessor 实例的运行顺序。但是,只有当 BeanFactoryPostProcessor 实现了 Ordered 接口时,才能设置此属性。如果您编写自己的BeanFactoryPostProcessor,您也应该考虑实现 Ordered 接口。有关更多详细信息,请参见 BeanFactoryPostProcessor 和 Ordered(有序)接口的 javadoc。

进入 Spring Javadoc

如果您想要更改实际的 bean 实例(即从配置元数据创建的对象),那么您需要使用 BeanPostProcessor (在前面的使用BeanPostProcessor 定制 bean 中有所描述)。虽然在技术上可以在 BeanFactoryPostProcessor 中使用 Bean 实例(例如,通过使用 BeanFactory.getBean() ),但这样做会导致 bean 实例化过早,违反了标准的容器生命周期。这可能会导致负面影响,比如绕过 bean 后处理。

此外,BeanFactoryPostProcessor 实例的范围是每个容器。这只有在使用容器层次结构时才有意义。如果您在一个容器中定义了一个 BeanFactoryPostProcessor,那么它只应用于该容器中的 bean 定义。一个容器中的 Bean 定义不会被另一个容器中的BeanFactoryPostProcessor 实例进行后处理,即使两个容器属于同一层次结构。

当在 ApplicationContext 中声明 BeanFactoryPostProcessor (BeanFactory后处理器)时,它会自动运行,以便将更改应用于定义容器的配置元数据。Spring 包括许多预定义的 BeanFactoryPostProcessor,比如 PropertyOverrideConfigurer 和 PropertySourcesPlaceholderConfigurer。还可以使用自定义的 BeanFactoryPostProcessor,例如,注册自定义属性编辑器。

ApplicationContext 会自动检测部署到其中的任何实现了 BeanFactoryPostProcessor 接口的 beans。它在适当的时候使用这些 bean 作为bean 工厂的后处理器。您可以像部署任何其他 bean 一样部署这些后处理器 bean。

与 BeanPostProcessors 一样,您通常不希望为延迟初始化配置 BeanFactoryPostProcessors。如果没有其他 bean 引用 Bean(Factory)PostProcessor (Bean (工厂)后处理器),则该后处理器根本不会被实例化。因此,将它标记为延迟初始化将被忽略,即使您在 <beans/> 元素的声明中将 default-lazy-init 属性设置为 true,Bean(Factory)PostProcessor(Bean(工厂)后处理器)也会被急切地实例化。

示例:类名替换 PropertySourcesPlaceholderConfigurer

通过使用标准的Java Properties(属性)格式,您可以使用 PropertySourcesPlaceholderConfigurer 将 bean 定义中的属性值外部化到一个单独的文件中。这样做使部署应用程序的人能够定制特定于环境的属性,如数据库 URL 和密码,而没有修改容器的主 XML 定义文件的复杂性或风险。

考虑以下基于 XML 的配置元数据片段,其中定义了带有占位符值的 DataSource:

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="locations" value="classpath:com/something/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>

该示例显示了从外部属性文件配置的属性。在运行时,PropertySourcesPlaceholderConfigurer 应用于元数据,替换数据源的某些属性。要替换的值被指定为 ${property-name} 形式的占位符,它遵循 Ant、log4j 和 JSP EL 样式。

实际值来自标准 Java Properties(属性)格式的另一个文件:

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

因此,${jdbc.username} 字符串在运行时被替换为值 “sa ”,这同样适用于与属性文件中的键匹配的其他占位符值。PropertySourcesPlaceholderConfigurer 检查 bean definition(定义) 的大多数属性和特性中的占位符。此外,您可以自定义占位符前缀和后缀。

使用 Spring 2.5 中引入的 context(上下文)名称空间,您可以使用专用的配置元素来配置属性占位符。您可以在 location 属性中以逗号分隔列表的形式提供一个或多个位置,如下例所示:

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

PropertySourcesPlaceholderConfigurer 不仅在您指定的属性文件中查找属性。默认情况下,如果在指定的属性文件中找不到属性,它会检查Spring Environment(环境属性) 和常规 Java System(系统)属性。

您可以使用 PropertySourcesPlaceholderConfigurer 来替换类名,当您必须在运行时选择特定的实现类时,这有时很有用。以下示例显示了如何实现这一点:

<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
    <property name="locations">
        <value>classpath:com/something/strategy.properties</value>
    </property>
    <property name="properties">
        <value>custom.strategy.class=com.something.DefaultStrategy</value>
    </property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>

如果在运行时无法将类解析为有效的类,则在即将创建 bean 时,bean 的解析会失败,这是在非 lazy-init bean 的 ApplicationContext 的preInstantiateSingletons() 阶段。

示例:PropertyOverrideConfigurer

另一个 bean factory post-processor(bean工厂后处理器)PropertyOverrideConfigurer 类似于 PropertySourcesPlaceholderConfigurer,但与后者不同的是,bean 属性的原始定义可以有默认值或根本没有值。如果覆盖 Properties(属性)文件没有某个 bean 属性的条目,则使用默认的上下文 definition (定义)。

注意,bean definition(定义)不知道被覆盖,所以从 XML 定义文件中不能立即看出正在使用覆盖配置器。如果多个PropertyOverrideConfigurer 实例为同一个 bean 属性定义了不同的值,则由于覆盖机制,最后一个实例会胜出。

Properties(属性)文件配置行采用以下格式:

beanName.property=value

下面的列表显示了该格式的一个示例:

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

该示例文件可以与包含名为 dataSource 的 bean 的容器 definition(定义) 一起使用,该bean具有 driver(驱动程序) 和url属性。

也支持复合属性名,只要除了被覆盖的最后一个属性之外,路径的每个组件都已经非空(假定由构造函数初始化)。在下面的示例中,tom bean 的 fred 属性的 bob 属性的 sammy 属性被设置为标量值123:

tom.fred.bob.sammy=123

指定的替代值始终是文字值。它们不会被转换成 bean 引用。当 XML bean definition (定义) 中的原始值指定 bean 引用时,该约定也适用。

通过 Spring 2.5 中引入的 context (上下文) 名称空间,可以使用专用的配置元素配置属性覆盖,如下例所示:

<context:property-override location="classpath:override.properties"/>
使用 FactoryBean 自定义实例化逻辑

可以为本身就是工厂的对象实现 org.springframework.beans.factory.FactoryBean 接口。

FactoryBean 接口是 Spring IoC 容器的实例化逻辑的可插入点。如果您有复杂的初始化代码,用 Java 比用冗长的 XML 更好地表达,您可以创建自己的 FactoryBean,在该类中编写复杂的初始化,然后将您的自定义 FactoryBean 插入容器中。

FactoryBean<T> 接口提供三种方法:

  • T getObject():返回该工厂创建的对象的实例。实例可能是共享的,这取决于这个工厂是返回单例还是原型。
  • boolean isSingleton():如果 FactoryBean 返回单例,则返回 true,否则返回 false。此方法的默认实现返回 true。
  • Class<?> getObjectType():返回 getObject() 方法返回的对象类型,如果事先不知道类型,则返回 null。

FactoryBean 的概念和接口在 Spring 框架中的许多地方都有使用。Spring 本身附带了 FactoryBean 接口的 50 多个实现。

当您需要向容器请求实际的 FactoryBean 实例本身而不是它生成的 Bean 时,在调用 ApplicationContext 的 getBean() 方法时,在 bean 的 id 前面加上 & 符号。因此,对于 id 为 myBean 的给定 FactoryBean,调用容器上的 getBean("myBean “) 将返回 FactoryBean 的产品,而调用 getBean(”&myBean ") 将返回 FactoryBean 实例本身。

基于注解的容器配置

对于配置 Spring,注释比 XML 更好吗?

基于注解的配置的引入提出了这种方法是否比 XML “更好”的问题。简短的回答是“视情况而定”最长的答案是每种方法都有其优点和缺点,通常,由开发人员决定哪种策略更适合他们。由于它们的定义方式,注解在其声明中提供了大量的上下文,从而导致更短、更简洁的配置。然而,XML 擅长于连接组件,而不接触它们的源代码或重新编译它们。一些开发人员喜欢让连接靠近源代码,而其他人则认为带注解的类不再是POJOs,此外,配置变得分散并且更难控制。

无论选择什么,Spring 都可以容纳两种风格,甚至可以将它们混合在一起。值得指出的是,通过它的 JavaConfig 选项,Spring 允许以一种非侵入性的方式使用注释,而不涉及目标组件源代码,并且,就工具而言,所有的配置风格都受 Spring Tools for Eclipse 的支持。

基于注解的配置提供了 XML 设置的替代方法,它依赖字节码元数据来连接组件,而不是尖括号声明。开发人员不使用 XML 来描述 bean依赖,而是通过使用相关类、方法或字段声明上的注解将配置移入组件类本身。如上述【容器扩展点】示例中所述:AutowiredAnnotationBeanPostProcessor,将 BeanPostProcessor 与注解结合使用是扩展 Spring IoC 容器的常见方式。例如,Spring 2.0 引入了用 @Required 注解强制依赖必需属性的可能性。Spring 2.5 使得遵循同样的通用方法来驱动 Spring 的依赖注入成为可能。本质上, @Autowired 注解提供了与 【自动装配协作者】中描述的相同的功能,但是具有更细粒度的控制和更广泛的适用性。Spring 2.5 还增加了对 JSR-250 注解的支持,比如 @PostConstruct 和 @PreDestroy。Spring 3.0 增加了对 javax.inject 包中包含的 JSR-330(Java 依赖注入)注解的支持,比如 @Inject 和 @Named。关于这些注解的详细信息可以在后续相关章节【使用JSR 330标准注解】中找到。

注解注入在 XML 注入之前执行。因此,XML 配置覆盖了通过这两种方法连接的属性的注解。

与往常一样,您可以将 post-processors(后处理器)注册为单独的 bean definitions(定义),但是也可以通过在基于 XML 的 Spring 配置中包含以下标记来隐式注册它们(注意包含了 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/><!-- 开启 Spring 注解支持-->

</beans>

<context:annotation-config/> 元素隐式注册以下 post-processors(后处理器):

<context:annotation-config/> 只在定义它的同一个应用程序上下文中查找 beans 上的注解。这意味着,如果您将 <context:annotation-config/> 放入 DispatcherServlet 的 WebApplicationContext 中,它只会检查控制器中的 @Autowired beans,而不会检查服务。有关更多信息,请参见后续Web篇 DispatcherServlet。

@Required

@Required 注解适用于 bean 属性 setter 方法,如下例所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Required	//必须注入 MovieFinder 实例
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

该注解表明,受影响的 bean 属性必须在配置时通过 bean definition (定义)中的显式属性值或通过自动连接来填充。如果受影响的 bean 属性尚未填充,容器将引发异常。这考虑到了急切和显式的失败,避免了以后的 NullPointerException 实例或类似情况。我们仍然建议您将断言放入 bean 类本身(例如,放入 init 方法)。这样做可以强制那些必需的引用和值,即使您在容器外使用该类。

RequiredAnnotationBeanPostProcessor 必须注册为 bean,以启用对 @Required 注解的支持。

从 Spring Framework 5.1 开始,@Required 注解和 RequiredAnnotationBeanPostProcessor 正式被弃用,取而代之的是对所需设置使用构造函数注入(或者 InitializingBean.afterPropertiesSet() 的自定义实现,或者自定义 @PostConstruct 方法以及bean 属性 setter 方法)。

使用 @Autowired

在本节包含的示例中,JSR 330 的 @Inject 注解可以用来代替 Spring 的 @Autowired 注解。更多详情请见后续【JSR 330】。

您可以将 @Autowired 注解应用于构造函数,如下例所示:

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired	// 初始化时,自动注入属性依赖的 CustomerPreferenceDao bean
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

从 Spring Framework 4.3 开始,如果目标 bean 只定义了一个构造函数,那么在这样的构造函数上不再需要 @Autowired 注解。但是,如果有几个可用的构造函数,并且没有主/默认构造函数,那么至少有一个构造函数必须用 @Autowired 进行注解,以便指示容器使用哪一个。有关详细信息,请参见后续关于构造函数解析的讨论。

您还可以将 @Autowired 注解应用于传统的 setter 方法,如下例所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

还可以将注解应用于具有任意名称和多个参数的方法,如下例所示:

public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

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

    // ...
}

您也可以将 @Autowired 应用于字段,甚至将其与构造函数混合使用,如下例所示:

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

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

    // ...
}

确保您的目标组件(例如,MovieCatalog 或 CustomerPreferenceDao )由您用于 @Autowired 注解的注入点的类型一致地声明。否则,注入可能会由于运行时的 “no type match found(未找到匹配类型)” 错误而失败。

对于通过 classpath(类路径)扫描找到的 XML 定义的 beans 或组件类,容器通常预先知道具体的类型。然而,对于 @Bean factory 方法,您需要确保声明的返回类型具有足够的表达能力。对于实现几个接口的组件,或者对于可能被它们的实现类型引用的组件,考虑在您的工厂方法上声明最具体的返回类型(至少像引用您的 bean 的注入点所要求的那样具体)。

您还可以指示 Spring 从 ApplicationContext 提供特定类型的所有 beans,方法是将 @Autowired 注解添加到需要该类型数组的字段或方法中,如下例所示:

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}

这同样适用于 typed (类型化)collections(集合),如下例所示:

public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;

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

    // ...
}

您的目标 beans 可以实现 org.springframework.core.Ordered 接口,或者使用 @Order 或标准的 @Priority 注解,如果您希望数组或列表中的项目按特定顺序排序的话。否则,它们的顺序遵循容器中相应目标 bean definitions(定义)的注册顺序。

您可以在目标类级别和 @bean 方法上声明 @Order 注解,可能是针对单个 Bean definitions (定义) (在多个 definitions (定义) 使用同一个 Bean 类的情况下)。@Order 值可能会影响注入点的优先级,但是要注意它们不会影响单例启动顺序,这是由依赖关系和 @DependsOn 声明决定的正交关系。

注意,标准的 javax.annotation.Priority 注解在 @Bean 级别不可用,因为它不能在方法上声明。对于每种类型,可以在单个bean 上通过 @Order 值结合 @Primary 对其语义进行建模。

即使是类型化的 Map 实例也可以自动连接,只要预期的键类型是 String。映射值包含预期类型的所有 bean,键包含相应的 bean 名称,如下例所示:

public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

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

    // ...
}

默认情况下,当给定的注入点没有匹配的候选 beans 时,自动连接会失败。对于声明的数组、集合或映射,至少需要一个匹配元素。

默认行为是将带注解的方法和字段视为指示所需的依赖关系。您可以更改此行为,如下例所示,通过将不可满足的注入点标记为非必需的(例如,通过将 @Autowired 中的 required 属性设置为 false),使框架能够跳过不可满足的注入点:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

如果一个非必需的方法的依赖项(或者在有多个参数的情况下,它的一个依赖项)不可用,那么它根本不会被调用。在这种情况下,非必填字段根本不会被填充,而是保留其默认值。

注入的构造函数和工厂方法参数是一个特例,因为由于 Spring 的构造函数解析算法可能会处理多个构造函数,所以 @Autowired 中的required 属性具有稍微不同的含义。默认情况下,构造函数和工厂方法参数实际上是必需的,但是在单构造函数场景中有一些特殊的规则,例如,如果没有匹配的 beans 可用,多元素注入点(数组、集合、maps)将解析为空实例。这允许一个通用的实现模式,其中所有的依赖项都可以在一个唯一的多参数构造函数中声明——例如,声明为一个不带 @Autowired 注解的公共构造函数。

在任何给定的 bean 类中,只有一个构造函数可以声明 @Autowired,并将 required 属性设置为 true,这表明当用作 Spring bean 时,构造函数将自动装配。因此,如果 required 属性保留默认值 true,那么只有一个构造函数可以用 @Autowired 进行注释。如果多个构造函数声明了注解,它们都必须声明 required = false,才能被视为自动连接的候选对象(类似于 XML 中的 autowire = constructor)。通过匹配 Spring 容器中的 beans,将选择具有最大数量的依赖项的构造函数。如果没有一个候选能够被满足,那么将使用主/默认构造器(如果存在)。类似地,如果一个类声明了多个构造函数,但是没有一个用 @Autowired 注解,那么将使用一个主/默认构造函数(如果存在的话)。如果一个类一开始只声明了一个构造函数,即使没有注解,它也会一直被使用。请注意,带注解的构造函数不一定是公共的。

建议在 setter 方法上使用 @Autowired 的 required 属性,而不推荐使用 @Required 注解。将 required 属性设置为 false 表示该属性不是自动注入所必需的,如果该属性不能自动注入,则会被忽略。另一方面,@Required 更强大,因为它强制通过容器支持的任何方式来设置属性,如果没有定义值,则会引发相应的异常。

或者,您可以通过 Java 8 的 java.util.Optional 来表达特定依赖项的非必需性质,如下例所示:

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        ...
    }
}

从 Spring Framework 5.0 开始,您还可以使用 @Nullable 注解(任何包中的任何类型——例如,来自 JSR-305 的javax.annotation.Nullable ),或者只利用 Kotlin 内置的空安全支持:

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        ...
    }
}

还可以将 @Autowired 用于众所周知的可解析依赖关系的接口:BeanFactory、ApplicationContext、Environment、ResourceLoader、ApplicationEventPublisher 和 MessageSource。这些接口及其扩展接口(如 ConfigurableApplicationContext 或 ResourcePatternResolver )是自动解析的,无需特殊设置。以下示例自动关联 ApplicationContext 对象:

public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...
}

@Autowired、@Inject、@Value 和 @Resource 注解由 Spring 的 BeanPostProcessor 实现处理。这意味着您不能在自己的BeanPostProcessor 或 BeanFactoryPostProcessor 类型(如果有)中应用这些注解。这些类型必须通过使用 XML 或 Spring 的 @Bean 方法显式“注入”。

使用 @Primary 微调基于注解的自动连接

因为按类型自动连接可能会导致多个候选项,所以通常有必要对选择过程进行更多的控制。实现这一点的一种方法是使用 Spring 的 @Primary 注释。@Primary 表示当多个 bean 是自动连接到单值依赖项的候选项时,应该优先考虑特定的 bean。如果候选项中恰好存在一个主 bean,它将成为自动连接的值。

考虑将 firstMovieCatalog 定义为主 MovieCatalog 的以下配置:

@Configuration
public class MovieConfiguration {

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

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

    // ...
}

在前面的配置中,以下 MovieRecommender 与 firstMovieCatalog 自动关联:

public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...
}

相应的 bean definitions (定义) 如下:

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog" primary="true">
        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

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

</beans>
使用 Qualifiers(限定符)微调基于注解的自动连接

当可以确定一个主要候选对象时,@Primary 是一种使用带有多个实例的按类型自动关联的有效方法。当您需要对选择过程进行更多控制时,可以使用 Spring 的 @Qualifier 注解。您可以将限定符值与特定的参数相关联,缩小类型匹配的范围,以便为每个参数选择特定的 bean。在最简单的情况下,这可以是一个简单的描述性值,如下例所示:

public class MovieRecommender {

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

    // ...
}

您也可以在个别建构函式引数或方法参数上指定 @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;
    }

    // ...
}

以下示例显示了相应的 bean definitions(定义)。

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <!-- 具有 main(主) qualifier (限定符)值的 bean 与构造函数参数关联,该参数用相同的值限定。-->
        <qualifier value="main"/> 

        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="action"/> <!-- 指定此 bean 的限定符或标识符 -->

        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

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

</beans>

对于回退匹配,bean 名称被视为默认限定符值。因此,您可以定义 id 为 main 的 bean,而不是嵌套的 qualifier 元素,从而得到相同的匹配结果。然而,尽管您可以使用这个约定通过名称来引用特定的 beans,但是 @Autowired 基本上是关于带有可选语义限定符的类型驱动注入。这意味着限定符值,即使有 bean 名称回退,也总是在类型匹配集中具有缩小语义。它们没有在语义上表达对唯一 bean id 的引用。好的限定符值是 main、EMEA 或 persistent,表示独立于 bean id 的特定组件的特征,在匿名 bean definition (定义) 的情况下,可以自动生成 bean id,例如前面的示例。

如前所述,限定符也适用于类型化集合,例如,Set<MovieCatalog>。在这种情况下,根据声明的限定符,所有匹配的 beans 都作为集合注入。这意味着限定符不必是唯一的。相反,它们构成了过滤标准。例如,可以用相同的限定符值 “action” 定义多个 MovieCatalog beans,所有这些 bean 都被注入到用 @Qualifier(“action”) 注解的 Set<MovieCatalog> 中。

在类型匹配的候选对象中,让限定符值根据目标 bean 名称进行选择,在注入点不需要 @Qualifier 注解。如果没有其他解析指示符(比如限定符或主标记),对于非惟一依赖情况,Spring 会将注入点名称(即字段名称或参数名称)与目标 bean 名称进行匹配,并选择同名的候选项(如果有的话)。

也就是说,如果您打算通过名称来表达注解驱动的注入,不要主要使用 @Autowired,即使它能够通过 bean 名称在类型匹配的候选项中进行选择。相反,使用 JSR-250 @Resource 注解,该注解在语义上被定义为通过唯一的名称来标识特定的目标组件,声明的类型与匹配过程无关。@Autowired 具有相当不同的语义:在按类型选择候选 bean 之后,指定的字符串限定符值仅在那些类型选择的候选 bean 中被考虑(例如,将帐户限定符与标有相同限定符标签的 bean 进行匹配)。

对于本身被定义为集合、Map 或数组类型的 bean 来说,@Resource 是一个很好的解决方案,它通过惟一的名称引用特定的集合或数组 bean。也就是说,从 4.3 开始,您也可以通过 Spring 的 @Autowired 类型匹配算法来匹配集合、Map 和数组类型,只要元素类型信息保存在 @Bean 返回类型签名或集合继承层次结构中。在这种情况下,您可以使用限定符值在相同类型的集合中进行选择,如前一段所述。

从 4.3 开始,@Autowired 也考虑自我引用进行注入(即引用回当前注入的 bean)。请注意,自我注入是一种退路。对其他组件的常规依赖总是优先。从这个意义上来说,自我推荐不参与常规的候选人选拔,因此特别是从来不是首要的。相反,它们总是以最低优先级结束。在实践中,您应该只在万不得已时才使用自我引用(例如,通过 bean 的 transactional proxy(事务代理)调用同一实例上的其他方法)。在这种情况下,考虑将受影响的方法分解到一个单独的委托 bean 中。或者,您可以使用 @Resource,它可以通过当前 bean 的惟一名称获得一个代理。

试图在同一个配置类上注入来自 @Bean 方法的结果实际上也是一种自我引用场景。要么在方法签名中真正需要的地方延迟解析这种引用(与配置类中的自动连接字段相反),要么将受影响的 @Bean 方法声明为静态的,将它们与包含的配置类实例及其生命周期分离。否则,此类 bean 仅在回退阶段被考虑,而其他配置类上的匹配 bean 被选为主要候选项(如果可用)。

@Autowired 适用于字段、构造函数和多参数方法,允许在参数级别通过限定符批注缩小范围。相比之下,@Resource 仅支持具有单个参数的字段和 bean 属性 setter 方法。因此,如果您的注入目标是构造函数或多参数方法,您应该坚持使用 qualifiers (限定符)。

您可以创建自己的自定义 qualifier (限定符)注解。为此,请定义一个注解,并在您的定义中提供 @Qualifier 注解,如下例所示:

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

    String value();
}

然后,您可以在自动连接的字段和参数上提供自定义 qualifier (限定符),如下例所示:

public class MovieRecommender {

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

    private MovieCatalog comedyCatalog;

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

    // ...
}

接下来,您可以为候选 bean definitions(定义) 提供信息。您可以添加 <qualifier/> 标记作为 <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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="Genre" value="Action"/>
        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="example.Genre" value="Comedy"/>
        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

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

</beans>

在【类路径扫描和托管组件】中,您可以看到一种基于注解的替代方法,可以用 XML 提供限定符元数据。具体来说,请参见【提供带注解的限定符元数据】。

在某些情况下,使用不带值的注解可能就足够了。当注解服务于更一般的目的,并且可以跨几种不同类型的依赖项应用时,这可能是有用的。例如,您可以提供一个脱机目录,以便在没有互联网连接时进行搜索。首先,定义简单的注解,如下例所示:

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

}

然后将注解添加到要自动连接的字段或属性,如下例所示:

public class MovieRecommender {

    @Autowired
    @Offline 	// 这一行添加了 @Offline 注释。
    private MovieCatalog offlineCatalog;

    // ...
}

现在,bean definition (定义) 只需要一个限定符类型,如下例所示:

<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline"/> <!-- 这个元素指定了限定符。 -->
    <!-- 注入此bean所需的任何依赖项 -->
</bean>

您还可以定义自定义限定符注解,这些注解除了接受简单值属性之外,还接受命名属性。如果在要自动注入的字段或参数上指定了多个属性值,则 bean definition (定义) 必须匹配所有这样的属性值,才能被视为自动注入候选项。例如,考虑以下注释定义:

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

    String genre();

    Format format();
}

在这种情况下, Format(格式) 是一个 enum(枚举),定义如下:

public enum Format {
    VHS, DVD, BLURAY
}

要自动注入的字段用自定义限定符进行了注解,并且包括两个属性的值:genre(流派)和 format(格式),如下例所示:

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;

    // ...
}

最后,bean definitions (定义) 应该包含匹配的限定符值。这个示例还演示了可以使用 bean 元属性来代替 <qualifier/> 元素。如果可用,则 <qualifier/> 元素及其属性优先,但是如果不存在这样的限定符,则自动注入机制会退回到 <meta/> 标记中提供的值,如下例中的最后两个 bean definitions (定义)所示:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://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>
        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Comedy"/>
        </qualifier>
        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="DVD"/>
        <meta key="genre" value="Action"/>
        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="BLURAY"/>
        <meta key="genre" value="Comedy"/>
        <!-- 注入此bean所需的任何依赖项 -->
    </bean>

</beans>
使用泛型作为自动注入限定符

除了 @Qualifier 注解,您还可以使用 Java 泛型类型作为限定的隐式形式。例如,假设您有以下配置:

@Configuration
public class MyConfiguration {

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

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

假设前面的 beans 实现了一个泛型接口(即 Store<String>Store<Integer>),您可以 @Autowire 注入 Store接口,泛型被用作限定符,如下例所示:

@Autowired
private Store<String> s1; // <String> qualifier(限定符),注入stringStore bean

@Autowired
private Store<Integer> s2; // <Integer> qualifier(限定符),注入integerStore bean

通用限定符也适用于自动注入列表、Map 映射实例和数组。以下示例自动关联通用列表:

// 注入所有存储 beans,只要它们具有 <Integer> 泛型
// 存储 <String> beans 不会出现在此列表中
@Autowired
private List<Store<Integer>> s;
使用 CustomAutowireConfigurer

CustomAutowireConfigurer是一个 BeanFactoryPostProcessor,它允许您注册自己的自定义限定符注解类型,即使它们没有用 Spring 的@Qualifier 注解进行注解。以下示例显示了如何使用 CustomAutowireConfigurer:

<bean id="customAutowireConfigurer"
        class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
    <property name="customQualifierTypes">
        <set>
            <value>example.CustomQualifier</value>
        </set>
    </property>
</bean>

AutowireCandidateResolver 通过以下方式确定候选自动装配:

  • 每个bean definition(定义)的 autowire-candidate(自动装配候选)值
  • <beans/> 元素上可用的任何 default-autowire-candidates(默认自动连线候选)模式
  • @Qualifier 注解和任何向 CustomAutowireConfigurer 注册的自定义注解的存在

当多个 bean 有资格作为 autowire 候选项时,“主要”的确定如下:如果候选项中恰好有一个 bean definition (定义)的主要属性设置为 true,则选择它。

用 @Resource 注入

Spring 还通过在字段或 bean 属性 setter 方法上使用 JSR-250 @Resource 注解(javax.annotation.Resource)来支持注入。这是 Java EE 中的常见模式:例如,在 JSF 管理的 beans 和 JAX-WS 端点中。对于 Spring 管理的对象,Spring 也支持这种模式。

@Resource 采用名称属性。默认情况下,Spring 将该值解释为要注入的 bean 名称。换句话说,它遵循按名称语义,如以下示例所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") 	// 这一行注入了一个@Resource。
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

如果没有显式指定名称,则默认名称是从字段名或 setter 方法派生的。对于字段,它采用字段名。对于 setter 方法,它采用 bean 属性名。以下示例将名为 movieFinder 的 bean 注入到其 setter 方法中:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

CommonAnnotationBeanPostProcessor 察觉的 ApplicationContext 将注解提供的名称解析为 bean 名称。如果您显式配置 Spring 的SimpleJndiBeanFactory ,则可以通过 JNDI 解析名称。但是,我们建议您依赖默认行为,并使用 Spring 的 JNDI 查找功能来保持间接级别。

在没有指定显式名称的 @Resource 用法的特殊情况下,与 @Autowired 类似,@Resource 找到一个主类型匹配,而不是一个特定的命名 bean,并解析众所周知的可解析依赖项:BeanFactory、ApplicationContext、ResourceLoader、ApplicationEventPublisher 和MessageSource 接口。

因此,在下面的示例中,CustomerPreferenceDao 字段首先查找名为 “customerPreferenceDao” 的 bean,然后返回到类型customerPreferenceDao 的主类型匹配:

public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; 
    // 基于已知的可解析依赖类型注入context(上下文)字段: ApplicationContext(应用程序上下文)

    public MovieRecommender() {
    }

    // ...
}
使用 @Value

@Value 通常用于注入外部化的属性:

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name}") String catalog) {
        this.catalog = catalog;
    }
}

具有以下配置:

@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }

和下面的 application.properties 文件:

catalog.name=MovieCatalog

在这种情况下,catalog 参数和字段将等于 MovieCatalog 值。

Spring 提供了一个默认的宽松嵌入式值解析器。它将尝试解析属性值,如果无法解析,属性名称(例如 ${catalog.name})将作为值注入。如果希望对不存在的值保持严格的控制,应该声明一个 PropertySourcesPlaceholderConfigurer bean,如下例所示:

@Configuration
public class AppConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

当使用 JavaConfig 配置 PropertySourcesPlaceholderConfigurer 时,@Bean 方法必须是静态的。

如果任何 ${} 占位符无法解析,使用上述配置可以确保 Spring 初始化失败。还可以使用 setPlaceholderPrefix、setPlaceholderSuffix 或setValueSeparator 等方法来自定义占位符。

默认情况下,Spring Boot 配置一个 PropertySourcesPlaceholderConfigurer bean,该 bean 将从 application.properties 和application.yml 文件中获取属性。

Spring 提供的内置转换器支持允许自动处理简单的类型转换(例如转换为 Integer 或 int)。多个逗号分隔的值可以自动转换为 String(字符串) 数组,无需额外的努力。

可以提供如下默认值:

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
        this.catalog = catalog;
    }
}

Spring BeanPostProcessor 在后台使用 ConversionService 来处理将 @Value 中的字符串值转换为目标类型的过程。如果希望为自己的自定义类型提供转换支持,可以提供自己的 ConversionService bean实例,如下例所示:

@Configuration
public class AppConfig {

    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        conversionService.addConverter(new MyCustomConverter());
        return conversionService;
    }
}

当 @Value 包含 【SpEL表达式】时,将在运行时动态计算该值,如下例所示:

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
        this.catalog = catalog;
    }
}

SpEL 还支持使用更复杂的数据结构:

@Component
public class MovieRecommender {

    private final Map<String, Integer> countOfMoviesPerCatalog;

    public MovieRecommender(
            @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
        this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
    }
}
使用 @PostConstruct 和 @PreDestroy

CommonAnnotationBeanPostProcessor 不仅识别 @Resource 注解,还识别 JSR-250 生命周期注解:javax.annotation.PostConstructjavax.annotation.PreDestroy。在 Spring 2.5 中引入的对这些注解的支持为参考前面文章【初始化回调】和【销毁回调】中描述的生命周期回调机制提供了一种替代方法。如果在 Spring ApplicationContext 中注册了 CommonAnnotationBeanPostProcessor,则在生命周期中的同一点上调用带有这些注解之一的方法,作为相应的 Spring 生命周期接口方法或显式声明的回调方法。在以下示例中,缓存在初始化时预填充,并在销毁时清除:

public class CachingMovieLister {

    @PostConstruct
    public void populateMovieCache() {
        // 初始化时填充电影缓存...
    }

    @PreDestroy
    public void clearMovieCache() {
        // 销毁时清除电影缓存...
    }
}

有关组合各种生命周期机制的效果的详细信息,请参见【自定义bean的性质-组合生命周期机制】。

像 @Resource 一样,@PostConstruct 和 @PreDestroy 注解类型是 JDK 6 到 8 的标准 Java 库的一部分。然而,在 JDK 9 中,整个 javax.annotation 包从核心 Java 模块中分离出来,并最终在 JDK 11 中被删除。如果需要,javax.annotation-api 工件现在需要通过 Maven 仓库获得,只需像其他库一样添加到应用程序的类路径中。

类路径扫描和托管组件

Classpath (类路径) 扫描和托管组件

本章中的大多数例子使用 XML 来指定在 Spring 容器中生成每个 BeanDefinition 的配置元数据。上一节(【基于注释的容器配置】)演示了如何通过源代码级注解提供大量配置元数据。然而,即使在这些例子中,“基本” bean definitions (定义)也是在 XML 文件中显式定义的,而注解只驱动依赖注入。本节描述了一个通过扫描类路径隐式检测候选组件的选项。候选组件是与过滤标准相匹配的类,并且具有向容器注册的相应 bean definitions (定义)。这消除了使用 XML 来执行 bean 注册的需要。相反,您可以使用注解(例如,@Component )、AspectJ 类型表达式或者您自己的定制过滤标准来选择哪些类具有向容器注册的 bean definitions(定义)。

从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多特性都是核心 Spring 框架的一部分。这允许您使用 Java 定义 beans,而不是使用传统的 XML 文件。看看 @Configuration、@Bean、@Import 和 @DependsOn 注解中如何使用这些新特性的例子。

@Component 和更多原型注解

@Repository 注解是实现存储库角色或原型的任何类的标记(也称为数据访问对象或 DAO)。该标记的用途之一是异常的自动翻译,如后续文章【异常翻译中所述】。

Spring 提供了进一步的原型注解:@Component、@Service 和 @Controller。@Component 是任何 Spring 管理的组件的通用原型。@Repository、@Service 和 @Controller 是 @Component 的专门化,用于更具体的用例(分别在持久层、服务层和表示层)。因此,您可以用 @Component 来注解组件类,但是,通过用 @Repository、@Service 或 @Controller 来注解它们,您的类更适合由工具处理或与方面相关联。例如,这些原型注解是切入点的理想目标。@Repository、@Service 和 @Controller 也可以在 Spring 框架的未来版本中携带额外的语义。因此,如果您要在使用 @Component 还是 @Service 作为服务层之间做出选择,那么 @Service 显然是更好的选择。类似地,如前所述,已经支持 @Repository 作为持久层中自动异常转换的标记。

使用元注解和组合注解

Spring 提供的许多注解可以在您自己的代码中用作元注解。元注解是可以应用于另一个注解的注解。例如,前面提到的 @Service 注解是用 @Component 进行元注解的,如下例所示:

@Target(ElementType.TYPE)	// 指定注解可以修饰在类还是方法上
@Retention(RetentionPolicy.RUNTIME)	// 指定注解的编译类型
@Documented	// 指定注解是否支持文档生成
@Component 
public @interface Service {

    // ...
}

@Component 使得 @Service 被以与 @Component 相同的方式对待。

您还可以组合元注解来创建 “组合注解”。比如 Spring MVC 的 @RestController 注解就是由 @Controller 和 @ResponseBody 组成的。

此外,组合注解可以有选择地从元注解中重新声明属性,以允许定制。当您只想公开元注解属性的子集时,这可能特别有用。例如,Spring 的 @SessionScope 注解将作用域名硬编码为 session,但仍然允许定制 proxyMode。以下清单显示了 SessionScope 注解的定义:

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

    /**
     * {@link Scope#proxyMode}的别名.
     * <p>默认为 {@link ScopedProxyMode#TARGET_CLASS}.
     */
    @AliasFor(annotation = Scope.class)
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

然后,您可以使用 @SessionScope 而无需声明 proxyMode,如下所示:

@Service
@SessionScope
public class SessionScopedService {
    // ...
}

您还可以覆盖 proxyMode 的值,如下例所示:

@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
    // ...
}

有关更多细节,请参见 Spring 注解编程模型 wiki 页面。

自动检测类和注册 Bean Definitions(定义)

Spring 可以自动检测构造型类,并向 ApplicationContext 注册相应的 BeanDefinition 实例。例如,以下两个类别适用于这种自动检测:

@Service
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}
@Repository
public class JpaMovieFinder implements MovieFinder {
    // 为清晰起见,省略了实现
}

要自动检测这些类并注册相应的 beans,需要将 @ComponentScan 添加到 @Configuration 类中,其中 basePackages 属性是这两个类的公共父包。(或者,您可以指定一个逗号、分号或空格分隔的列表,其中包含每个类的父包。)

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

为简洁起见,前面的示例可以使用注解的 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

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

</beans>

使用 <context:component-scan> 隐式启用 <context:annotation-config> 的功能。使用 <context:component-scan> 时,通常不需要包含 <context:annotation-config> 元素。

扫描类路径包要求类路径中存在相应的目录条目。当您使用Ant构建JAR时,确保您没有激活JAR任务的仅文件开关。此外,根据某些环境中的安全策略,类路径目录可能不会公开,例如,JDK 1.7.0_45及更高版本中的独立应用程序(需要在清单中设置“可信库”,请参见此处说明

在 JDK 9 的模块路径( Jigsaw )上,Spring 的类路径扫描通常按预期工作。但是,要确保组件类是在模块信息描述符中导出的。如果您希望 Spring 调用您的类的非公共成员,请确保它们是“开放的”(也就是说,它们在您的 module-info 描述符中使用 opens 声明而不是 exports 声明)。

此外,当您使用 component-scan 元素时,AutowiredAnnotationBeanPostProcessor 和 commannotationbeanpostprocessor 都是隐式包含的。这意味着这两个组件是自动检测并连接在一起的——所有这些都不需要 XML 中提供的任何 bean 配置元数据。

通过包含值为 false 的 annotation-config 属性,可以禁用 AutowiredAnnotationBeanPostProcessor 和 commannotationbeanpostprocessor 的注册。

使用过滤器自定义扫描

默认情况下,用 @Component、@Repository、@Service、@Controller、@Configuration 注解的类或者本身用 @Component 注解的自定义注解是唯一检测到的候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。将它们添加为 @ComponentScan 注解的includeFilters 或 excludeFilters 属性(或者作为 XM L配置中 <context:component-scan> 元素的 <context:include-filter/><context:exclude-filter/> 子元素)。每个筛选器元素都需要 type(类型)和 expression(表达式)属性。下表描述了过滤选项:

includeFilters : 添加过滤器 excludeFilters:排除过滤

过滤方式示例表达式说明
annotation (default):注解(默认)org.example.SomeAnnotation在目标组件的类型级别上存在或元存在的注解。
assignable:可分配的org.example.SomeClass目标组件可分配到的类(或接口)(扩展或实现)。
aspectjorg.example…*Service+由目标组件匹配的AspectJ类型表达式。
regex:正则表达式org.example.Default.*由目标组件的类名匹配的正则表达式。
custom:定制的org.example.MyTypeFilterorg.springframework.core.type.TypeFilter接口的自定义实现。

以下示例显示了忽略所有 @Repository 注解并使用“stub(存根)”存储库的配置:

@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    // ...
}

下面的清单显示了等效的 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>

还可以通过在注解上设置 useDefaultFilters = false 或提供 use-default-filters="false " 作为 <component-scan/> 元素的属性来禁用默认过滤器。这实际上禁用了对用 @Component、@Repository、@Service、@Controller、@RestController 或@Configuration 注解或元注解的类的自动检测。

在组件中定义 Bean 元数据

Spring 组件也可以向容器提供 bean definition (定义)元数据。您可以使用用于在 @Configuration 注解类中定义 Bean 元数据的同一个@Bean 注解来实现这一点。以下示例显示了如何实现这一点:

@Component
public class FactoryMethodComponent {

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

    public void doWork() {
        // 省略组件方法实现
    }
}

前面的类是一个 Spring 组件,在其 doWork() 方法中有特定于应用程序的代码。但是,它也提供了一个 bean definition(定义),该definition(定义) 有一个引用 publicInstance() 方法的工厂方法。@Bean 注解标识工厂方法和其他 Bean definition(定义) 属性,例如通过@Qualifier 注解的限定符值。其他可以指定的方法级注解有 @Scope、@Lazy 和自定义 qualifier (限定符)注解。

除了它在组件初始化中的作用,您还可以将 @Lazy 注解放在标有 @Autowired 或 @Inject 的注入点上。在这种情况下,它导致了延迟解析代理的注入。然而,这种代理方法是相当有限的。对于复杂的延迟交互,特别是与可选依赖项结合使用时,我们建议使用 ObjectProvider<MyTargetBean>

如前所述,支持自动连接的字段和方法,另外还支持 @Bean 方法的自动连接。以下示例显示了如何实现这一点:

@Component
public class FactoryMethodComponent {

    private static int i;

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

    // 使用自定义 qualifier(限定符)和方法参数的自动装配
    @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);
    }
}

该示例将字符串方法参数 country 自动绑定到另一个名为 privateInstance 的 bean 上的 age 属性值。Spring 表达式语言元素通过符号 #{ <表达式> } 定义属性的值。对于 @Value 注解,表达式解析器被预先配置为在解析表达式文本时查找 bean 名称。

从 Spring Framework 4.3 开始,您还可以声明 InjectionPoint 类型的工厂方法参数(或其更具体的子类:DependencyDescriptor)来访问触发当前 bean 创建的请求注入点。请注意,这仅适用于 bean 实例的实际创建,而不适用于现有实例的注入。因此,这个特性对于原型范围的 beans 最有意义。对于其他作用域,工厂方法只会看到在给定作用域中触发创建新 bean 实例的注入点(例如,触发创建延迟单例bean 的依赖项)。在这种情况下,您可以使用提供的注入点元数据,并注意语义。以下示例显示了如何使用 InjectionPoint:

@Component
public class FactoryMethodComponent {

    @Bean @Scope("prototype")
    public TestBean prototypeInstance(InjectionPoint injectionPoint) {
        return new TestBean("prototypeInstance for " + injectionPoint.getMember());
    }
}

常规 Spring 组件中的 @Bean 方法的处理方式不同于Spring @Configuration 类中的对应方法。不同之处在于,@Component 类没有用CGLIB 增强来拦截方法和字段的调用。CGLIB 代理是调用 @Configuration 类中的 @Bean 方法内的方法或字段来创建对协作对象的Bean 元数据引用的方法。这种方法不是用普通的 Java 语义调用的,而是通过容器来提供 Spring beans 通常的生命周期管理和代理,甚至在通过编程调用 @Bean 方法来引用其他 Bean 时也是如此。相比之下,在普通的 @Component 类中调用 @Bean 方法中的方法或字段具有标准的 Java 语义,没有特殊的 CGLIB 处理或其他约束。

您可以将 @Bean 方法声明为 static(静态) 的,这样就可以在不创建包含它们的配置类作为实例的情况下调用它们。这在定义后处理器 bean (例如,类型为 BeanFactoryPostProcessor 或 BeanPostProcessor)时特别有意义,因为这样的 bean 在容器生命周期的早期就被初始化,应该避免在那时触发配置的其他部分。

由于技术限制,对静态 @Bean 方法的调用永远不会被容器截获,即使在 @Configuration 类中也是如此(如本节前面所述):CGLIB 子类化只能覆盖非静态方法。因此,对另一个 @Bean 方法的直接调用具有标准的 Java 语义,导致直接从工厂方法本身返回一个独立的实例。

@Bean 方法的 Java 语言可见性对 Spring 容器中生成的 Bean definition (定义)没有直接影响。您可以在非 @Configuration 类中自由声明您认为合适的工厂方法,也可以在任何地方声明静态方法。然而,@Configuration 类中的常规 @Bean 方法需要是可重写的——也就是说,它们不能被声明为 private 或 final。

@Bean 方法也可以在给定组件或配置类的基类上发现,以及在由组件或配置类实现的接口中声明的 Java 8 默认方法上发现。这为组合复杂的配置安排提供了很大的灵活性,从 Spring 4.2 开始,甚至可以通过 Java 8 的默认方法实现多重继承。

最后,单个类可以为同一个 Bean 保存多个 @Bean 方法,作为多个工厂方法的安排,根据运行时可用的依赖关系来使用。这与在其他配置场景中选择“最贪婪”的构造函数或工厂方法的算法相同:在构造时选择具有最大数量可满足依赖关系的变量,类似于容器如何在多个 @Autowired 构造函数之间进行选择。

命名自动检测的组件

当在扫描过程中自动检测到一个组件时,它的 bean 名称由该扫描器已知的 BeanNameGenerator 策略生成。默认情况下,任何包含名称value(值) 的 Spring 原型注解(@Component、@Repository、@Service 和 @Controller)都会将该名称提供给相应的 bean definition(定义)。

如果这样的注解不包含名称 value(值),或者不包含任何其他检测到的组件(例如由自定义过滤器发现的组件),则默认bean名称生成器将返回未大写的非限定类名。例如,如果检测到以下组件类,其名称将为 myMovieLister 和 movieFinderImpl:

@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

如果不想依赖默认的 bean 命名策略,可以提供自定义的 bean 命名策略。首先,实现 BeanNameGenerator 接口,并确保包含默认的无参数构造函数。然后,在配置扫描器时提供完全限定的类名,如下面的示例注解和 bean definition(定义) 所示。

如果由于多个自动检测的组件具有相同的非限定类名(即,名称相同但位于不同包中的类)而遇到命名冲突,则可能需要配置一个BeanNameGenerator,该生成器默认为生成的 bean 名称的完全限定类名。从 Spring Framework 5.2.3 开始,位于包org.springframework.context.annotation中的 FullyQualifiedAnnotationBeanNameGenerator 可以用于此目的。

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

一般来说,只要其他组件可能会显式引用它,就应该考虑用注解指定名称。另一方面,只要容器负责连接,自动生成的名称就足够了。

为自动检测的组件提供 Scope(作用域)

与 Spring 管理的组件一样,自动检测的组件的默认和最常见的作用域是 singleton。但是,有时您需要一个不同的作用域,可以通过@Scope 注解来指定。您可以在注解中提供作用域的名称,如下例所示:

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

@Scope 注解只在具体的 bean 类(对于带注解的组件)或工厂方法(对于 @Bean 方法)上进行内省。与 XML bean definitions(定义)相比,没有 bean definitions(定义) 继承的概念,并且类级别的继承层次结构与元数据无关。

有关特定于 web 的作用域的详细信息,例如 Spring context(上下文中) 的 “request(请求)” 或 “session(会话)”,请参见前面文章的 【Request、Session、Application、和WebSocket 作用域】。与那些作用域的预构建注解一样,您也可以通过使用 Spring 的元注解方法来编写自己的作用域注解:例如,用 @Scope("prototype ") 进行元注解的自定义注解,还可能声明一个自定义作用域代理模式。

为了提供自定义的范围解析策略,而不是依赖基于注解的方法,可以实现 ScopeMetadataResolver 接口。确保包含默认的无参数构造函数。然后,您可以在配置扫描器时提供完全限定的类名,如以下注解和 bean definition(定义)的示例所示:

@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 作用域 - 具有作用域的 bean 作为依赖项】。为此,在 component-scan 元素上有一个作用域代理属性。三个可能的值是:no、interfaces 和 targetClass。例如,以下配置会产生标准的 JDK 动态代理:

@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
    // ...
}
<beans>
    <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>
提供带注释的 Qualifier Metadata (限定符元数据)

@Qualifier 注解在前面文章【基于注解的容器配置 - 使用限定符微调基于注释的自动连接】中讨论。该节中的示例演示了在解析自动装配候选对象时,如何使用 @Qualifier 注解和自定义限定符注释来提供细粒度的控制。因为这些示例基于 XML bean definitions(定义),所以通过使用 XML 中 bean 元素的限定符或 meta 子元素,在候选 bean definitions(定义)上提供了限定符元数据。当依靠类路径扫描来自动检测组件时,可以在候选类上提供带有类型级注解的 qualifier (限定符)元数据。以下三个示例演示了这种技术:

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}

与大多数基于注解的替代方法一样,请记住注解元数据绑定到 class definition(类定义)本身,而 XML 的使用允许相同类型的多个 beans 在其限定符元数据中提供变化,因为元数据是按实例而不是按类提供的。

生成候选组件的索引

每一个带可以扫描的类,都可以称为组件

虽然类路径扫描非常快,但是通过在编译时创建一个静态候选列表,可以提高大型应用程序的启动性能。在这种模式下,作为组件扫描目标的所有模块都必须使用这种机制。

您现有的 @ComponentScan 或 <context:component-scan/> 指令必须保持不变,以请求上下文扫描某些包中的候选包。当ApplicationContext 检测到这样的索引时,它会自动使用它,而不是扫描类路径。

要生成索引,请向包含组件扫描指令目标组件的每个模块添加一个附加依赖项。以下示例显示了如何使用 Maven 实现这一点:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.22.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

在 Gradle 4.5 和更低版本中,依赖项应在 compileOnly 配置中声明,如下例所示:

dependencies {
    compileOnly "org.springframework:spring-context-indexer:5.2.22.RELEASE"
}

对于 Gradle 4.6 和更高版本,应该在 annotationProcessor 配置中声明依赖关系,如下例所示:

dependencies {
    annotationProcessor "org.springframework:spring-context-indexer:5.2.22.RELEASE"
}

spring-context-indexer 工件生成一个包含在 jar 文件中的 META-INF/spring.components 文件。

在 IDE 中使用这种模式时,spring-context-indexer 必须注册为注解处理器,以确保在候选组件更新时索引是最新的。

当在类路径中找到 META-INF/spring.components 文件时,索引会自动启用。如果某个索引对于某些库(或用例)是部分可用的,但不能为整个应用程序构建,那么可以通过将 spring.index.ignore 设置为 true (作为 JVM 系统属性或通过 SpringProperties 机制)来退回到常规的类路径安排(就像根本没有索引一样)。

使用 JSR 330 标准注解

从 Spring 3.0 开始,Spring 提供了对 JSR-330 标准注解(依赖注入)的支持。这些注解的扫描方式与 Spring 注解相同。要使用它们,您需要在您的类路径中有相关的 jar。

如果您使用 Maven,javax.inject 工件可以在标准的 Maven 存储库中找到( https://repo1.maven.org/maven2/javax/inject/javax.inject/1/),您可以将以下依赖项添加到 pom.xml 文件中:

<dependency>
 <groupId>javax.inject</groupId>
 <artifactId>javax.inject</artifactId>
 <version>1</version>
</dependency>
带有 @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(...);
        // ...
    }
}

与 @Autowired 一样,您可以在字段级、方法级和构造函数参数级使用 @Inject。此外,您可以将您的注入点声明为 Provider(提供者),允许通过 Provider.get() 调用按需访问较短范围的 bean 或延迟访问其他 bean。以下示例提供了前一示例的变体:

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

public class SimpleMovieLister {

    private Provider<MovieFinder> movieFinder;

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

    public void listMovies() {
        this.movieFinder.get().findMovies(...);
        // ...
    }
}

如果您希望对应该注入的依赖项使用限定名,您应该使用 @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;
    }

    // ...
}

与 @Autowired 一样,@Inject 也可以与 java.util.Optional 或 @Nullable 一起使用。这在这里更加适用,因为 @Inject 没有必需的属性。下面的一对示例显示了如何使用 @Inject 和 @Nullable:

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        // ...
    }
}
public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        // ...
    }
}
@Named 和 @ManagedBean:相当于 @Component 注解的标准

可以使用 @javax.inject.Named 或 javax.annotation.ManagedBean 代替 @Component,如下例所示:

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

@Named("movieListener")  // 也可以使用 @ManagedBean("movieListener") 
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

使用 @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;
    }

    // ...
}

当使用 @Named 或 @ManagedBean 时,可以按照与使用 Spring 注解完全相同的方式使用组件扫描,如下例所示:

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

与 @Component 不同,JSR-330 @Named 和 JSR-250 ManagedBean 注解是不可组合的。您应该使用 Spring 的原型模型来构建定制组件注解。

JSR-330 标准注解的局限性

使用标准注解时,您应该知道一些重要的功能是不可用的,如下表所示:

Springjavax.inject.*javax.inject 限制/评论
@Autowired@Inject@Inject 没有 “required” 属性。可以用 Java 8 的 Optional(可选)代替。
@Component@Named / @ManagedBeanJSR-330 没有提供一个可组合的模型,只提供了一种识别命名组件的方法。
@Scope(“singleton”)@SingletonJSR-330 的默认范围就像 Spring 的 prototype(原型)。然而,为了与 Spring 的通用默认设置保持一致,Spring 容器中声明的 JSR-330 bean 在默认情况下是 singleton(单例)的。为了使用 singleton 以外的作用域,您应该使用 Spring 的 @Scope 注解。javax.inject还提供了一个 @Scope 注解。然而,这个只是用来创建你自己的注解。
@Qualifier@Qualifier / @Namedjavax.inject.Qualifier 只是构建定制限定符的元注解。具体的字符串限定符(比如 Spring 的带有值的 @Qualifier)可以通过 javax.inject.Named 进行关联。
@Value-没有对等物
@Required-没有对等物
@Lazy-没有对等物
ObjectFactoryProviderjavax.inject.Provider 是 Spring 的 ObjectFactory 的直接替代,只是 get() 方法名更短。它也可以与 Spring 的 @Autowired 或无注解的构造函数和 setter 方法结合使用。
基于 Java 的容器配置

本节介绍如何在 Java 代码中使用注解来配置 Spring 容器。它包括以下主题:

  • 基本概念:@Bean 和 @Configuration
  • 使用 AnnotationConfigApplicationContext 实例化 Spring 容器
  • 使用 @Bean 注解
  • 使用 @Configuration 注解
  • 构建基于 Java 的配置
  • Bean Definition(定义)配置文件
  • PropertySource (属性源)抽象
  • 使用 @PropertySource
  • 语句中的占位符解析
基本概念:@Bean 和 @Configuration

Spring 新的 Java 配置支持中的核心构件是 @Configuration 注解的类和 @Bean 注解的方法。

@Bean 注解用于指示一个方法实例化、配置和初始化一个由 Spring IoC 容器管理的新对象。对于那些熟悉 Spring 的 <bean/> XML 配置的人来说,@Bean 注解扮演着与 <Bean/> 元素相同的角色。您可以对任何 Spring @Component(组件) 使用 @Bean 注解的方法。但是,它们通常与 @Configuration beans 一起使用。

用 @Configuration 注解一个类表明它的主要目的是作为 bean definitions(定义)的来源。此外,@Configuration 类允许通过调用同一个类中的其他 @bean 方法来定义 Bean 间的依赖关系。最简单的 @Configuration 类如下所示:

@Configuration
public class AppConfig {

    @Bean	// 相当于向 Spring 容器注入了一个 bean definitions
    public MyService myService() {
        return new MyServiceImpl();
    }
}

前面的 AppConfig 类等效于下面的 Spring <beans/> XML:

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

完整的 @Configuration vs “lite(精简)” @Bean 模式?

当在没有用 @Configuration 注解的类中声明 @Bean 方法时,它们被认为是在 “lite(精简)” 模式下处理的。在 @Component 中或者甚至在一个普通的旧类中声明的 Bean 方法被认为是 “lite(精简)”,包含类的主要目的不同,而 @Bean 方法是一种额外的好处。例如,服务组件可以通过每个适用组件类上的附加 @Bean 方法向容器公开管理视图。在这样的场景中,@Bean 方法是一种通用的工厂方法机制。

与full(完整) @Configuration 不同,lite(精简) @Bean 方法不能声明 Bean 间的依赖关系。相反,它们对包含它们的组件的内部状态进行操作,或者对它们可能声明的参数进行操作。因此,这样的 @Bean 方法不应该调用其他 @Bean 方法。每个这样的方法实际上只是一个特定 bean 引用的工厂方法,没有任何特殊的运行时语义。这里的积极副作用是,在运行时不必应用 CGLIB 子类,因此在类设计方面没有限制(也就是说,包含类可能是 final 类等等)。

在常见的场景中,@Bean 方法将在 @Configuration 类中声明,确保总是使用 “完整” 模式,因此跨方法引用被重定向到容器的生命周期管理。这可以防止通过常规的 Java 调用意外调用同一个 @Bean 方法,这有助于减少在 “lite” 模式下运行时难以跟踪的细微错误。

@Bean 和 @Configuration 注解将在下面的章节中深入讨论。然而,首先,我们将介绍使用基于 Java 的配置创建 spring 容器的各种方法。

使用 AnnotationConfigApplicationContext 实例化 Spring 容器

AnnotationConfigApplicationContext : 注解配置应用程序上下文

以下部分记录了 Spring 3.0 中引入的 Spring 的 AnnotationConfigApplicationContext。这个通用的 ApplicationContext 实现不仅能够接受@Configuration 类作为输入,还能够接受普通的 @Component 类和用 JSR-330 元数据注解的类。

当 @Configuration 类作为输入提供时,@Configuration 类本身被注册为 bean definition (定义),并且该类中所有声明的 @Bean 方法也被注册为 Bean definition (定义)。

当提供了 @Component 和 JSR-330 类时,它们被注册为 bean definitions(定义),并且假定在这些类中必要的地方使用了 DI 元数据,如 @Autowired 或 @Inject。

简单结构

与在实例化 ClassPathXmlApplicationContext 时使用 Spring XML 文件作为输入非常相似,您可以在实例化AnnotationConfigApplicationContext 时使用 @Configuration 类作为输入。这使得 Spring 容器的使用完全与 XML 无关,如下例所示:

public static void main(String[] args) {
    // 通过java配置进行初始化ioc容器
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

如前所述,AnnotationConfigApplicationContext 不仅限于使用 @Configuration 类。任何 @Component 或 JSR-330 注解类都可以作为输入提供给构造函数,如下例所示:

public static void main(String[] args) {
	// 使用@Component或JSR-330注解的类来初始化ioc容器
    ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, 
                                                                    Dependency1.class, Dependency2.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

前面的示例假设 MyServiceImpl、Dependency1 和 Dependency2 使用 Spring 依赖注入注解,如@Autowired。

使用 register(Class<?>…) 以编程方式构建容器

可以使用无参数构造函数实例化 AnnotationConfigApplicationContext,然后使用 register() 方法配置它。当以编程方式构建AnnotationConfigApplicationContext 时,这种方法特别有用。以下示例显示了如何实现这一点:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    // 通过register方法注册组件
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh();	//注册后需要刷新容器
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
使用 scan(String…) 启用组件扫描

要启用组件扫描,可以如下注解 @Configuration 类:

@Configuration
@ComponentScan(basePackages = "com.acme") //此注解启用组件扫描。
public class AppConfig  {
    // ...
}

有经验的 Spring 用户可能熟悉来自 Spring 的 context: 命名空间的 XML 声明等价物,如下例所示:

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

在前面的示例中,扫描 com.acme 包以查找任何 @Component 注解的类,这些类在容器中注册为 Spring bean definitions (定义)。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);
}

记住,@Configuration 类是用 @Component 进行元注解的,所以它们是组件扫描的候选对象。在前面的示例中,假设AppConfig 是在 com.acme 包(或其下的任何包)中声明的,则在调用 scan() 的过程中会选择它。在 refresh() 时,它的所有@Bean 方法都被处理并注册为容器中的 Bean definitions (定义)。

元注解参考前面文章【类路径扫描和托管组件 - 使用元注释和组合注释】

使用 AnnotationConfigWebApplicationContext 支持 Web 应用程序

AnnotationConfigApplicationContext 的 WebApplicationContext 变体可用于 AnnotationConfigWebApplicationContext。Spring ContextLoaderListener servlet listener(监听器), Spring MVC DispatcherServlet等时,可以使用这个实现。以下 web.xml 片段配置了一个典型的 Spring MVC web 应用程序(注意 contextClass context-param 和 init-param 的使用):

<web-app>
    <!-- 将 ContextLoaderListener 配置为使用 AnnotationConfigWebApplicationContext 
        而不是默认的 XmlWebApplicationContext -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <!-- 配置位置必须由一个或多个逗号或空格分隔的
        完全合格的 @Configuration 类。 完全合格的包也可以
        为组件扫描指定 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.AppConfig</param-value>
    </context-param>

    <!-- 使用 ContextLoaderListener 照常 Bootstrap(引导) root (根)application context(应用程序上下文) -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- 像往常一样声明一个 Spring MVC DispatcherServlet -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 将 DispatcherServlet 配置为使用 AnnotationConfigWebApplicationContext 
            而不是默认的 XmlWebApplicationContext -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!-- 同样,配置位置必须由一个或多个逗号或空格分隔的
            和完全合格的 @Configuration 类 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.acme.web.MvcConfig</param-value>
        </init-param>
    </servlet>

    <!-- 将 /app/* 的所有请求映射到 dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>
使用 @Bean 注解

@Bean 是一个方法级注解,是XML <bean/> 元素的直接模拟。该注解支持 <bean/> 提供的一些属性,例如:

  • init-method:初始化回调
  • destroy-method:销毁回调
  • autowiring:自动装配协作者
  • name:name 属性

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

声明一个Bean

要声明 bean,可以用 @Bean 注解来注解方法。您可以使用此方法在 ApplicationContext 中注册一个 bean definition (定义),该definition (定义)的类型被指定为该方法的返回值。默认情况下,bean 名称与方法名称相同。以下示例显示了 @Bean 方法声明:

@Configuration
public class AppConfig {

    @Bean	// bean类型为TransferServiceImpl,bean名称为transferService
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}

前面的配置完全等同于下面的 Spring XML:

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

这两个声明都使名为 transferService 的 bean 在 ApplicationContext 中可用,并绑定到类型为 TransferServiceImpl 的对象实例,如下图所示:

transferService -> com.acme.TransferServiceImpl

还可以用接口(或基类)返回类型声明 @Bean 方法,如下例所示:

@Configuration
public class AppConfig {

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

但是,这将高级类型预测的可见性限制为指定的接口类型 (TransferService)。然后,只有在受影响的单例 bean 被实例化后,容器才知道完整的类型 (TransferServiceImpl)。非延迟的单例 bean 根据它们的声明顺序进行实例化,因此您可能会看到不同的类型匹配结果,这取决于另一个组件何时尝试通过未声明的类型进行匹配(例如 @Autowired TransferServiceImpl,它只在 transferService bean 实例化后才解析)。

如果您始终通过声明的服务接口引用您的类型,那么您的 @Bean 返回类型可以安全地加入到设计决策中。但是,对于实现了几个接口的组件或者由它们的实现类型潜在引用的组件,尽可能声明最具体的返回类型会更安全(至少要像引用您的 bean 的注入点所要求的那样具体)。

Bean Dependencies(依赖项)

带有 @Bean 注解的方法可以有任意数量的参数,这些参数描述了构建 Bean 所需的依赖关系。例如,如果我们的 TransferService 需要一个 AccountRepository,我们可以用一个方法参数来实现这种依赖,如下例所示:

@Configuration
public class AppConfig {

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

解析机制与基于构造函数的依赖注入非常相似。更多详情参见前面【Dependencies 依赖 - 基于构造函数的依赖注入】相关章节。

接收生命周期回调

用 @Bean 注解定义的任何类都支持常规的生命周期回调,并且可以使用来自 JSR-250 的 @PostConstruct 和 @PreDestroy 注解。更多细节见 JSR-250 注解前面文章【基于注解的容器配置 - 使用 @PostConstruct 和 @PreDestroy】。

也完全支持常规的 Spring 生命周期回调。如果 bean 实现 InitializingBean、DisposableBean 或 Lifecycle,它们各自的方法由容器调用。

还完全支持标准的 *Aware 接口集(如 BeanFactoryAware、BeanNameAware、MessageSourceAware、ApplicationContextAware 等)。

BeanFactoryAware 在容器最后一章【BeanFactory】讲解

BeanNameAware 在【自定义 Bean 的性质 - ApplicationContextAware 和 BeanNameAware】讲解

MessageSourceAware 在【ApplicationContext 的附加功能 - 使用MessageSource的国际化】讲解

ApplicationContextAware 同 BeanNameAware

@Bean 注解支持指定任意的初始化和销毁回调方法,很像 Spring XML在 Bean 元素上的 init-method 和 destroy-method 属性,如下例所示:

public class BeanOne {

    public void init() {
        // 初始化逻辑
    }
}

public class BeanTwo {

    public void cleanup() {
        // 销毁逻辑
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public BeanOne beanOne() {
        return new BeanOne();
    }

    @Bean(destroyMethod = "cleanup")
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

默认情况下,用 Java 配置定义的具有公共 close 或 shutdown 方法的 beans 会自动使用销毁回调进行登记。如果您有一个公共的 close 或 shutdown 方法,并且您不希望在容器关闭时调用它,那么您可以将 @Bean(destroyMethod= " ") 添加到 Bean definition (定义)中,以禁用默认(推断)模式。

对于通过 JNDI 获得的资源,您可能希望在默认情况下这样做,因为它的生命周期是在应用程序之外管理的。特别是,要确保总是对 DataSource(数据源) 这样做,因为众所周知,这在 Java EE 应用服务器上是有问题的。

以下示例显示了如何防止数据源的自动销毁回调:

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

此外,对于 @Bean 方法,您通常使用编程式 JNDI 查找,或者通过使用 Spring 的 JndiTemplate 或 JndiLocatorDelegate 帮助器,或者直接使用 JNDI InitialContext,而不是 JndiObjectFactoryBean 变体(这将迫使您将返回类型声明为 FactoryBean 类型,而不是实际的目标类型,从而使它更难用于其他 @Bean 方法中旨在引用此处提供的资源的交叉引用调用)。

对于上述示例中的 BeanOne,在构造期间直接调用 init() 方法同样有效,如下例所示:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        BeanOne beanOne = new BeanOne();
        beanOne.init();
        return beanOne;
    }

    // ...
}

当您直接在 Java 中工作时,您可以对您的对象做任何您喜欢的事情,并且不总是需要依赖容器生命周期。

指定 Bean Scope (作用域)

Spring 包含 @Scope 注解,这样您就可以指定 bean 的范围。

使用 @Scope 注释

您可以指定用 @Bean 注解定义的 Bean 应该有一个特定的 Scope (范围)。您可以使用前面文章 【Bean Scopes(作用域)】 部分中指定的任何标准作用域。

默认范围是 singleton,但是您可以使用 @Scope 注解覆盖它,如下例所示:

@Configuration
public class MyConfiguration {

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

@Scope 和 scoped-proxy(作用域代理)

Spring 提供了一种通过作用域代理处理作用域依赖的便捷方式,参考前面文章【Bean Scopes(作用域)- 具有作用域的 bean 作为依赖项】。在使用 XML 配置时,创建这样一个代理的最简单的方法是使用 <aop:scoped-proxy/> 元素。用 @Scope 注解在 Java 中配置beans 可以为 proxyMode 属性提供等效的支持。默认值为 ScopedProxyMode.DEFAULT,通常表示除非在组件扫描指令级别配置了不同的默认值,否则不应创建作用域代理。您可以指定 ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACESScopedProxyMode.NO

如果您使用 Java 将 XML 参考文档中的作用域代理示例(参见前面文章【Bean Scopes(作用域)- 具有作用域的 bean 作为依赖项】)移植到我们的 @Bean,它类似于以下内容:

// 作为代理公开的 HTTP 会话范围的 bean
@Bean
@SessionScope
public UserPreferences userPreferences() {
    return new UserPreferences();
}

@Bean
public Service userService() {
    UserService service = new SimpleUserService();
    // 对代理 userPreferences bean 的引用
    service.setUserPreferences(userPreferences());
    return service;
}
自定义 Bean 命名

默认情况下,配置类使用 @Bean 方法的名称作为结果 Bean 的名称。但是,可以用 name 属性覆盖此功能,如下例所示:

@Configuration
public class AppConfig {

    @Bean("myThing")
    public Thing thing() {
        return new Thing();
    }
}
Bean 别名

正如前面文章【Bean 概述 - 命名 Beans】中所讨论的,有时需要为一个 bean 提供多个名称,也称为 bean 别名。为此,@Bean 注解的 name 属性接受一个字符串数组。以下示例显示了如何为 bean 设置多个别名:

@Configuration
public class AppConfig {

    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // 实例化、配置并返回 DataSource (数据源) bean...
    }
}
Bean 描述

有时,为 bean 提供更详细的文本描述会很有帮助。当出于监控目的暴露 beans (可能通过 JMX )时,这可能特别有用。

要向 @Bean 添加描述,可以使用 @Description 注解,如下例所示:

@Configuration
public class AppConfig {

    @Bean
    @Description("提供了一个 bean 的基本示例")
    public Thing thing() {
        return new Thing();
    }
}
使用 @Configuration 注解

@Configuration 是一个类级别的注解,表示一个对象是 bean definitions(定义)的来源。@Configuration 类通过 @Bean 注解的方法声明 Bean。对 @Configuration 类上的 @Bean 方法的调用也可以用来定义 Bean 间的依赖关系。一般介绍见前面文章【基本概念:@Bean和@Configuration】。

注入 bean 间的依赖关系

当 bean 相互依赖时,表达这种依赖就像让一个 bean 方法调用另一个 bean 方法一样简单,如下例所示:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());	// 调用 beanTwo()
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

在前面的示例中,beanOne 通过构造函数注入接收对 beanTwo 的引用。

只有在 @Configuration 类中声明 @bean 方法时,这种声明 Bean 间依赖关系的方法才有效。不能通过使用简易的 @Component 类来声明 bean 间的依赖关系。

查找方法注入

如前所述,查找方法注入是一个高级特性,参考前面文章【Dependencies 依赖 - 方法注入】,应该很少使用。在 singleton(单例)作用域的 bean 依赖于 prototype(原型)作用域的 bean 的情况下,这很有用。使用 Java 进行这种类型的配置为实现这种模式提供了一种自然的方法。以下示例显示了如何使用查找方法注入:

public abstract class CommandManager {
    public Object process(Object commandState) {
        // 获取适当命令接口的新实例
        Command command = createCommand();
        // 在(希望是全新的)命令实例上设置状态
        command.setState(commandState);
        return command.execute();
    }

    // 行...但是这种方法的实现在哪里呢?
    protected abstract Command createCommand();
}

通过使用 Java 配置,您可以创建 CommandManager 的一个子类,其中抽象的 createCommand() 方法被覆盖,以便查找新的(prototype(原型))命令对象。以下示例显示了如何实现这一点:

@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
    AsyncCommand command = new AsyncCommand();
    // 根据需要在此注入依赖项
    return command;
}

@Bean
public CommandManager commandManager() {
    // 使用 createCommand()返回 CommandManager 的新匿名实现
    // 被重写以返回新的原型命令对象
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}
关于基于 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();
    }
}

clientDao() 在 clientService1() 和 clientService2() 中分别被调用过一次。由于该方法创建了 ClientDaoImpl 的一个新实例并返回它,因此通常会有两个实例(每个服务一个)。这肯定会有问题:在 Spring 中,实例化的 beans 默认有一个单独的作用域。这就是神奇之处:所有@Configuration 类在启动时都用 CGLIB 子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存的(有scoped(作用域)的) beans。

根据 bean 的 scoped (范围),行为可能会有所不同。我们在这里谈论的是 singletons(单例)。

从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的类路径中,因为 CGLIB 类已经在 org.springframework.cglib 下重新打包,并直接包含在 spring-core JAR 中。

由于 CGLIB 在启动时动态添加特性,所以有一些限制。特别是,配置类不能是最终的。但是,从 4.3 开始,在配置类上允许使用任何构造函数,包括使用 @Autowired 或单个非默认构造函数声明进行默认注入。

如果您希望避免任何 CGLIB 强加的限制,可以考虑在非 @Configuration 类上声明您的 @Bean 方法(例如,改为在普通的@Component 类上)。@Bean 方法之间的跨方法调用不会被拦截,因此您必须完全依赖于构造函数或方法级别的依赖注入。

构建基于Java 的配置

Spring 基于 Java 的 configuration(配置)特性允许您编写注解,这可以降低配置的复杂性。

使用 @Import 注解

就像在 Spring XML 文件中使用 <import/> 元素来帮助模块化配置一样,@Import 注解允许从另一个配置类加载 @Bean definitions (定义),如下例所示:

@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)	// 导入 ConfigA
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }
}

现在,无需在实例化上下文时同时指定 ConfigA.class 和 ConfigB.class,只需显式提供 ConfigB,如下例所示:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // 现在 beans A 和 B 都可用了...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

这种方法简化了容器实例化,因为只需要处理一个类,而不是要求您在构造过程中记住大量的 @Configuration 类。

从 Spring Framework 4.2 开始,@Import 也支持对常规组件类的引用,类似于 AnnotationConfigApplicationContext.register 方法。如果您想通过使用一些配置类作为显式定义所有组件的入口点来避免组件扫描,这将非常有用。

在导入的 @Bean Definitions(定义) 上注入依赖项

前面的例子是可行的,但是过于简单。在大多数实际场景中,beans 在配置类之间相互依赖。当使用 XML 时,这不是问题,因为不涉及编译器,您可以声明 ref="someBean" 并信任 Spring 在容器初始化期间解决它。当使用 @Configuration 类时,Java 编译器对配置模型进行约束,因为对其他 beans 的引用必须是有效的 Java 语法。

幸运的是,解决这个问题很简单。正如我们已经讨论过的,参考【基于 Java 的容器配置 - 使用 @Bean 注解 - Bean Dependencies(依赖项)】,一个 @Bean 方法可以有任意数量的描述 Bean 依赖关系的参数。考虑以下更真实的场景,其中有几个 @Configuration 类,每个类都依赖于其他类中声明的 beans:

@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() {
        // 返回新数据源
    }
}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // 所有东西都跨配置类连接在一起...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

还有另一种方法可以达到同样的效果。记住 @Configuration 类最终只是容器中的另一个bean:这意味着它们可以像任何其他 bean 一样利用 @Autowired 和 @Value 注入以及其他特性。

确保您以这种方式注入的依赖项只是最简单的类型。@Configuration 类在上下文初始化期间很早就被处理,以这种方式强制注入依赖项可能会导致意外的提前初始化。只要有可能,就采用基于参数的注入,如前面的示例所示。

另外,要特别注意通过 @Bean 定义的 BeanPostProcessor 和 BeanFactoryPostProcessor。这些方法通常应该被声明为 static @Bean 方法,而不是触发它们包含的配置类的实例化。否则,@Autowired 和 @Value 可能无法在配置类本身上工作,因为可以在 AutowiredAnnotationBeanPostProcessor 之前将其创建为 bean 实例。

以下示例显示了如何将一个 bean 自动装配到另一个 bean:

@Configuration
public class ServiceConfig {

    @Autowired
    private AccountRepository accountRepository;

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

@Configuration
public class RepositoryConfig {

    private final DataSource dataSource;

    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() {
        // 返回新数据源
    }
}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // 所有东西都跨配置类连接在一起...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

只有从 Spring Framework 4.3 开始,才支持 @Configuration 类中的构造函数注入。还要注意,如果目标 bean 只定义了一个构造函数,就不需要指定 @Autowired。

完全合格的导入 beans,便于导航

在前面的场景中,使用 @Autowired 效果很好,并且提供了期望的模块性,但是确定在哪里声明 autowired bean definitions (定义) 仍然有些模糊。例如,作为一名查看 ServiceConfig 的开发人员,您如何知道 @Autowired AccountRepository bean 的确切声明位置?它在代码中并不显式,这可能是好的。请记住,Spring Tools for Eclipse 提供了可以呈现图形的工具,这些图形显示了一切是如何连接的,这可能就是您所需要的。此外,您的 Java IDE 可以很容易地找到 AccountRepository 类型的所有声明和使用,并快速向您显示返回该类型的@Bean 方法的位置。

如果这种模糊性是不可接受的,并且您希望在 IDE 中从一个 @Configuration 类直接导航到另一个 @Configuration 类,那么可以考虑自动装配配置类本身。以下示例显示了如何实现这一点:

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // 浏览 config 类到 @Bean 方法!
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

在前面的情况中,AccountRepository 的定义位置是完全明确的。但是,ServiceConfig 现在与 RepositoryConfig 紧密耦合。这就是权衡。这种紧密耦合可以通过使用基于接口或基于抽象类的 @Configuration 类来缓解。考虑下面的例子:

@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})  // 导入具体配置!
public class SystemTestConfig {

    @Bean
    public DataSource 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");
}

现在,ServiceConfig 相对于具体的 DefaultRepositoryConfig 是松散耦合的,内置的 IDE 工具仍然有用:您可以很容易地获得RepositoryConfig 实现的类型层次结构。这样,导航 @Configuration 类及其依赖项与导航基于接口的代码的通常过程没有什么不同。

如果您想影响某些 bean 的启动创建顺序,可以考虑将它们中的一些声明为 @Lazy (用于在第一次访问时而不是启动时创建)或者声明为 @DependsOn 某些其他 bean (确保在当前 bean 之前创建特定的其他 bean,而不是后者的直接依赖关系)。

有条件地包含 @Configuration 类或 @Bean 方法

根据某些任意的系统状态,有条件地启用或禁用完整的 @Configuration 类甚至单个的 @Bean 方法通常很有用。一个常见的例子是,只有在 Spring Environment(环境) 中启用了特定的 profile (配置) 文件时,才使用 @Profile 注解来激活 Bean (有关详细信息,请参见后续【 环境 - Bean定义配置文件】)。

@Profile 注解实际上是通过使用一种更加灵活的注解来实现的,这种注解叫做 @Conditional。@Conditional 注解指示在注册 @Bean 之前应该参考的特定 org.springframework.context.annotation.Condition 实现。

Condition 接口的实现提供了返回 true 或 false 的 matches(…) 方法。例如,下面的清单显示了用于 @Profile 的实际条件实现:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // 阅读 @Profile 注解属性
    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;
}

有关更多详细信息,请参见@Conditional javadoc。

结合 Java 和 XML 配置

Spring 的 @Configuration 类支持并不打算100%完全替代 Spring XML。一些工具,比如 Spring XML名称空间,仍然是配置容器的理想方式。如果 XML 是方便的或必要的,您可以选择:或者使用 ClassPathXmlApplicationContext 以 “以XML为中心” 的方式实例化容器,或者使用 AnnotationConfigApplicationContext 和 @ImportResource 注解以 “以Java为中心” 的方式实例化容器,以便根据需要导入 XML。

以 XML 为中心使用 @Configuration 类

最好从 XML 引导 Spring 容器,并以特别的方式包含 @Configuration 类。例如,在使用 Spring XML 的大型现有代码库中,根据需要创建 @Configuration 类并从现有的 XML 文件中包含它们会更容易。在本节的后面,我们将讨论在这种 “以XML为中心” 的情况下使用@Configuration 类的选项。

将 @Configuration 类声明为普通的 Spring <bean/> 元素

记住 @Configuration 类最终是容器中的 bean definitions(定义)。在本系列示例中,我们创建了一个名为 AppConfig 的 @Configuration 类,并将其作为 <bean/> definitions(定义) 包含在 system-test-config.xml 中。因为 <context:annotation-config/> 是打开的,所以容器识别 @Configuration 注解并正确处理 AppConfig 中声明的 @Bean 方法。

以下示例显示了 Java 中的一个普通配置类:

@Configuration
public class AppConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

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

以下示例显示了示例 system-test-config.xml 文件的一部分:

<beans>
    <!-- 启用注解处理,如 @Autowired 和 @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>

以下示例显示了一个可能的 jdbc.properties 文件:

jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

在 system-test-config.xml 文件中,AppConfig <bean/> 没有声明 id 元素。虽然这样做是可以接受的,但这是不必要的,因为没有其他 bean 引用过它,而且也不可能通过名称从容器中显式获取它。类似地,DataSource bean 仅通过类型自动连接,因此并不严格要求显式的 bean id。

使用 <context:component-scan/> 选择 @Configuration 类

因为 @Configuration 是用 @Component 元注解的,所以 @Configuration 注解的类自动成为组件扫描的候选对象。使用上一个例子中描述的相同场景,我们可以重新定义 system-test-config.xml 来利用组件扫描。注意,在这种情况下,我们不需要显式声明 < context:annotation-config/>,因为 <context:component-scan/> 支持相同的功能。

以下示例显示了修改后的 system-test-config.xml 文件:

<beans>
    <!-- 选择 AppConfig 并将其注册为 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>

@Configuration 以类为中心使用 XML 和 @ImportResource

在 @Configuration 类是配置容器的主要机制的应用程序中,仍然有可能至少需要使用一些 XML。在这些场景中,您可以使用@ImportResource 并只定义您需要的 XML。这样做实现了一种 “以Java为中心” 的方法来配置容器,并将 XML 保持在最低限度。以下示例 (包括配置类、定义 bean 的 XML 文件、属性文件和主类)显示了如何使用 @ImportResource 注解来实现 “以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);
    }
}
properties-config.xml
<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
Environment (环境)抽象

Environment环境接口是集成在容器中的抽象,它对应用程序环境的两个关键方面进行建模:profiles(配置文件) 和 properties (属性)。

配置文件是一个命名的、逻辑的 bean definitions (定义)组,只有当给定的 profile (配置文件)处于活动状态时,才会向容器注册。无论是用 XML 还是用注解定义的,Beans 都可以被分配给一个 profile(配置文件)。与配置文件相关的 Environment(环境)对象的作用是确定哪些配置文件(如果有的话)当前是活动的,以及哪些配置文件(如果有的话)在默认情况下应该是活动的。

Properties(属性)在几乎所有的应用程序中都扮演着重要的角色,并且可能有多种来源:Properties(属性)文件、JVM 系统属性、系统环境变量、JNDI、servlet context parameters(上下文参数)、专用 Properties(属性)对象、Map(映射)对象等等。与 Properties(属性)相关的 Environment(环境)对象的作用是为用户提供一个方便的服务接口,用于配置属性源和从它们解析属性。

Bean Definition Profiles(配置文件)

Bean definition (定义)profiles (配置文件)在核心容器中提供了一种机制,允许在不同的 environments(环境)中注册不同的bean。“environments(环境)” 这个词对于不同的用户可能有不同的含义,这个特性可以帮助很多用例,包括:

  • 在开发中使用内存中的数据源,而不是在 QA 或生产中从 JNDI 中查找相同的数据源。
  • 仅在将应用程序部署到性能环境中时注册监控基础结构
  • 为客户 A 和客户 B 部署注册定制的 beans 实现。

考虑需要 DataSource (数据源)的实际应用程序中的第一个用例。在测试环境中,配置可能如下所示:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

现在考虑如何将这个应用程序部署到 QA 或生产环境中,假设应用程序的 dataSource(数据源) 已经注册到生产应用服务器的 JNDI 目录中。我们的数据源 bean 现在看起来如下所示:

@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是如何根据当前环境在使用这两种变体之间切换。随着时间的推移,Spring 用户设计了许多方法来实现这一点,通常依赖于系统环境变量和包含 ${placeholder} 标记的 XML <import/> 语句的组合,这些标记根据环境变量的值解析到正确的配置文件路径。Bean definition profiles(定义配置)是一个核心容器特性,它为这个问题提供了一个解决方案。

如果我们概括前面特定于环境的 bean definitions(定义)的例子中显示的用例,我们最终需要在特定的 contexts (上下文)中注册特定的 bean definitions (定义),而在其他上下文中不注册。您可以说,您希望在情况 A 中注册 bean definitions (定义)的某个 profile (配置) 文件,而在情况 b 中注册另一个 profile(配置)文件。

使用 @Profile

@Profile 注解允许您指出当一个或多个指定的配置文件处于活动状态时,一个组件可以注册。使用前面的例子,我们可以如下重写数据源配置:

@Configuration
@Profile("development")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}
@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

如前所述,对于 @Bean 方法,您通常选择使用编程式 JNDI 查找,方法是使用 Spring 的 JndiTemplate/JndiLocatorDelegate 帮助器或前面显示的直接 JNDI InitialContext 用法,而不是 JndiObjectFactoryBean 变体,后者会强制您将返回类型声明为FactoryBean 类型。

配置文件字符串可能包含一个简单的配置文件名称(例如,production )或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,production & us-east(生产和美国东部) )。配置文件表达式支持以下运算符:

  • !:配置文件的逻辑“非”
  • & :配置文件的逻辑“与”
  • | :配置文件的逻辑“或”

不使用括号就不能混合 & 和 | 运算符。例如,production & us-east | eu-central 不是有效的表达式。它必须表示为 production & (us-east | eu-central) 生产&(美国东部|欧盟中部)。

您可以使用 @Profile 作为元注解来创建定制的组合注解。下面的示例定义了一个自定义的 @Production 注解,您可以使用它作为@Profile("production ") 的直接替代:

元注解组合参考【类路径扫描和托管组件 - 使用元注解和组合注解】

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

如果 @Configuration 类用 @Profile 注解,则所有与该类关联的 @Bean 方法和 @Import 注解都会被忽略,除非一个或多个指定的概要文件是活动的。如果 @Component 或 @Configuration 类标有 @Profile({“p1 “,” p2”}),则该类不会被注册或处理,除非配置文件 ’ p1 ’ 或 ’ p2 ’ 已被激活。如果给定的配置文件以 NOT 运算符(!)为前缀,只有当配置文件不活动时,带注解的元素才会被注册。例如,给定 @Profile({“p1 “,”!p2”}) ,如果配置文件 “p1” 处于活动状态,或者如果配置文件 “p2” 处于非活动状态,则会进行注册。

@Profile 也可以在方法级别声明为仅包含配置类的一个特定 bean(例如,特定 bean 的可选变体),如下例所示:

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development") // standaloneDataSource 方法仅在 development(开发) 配置文件中可用。
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production") // jndiDataSource方法仅在 production(生产) 配置文件中可用。
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

对于 @Bean 方法上的 @Profile,可能会出现一种特殊的情况:在重载相同 Java 方法名的 @Bean 方法的情况下(类似于构造函数重载),需要在所有重载方法上一致地声明 @Profile 条件。如果条件不一致,只有重载方法中第一个声明的条件才重要。因此,@Profile 不能用于选择一个重载方法,该重载方法的某个参数签名优于另一个参数签名。在创建时,同一个 bean 的所有工厂方法之间的解析遵循 Spring 的构造函数解析算法。

如果您想要定义具有不同配置文件条件的备选 bean,请通过使用 @Bean name 属性,使用指向相同 Bean 名称的不同 Java 方法名称,如前面的示例所示。如果参数签名都是相同的(例如,所有的变量都没有参数工厂方法),这是首先在有效的Java类中表示这种安排的唯一方法(因为只能有一个具有特定名称和参数签名的方法)。

XML Bean Definition Profiles (定义配置)文件

对应的 XML 是 <beans> 元素的 profile 属性。我们前面的示例配置可以重写为两个 XML 文件,如下所示:

<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
<beans profile="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

还可以避免在同一文件中拆分和嵌套 <beans/> 元素,如下例所示:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <!-- 其他 bean 定义 -->

    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

spring-bean.xsd 被限制为只允许这些元素作为文件中的最后一个元素。这应该有助于提供灵活性,而不会导致 XML 文件混乱。

XML 对应物不支持前面描述的配置文件表达式。但是,可以通过使用!操作员。还可以通过嵌套配置文件来应用逻辑 “与”,如下例所示:

<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:jdbc="http://www.springframework.org/schema/jdbc"
 xmlns:jee="http://www.springframework.org/schema/jee"
 xsi:schemaLocation="...">

 <!-- 其他 bean definitions(定义) -->

 <beans profile="production">
     <beans profile="us-east">
         <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
     </beans>
 </beans>
</beans>

在前面的例子中,如果 production 和 us-east 配置文件都是活动的,那么 dataSource bean是公开的。

激活配置文件

现在我们已经更新了我们的配置,我们仍然需要指示 Spring 哪个概要文件是活动的。如果我们现在启动我们的示例应用程序,我们将会看到抛出 NoSuchBeanDefinitionException,因为容器找不到名为 dataSource 的 Spring bean。

激活一个概要文件可以通过几种方式来完成,但是最直接的方式是通过 ApplicationContext 提供的 Environment(环境) API 以编程方式来完成。以下示例显示了如何实现这一点:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");// 激活环境配置文件
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

此外,您还可以通过 spring.profiles.active 属性以声明方式激活概要文件,该属性可以通过系统环境变量、JVM 系统属性、web.xml 中的servlet 上下文参数来指定,甚至可以作为 JNDI 中的一个条目来指定(请参见 【PropertySource Abstraction(抽象)】)。在集成测试中,可以通过使用 spring-test 模块中的 @ActiveProfiles 注解来声明活动概要文件(请参见后续【使用环境概要文件的上下文配置】)。

请注意,概要文件不是一个“非此即彼”的命题。您可以一次激活多个配置文件。以编程方式,您可以向 setActiveProfiles() 方法提供多个配置文件名,该方法接受 String… varargs。以下示例激活多个配置文件:

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

以声明方式,spring.profiles.active 可以接受逗号分隔的配置文件名列表,如下例所示:

-Dspring.profiles.active="profile1,profile2"
默认配置文件

默认配置文件代表默认启用的配置文件。考虑下面的例子:

@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}

如果没有活动的概要文件,则创建数据源。您可以将此视为为一个或多个 beans 提供默认定义的一种方式。如果启用了任何配置文件,则默认配置文件不适用。

您可以通过在 Environment(环境) 中使用 setDefaultProfiles() 或者以声明方式使用 spring.profiles.default 属性来更改默认配置文件的名称。

PropertySource Abstraction (属性源抽象)

Spring 的 Environment(环境) 抽象提供了对可配置的属性源层次结构的搜索操作。考虑下面的清单:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("我的环境是否包含 “my-property” 属性?" + containsMyProperty);

在前面的代码片段中,我们看到了一种询问 my-property ,property (属性)是否是为当前环境定义的高级方法。为了回答这个问题,Environment(环境)对象在一组 PropertySource 对象上执行搜索。PropertySource 是对任何键值对源的简单抽象,Spring 的StandardEnvironment 配置有两个 PropertySource 对象——一个表示 JVM 系统属性集 (System.getProperties()),另一个表示系统环境变量集 (System.getenv())。

这些默认属性源是为 StandardEnvironment 提供的,供独立应用程序使用。StandardServletEnvironment 由附加的默认属性源填充,包括 servlet 配置和 servlet 上下文参数。它可以选择启用 JndiPropertySource。有关详细信息,请参见 javadoc。

具体来说,当您使用 StandardEnvironment 时,如果运行时存在 my-property 系统属性或 my-property 环境变量,则对 env.containsProperty("my-property") 的调用将返回 true。

执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用 env.getProperty("my-property ") 的过程中碰巧在这两个位置都设置了 my-property 属性,则系统属性值"获胜"并被返回。请注意,属性值不会被合并,而是被前面的条目完全覆盖。

对于常见的 StandardServletEnvironment,完整的层次结构如下,优先级最高的条目位于顶部:

  1. ServletConfig 参数(如果适用—例如,在 DispatcherServlet 上下文中)
  2. ServletContext 参数( web.xml context-param 条目)
  3. JNDI 环境变量 (java:comp/env/ 条目)
  4. JVM 系统属性(-D 命令行参数)
  5. JVM 系统环境(操作系统环境变量)

最重要的是,整个机制是可配置的。也许您有一个自定义的属性源,您希望将其集成到此搜索中。为此,请实现并实例化您自己的PropertySource,并将其添加到当前环境的 PropertySources 集中。以下示例显示了如何实现这一点:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在前面的代码中,MyPropertySource 已在搜索中以最高优先级添加。如果它包含 my-property 属性,则检测并返回该属性,优先于任何其他 PropertySource 中的任何 my-property 属性。MutablePropertySources API 公开了许多方法,这些方法允许对属性源集进行精确操作。

使用 @PropertySource

@PropertySource 注解为将 PropertySource 添加到 Spring 的环境中提供了一种方便的声明性机制。

给定一个名为 app.properties 的文件,该文件包含键值对 testbean.name=myTestBean,下面的 @Configuration 类以这样一种方式使用 @PropertySource,即调用 testBean.getName() 返回 myTestBean:

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

@PropertySource 资源位置中出现的任何 ${… } 占位符都将根据已针对环境注册的属性源集进行解析,如下例所示:

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

假设 my.placeholder 存在于一个已经注册的属性源中(例如,系统属性或环境变量),占位符将被解析为相应的值。如果没有,则默认使用 /path 作为默认值。如果未指定默认值并且无法解析属性,则会引发 IllegalArgumentException。

根据 Java 8 的约定,@PropertySource 注解是可重复的。但是,所有这样的 @PropertySource 注解都需要在同一级别声明,要么直接在配置类上声明,要么作为同一自定义注解中的元注解声明。不建议混合使用直接注解和元注解,因为直接注解会有效地覆盖元注解。

语句中的占位符解析

过去,元素中占位符的值只能根据 JVM 系统属性或环境变量来解析。现在情况不再是这样了。因为 Environment(环境)抽象集成在整个容器中,所以很容易通过它路由占位符的解析。这意味着您可以按照自己喜欢的任何方式配置解析过程。您可以更改搜索系统属性和环境变量的优先级,或者完全删除它们。您还可以根据需要将自己的属性源添加到组合中。

具体来说,只要 customer(客户) 属性在环境中可用,无论 customer(客户) Environment(属性)在哪里定义,下面的语句都有效:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
注册 LoadTimeWeaver

Spring 使用 LoadTimeWeaver 来动态转换加载到 Java 虚拟机(JVM) 中的类。

要启用加载时织入,可以将 @EnableLoadTimeWeaving 添加到一个 @Configuration 类中,如下例所示:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

或者,对于 XML 配置,您可以使用 context:load-time-weaver 元素:

<beans>
    <context:load-time-weaver/>
</beans>

一旦为 ApplicationContext 进行了配置,该 ApplicationContext 中的任何 bean 都可以实现 LoadTimeWeaverAware,从而接收对加载时织入实例的引用。这在后续文章【结合 Spring 的 JPA 支持】时特别有用,因为加载时织入对于 JPA 类转换可能是必要的。有关更多详细信息,请参考LocalContainerEntityManagerFactoryBean javadoc。有关 AspectJ 加载时织入的更多信息,请参见后续 【Spring框架中使用 AspectJ 的加载时织入】。

LoadTimeWeaverAware 也被称为静态 AOP

ApplicationContext 的附加功能

ApplicationContext : 应用程序上下文

正如在 “简介【Spring IoC容器和Beans介绍】” 一章中所讨论的,org.springframework.beans.factory 包提供了管理和操作 bean 的基本功能,包括以编程的方式。org.springframework.context 包添加了 ApplicationContext 接口,该接口扩展了 BeanFactory 接口,此外还扩展了其他接口,以更加面向应用程序框架的方式提供附加功能。许多人以完全声明的方式使用 ApplicationContext,甚至没有以编程方式创建它,而是依赖于 ContextLoader 等支持类来自动实例化 ApplicationContext,作为 Java EE web 应用程序正常启动过程的一部分。

为了以更加面向框架的方式增强 BeanFactory 的功能,context (上下文) 包还提供了以下功能:

  • 通过 MessageSource 接口访问 i18n 风格的消息。
  • 通过 ResourceLoader 接口访问资源,如 URL 和文件。
  • 事件发布,即通过使用 ApplicationEventPublisher 接口发布到实现 ApplicationListener 接口的 beans。
  • 通过 HierarchicalBeanFactory 接口加载多个(分层)上下文,让每个上下文都集中在一个特定的层上,例如应用程序的 web 层。
使用 MessageSource 的国际化

ApplicationContext 接口扩展了一个名为 MessageSource 的接口,因此提供了国际化 (“i18n”) 功能。Spring 还提供了HierarchicalMessageSource 接口,可以分层解析消息。这些接口共同提供了 Spring 实现消息解析的基础。这些接口上定义的方法包括:

  • String getMessage(String code,Object[] args,String default,Locale loc):用于从 MessageSource 检索消息的基本方法。当找不到指定区域设置的消息时,将使用默认消息。使用标准库提供的 MessageFormat 功能,传入的任何参数都将成为替换值。
  • String getMessage(String code,Object[] args,Locale loc):本质上与前面的方法相同,但有一点不同:不能指定默认消息。如果找不到该消息,将引发 NoSuchMessageException。
  • string getMessage(MessageSourceResolvable resolvable,Locale):上述方法中使用的所有属性也包装在一个名为MessageSourceResolvable 的类中,您可以在此方法中使用该类。

当加载 ApplicationContext 时,它会自动搜索上下文中定义的 MessageSource bean。该 bean 的名称必须为 messageSource。如果找到了这样的 bean,那么对前面方法的所有调用都将委托给消息源。如果找不到消息源,ApplicationContext 将尝试查找包含同名 bean 的父级。如果是,它将使用该 bean 作为消息源。如果 ApplicationContext 找不到任何消息源,则实例化一个空的 DelegatingMessageSource,以便能够接受对上面定义的方法的调用。

Spring 提供了三个 MessageSource 实现,ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 和 StaticMessageSource。它们都实现了 HierarchicalMessageSource,以便进行嵌套消息传递。StaticMessageSource 很少使用,但它提供了向源添加消息的编程方式。以下示例显示了 ResourceBundleMessageSource:

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

该示例假设在类路径中定义了三个资源包,分别名为 format、exceptions 和 windows。任何解析消息的请求都是通过 ResourceBundle 对象以 JDK 标准的方式进行处理的。出于示例的目的,假设上述两个资源包文件的内容如下:

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required={0} 参数是必需的。

下一个示例显示了运行 MessageSource 功能的程序。请记住,所有 ApplicationContext 实现也是 MessageSource 实现,因此可以转换为 MessageSource 接口。

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

上述程序的结果输出如下:

Alligators rock!

总而言之,MessageSource 是在名为 beans.xml 的文件中定义的,该文件位于类路径的根目录下。messageSource bean definition (定义)通过其 basenames 属性引用许多资源包。在列表中传递给 basenames 属性的三个文件作为文件存在于类路径的根目录下,分别称为 format.properties、exceptions.properties 和 windows.properties。

下一个示例显示了传递给消息查找的参数。这些参数被转换为字符串对象,并插入查找消息的占位符中。

<beans>

    <!-- 此 MessageSource (消息源) 正在 web 应用程序中使用 -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <!-- 让我们将上面的消息源注入到这个 POJO 中 -->
    <bean id="example" class="com.something.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;	// 注入消息

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

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

调用 execute() 方法的结果输出如下:

userDao 参数是必需的。

关于国际化 (“i18n”),Spring 的各种 MessageSource 实现遵循与标准 JDK ResourceBundle 相同的语言环境解析和回退规则。简而言之,继续前面定义的示例 messageSource,如果您想要根据 英国(en-GB) 语言环境解析消息,您将分别创建名为format_en_GB.properties、exceptions_en_GB.properties 和 windows_en_GB.properties 的文件。

通常,区域设置解析由应用程序的周围环境管理。在以下示例中,解析 (英国) 消息所依据的区域设置是手动指定的:

# 在 exceptions_en_GB.properties
argument.required=“{0}”参数是必需的,我说,必需的。
public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

运行上述程序的结果输出如下:

“userDao”参数是必需的,我说,必需的。

您还可以使用 MessageSourceAware 接口来获取对任何已定义的 MessageSource 的引用。当创建和配置 bean 时,在实现MessageSourceAware 接口的 ApplicationContext 中定义的任何 bean 都会被注入 application context’s(应用程序上下文)的 MessageSource。

因为 Spring 的 MessageSource 是基于 Java 的 ResourceBundle 的,所以它不会合并具有相同基本名称的 Bundle,而只会使用找到的第一个 bundle。具有相同基本名称的后续消息包将被忽略。

作为 ResourceBundleMessageSource 的替代,Spring 提供了一个 ReloadableResourceBundleMessageSource 类。该变体支持相同的包文件格式,但比基于标准 JDK 的 ResourceBundleMessageSource 实现更灵活。特别是,它允许从任何 Spring 资源位置读取文件(不仅仅是从类路径),并支持 bundle 属性文件的热重新加载(同时有效地缓存它们)。有关详细信息,请参见ReloadableResourceBundleMessageSource javadoc。

标准和自定义事件

ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。如果将实现 ApplicationListener 接口的bean 部署到上下文中,则每次 ApplicationEvent 发布到 ApplicationContext 时,都会通知该 bean。本质上,这是标准的观察者设计模式。

从 Spring 4.2 开始,事件基础设施得到了显著的改进,并提供了基于注解的模型以及发布任意事件的能力(即,不一定从 ApplicationEvent 扩展的对象)。当这样的对象被发布时,我们为您将它包装在一个事件中。

下表描述了 Spring 提供的标准事件:

Event(事件)说明
ContextRefreshedEvent在初始化或刷新 ApplicationContext 时发布(例如,通过在 ConfigurableApplicationContext 接口上使用 refresh() 方法)。这里,“已初始化” 意味着所有 bean 都已加载,后处理器 bean 已被检测和激活,singletons (单件)已预实例化,并且 ApplicationContext 对象已准备就绪。只要上下文没有被关闭,刷新就可以被触发多次,只要所选择的 ApplicationContext 实际上支持这样的“热”刷新。例如,XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。
ContextStartedEvent通过使用 ConfigurableApplicationContext 接口上的 start() 方法启动 ApplicationContext 时发布。这里,“started(开始)” 意味着所有 Lifecycle(生命周期)beans 都接收到一个明确的开始信号。通常,该信号用于在显式停止后重新启动 bean,但也可以用于启动尚未配置为自动启动的组件(例如,在初始化时尚未启动的组件)。
ContextStoppedEvent当使用 ConfigurableApplicationContext 接口上的 stop() 方法停止 ApplicationContext 时发布。在这里,“stopped(停止)”意味着所有生命周期 beans 都收到一个明确的停止信号。停止的上下文可以通过 start() 调用重新启动。
ContextClosedEvent当使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM 关闭挂钩关闭ApplicationContext 时发布。在这里,“closed(关闭)”意味着所有 singleton(单独)的 beans 都将被销毁。一旦上下文被关闭,它就到达了生命的终点,并且不能被刷新或重新启动。
RequestHandledEvent一个特定于 web 的事件,告诉所有 bean 一个 HTTP 请求已经得到服务。该事件在请求完成后发布。该事件仅适用于使用 Spring 的 DispatcherServlet 的 web 应用程序。
ServletRequestHandledEventRequestHandledEvent 的子类,用于添加特定于 Servlet 的上下文信息。

您还可以创建和发布自己的自定义事件。以下示例显示了一个扩展 Spring 的 ApplicationEvent 基类的简单类:

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // 访问器和其他方法...
}

若要发布自定义 ApplicationEvent,请对 ApplicationEventPublisher 调用 publishEvent() 方法。通常,这是通过创建一个实现ApplicationEventPublisherAware 的类并将其注册为 Spring bean 来实现的。以下示例显示了这样一个类:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // 发送电子邮件...
    }
}

在配置时,Spring 容器检测到 EmailService 实现了 ApplicationEventPublisherAware,并自动调用 setApplicationEventPublisher()。实际上,传入的参数是 Spring 容器本身。您通过 ApplicationEventPublisher 接口与应用程序上下文进行交互。

要接收自定义 ApplicationEvent,您可以创建一个实现 ApplicationListener 的类,并将其注册为 Spring bean。以下示例显示了这样一个类:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

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

    public void onApplicationEvent(BlockedListEvent event) {
        // 通过通知地址通知相关方...
    }
}

请注意,ApplicationListener 通常使用自定义事件的类型进行参数化(在前面的示例中为 BlockedListEvent)。这意味着onApplicationEvent() 方法可以保持类型安全,避免任何向下转换的需要。您可以注册任意数量的事件侦听器,但是请注意,默认情况下,事件侦听器同步接收事件。这意味着 publishEvent() 方法会一直阻塞,直到所有侦听器都处理完该事件。这种同步单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文中操作。如果需要另一种事件发布策略,请参见Spring 的 ApplicationEventMulticaster 接口的 javadoc 和配置选项的 SimpleApplicationEventMulticaster 实现。

以下示例显示了用于注册和配置上述每个类的 bean 定义:

<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="blockedlist@example.org"/>
</bean>

综上所述,当调用 emailService bean 的 sendEmail() 方法时,如果有任何电子邮件应该被阻止,就会发布一个 BlockedListEvent 类型的自定义事件。blockedListNotifier bean 注册为 ApplicationListener 并接收 BlockedListEvent,此时它可以通知适当的方。

Spring 的事件机制是为同一个应用程序上下文中 Spring beans 之间的简单通信而设计的。然而,对于更复杂的企业集成需求,单独维护的 Spring Integration 项目为构建轻量级、面向模式、事件驱动的架构提供了完整的支持,这些架构基于众所周知的Spring 编程模型。

基于注解的事件侦听器

通过使用 @EventListener 注解,可以在受管 bean 的任何方法上注册事件侦听器。BlockedListNotifier 可以重写如下:

public class BlockedListNotifier {

    private String notificationAddress;

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

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // 通过通知地址通知相关方...
    }
}

方法注解再次声明它所监听的事件类型,但是这一次使用了灵活的名称,并且没有实现特定的监听器接口。事件类型也可以通过泛型来缩小范围,只要实际的事件类型在其实现层次结构中解析您的泛型参数。

如果您的方法应该监听几个事件,或者如果您想定义它而不使用任何参数,也可以在注解本身上指定事件类型。以下示例显示了如何实现这一点:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

还可以通过使用定义【SpEL表达式】的注解的 condition 属性来添加额外的运行时过滤,这应该与实际调用特定事件的方法相匹配。

以下示例显示了如何重写我们的通知程序,使其仅在事件的内容属性等于 my-event 时才被调用:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
    // 通过通知地址通知相关方...
}

每个 SpEL 表达式针对一个专用的上下文进行计算。下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:

事件指定可用元数据:

名称位置描述例子
Event:事件root object:跟对象实际的 ApplicationEvent(应用程序事件。)#root.event 或 event
Arguments array:参数数组root object:跟对象用于调用方法的参数(作为对象数组)。#root.args 或 args,args[0] 来访问第一个参数,等等。
Argument name:参数名称evaluation context:评估环境任何方法参数的名称。如果由于某种原因,名称不可用(例如,因为编译后的字节代码中没有调试信息),也可以使用 #a<#arg> 语法获得单个参数,其中< #arg > 代表参数索引(从 0 开始)。#blEvent 或 #a0 (也可以使用 #p0 或 #p<#arg > 参数符号作为别名)

请注意,#root.event 允许您访问底层事件,即使您的方法签名实际上引用了一个已发布的任意对象。

如果需要发布一个事件作为处理另一个事件的结果,可以更改方法签名以返回应该发布的事件,如下例所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // 通过通知地址通知相关方
    // 然后发布一个ListUpdateEvent...
}

【异步侦听器】不支持此功能。

handleBlockedListEvent() 方法为它处理的每个 BlockedListEvent 发布一个新的 ListUpdateEvent。如果需要发布几个事件,可以返回一个集合或一个事件数组。

异步侦听器

Asynchronous Listeners : 异步侦听器

如果希望特定的侦听器异步处理事件,可以重用【后续文章-常规的 @Async 支持】。以下示例显示了如何实现这一点:

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent在单独的线程中处理
}

使用异步事件时,请注意以下限制:

  • 如果异步事件侦听器引发【Exception】异常,则该异常不会传播到调用方。有关更多详细信息,请参见AsyncUncaughtExceptionHandler
  • 异步事件侦听器方法不能通过返回值来发布后续事件。如果需要发布另一个事件作为处理结果,请注入 ApplicationEventPublisher 来手动发布该事件。
排序侦听器

Ordering Listeners : 排序监听器

如果需要在调用另一个侦听器之前调用一个侦听器,可以将 @Order 注解添加到方法声明中,如下例所示:

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // 通过通知地址通知相关方...
}
泛型事件

您还可以使用泛型来进一步定义事件的结构。考虑使用 EntityCreatedEvent<T>,其中 T 是被创建的实际实体的类型。例如,您可以创建以下侦听器定义,以便只接收人员的 EntityCreatedEvent:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

由于类型擦除,只有当被触发的事件解析了事件侦听器过滤的一般参数时,这种方法才有效(也就是说,类似 PersonCreatedEvent 类 extends (扩展) EntityCreatedEvent<Person>{ … } )。

在某些情况下,如果所有事件都遵循相同的结构,这可能会变得非常乏味(就像前面示例中的事件一样)。在这种情况下,您可以实现ResolvableTypeProvider 来指导超出运行时环境所提供的框架。以下事件显示了如何执行此操作:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

这不仅适用于 ApplicationEvent,也适用于任何作为事件发送的任意对象。

方便地访问低级资源

为了更好地使用和理解应用程序上下文,您应该熟悉 Spring 的 Resource(资源) 抽象,如后续文章【核心技术 - 资源】所述。

application context(应用程序上下文)是一个 ResourceLoader(资源加载器),可用于加载 Resource(资源) 对象。Resource(资源) 本质上是 JDK java.net.URL 类的一个功能更丰富的版本。事实上,Resource(资源) 的实现在适当的地方包装了 java.net.URL 的实例。Resource(资源) 可以以透明的方式从几乎任何位置获取低级资源,包括从类路径、文件系统位置、任何可以用标准 URL 描述的地方,以及其他一些地方。如果资源位置字符串是没有任何特殊前缀的简单路径,那么这些资源来自哪里是特定的,并且适合于实际的应用程序上下文类型。

您可以将部署到应用程序上下文中的 bean 配置为实现特殊的回调接口 ResourceLoaderAware,以便在初始化时自动回调,同时将应用程序上下文本身作为 ResourceLoader 传入。还可以公开 Resource 类型的属性,用于访问静态资源。它们像任何其他属性一样被注入其中。您可以将这些 Resource(资源) 属性指定为简单的 String(字符串) 路径,并在部署 bean 时依赖于从这些文本字符串到实际 Resource(资源) 对象的自动转换。

提供给 ApplicationContext 构造函数的一个或多个位置路径实际上是资源字符串,以简单的形式,根据特定的上下文实现进行适当的处理。例如,ClassPathXmlApplicationContext 将简单的位置路径视为类路径位置。还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或 URL 加载定义,而不管实际的上下文类型。

方便的 Web 应用程序上下文实例化

例如,您可以使用 ContextLoader 以声明方式创建 ApplicationContext 实例。当然,您也可以通过使用 ApplicationContext 实现之一以编程方式创建 ApplicationContext 实例。

您可以使用 ContextLoaderListener 注册 ApplicationContext,如下例所示:

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

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

侦听器检查 contextConfigLocation 参数。如果该参数不存在,侦听器将使用 /WEB-INF/applicationContext.xml 作为默认值。当参数确实存在时,侦听器通过使用预定义的分隔符(逗号、分号和空白)来分隔字符串,并将值用作搜索应用程序上下文的位置。也支持 Ant 风格的路径模式。例如 /WEB-INF/*Context.xml (适用于所有文件名以 Context.xml 结尾且位于 WEB-INF 目录中的文件)和 /WEB-INF/**/*Context.xml (适用于 WEB-INF 的任何子目录中的所有此类文件)。

将 Spring ApplicationContext 部署为 Java EE RAR 文件

可以将 Spring ApplicationContext 部署为 RAR 文件,将上下文及其所有必需的 bean 类和库 jar 封装在一个 Java EE RAR 部署单元中。这相当于引导独立的 ApplicationContext (仅在 Java EE 环境中托管)能够访问 Java EE 服务器设施。RAR 部署是部署无头 WAR 文件场景的更自然的替代方案——实际上,没有任何 HTTP 入口点的 WAR 文件仅用于在 Java EE 环境中引导 Spring ApplicationContext。

RAR 部署非常适合于不需要 HTTP 入口点,而只包含消息端点和调度作业的应用程序上下文。这种环境中的 Beans 可以使用应用服务器资源,例如 JTA 事务管理器和 JNDI 绑定的 JDBC 数据源实例以及 JMS ConnectionFactory 实例,还可以向平台的 JMX 服务器注册——所有这些都是通过 Spring 的标准事务管理以及 JNDI 和 JMX 支持设施完成的。应用程序组件还可以通过 Spring 的 TaskExecutor 抽象与应用服务器的 JCA WorkManager (工作管理器) 进行交互。

有关 RAR 部署中涉及的配置详细信息,请参见 SpringContextResourceAdapter 类的 javadoc。

对于作为 Java EE RAR 文件的 Spring ApplicationContext 的简单部署:

  1. 将所有应用程序类打包到一个 RAR 文件中(这是一个具有不同文件扩展名的标准 JAR 文件)。
  2. 将所有需要的库 jar 添加到 RAR 归档文件的根目录中。
  3. 添加一个 META-INF/ra.xml 部署描述符(如 SpringContextResourceAdapter 的 javadoc 中所示)和相应的 Spring XML bean definition (定义)文件(通常是 META-INF/applicationContext.xml)。
  4. 将生成的 RAR 文件放到应用服务器的部署目录中。

这种 RAR 部署单元通常是独立的。它们不向外界公开组件,甚至不向同一应用程序的其他模块公开。与基于 RAR 的ApplicationContext 的交互通常通过与其他模块共享的 JMS 目的地进行。例如,基于 RAR 的应用上下文还可以调度一些作业或对文件系统中的新文件作出反应(等等)。如果它需要允许来自外部的同步访问,它可以(例如)导出 RMI 端点,这些端点可以被同一台机器上的其他应用程序模块使用。

BeanFactory

BeanFactory API 为 Spring 的 IoC 功能提供了底层基础。它的特定契约主要用于与 Spring 的其他部分和相关的第三方框架集成,它的DefaultListableBeanFactory 实现是更高级别 GenericApplicationContext 容器内的关键委托。

BeanFactory 和相关接口(如 BeanFactoryAware、InitializingBean、DisposableBean )是其他框架组件的重要集成点。由于不需要任何注解甚至反射,它们允许容器及其组件之间非常有效的交互。应用程序级 beans 可能使用相同的回调接口,但通常更喜欢声明性依赖注入,或者通过注解,或者通过编程配置。

请注意,核心 BeanFactory API 级别及其 DefaultListableBeanFactory 实现并不假设配置格式或要使用的任何组件注解。所有这些风格都是通过扩展(如 XmlBeanDefinitionReader 和 AutowiredAnnotationBeanPostProcessor )实现的,并作为核心元数据表示对共享的BeanDefinition 对象进行操作。这是 Spring 容器如此灵活和可扩展的本质。

BeanFactory 还是 ApplicationContext?

本节解释 BeanFactory 和 ApplicationContext 容器级别之间的差异以及对引导的影响。

除非有充分的理由,否则应该使用 ApplicationContext,将 GenericApplicationContext 及其子类 AnnotationConfigApplicationContext 作为自定义引导的常见实现。这些是 Spring 核心容器所有常见用途的主要入口点:加载配置文件、触发类路径扫描、以编程方式注册 beandefinitions (定义)和带注解的类,以及(从5.0开始)注册功能 bean definitions(定义)。

因为 ApplicationContext 包含了 BeanFactory 的所有功能,所以除了需要完全控制 bean 处理的场景之外,通常推荐使用它而不是普通的BeanFactory。在 ApplicationContext (如 GenericApplicationContext 实现)中,根据约定(即,根据 bean 名称或 bean 类型,特别是后处理器)检测几种 bean,而普通的 DefaultListableBeanFactory 不知道任何特殊的 bean。

对于许多扩展的容器特性,比如注释处理和 AOP 代理,BeanPostProcessor 扩展点参考【容器扩展点 - 使用 BeanPostProcessor 定制 Beans 】是必不可少的。如果只使用普通的 DefaultListableBeanFactory,默认情况下不会检测和激活这样的后处理器。这种情况可能会令人困惑,因为您的 bean 配置实际上没有任何问题。相反,在这种情况下,容器需要通过额外的设置来完全引导。

下表列出了 BeanFactory 和 ApplicationContext 接口和实现提供的功能。

特点BeanFactoryApplicationContext
Bean实例化/连接YesYes
集成的生命周期管理NoYes
自动 BeanPostProcessor 注册NoYes
自动 BeanFactoryPostProcessor 注册NoYes
方便的 MessageSource(消息源)访问(用于国际化)NoYes
内置的 ApplicationEvent(应用程序事件)发布机制NoYes

要使用 DefaultListableBeanFactory 显式注册 bean 后处理器,您需要以编程方式调用 addBeanPostProcessor,如下例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 用 bean definitions(定义) 填充工厂

// 现在注册任何需要的 BeanPostProcessor 实例
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// 现在开始使用工厂

要将 BeanFactoryPostProcessor 应用于普通的 DefaultListableBeanFactory,需要调用它的 postProcessBeanFactory 方法,如下例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// 从 Properties (属性) 文件中引入一些属性值
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// 现在实际上做替换
cfg.postProcessBeanFactory(factory);

在这两种情况下,显式注册步骤都不方便,这就是为什么在 Spring 支持的应用程序中,各种 ApplicationContext 变体比普通的DefaultListableBeanFactory 更受欢迎,特别是在典型的企业设置中,当依赖于 BeanFactoryPostProcessor 和 BeanPostProcessor 实例来实现扩展容器功能时。

AnnotationConfigApplicationContext 注册了所有常见的注解后处理器,并可能通过配置注解(如@EnableTransactionManagement) 在幕后引入其他处理器。在 Spring 基于注解的配置模型的抽象层次上,bean 后处理器的概念变成了纯粹的内部容器细节。

以上即是 Spring 官方文档 - 核心技术 - IoC容器的全部内容。
整个 IoC 容器篇幅占总核心文档的二分之一。
参考文档版本:Version 5.2.22.RELEASE

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值