Core Technologies

参考文档的这一部分涵盖了 Spring 框架必不可少的所有技术。

其中最重要的是 Spring 框架的控制反转(IoC)容器。对 Spring 框架的 IoC 容器进行彻底处理之后,将全面介绍 Spring 的面向方面编程(AOP)技术。 Spring 框架具有自己的 AOP 框架,该框架在概念上易于理解,并且成功解决了 Java 企业编程中 AOP 要求的 80%的难题。

还提供了 Spring 与 AspectJ 的集成(就功能而言,目前是功能最丰富的 Java,当然还有 Java 企业领域中最成熟的 AOP 实现)。

1. IoC 容器

本章介绍了 Spring 的控制反转(IoC)容器。

1.1. Spring IoC 容器和 Bean 简介

本章介绍了反转控制(IoC)原则的 Spring 框架实现。 (请参阅控制反转。)IoC 也称为依赖项注入(DI)。在此过程中,对象仅通过构造函数参数,工厂方法的参数或在构造或从工厂方法返回后在对象实例上设置的属性来定义其依赖项(即,与它们一起使用的其他对象) 。然后,容器在创建 bean 时注入那些依赖项。此过程从根本上讲是通过使用类的直接构造或诸如服务定位器模式之类的控件来控制其依赖项的实例化或位置的 bean 本身的逆过程(因此称为 Control Inversion)。

org.springframework.beansorg.springframework.context软件包是 Spring Framework 的 IoC 容器的基础。 BeanFactory接口提供了一种高级配置机制,能够 Management 任何类型的对象。 ApplicationContextBeanFactory的子接口。它增加了:

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

在 Spring 中,构成应用程序主干并由 Spring IoC 容器 Management 的对象称为 bean。 Bean 是由 Spring IoC 容器实例化,组装和以其他方式 Management 的对象。否则,bean 仅仅是应用程序中许多对象之一。 Bean 及其之间的依赖关系反映在容器使用的配置元数据中。

1.2. 容器概述

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

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

在大多数应用场景中,不需要实例化用户代码即可实例化一个 Spring IoC 容器的一个或多个实例。例如,在 Web 应用程序场景中,应用程序web.xml文件中的简单八行(约)样板 WebDescriptorsXML 通常就足够了(请参阅Web 应用程序的便捷 ApplicationContext 实例化)。如果使用Spring 工具套件(基于 Eclipse 的开发环境),则只需单击几次鼠标或击键即可轻松创建此样板配置。

下图显示了 Spring 的工作原理的高级视图。您的应用程序类与配置元数据结合在一起,以便在创建和初始化ApplicationContext之后,您将具有完全配置且可执行的系统或应用程序。

图 1. Spring IoC 容器

1.2.1. 配置元数据

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

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

Note

基于 XML 的元数据不是配置元数据的唯一允许形式。 Spring IoC 容器本身与实际写入此配置元数据的格式完全脱钩。如今,许多开发人员为自己的 Spring 应用程序选择Java-based configuration

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

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

这些 bean 定义对应于组成应用程序的实际对象。通常,您定义服务层对象,数据访问对象(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
        http://www.springframework.org/schema/beans/spring-beans.xsd">

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

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

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

</beans>

id属性的值是指协作对象。在此示例中未显示用于引用协作对象的 XML。有关更多信息,请参见Dependencies

1.2.2. 实例化容器

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

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

Note

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

以下示例显示了服务层对象(services.xml)配置文件:

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

    <!-- services -->

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

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

</beans>

以下示例显示了数据访问对象daos.xml文件:

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

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

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

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

</beans>

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

构成基于 XML 的配置元数据

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

您可以使用应用程序上下文构造函数从所有这些 XML 片段中加载 bean 定义。该构造函数具有多个Resource位置,如previous section所示。或者,使用一个或多个<import/>元素从另一个文件中加载 bean 定义。以下示例显示了如何执行此操作:

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

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

在前面的示例中,从三个文件services.xmlmessageSource.xmlthemeSource.xml加载了外部 bean 定义。所有位置路径都相对于进行导入的定义文件,因此services.xml必须与进行导入的文件位于同一目录或 Classpath 位置,而messageSource.xmlthemeSource.xml必须位于导入文件位置下方的resources位置。如您所见,斜杠被忽略。但是,鉴于这些路径是相对的,最好不要使用任何斜线。根据 Spring Schema,导入的文件的内容(包括顶级<beans/>元素)必须是有效的 XML bean 定义。

Note

可以但不建议使用相对的“ ../”路径引用父目录中的文件。这样做会创建对当前应用程序外部文件的依赖关系。特别是,不建议对classpath: URL(例如classpath:../services.xml)使用此引用,在 URL 中,运行时解析过程选择“最近”Classpath 根,然后查看其父目录。Classpath 配置的更改可能导致选择其他错误的目录。

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

名称空间本身提供了导入指令功能。 Spring 提供的一系列 XML 名称空间(例如contextutil名称空间)中提供了超出普通 bean 定义的其他配置功能。

Groovy Bean 定义 DSL

作为外部化配置元数据的另一个示例,Bean 定义也可以在 Spring 的 Groovy Bean 定义 DSL 中表达,如 Grails 框架所知。通常,这种配置位于“ .groovy”文件中,其结构如以下示例所示:

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

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

1.2.3. 使用容器

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

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

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

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

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

使用 Groovy 配置,引导看起来非常相似。它有一个不同的上下文实现类,该类可识别 Groovy(但也了解 XML Bean 定义)。以下示例显示了 Groovy 配置:

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

最灵活的变体是GenericApplicationContext与阅 Reader 委托组合,例如,对于 XML 文件,是XmlBeanDefinitionReader,如以下示例所示:

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

您还可以将GroovyBeanDefinitionReader用于 Groovy 文件,如以下示例所示:

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

您可以在相同的ApplicationContext上混合并匹配此类阅读器委托,从不同的配置源读取 Bean 定义。

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

1.3. Bean 总览

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

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

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

表 1. bean 定义

Property Explained in…
Class Instantiating Beans
Name Naming Beans
Scope Bean Scopes
Constructor arguments Dependency Injection
Properties Dependency Injection
Autowiring mode Autowiring Collaborators
延迟初始化模式 Lazy-initialized Beans
Initialization method Initialization Callbacks
Destruction method Destruction Callbacks

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

Note

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

1.3.1. 命名 bean

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

在基于 XML 的配置元数据中,您可以使用id属性和name属性,或同时使用这两者来指定 bean 标识符。 id属性可让您精确指定一个 ID。按照惯例,这些名称是字母数字(“ myBean”,“ someService”等),但它们也可以包含特殊字符。如果要为 bean 引入其他别名,还可以在name属性中指定它们,并用逗号(,),分号(;)或空格分隔。作为历史记录,在 Spring 3.1 之前的版本中,id属性定义为xsd:ID类型,该类型限制了可能的字符。从 3.1 开始,它被定义为xsd:string类型。请注意,bean id的唯一性仍由容器强制执行,尽管不再由 XML 解析器执行。

您不需要为 Bean 提供nameid。如果未明确提供nameid,则容器将为该 bean 生成一个唯一的名称。但是,如果要按名称引用该 bean,则通过使用ref元素或Service Locator样式查找,必须提供一个名称。不提供名称的动机与使用inner beansautowiring collaborators有关。

Bean 命名约定

约定是在命名 bean 时将标准 Java 约定用于实例字段名称。也就是说,bean 名称以小写字母开头,并从那里用驼峰式大小写。这样的名称的示例包括accountManageraccountServiceuserDaologinController等。

一致地命名 Bean 使您的配置更易于阅读和理解。另外,如果您使用 Spring AOP,则在将建议应用于按名称相关的一组 bean 时,它会很有帮助。

Note

通过在 Classpath 中进行组件扫描,Spring 会按照前面描述的规则为未命名的组件生成 Bean 名称:从本质上讲,采用简单的类名称并将其初始字符转换为小写。但是,在(不寻常的)特殊情况下,如果有多个字符并且第一个和第二个字符均为大写字母,则会保留原始大小写。这些规则与java.beans.Introspector.decapitalize(Spring 在此使用)定义的规则相同。

在 Bean 定义之外别名 Bean

在 bean 定义本身中,可以使用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 配置,则@Bean注解可用于提供别名。有关详情,请参见使用@Bean注解

1.3.2. 实例化 bean

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

如果使用基于 XML 的配置元数据,则可以在<bean/>元素的class属性中指定要实例化的对象的类型(或类)。通常,此class属性(在内部是BeanDefinition实例上的Class属性)。 (有关 exceptions,请参阅使用实例工厂方法实例化Bean 定义继承。)可以通过以下两种方式之一使用Class属性:

内部类名称

如果要为static嵌套类配置 Bean 定义,则必须使用嵌套类的二进制名称。

例如,如果您在com.example包中有一个名为SomeThing的类,并且此SomeThing类具有一个名为OtherThingstatic嵌套类,则 Bean 定义上class属性的值为com.example.SomeThing$OtherThing

请注意,名称中使用了$字符以将嵌套的类名与外部类名分开。

用构造函数实例化

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

Spring IoC 容器几乎可以 Management 您要 Management 的任何类。它不仅限于 Managementtrue 的 JavaBean。大多数 Spring 用户更喜欢实际的 JavaBean,它仅具有默认(无参数)构造函数,并具有根据容器中的属性建模的适当的 setter 和 getter。您还可以在容器中具有更多奇特的非 bean 样式类。例如,如果您需要使用绝对不符合 JavaBean 规范的旧式连接池,则 Spring 也可以对其进行 Management。

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

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

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

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

使用静态工厂方法实例化

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

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

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

以下示例显示了可与前面的 bean 定义一起使用的类:

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

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

有关为工厂方法提供(可选)参数并在从工厂返回对象后设置对象实例属性的机制的详细信息,请参见依赖性和详细配置

使用实例工厂方法实例化

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

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

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

以下示例显示了相应的 Java 类:

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}

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

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

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

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

以下示例显示了相应的 Java 类:

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    private static AccountService accountService = new AccountServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }

    public AccountService createAccountServiceInstance() {
        return accountService;
    }
}

这种方法表明,工厂 Bean 本身可以通过依赖项注入(DI)进行 Management 和配置。参见依赖性和详细配置

Note

在 Spring 文档中,“ factory bean”是指在 Spring 容器中配置并通过instancestatic工厂方法创建对象的 Bean。相反,FactoryBean(注意大小写)是指特定于 Spring 的FactoryBean

1.4. Dependencies

典型的企业应用程序不包含单个对象(或 Spring 术语中的 bean)。即使是最简单的应用程序,也有一些对象可以协同工作,以呈现最终用户视为一致的应用程序。下一部分将说明如何从定义多个独立的 Bean 定义到实现对象协作以实现目标的完全实现的应用程序。

1.4.1. 依赖注入

依赖注入(DI)是一个过程,通过该过程,对象只能通过构造函数参数,工厂方法的参数或在构造或创建对象实例后在对象实例上设置的属性来定义其依赖关系(即,与它们一起工作的其他对象)。从工厂方法返回。然后,容器在创建 bean 时注入那些依赖项。从根本上讲,此过程是通过使用类的直接构造或服务定位器模式来自己控制其依赖关系的实例化或位置的 Bean 本身的逆过程(因此称为 Control Inversion)。

使用 DI 原理,代码更简洁,当为对象提供依赖项时,去耦会更有效。该对象不查找其依赖项,也不知道依赖项的位置或类。结果,您的类变得更易于测试,尤其是当依赖项依赖于接口或抽象 Base Class 时,它们允许在单元测试中使用存根或模拟实现。

DI 存在两个主要变体:基于构造函数的依赖注入基于 Setter 的依赖注入

基于构造函数的依赖关系注入

基于构造函数的 DI 是通过容器调用具有多个参数(每个参数代表一个依赖项)的构造函数来完成的。调用带有特定参数的static工厂方法来构造 Bean 几乎是等效的,并且本次讨论也将构造函数和static工厂方法的参数视为类似。以下示例显示了只能通过构造函数注入进行依赖项注入的类:

public class SimpleMovieLister {

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

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

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

注意,该类没有什么特别的。它是一个 POJO,不依赖于特定于容器的接口,Base Class 或 注解。

构造函数参数解析

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

package x.y;

public class ThingOne {

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

假设ThingTwoThingThree类没有通过继承关联,则不存在潜在的歧义。因此,以下配置可以正常工作,并且您无需在<constructor-arg/>元素中显式指定构造函数参数索引或类型。

<beans>
    <bean id="thingOne" class="x.y.ThingOne">
        <constructor-arg ref="thingTwo"/>
        <constructor-arg ref="thingThree"/>
    </bean>

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

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

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

package examples;

public class ExampleBean {

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

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

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

构造函数参数类型匹配

在上述情况下,如果您使用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>

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

Note

索引从 0 开始。

构造函数参数名称

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

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

请记住,要立即使用该功能,必须在启用调试标志的情况下编译代码,以便 Spring 可以从构造函数中查找参数名称。如果您不能或不想使用 debug 标志编译代码,则可以使用@ConstructorProperties JDK注解 显式命名构造函数参数。然后,该示例类必须如下所示:

package examples;

public class ExampleBean {

    // Fields omitted

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

基于设置器的 DI 是通过在调用无参数构造函数或无参数static工厂方法以实例化您的 bean 之后,在您的 bean 上调用 setter 方法来完成的。

下面的示例显示只能通过使用纯 setter 注入来依赖注入的类。此类是常规的 Java。它是一个 POJO,不依赖于容器特定的接口,Base Class 或 注解。

public class SimpleMovieLister {

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

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

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

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

基于构造函数或基于 setter 的 DI?

由于可以混合使用基于构造函数的 DI 和基于 setter 的 DI,因此将构造函数用于强制性依赖项并将 setter 方法或配置方法用于可选依赖性是一个很好的经验法则。请注意,在 setter 方法上使用@Required注解可用于使属性成为必需的依赖项。

Spring 团队通常提倡构造函数注入,因为它可以让您将应用程序组件实现为不可变对象,并确保所需的依赖项不是null。此外,注入构造函数的组件始终以完全初始化的状态返回到 Client 端(调用)代码。附带说明一下,大量的构造函数自变量是一种不好的代码味道,这意味着该类可能承担了太多的职责,应该对其进行重构以更好地解决关注点分离问题。

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

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

依赖关系解决流程

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

在创建容器时,Spring 容器会验证每个 bean 的配置。但是,在实际创建 Bean 之前,不会设置 Bean 属性本身。创建容器时,将创建具有单例作用域并设置为预先实例化(默认)的 Bean。范围在Bean Scopes中定义。否则,仅在请求时才创建 Bean。创建和分配 bean 的依赖关系及其依赖关系(依此类推)时,创建 bean 可能会导致创建一个 bean 图。请注意,这些依赖项之间的分辨率不匹配可能会在后期出现,即在第一次创建受影响的 bean 时。

Circular dependencies

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

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

一种可能的解决方案是编辑某些类的源代码,这些类的源代码由设置者而不是构造函数来配置。或者,避免构造函数注入,而仅使用 setter 注入。换句话说,尽管不建议这样做,但是您可以使用 setter 注入配置循环依赖关系。

与典型情况(没有循环依赖项)不同,bean A 和 bean B 之间的循环依赖关系迫使其中一个 bean 在完全初始化之前被注入另一个 bean(经典的“养鸡和鸡蛋”场景)。

通常,您可以信任 Spring 做正确的事。它在容器加载时检测配置问题,例如对不存在的 Bean 的引用和循环依赖项。在实际创建 Bean 时,Spring 设置属性并尽可能晚地解决依赖关系。这意味着如果创建该对象或其依赖项之一时出现问题,则正确加载了 Spring 的容器以后可以在您请求对象时生成异常-例如,由于缺少或无效,Bean 引发异常属性。某些配置问题的这种潜在的延迟可见性是为什么默认情况下ApplicationContext实现会实例化单例 bean。在实际需要它们之前,要花一些前期时间和内存来创建它们,您会在创建ApplicationContext时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是预先实例化。

如果不存在循环依赖关系,则在将一个或多个协作 Bean 注入从属 Bean 时,每个协作 Bean 都将被完全配置,然后再注入到从属 Bean 中。这意味着,如果 bean A 依赖于 bean B,则 Spring IoC 容器会在对 bean A 调用 setter 方法之前完全配置 beanB。换句话说,实例化了 bean(如果它不是预先实例化的单例) ),设置其依赖项,并调用相关的生命周期方法(例如配置的 init 方法InitializingBean 回调方法)。

依赖项注入的示例

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

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

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

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

以下示例显示了相应的ExampleBean类:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

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

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

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

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

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

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

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

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

以下示例显示了相应的ExampleBean类:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

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

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

现在考虑这个示例的一个变体,在该变体中,不是使用构造函数,而是告诉 Spring 调用static工厂方法以返回对象的实例:

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

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

以下示例显示了相应的ExampleBean类:

public class ExampleBean {

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

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

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

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

1.4.2. 依赖性和详细配置

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

直值(Primitives,字符串等)

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

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

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

</beans>

前面的 XML 更简洁。但是,除非在创建 bean 定义时使用支持自动属性完成的 IDE(例如IntelliJ IDEASpring 工具套件),否则错字是在运行时而不是设计时发现的。强烈建议您使用此类 IDE 帮助。

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

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

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

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 定义代码段(在运行时)与以下代码段完全等效:

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

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

第一种形式优于第二种形式,因为使用idref标签可以使容器在部署时验证所引用的命名 Bean 实际上是否存在。在第二个变体中,不对传递给client bean 的targetName属性的值执行验证。仅在实际实例化client bean 时才发现拼写错误(极有可能导致致命的结果)。如果client bean 是prototype bean,则只能在部署容器很长时间之后发现此错字和所产生的异常。

Note

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

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

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

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

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

<ref bean="someBean"/>

通过parent属性指定目标 bean 将创建对当前容器的父容器中的 bean 的引用。 parent属性的值可以与目标 Bean 的id属性或目标 Bean 的name属性中的值相同。目标 Bean 必须位于当前容器的父容器中。主要在具有容器层次结构并且要使用与父 bean 名称相同的代理将现有 bean 封装在父容器中时,才应使用此 bean 参考变量。以下一对清单显示了如何使用parent属性:

<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
</bean>

Note

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

Inner Beans

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

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

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

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

Collections

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

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

Map 键或值的值或设置值也可以是以下任意元素:

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

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

关于合并的本节讨论了父子 bean 机制。不熟悉父 bean 和子 bean 定义的 Reader 可能希望先阅读relevant section,然后再 continue。

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

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">[emailprotected]</prop>
                <prop key="support">[emailprotected]</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">[emailprotected]</prop>
                <prop key="support">[emailprotected]</prop>
            </props>
        </property>
    </bean>
<beans>

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

[emailprotected]
[emailprotected]
[emailprotected]

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

此合并行为类似地适用于<list/><map/><set/>集合类型。在<list/>元素的特定情况下,将保留与List集合类型关联的语义(即,值ordered集合的概念)。父级的值位于所有子级列表的值之前。对于MapSetProperties集合类型,不存在排序。因此,对于容器内部使用的相关MapSetProperties实现类型基础的集合类型,没有排序语义有效。

集合合并的限制

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

Strongly-typed collection

随着 Java 5 中泛型类型的引入,您可以使用强类型集合。也就是说,可以声明Collection类型,使其只能包含(例如)String元素。如果使用 Spring 将强类型的Collection依赖项注入到 bean 中,则可以利用 Spring 的类型转换支持,以便将强类型Collection实例的元素转换为适当的类型,然后再添加到Collection。以下 Java 类和 bean 定义显示了如何执行此操作:

public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
<beans>
    <bean id="something" class="x.y.SomeClass">
        <property name="accounts">
            <map>
                <entry key="one" value="9.99"/>
                <entry key="two" value="2.75"/>
                <entry key="six" value="3.99"/>
            </map>
        </property>
    </bean>
</beans>

当准备注入something bean 的accounts属性时,可以通过反射获得有关强类型Map<String, Float>的元素类型的泛型信息。因此,Spring 的类型转换基础结构将各种值元素识别为Float类型,并且字符串值(9.99, 2.753.99)被转换为实际的Float类型。

空字符串值和空字符串

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

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

前面的示例等效于以下 Java 代码:

exampleBean.setEmail("");

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

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

前面的配置等效于下面的 Java 代码:

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

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

Spring 支持基于 XML Schema 定义的可扩展配置格式with namespaces。本章讨论的beans配置格式在 XML Schema 文档中定义。但是,p 命名空间未在 XSD 文件中定义,仅存在于 Spring 的核心中。

下面的示例显示了两个 XML 代码段(第一个使用标准 XML 格式,第二个使用 p 命名空间),它们可以解析为相同的结果:

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

    <bean name="classic" class="com.example.ExampleBean">
        <property name="email" value="[emailprotected]"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="[emailprotected]"/>
</beans>

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

下一个示例包括另外两个 bean 定义,它们都引用了另一个 bean:

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

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

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

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

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

Note

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

具有 c-namespace 的 XML 快捷方式

具有 p-命名空间的 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
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="thingOne" class="x.y.ThingTwo"/>
    <bean id="thingTwo" class="x.y.ThingThree"/>

    <!-- traditional declaration -->
    <bean id="thingOne" class="x.y.ThingOne">
        <constructor-arg ref="thingTwo"/>
        <constructor-arg ref="thingThree"/>
        <constructor-arg value="[emailprotected]"/>
    </bean>

    <!-- c-namespace declaration -->
    <bean id="thingOne" class="x.y.ThingOne" c:thingTwo-ref="thingTwo" c:thingThree-ref="thingThree" c:email="[emailprotected]"/>

</beans>

c:名称空间使用与p:相同的约定(对于 Bean 引用,尾随-ref)以其名称设置构造函数参数。同样,即使未在 XSD 模式中定义它(也存在于 Spring 内核中)也需要声明它。

对于极少数情况下无法使用构造函数自变量名称的情况(通常,如果字节码是在没有调试信息的情况下编译的),可以对参数索引使用后备,如下所示:

<!-- c-namespace index declaration -->
<bean id="thingOne" class="x.y.ThingOne" c:_0-ref="thingTwo" c:_1-ref="thingThree"/>

Note

由于 XML 语法的原因,索引符号要求前导_的存在,因为 XML 属性名称不能以数字开头(即使某些 IDE 允许)。

实际上,构造函数解析mechanism在匹配参数方面非常有效,因此,除非您确实需要,否则我们建议在整个配置过程中使用名称符号。

复合属性名称

设置 bean 属性时,可以使用复合属性名称或嵌套属性名称,只要路径中除最终属性名称之外的所有组件都不是null即可。考虑以下 bean 定义:

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

something bean 具有fred属性,该属性具有bob属性,该属性具有sammy属性,并且最终的sammy属性被设置为123的值。为了使它起作用,在构造 bean 之后,somethingfred属性和fredbob属性一定不能为null。否则,将抛出NullPointerException

1.4.3. 使用依赖

如果一个 bean 是另一个 bean 的依赖项,则通常意味着将一个 bean 设置为另一个 bean 的属性。通常,您可以使用基于 XML 的配置元数据中的<ref/> element完成此操作。但是,有时 bean 之间的依赖性不太直接。一个示例是何时需要触发类中的静态初始值设定项,例如用于数据库驱动程序注册。 depends-on属性可以在初始化使用此元素的 bean 之前显式强制初始化一个或多个 bean。以下示例使用depends-on属性表示对单个 bean 的依赖关系:

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

要表示对多个 bean 的依赖性,请提供一个 bean 名称列表作为depends-on属性的值(逗号,空格和分号是有效的分隔符):

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

Note

depends-on属性既可以指定初始化时间依赖性,也可以仅在singleton bean 的情况下指定相应的销毁时间依赖性。与给定 bean 定义depends-on关系的从属 bean 首先被销毁,然后再销毁给定 bean 本身。因此,depends-on也可以控制关闭 Sequences。

1.4.4. 懒初始化 bean

默认情况下,作为初始化过程的一部分,ApplicationContext实现会急于创建和配置所有singleton bean。通常,这种预初始化是可取的,因为与数小时甚至数天后相比,会立即发现配置或周围环境中的错误。如果不希望使用此行为,则可以通过将 bean 定义标记为延迟初始化来防止单例 bean 的预实例化。延迟初始化的 bean 告诉 IoC 容器在首次请求时而不是在启动时创建一个 bean 实例。

在 XML 中,此行为由<bean/>元素上的lazy-init属性控制,如以下示例所示:

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

ApplicationContext消耗了前面的配置时,ApplicationContext启动时就不会急于预先实例化lazy bean,而not.lazy Bean 则会急于预先实例化。

但是,当延迟初始化的 bean 是未延迟初始化的单例 bean 的依赖项时,ApplicationContext将在启动时创建延迟初始化的 bean,因为它必须满足单例的依赖关系。延迟初始化的 bean 被注入到其他未延迟初始化的单例 bean 中。

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

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

1.4.5. 自动布线合作者

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

使用基于 XML 的配置元数据时(请参见Dependency Injection),可以使用<bean/>元素的autowire属性为 bean 定义指定自动装配模式。自动装配功能具有四种模式。您可以为每个 bean 指定自动装配,因此可以选择要自动装配的装配。下表描述了四种自动装配模式:

表 2.自动装配模式

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

byTypeconstructor自动装配模式下,您可以连接阵列和键入的集合。在这种情况下,将提供容器中与期望类型匹配的所有自动装配候选,以满足相关性。如果预期的密钥类型为String,则可以自动连接强类型的Map实例。自动连接的Map实例的值包含与期望的类型匹配的所有 bean 实例,并且Map实例的键包含相应的 Bean 名称。

自动装配的局限性和缺点

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

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

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

从自动装配中排除 Bean

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

Note

autowire-candidate属性旨在仅影响基于类型的自动装配。它不会影响按名称显示的显式引用,即使未将指定的 Bean 标记为自动装配候选,该名称也可得到解析。因此,如果名称匹配,按名称自动装配仍会注入 Bean。

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

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

1.4.6. 方法注入

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

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

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

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

public class CommandManager implements ApplicationContextAware {

    private ApplicationContext applicationContext;

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

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

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

前面的内容是不理想的,因为业务代码知道并耦合到 Spring 框架。方法注入是 Spring IoC 容器的一项高级功能,使您可以干净地处理此用例。

您可以在此博客条目中阅读有关方法注入动机的更多信息。

查找方法注入

查找方法注入是容器重写容器 Management 的 Bean 上的方法并返回容器中另一个命名 Bean 的查找结果的能力。查找通常涉及原型 bean,如上一节中所述。 Spring 框架通过使用从 CGLIB 库生成字节码来动态生成覆盖该方法的子类来实现此方法注入。

Note

  • 为了使此动态子类起作用,Spring bean 容器子类的类也不能为final,并且要覆盖的方法也不能为final

  • 对具有abstract方法的类进行单元测试需要您自己对该类进行子类化,并提供abstract方法的存根实现。

  • 组件扫描也需要具体方法,这需要具体的类别。

  • 另一个关键限制是,查找方法不适用于工厂方法,尤其不适用于配置类中的@Bean方法,因为在这种情况下,容器不负责创建实例,因此无法创建运行时生成的子类在飞行中。

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

package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

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

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

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

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

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

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

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

每当需要myCommand bean 的新实例时,标识为commandManager的 bean 就会调用其自己的createCommand()方法。如果确实需要myCommand bean 作为原型,则必须小心。如果它是singleton,则每次都返回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 根据查找方法的声明的返回类型来解析:

public abstract class CommandManager {

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

    @Lookup
    protected abstract MyCommand createCommand();
}

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

Tip

访问范围不同的目标 bean 的另一种方法是ObjectFactory/Provider注入点。参见范围 bean 作为依赖项

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

任意方法替换

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

借助基于 XML 的配置元数据,您可以使用replaced-method元素将现有的方法实现替换为已部署的 Bean。考虑下面的类,它具有一个名为computeValue的方法,我们想重写该方法:

public class MyValueCalculator {

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

    // some other methods...
}

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

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

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

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

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

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

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

java.lang.String
String
Str

因为参数的数量通常足以区分每个可能的选择,所以通过让您仅键入与参数类型匹配的最短字符串,此快捷方式可以节省很多 Importing。

1.5. bean 范围

创建 bean 定义时,将创建一个配方来创建该 bean 定义所定义的类的实际实例。 bean 定义是配方的想法很重要,因为它意味着与类一样,您可以从一个配方中创建许多对象实例。

您不仅可以控制要插入到从特定 bean 定义创建的对象中的各种依赖项和配置值,还可以控制从特定 bean 定义创建的对象的范围。这种方法功能强大且灵活,因为您可以选择通过配置创建的对象的范围,而不必在 Java 类级别上烘烤对象的范围。可以将 Bean 定义为部署在多个范围之一中。 Spring 框架支持六个范围,其中只有在使用网络感知ApplicationContext时才可用。您也可以创建自定义范围。

下表描述了受支持的范围:

表 3. Bean 作用域

Scope Description
singleton (默认)将每个 Spring IoC 容器的单个 bean 定义范围限定为单个对象实例。
prototype 将单个 bean 定义的作用域限定为任意数量的对象实例。
request 将单个 bean 定义的范围限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有一个在单个 bean 定义后面创建的 bean 实例。仅在可感知网络的 Spring ApplicationContext中有效。
session 将单个 bean 定义的范围限定为 HTTP Session的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。
application 将单个 bean 定义的范围限定为ServletContext的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。
websocket 将单个 bean 定义的范围限定为WebSocket的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。

Note

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

1.5.1. 单例范围

仅 Management 一个 singleton bean 的一个共享实例,并且所有对具有 ID 或与该 bean 定义相匹配的 ID 的 bean 的请求都会导致该特定的 bean 实例由 Spring 容器返回。

换句话说,当您定义一个 bean 定义并且其作用域为单例时,Spring IoC 容器将为该 bean 定义所定义的对象创建一个实例。该单个实例存储在此类单例 bean 的高速缓存中,并且对该命名 bean 的所有后续请求和引用都返回该高速缓存的对象。下图显示了单例作用域如何工作:

Spring 的 singleton bean 的概念与“四人帮”(GoF)模式一书中定义的 singleton 模式不同。 GoF 单例对对象的范围进行硬编码,以使每个 ClassLoader 只能创建一个特定类的一个实例。最好将 Spring 单例的范围描述为每个容器和每个 bean。这意味着,如果您在单个 Spring 容器中为特定类定义一个 bean,则 Spring 容器将创建该 bean 定义所定义的类的一个且只有一个实例。 Singleton 范围是 Spring 中的默认范围。要将 bean 定义为 XML 中的单例,可以定义 bean,如以下示例所示:

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

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

1.5.2. 原型范围

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

下图说明了 Spring 原型范围:

(数据访问对象(DAO)通常不配置为原型,因为典型的 DAO 不拥有任何对话状态。对于我们而言,重用单例图的核心更为容易。)

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

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

与其他作用域相反,Spring 不 Management 原型 Bean 的完整生命周期。容器将实例化,配置或组装原型对象,然后将其交给 Client 端,而无需对该原型实例的进一步记录。因此,尽管在不考虑范围的情况下在所有对象上都调用了初始化生命周期回调方法,但在原型的情况下,不会调用已配置的销毁生命周期回调。Client 端代码必须清除原型作用域内的对象,并释放原型 Bean 拥有的昂贵资源。要使 Spring 容器释放由原型作用域的 bean 占用的资源,请尝试使用自定义bean post-processor,该自变量_1 包含对需要清理的 bean 的引用。

在某些方面,Spring 容器在原型作用域 bean 方面的角色是 Java new运算符的替代。超过该时间点的所有生命周期 Management 必须由 Client 端处理。 (有关 Spring 容器中 bean 的生命周期的详细信息,请参见Lifecycle Callbacks。)

1.5.3. 具有原型 Bean 依赖关系的 Singleton Bean

当您使用对原型 bean 有依赖性的单例作用域 Bean 时,请注意,依赖关系在实例化时已解决。因此,如果将依赖项原型的 bean 依赖项注入到单例范围的 bean 中,则将实例化新的原型 bean,然后将依赖项注入到单例 bean 中。原型实例是曾经提供给单例范围的 bean 的唯一实例。

但是,假设您希望单例作用域的 bean 在运行时重复获取原型作用域的 bean 的新实例。您不能将原型作用域的 bean 依赖项注入到您的单例 bean 中,因为当 Spring 容器实例化单例 bean 并解析并注入其依赖项时,该注入仅发生一次。如果在运行时多次需要一个原型 bean 的新实例,请参见Method Injection

1.5.4. 请求,会话,应用程序和 WebSocket 范围

requestsessionapplicationwebsocket范围仅在使用 Web 感知的 Spring ApplicationContext实现(例如XmlWebApplicationContext)时可用。如果将这些作用域与常规的 Spring IoC 容器(例如ClassPathXmlApplicationContext)一起使用,则会引发抱怨未知 bean 作用域的IllegalStateException

初始 Web 配置

为了支持requestsessionapplicationwebsocket级别(网络范围的 Bean)的 Bean 范围界定,在定义 Bean 之前,需要一些较小的初始配置。 (对于标准范围singletonprototype,不需要此初始设置.)

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

如果实际上在 Spring DispatcherServlet处理的请求中访问 Spring Web MVC 中的作用域 Bean,则不需要特殊的设置。 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。过滤器 Map 取决于周围的 Web 应用程序配置,因此您必须适当地对其进行更改。以下清单显示了 Web 应用程序的过滤器部分:

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

DispatcherServletRequestContextListenerRequestContextFilter都做完全相同的事情,即将 HTTP 请求对象绑定到正在为该请求提供服务的Thread上。这使得在请求链和会话范围内的 Bean 可以在调用链的更下游使用。

Request scope

考虑以下 XML 配置来定义 bean:

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

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

使用注解驱动的组件或 Java 配置时,可以使用@RequestScope注解 将组件分配给request范围。以下示例显示了如何执行此操作:

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

考虑以下 XML 配置来定义 bean:

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

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

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

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

考虑以下 XML 配置来定义 bean:

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

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

使用注解驱动的组件或 Java 配置时,可以使用@ApplicationScope注解 将组件分配给application范围。以下示例显示了如何执行此操作:

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}
作用域 Bean 作为依赖项

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

Note

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

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

同样,作用域代理不是以生命周期安全的方式从较短的作用域访问 bean 的唯一方法。您也可以将注入点(即构造器或 setter 参数或自动装配的字段)声明为ObjectFactory<MyTargetBean>,从而允许getObject()调用在需要时每次按需检索当前实例-holding 无需保留该实例或对其进行存储分别。

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

JSR-330 的这种变体称为Provider,并且每次检索尝试都使用Provider<MyTargetBean>声明和相应的get()调用。有关 JSR-330 总体的更多详细信息,请参见here

以下示例中的配置仅一行,但是了解其背后的“原因”和“方式”很重要:

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

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- instructs the container to proxy the surrounding bean -->
        <aop:scoped-proxy/> (1)
    </bean>

    <!-- a singleton-scoped bean injected with a proxy to the above bean -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

要创建这样的代理,请将子<aop:scoped-proxy/>元素插入到作用域 bean 定义中(请参见选择要创建的代理类型基于 XML 模式的配置)。为什么在requestsession和自定义范围级别定义的 bean 定义需要<aop:scoped-proxy/>元素?考虑以下单例 bean 定义,并将其与需要为上述范围定义的内容进行对比(请注意,下面的userPreferences bean 定义不完整):

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

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

在前面的示例中,单例 bean(userManager)注入了对 HTTP Session范围的 bean(userPreferences)的引用。这里的重点是userManager bean 是单例的:每个容器仅实例化一次,并且它的依赖项(在这种情况下,仅一个userPreferences bean)也仅注入一次。这意味着userManager bean 仅在完全相同的userPreferences对象(即最初与之注入对象)上操作。

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

因此,将request-session-scoped bean 注入到协作对象中时,您需要以下(正确和完整)配置,如以下示例所示:

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

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>
选择要创建的代理类型

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

Note

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

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

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

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

有关选择基于类或基于接口的代理的更多详细信息,请参见Proxying Mechanisms

1.5.5. 自定义范围

Bean 作用域机制是可扩展的。您可以定义自己的范围,甚至重新定义现有范围,尽管后者被认为是不好的做法,并且您不能覆盖内置的singletonprototype范围。

创建自定义范围

要将自定义范围集成到 Spring 容器中,您需要实现org.springframework.beans.factory.config.Scope接口,本节对此进行了介绍。有关如何实现自己的范围的想法,请参阅 Spring Framework 本身提供的Scope实现和Scope javadoc,它们详细说明了需要实现的方法。

Scope接口有四种方法可以从范围中获取对象,从范围中删除对象,然后销毁它们。

例如,会话范围实现返回会话范围的 Bean(如果不存在,则该方法将其绑定到会话以供将来参考之后,将返回该 Bean 的新实例)。以下方法从基础范围返回对象:

Object get(String name, ObjectFactory objectFactory)

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

Object remove(String name)

以下方法注册在销毁作用域或销毁作用域中的指定对象时作用域应执行的回调:

void registerDestructionCallback(String name, Runnable destructionCallback)

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

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

String getConversationId()

每个范围的标识符都不相同。对于会话范围的实现,此标识符可以是会话标识符。

使用自定义范围

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

void registerScope(String scopeName, Scope scope);

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

registerScope(..)方法的第一个参数是与范围关联的唯一名称。 Spring 容器本身中的此类名称示例为singletonprototyperegisterScope(..)方法的第二个参数是您希望注册和使用的自定义Scope实现的实际实例。

假设您编写了自定义Scope实现,然后如以下示例中所示进行注册。

Note

下一个示例使用SimpleThreadScope,它已包含在 Spring 中,但默认情况下未注册。这些说明与您自己的自定义Scope实现相同。

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

然后,您可以创建符合自定义Scope范围规则的 bean 定义,如下所示:

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

使用自定义Scope实现,您不仅可以通过程序注册该范围。您还可以通过使用CustomScopeConfigurer类以声明方式进行Scope注册,如以下示例所示:

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

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="thread">
                    <bean class="org.springframework.context.support.SimpleThreadScope"/>
                </entry>
            </map>
        </property>
    </bean>

    <bean id="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>

Note

当您将<aop:scoped-proxy/>放在FactoryBean实现中时,作用域是工厂 bean 本身,而不是getObject()返回的对象。

1.6. 自定义 bean 的性质

Spring 框架提供了许多接口,可用于自定义 Bean 的性质。本节将它们分组如下:

1.6.1. 生命周期回调

要与容器对 bean 生命周期的 Management 进行交互,可以实现 Spring InitializingBeanDisposableBean接口。容器对前者调用afterPropertiesSet(),对后者调用destroy(),以使 Bean 在初始化和销毁 Bean 时执行某些操作。

Tip

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

如果您不想使用 JSR-250 注解,但仍然想删除耦合,请考虑使用init-methoddestroy-method对象定义元数据。

在内部,Spring 框架使用BeanPostProcessor实现来处理它可以找到的任何回调接口并调用适当的方法。如果您需要自定义功能或其他生命周期行为,Spring 默认不提供,则您可以自己实现BeanPostProcessor。有关更多信息,请参见集装箱延伸点

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

本节介绍了生命周期回调接口。

Initialization Callbacks

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

void afterPropertiesSet() throws Exception;

我们建议您不要使用InitializingBean接口,因为它不必要地将代码耦合到 Spring。另外,我们建议使用@PostConstruct注解或指定 POJO 初始化方法。对于基于 XML 的配置元数据,可以使用init-method属性指定具有无效无参数签名的方法的名称。通过 Java 配置,可以使用@BeaninitMethod属性。参见接收生命周期回调。考虑以下示例:

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

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

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

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

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

但是,前面两个示例中的第一个示例并未将代码耦合到 Spring。

Destruction Callbacks

实现org.springframework.beans.factory.DisposableBean接口后,当包含 bean 的容器被销毁时,bean 可以获取回调。 DisposableBean接口指定一个方法:

void destroy() throws Exception;

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

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

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

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

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

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

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

Tip

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

默认初始化和销毁方法

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

您可以将 Spring 容器配置为“查找”命名的初始化,并销毁每个 bean 上的回调方法名称。这意味着,作为应用程序开发人员,您可以编写应用程序类并使用名为init()的初始化回调,而不必为每个 bean 定义配置init-method="init"属性。 Spring IoC 容器在创建 bean 时(并根据标准生命周期回调协定described previously)调用该方法。此功能还对初始化和销毁方法回调强制执行一致的命名约定。

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

public class DefaultBlogService implements BlogService {

    private BlogDao blogDao;

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

    // this is (unsurprisingly) the initialization callback method
    public void init() {
        if (this.blogDao == null) {
            throw new IllegalStateException("The [blogDao] property must be set.");
        }
    }
}

然后,您可以在类似于以下内容的 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属性类似地(在 XML 中)配置 destroy 方法回调。

如果现有的 Bean 类已经具有按惯例命名的回调方法,则可以通过使用<bean/>本身的init-methoddestroy-method属性指定(在 XML 中)方法名称来覆盖默认值。

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

组合生命周期机制

从 Spring 2.5 开始,您可以使用三个选项来控制 Bean 生命周期行为:

Note

如果为一个 bean 配置了多个生命周期机制,并且为每个机制配置了不同的方法名称,则将按照此注解后列出的 Sequences 执行每个已配置的方法。但是,如果为多个生命周期机制中的多个生命周期配置了相同的方法名称(例如,对于初始化方法使用init()),则该方法将执行一次,如preceding section中所述。

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

销毁方法的调用 Sequences 相同:

启动和关闭回调

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

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

任何 SpringManagement 的对象都可以实现Lifecycle接口。然后,当ApplicationContext本身接收到启动和停止 signal 时(例如,对于运行时的停止/重新启动场景),它将把这些调用级联到在该上下文中定义的所有Lifecycle实现。它通过委托LifecycleProcessor来完成此任务,如以下清单所示:

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

请注意,LifecycleProcessor本身是Lifecycle接口的扩展。它还添加了两种其他方法来响应正在刷新和关闭的上下文。

Tip

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

另外,请注意,不能保证会在销毁之前发出停止通知。在常规关闭时,在传播常规销毁回调之前,所有Lifecycle bean 都首先收到停止通知。但是,在上下文生存期内的热刷新或中止的刷新尝试中,仅调用 destroy 方法。

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

public interface Phased {

    int getPhase();
}

以下清单显示了SmartLifecycle接口的定义:

public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);
}

启动时,相位最低的对象首先启动。停止时,遵循相反的 Sequences。因此,实现SmartLifecycle且其getPhase()方法返回Integer.MIN_VALUE的对象将是第一个启动且最后一个停止的对象。在频谱的另一端,相位值Integer.MAX_VALUE表示该对象应最后启动并首先停止(可能是因为它取决于正在运行的其他进程)。考虑相位值时,重要的是要知道,任何未实现SmartLifecycle的“正常” Lifecycle对象的默认相位是0。因此,任何负相位值都表明对象应在这些标准组件之前开始(并在它们之后停止)。对于任何正相位值,反之亦然。

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

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

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

在非 Web 应用程序中正常关闭 Spring IoC 容器

Note

本节仅适用于非 Web 应用程序。 Spring 的基于 Web 的ApplicationContext实现已经具有适当的代码,可以在相关 Web 应用程序关闭时正常关闭 Spring IoC 容器。

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

要注册关闭钩子,请调用在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");

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

        // app runs here...

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

1.6.2. ApplicationContextAware 和 BeanNameAware

ApplicationContext创建实现org.springframework.context.ApplicationContextAware接口的对象实例时,该实例将获得对该ApplicationContext的引用。以下清单显示了ApplicationContextAware接口的定义:

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

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

从 Spring 2.5 开始,自动装配是获得对ApplicationContext的引用的另一种选择。 “传统” constructorbyType自动装配模式(如Autowiring Collaborators中所述)可以分别为构造函数参数或 setter 方法参数提供ApplicationContext类型的依赖项。为了获得更大的灵 Active,包括能够自动连接字段和使用多个参数方法,请使用基于注解的新自动装配功能。如果这样做,ApplicationContext将自动连接到期望使用ApplicationContext类型的字段,构造函数自变量或方法参数中(如果有问题的字段,构造函数或方法带有@Autowired注解)。有关更多信息,请参见Using @Autowired

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

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

在填充常规 bean 属性之后但在初始化回调(例如InitializingBeanafterPropertiesSet或自定义 init-method)之前调用该回调。

1.6.3. 其他感知接口

除了ApplicationContextAwareBeanNameAware(已讨论earlier)之外,Spring 还提供了Aware接口范围,这些接口使 Bean 向容器指示它们需要某种基础结构依赖性。通常,该名称很好地表明了依赖项类型。下表总结了最重要的Aware接口:

表 4.感知接口

Name Injected Dependency Explained in…
ApplicationContextAware 声明ApplicationContext ApplicationContextAware 和 BeanNameAware
ApplicationEventPublisherAware 附件ApplicationContext的事件发布者。 ApplicationContext 的其他功能
BeanClassLoaderAware 类加载器,用于加载 Bean 类。 Instantiating Beans
BeanFactoryAware 声明BeanFactory ApplicationContextAware 和 BeanNameAware
BeanNameAware 声明 bean 的名称。 ApplicationContextAware 和 BeanNameAware
BootstrapContextAware 运行容器的资源适配器BootstrapContext。通常仅在支持 JCA 的ApplicationContext实例中可用。 JCA CCI
LoadTimeWeaverAware 定义的编织器,用于在加载时处理类定义。 在 Spring Framework 中使用 AspectJ 进行加载时编织
MessageSourceAware 解决消息的已配置策略(支持参数化和国际化)。 ApplicationContext 的其他功能
NotificationPublisherAware Spring JMX 通知发布者。 Notifications
ResourceLoaderAware 配置的加载程序,用于对资源的低级别访问。 Resources
ServletConfigAware 当前容器运行的ServletConfig。仅在可感知网络的 Spring ApplicationContext中有效。 Spring MVC
ServletContextAware 当前容器运行的ServletContext。仅在可感知网络的 Spring ApplicationContext中有效。 Spring MVC

再次注意,使用这些接口会将您的代码与 Spring API 绑定在一起,并且不遵循“控制反转”样式。因此,我们建议将它们用于需要以编程方式访问容器的基础结构 Bean。

1.7. Bean 定义继承

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

如果您以编程方式使用ApplicationContext接口,则子 Bean 定义由ChildBeanDefinition类表示。大多数用户不在此级别上与他们合作。相反,它们在ClassPathXmlApplicationContext之类的类中声明性地配置 bean 定义。使用基于 XML 的配置元数据时,可以通过使用parent属性(将父 bean 指定为该属性的值)来指示子 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">  (1)
    <property name="name" value="override"/>
    <!-- the age property value of 1 will be inherited from parent -->
</bean>

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

子 bean 定义从父对象继承范围,构造函数参数值,属性值和方法替代,并可以选择添加新值。您指定的任何范围,初始化方法,destroy 方法或static工厂方法设置都会覆盖相应的父设置。

其余设置始终从子定义中获取:依赖项,自动装配模式,依赖项检查,单例和惰性初始化。

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

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

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

父 bean 不能单独实例化,因为它是不完整的,并且还被明确标记为abstract。当定义为abstract时,它只能用作纯模板 bean 定义,用作子定义的父定义。尝试单独使用此类abstract父 Bean(将其称为另一个 bean 的 ref 属性)或使用父 Bean ID 进行显式getBean()调用会返回错误。同样,容器的内部preInstantiateSingletons()方法将忽略定义为抽象的 bean 定义。

Note

ApplicationContext默认情况下预先实例化所有单例。因此,重要的是(至少对于单例 bean),如果您有一个(父)bean 定义仅打算用作模板,并且此定义指定了一个类,则必须确保设置* abstract 属性为 true *,否则应用程序上下文将实际(尝试)预先实例化abstract bean。

1.8. 集装箱延伸点

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

1.8.1. 使用 BeanPostProcessor 自定义 Bean

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

您可以配置多个BeanPostProcessor实例,并且可以通过设置order属性来控制这些BeanPostProcessor实例的执行 Sequences。仅当BeanPostProcessor实现Ordered接口时,才可以设置此属性。如果您编写自己的BeanPostProcessor,则也应该考虑实现Ordered接口。有关更多详细信息,请参见BeanPostProcessorOrdered接口的 javadoc。另请参见BeanPostProcessor 实例的编程注册上的 注解。

Note

BeanPostProcessor个实例在 bean(或对象)实例上运行。也就是说,Spring IoC 容器实例化一个 bean 实例,然后BeanPostProcessor个实例完成其工作。

BeanPostProcessor个实例按容器划分范围。仅在使用容器层次结构时,这才有意义。如果在一个容器中定义BeanPostProcessor,则它将仅对该容器中的 bean 进行后处理。换句话说,一个容器中定义的 Bean 不会被另一个容器中定义的BeanPostProcessor后处理,即使这两个容器是同一层次结构的一部分。

要更改实际的 bean 定义(即定义 bean 的蓝图),您需要使用BeanFactoryPostProcessor,如使用 BeanFactoryPostProcessor 自定义配置元数据中所述。

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

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

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

Programmatically registering BeanPostProcessor instances

推荐的BeanPostProcessor注册方法是通过ApplicationContext自动检测(如前所述),但是您可以使用addBeanPostProcessor通过ConfigurableBeanFactory以编程方式注册它们。当您需要在注册之前评估条件逻辑,甚至需要跨层次结构的上下文复制 Bean 后处理器时,这将非常有用。但是请注意,以编程方式添加的BeanPostProcessor实例不遵守Ordered接口。在这里,注册的 Sequences 决定了执行的 Sequences。另请注意,以编程方式注册的BeanPostProcessor实例始终在通过自动检测注册的实例之前进行处理,而不考虑任何明确的 Sequences。

BeanPostProcessor instances and AOP auto-proxying

实现BeanPostProcessor接口的类是特殊的,并且容器对它们的处理方式有所不同。它们直接引用的所有BeanPostProcessor实例和 Bean 在启动时都会实例化,作为ApplicationContext特殊启动阶段的一部分。接下来,以排序方式注册所有BeanPostProcessor实例,并将其应用于容器中的所有其他 bean。由于 AOP 自动代理本身是由BeanPostProcessor实现的,因此BeanPostProcessor实例或它们直接引用的 bean 都不适合进行自动代理,因此,没有编织的方面。

对于任何此类 bean,您应该看到一条参考日志消息:Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)

如果您通过自动装配或@Resource将 Bean 连接到BeanPostProcessor(可能会退回到自动装配),则 Spring 在搜索类型匹配的依赖项候选对象时可能会访问意外的 bean,因此使它们不符合自动代理或其他类型的条件。 Bean 后处理。例如,如果您有一个用@Resource注解 的依赖项,其中字段或设置器名称不直接与 bean 的声明名称相对应,并且不使用 name 属性,那么 Spring 将访问其他 bean 以按类型匹配它们。

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

示例:Hello World,BeanPostProcessor 风格

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

以下清单显示了自定义BeanPostProcessor实现类的定义:

package scripting;

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

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

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

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

    <lang:groovy id="messenger"
            script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
        <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
    </lang:groovy>

    <!--
    when the above bean (messenger) is instantiated, this custom
    BeanPostProcessor implementation will output the fact to the system console
    -->
    <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

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

以下 Java 应用程序运行上述代码和配置:

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

public final class Boot {

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

}

前面的应用程序的输出类似于以下内容:

Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMesse[emailprotected]
[emailprotected]
示例:RequiredAnnotationBeanPostProcessor

将回调接口或注解与自定义BeanPostProcessor实现结合使用是扩展 Spring IoC 容器的常用方法。一个示例是 Spring 的``实现,该实现随 Spring 发行版一起提供,并确保使用(任意)注解标记的 bean 上的 JavaBean 属性实际上(配置为)依赖注入了一个值。

1.8.2. 使用 BeanFactoryPostProcessor 自定义配置元数据

我们要看的下一个扩展点是org.springframework.beans.factory.config.BeanFactoryPostProcessor。此接口的语义与BeanPostProcessor相似,但有一个主要区别:BeanFactoryPostProcessor对 Bean 配置元数据进行操作。也就是说,Spring IoC 容器允许BeanFactoryPostProcessor读取配置元数据,并可能在容器实例化BeanFactoryPostProcessor实例以外的任何 bean 之前*对其进行更改。

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

Note

如果要更改实际的 bean 实例(即,从配置元数据创建的对象),则需要使用BeanPostProcessor(在使用 BeanPostProcessor 自定义 Bean前面进行了介绍)。尽管在技术上可以在BeanFactoryPostProcessor内使用 bean 实例(例如,通过使用BeanFactory.getBean()),但这样做会导致 bean 实例化过早,从而违反了标准容器的生命周期。这可能会导致负面影响,例如绕过 bean 后处理。

此外,每个容器都限制了BeanFactoryPostProcessor个实例的作用域。仅在使用容器层次结构时才有意义。如果在一个容器中定义BeanFactoryPostProcessor,则仅将其应用于该容器中的 bean 定义。一个容器中的 Bean 定义不会由另一个容器中的BeanFactoryPostProcessor实例进行后处理,即使两个容器都属于同一层次结构也是如此。

ApplicationContext内部声明 Bean 工厂后处理器时,将自动执行该处理器,以便将更改应用于定义容器的配置元数据。 Spring 包含许多 sched 义的 bean 工厂后处理器,例如PropertyOverrideConfigurerPropertyPlaceholderConfigurer。您还可以使用自定义BeanFactoryPostProcessor,例如,注册自定义属性编辑器。

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

Note

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

示例:类名替换 PropertyPlaceholderConfigurer

您可以使用标准 Java Properties格式,使用PropertyPlaceholderConfigurer来从单独文件中的 bean 定义外部化属性值。这样做使部署应用程序的人员可以自定义特定于环境的属性,例如数据库 URL 和密码,而不会为修改容器的一个或多个主要 XML 定义文件带来复杂性或风险。

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

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <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>

该示例显示了从外部Properties文件配置的属性。在运行时,将PropertyPlaceholderConfigurer应用于替换数据源的某些属性的元数据。将要替换的值指定为${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”,并且其他与属性文件中的键匹配的占位符值也是如此。 PropertyPlaceholderConfigurer检查 bean 定义的大多数属性和属性中的占位符。此外,您可以自定义占位符前缀和后缀。

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

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

PropertyPlaceholderConfigurer不仅在您指定的Properties文件中查找属性。默认情况下,如果无法在指定的属性文件中找到属性,则还会检查 Java System属性。您可以通过使用以下三个受支持的整数值之一设置配置程序的systemPropertiesMode属性来自定义此行为:

有关更多信息,请参见PropertyPlaceholderConfigurer javadoc。

Tip

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

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<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 的ApplicationContextpreInstantiateSingletons()阶段。

示例:PropertyOverrideConfigurer

PropertyOverrideConfigurer是另一个 Bean 工厂后处理程序,类似于PropertyPlaceholderConfigurer,但是与后者不同,原始定义对于 Bean 属性可以具有默认值或完全没有值。如果覆盖的Properties文件没有某个 bean 属性的条目,则使用默认的上下文定义。

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

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

beanName.property=value

下面的清单显示了格式的示例:

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

此示例文件可与包含定义为dataSource且具有driverurl属性的 Bean 的容器定义一起使用。

只要路径的每个组成部分(最终属性被覆盖)之外的所有组成部分都已经为非空(可能是由构造函数初始化),则也支持复合属性名。在以下示例中,将tom bean 的fred属性的bob属性的sammy属性设置为标量值123

tom.fred.bob.sammy=123

Note

指定的替代值始终是 Literals 值。它们不会转换为 bean 引用。当 XML bean 定义中的原始值指定 bean 引用时,此约定也适用。

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

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

1.8.3. 使用 FactoryBean 自定义实例化逻辑

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

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

FactoryBean界面提供了三种方法:

FactoryBean概念和界面在 Spring Framework 中的许多地方都使用过。 Spring 本身附带了FactoryBean接口的 50 多种实现。

当您需要向容器请求一个实际的FactoryBean实例本身而不是它生成的 bean 时,请在调用ApplicationContextgetBean()方法时在 bean 的id前面加上一个&符号(&)。因此,对于给定的id myBeanFactoryBean,在容器上调用getBean("myBean")返回FactoryBean的乘积,而调用getBean("&myBean")则返回FactoryBean实例本身。

1.9. 基于注解的容器配置

注解 在配置 Spring 方面比 XML 更好吗?

基于注解的配置的引入提出了一个问题,即这种方法是否比 XML“更好”。简短的答案是“取决于情况”。长话短说,每种方法都有其优缺点,通常,由开发人员决定哪种策略更适合他们。由于定义方式的不同,注解 在声明中提供了很多上下文,从而使配置更短,更简洁。但是,XML 擅长连接组件而不接触其源代码或重新编译它们。一些开发人员更喜欢将布线放置在靠近源的位置,而另一些开发人员则认为带注解的类不再是 POJO,而且,该配置变得分散且难以控制。

无论选择如何,Spring 都可以容纳两种样式,甚至可以将它们混合在一起。值得指出的是,通过其JavaConfig选项,Spring 允许以非侵入方式使用 注解,而无需接触目标组件的源代码,并且就工具而言,Spring 工具套件支持所有配置样式。

基于注解的配置提供了 XML 设置的替代方法,该配置依赖字节码元数据来连接组件,而不是尖括号声明。通过使用相关类,方法或字段声明上的 注解,开发人员无需使用 XML 来描述 bean 的连接,而是将配置移入组件类本身。如示例:RequiredAnnotationBeanPostProcessor中所述,结合使用BeanPostProcessor和注解是扩展 Spring IoC 容器的常用方法。例如,Spring 2.0 引入了使用@Required注解 强制执行必需属性的可能性。 Spring 2.5 使遵循相同的通用方法来驱动 Spring 的依赖注入成为可能。本质上,@Autowired注解提供了与Autowiring Collaborators中描述的功能相同的功能,但具有更细粒度的控制和更广泛的适用性。 Spring 2.5 还添加了对 JSR-250 注解的支持,例如@PostConstruct@PreDestroy。 Spring 3.0 添加了对javax.inject软件包(例如@Inject@Named)中包含的 JSR-330(Java 依赖性注入)注解 的支持。有关这些注解的详细信息,请参见relevant section

Note

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

与往常一样,您可以将它们注册为单独的 bean 定义,但是也可以通过在基于 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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

</beans>

(隐式注册的后处理器包括AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessorPersistenceAnnotationBeanPostProcessor和上述RequiredAnnotationBeanPostProcessor。)

Note

<context:annotation-config/>仅在定义它的相同应用程序上下文中查找 bean 上的 注解。这意味着,如果您将<context:annotation-config/>放在WebApplicationContext中而不是DispatcherServlet,则它仅检查控制器中的@Autowired bean,而不检查服务。有关更多信息,请参见The DispatcherServlet

1.9.1. @Required

@Required注解 适用于 bean 属性设置器方法,如以下示例所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

此注解指示必须在配置时通过 bean 定义中的显式属性值或通过自动装配来填充受影响的 bean 属性。如果受影响的 bean 属性尚未填充,则容器将引发异常。这允许急切和显式的故障,避免以后再出现NullPointerException个实例等。我们仍然建议您将 assert 放入 bean 类本身中(例如,放入 init 方法中)。这样做会强制执行那些必需的引用和值,即使您在容器外部使用该类也是如此。

1.9.2. 使用@Autowired

Note

在本节中包含的示例中,可以使用 JSR 330 的@Inject注解 代替 Spring 的@Autowired注解。有关更多详细信息,请参见here

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

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

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

    // ...
}

Note

从 Spring Framework 4.3 开始,如果目标 bean 仅定义一个构造函数作为开始,则不再需要在此类构造函数上使用@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;
    }

    // ...
}

Tip

确保目标组件(例如MovieCatalogCustomerPreferenceDao)由用于@Autowired注解 的注入点的类型一致地声明。否则,由于在运行时找不到类型匹配,注入可能会失败。

对于通过 Classpath 扫描找到的 XML 定义的 bean 或组件类,容器通常预先知道具体的类型。但是,对于@Bean工厂方法,您需要确保声明的返回类型具有足够的表现力。对于实现多个接口的组件或可能由其实现类型引用的组件,请考虑在工厂方法中声明最具体的返回类型(至少根据引用您的 bean 的注入点的要求具体声明)。

您还可以通过将注解添加到需要该类型数组的字段或方法中,来提供ApplicationContext中所有特定类型的 bean,如以下示例所示:

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}

如以下示例所示,这同样适用于类型化集合:

public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;

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

    // ...
}

Tip

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

您可以在目标类级别和@Bean方法上声明@Order注解,可能通过单个 bean 定义来声明(如果使用同一 bean 类的多个定义)。 @Order值可能会影响注入点的优先级,但是要注意,它们不会影响单例启动 Sequences,这是由依赖关系和@DependsOn声明确定的正交关注点。

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

只要预期的密钥类型为String,即使是键入的Map实例也可以自动装配。 Map 值包含所有预期类型的 bean,并且键包含相应的 bean 名称,如以下示例所示:

public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

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

    // ...
}

默认情况下,只要有零个候选 bean 可用,自动装配就会失败。默认行为是将带注解的方法,构造函数和字段视为指示所需的依赖项。在下面的示例中,您可以按照说明更改此行为:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

Note

每个类仅可以将一个带注解的构造函数标记为必需,但可以对多个非必需的构造函数进行 注解。在这种情况下,每个候选对象都将被考虑在内,并且 Spring 使用最贪婪的构造函数,其依赖关系可以得到满足-即具有最多参数的构造函数。

建议在@Required注解 上使用@Autowired的必需属性。 required 属性表示自动装配不需要该属性。如果无法自动装配该属性,则将其忽略。另一方面,@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):

public class SimpleMovieLister {

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

您也可以将@Autowired用于众所周知的可解决依赖项:BeanFactoryApplicationContextEnvironmentResourceLoaderApplicationEventPublisherMessageSource。这些接口及其扩展接口(例如ConfigurableApplicationContextResourcePatternResolver)将自动解析,而无需进行特殊设置。下面的示例自动连接ApplicationContext对象:

public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...
}

Note

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

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

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

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

@Configuration
public class MovieConfiguration {

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

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

    // ...
}

使用前面的配置,下面的MovieRecommenderfirstMovieCatalog自动连接:

public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...
}

相应的 bean 定义如下:

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

    <context:annotation-config/>

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

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

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

</beans>

1.9.4. 使用限定符对基于注解的自动装配进行微调

当可以确定一个主要候选对象时,@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 定义。

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

    <context:annotation-config/>

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

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

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

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

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

</beans>

对于后备匹配,bean 名称被视为默认的限定符值。因此,您可以用idmain定义 bean,而不是嵌套的限定符元素,从而得到相同的匹配结果。但是,尽管您可以使用此约定按名称引用特定的 bean,但@Autowired基本上是关于带有可选语义限定符的类型驱动的注入。这意味着,即使带有 Bean 名称后备的限定符值,在类型匹配集中也始终具有狭窄的语义。它们没有在语义上表示对唯一 bean id的引用。好的限定符值为mainEMEApersistent,它们表示特定组件的特性,它们独立于 Bean id,如果是匿名 Bean 定义(例如上例中的定义),则可以自动生成这些组件。

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

Tip

在类型匹配的候选对象中,让限定符值针对目标 bean 名称进行选择,在注入点不需要@Qualifier注解。如果没有其他解析度指示符(例如限定词或主标记),则对于非唯一依赖性情况,Spring 将注入点名称(即字段名称或参数名称)与目标 Bean 名称进行匹配,然后选择同名候选人(如果有)。

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

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

从 4.3 开始,@Autowired还考虑了自我引用以进行注入(即,引用回当前注入的 Bean)。请注意,自我注入是一个后备。对其他组件的常规依赖始终优先。从这个意义上说,自我推荐不参与常规的候选人选择,因此尤其是绝不是主要的。相反,它们总是以最低优先级结束。实际上,您应该仅将自我引用用作最后的手段(例如,通过 bean 的事务代理在同一实例上调用其他方法)。考虑在这种情况下将受影响的方法分解为单独的委托 Bean。或者,您可以使用@Resource,它可以通过其唯一名称获取返回到当前 bean 的代理。

@Autowired适用于字段,构造函数和多参数方法,从而允许在参数级别缩小限定符注解的范围。相反,仅具有单个参数的字段和 bean 属性设置器方法支持@Resource。因此,如果注入目标是构造函数或多参数方法,则应坚持使用限定符。

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

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

    String value();
}

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

public class MovieRecommender {

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

    private MovieCatalog comedyCatalog;

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

    // ...
}

接下来,您可以提供有关候选 bean 定义的信息。您可以将<qualifier/>标记添加为<bean/>标记的子元素,然后指定typevalue以匹配您的自定义限定符 注解。该类型与注解的完全限定的类名匹配。另外,为方便起见,如果不存在名称冲突的风险,则可以使用简短的类名。下面的示例演示了两种方法:

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

    <context:annotation-config/>

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

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

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

</beans>

Classpath 扫描和托管组件中,您可以看到基于注解的替代方法,以 XML 形式提供限定符元数据。具体来说,请参见提供带注解的限定符元数据

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

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

}

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

public class MovieRecommender {

    @Autowired
    @Offline (1)
    private MovieCatalog offlineCatalog;

    // ...
}

现在,bean 定义只需要一个限定符type,如以下示例所示:

<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline"/> (1)
    <!-- inject any dependencies required by this bean -->
</bean>

您还可以定义自定义限定符注解,除了简单的value属性之外,还可以接受命名属性。如果随后在要自动装配的字段或参数上指定了多个属性值,则 bean 定义必须与所有此类属性值匹配才能被视为自动装配候选。例如,请考虑以下注解定义:

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

    String genre();

    Format format();
}

在这种情况下,Format是一个枚举,定义如下:

public enum Format {
    VHS, DVD, BLURAY
}

要自动装配的字段将用定制限定符进行 注解,并包括两个属性genreformat的值,如以下示例所示:

public class MovieRecommender {

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

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

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

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

    // ...
}

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

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

    <context:annotation-config/>

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

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

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

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

</beans>

1.9.5. 将泛型用作自动装配限定符

除了@Qualifier注解,您还可以将 Java 泛型类型用作资格的隐式形式。例如,假设您具有以下配置:

@Configuration
public class MyConfiguration {

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

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

假设前面的 bean 实现了通用接口(即Store<String>Store<Integer>),则可以@Autowire Store接口,并且通用接口用作限定符,如以下示例所示:

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

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean

自动连接列表,Map实例和数组时,通用限定符也适用。以下示例将自动连接通用List

// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;

1.9.6. 使用 CustomAutowireConfigurer

CustomAutowireConfigurerBeanFactoryPostProcessor,即使您没有使用 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 可以作为自动装配候选者时,确定“主要”的步骤如下:如果候选者中恰好有一个 bean 定义具有primary属性设置为true,则将其选中。

1.9.7. 用@Resource 注入

Spring 还通过在字段或 bean 属性设置器方法上使用 JSR-250 @Resource注解 来支持注入。这是 Java EE 5 和 6 中的常见模式(例如,在 JSF 1.2 托管 Bean 或 JAX-WS 2.0 端点中)。 Spring 也为 SpringManagement 的对象支持此模式。

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

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") (1)
    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;
    }
}

Note

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

在未使用@Resource且未指定显式名称且与@Autowired类似的特殊情况下,@Resource查找主类型匹配而不是特定的命名 bean,并解析众所周知的可解决依赖项:BeanFactoryApplicationContextResourceLoaderApplicationEventPublisherMessageSource接口。

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

public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; (1)

    public MovieRecommender() {
    }

    // ...
}

1.9.8. 使用@PostConstruct 和@PreDestroy

CommonAnnotationBeanPostProcessor不仅可以识别@Resource注解,还可以识别 JSR-250 生命周期 注解。在 Spring 2.5 中引入了对这些注解的支持,为initialization callbacksdestruction callbacks中描述的注解提供了另一种选择。假设CommonAnnotationBeanPostProcessor已在 Spring ApplicationContext中注册,则在生命周期的同一点与相应的 Spring 生命周期接口方法或显式声明的回调方法一起调用带有这些注解之一的方法。在以下示例中,缓存在初始化时预先填充,并在销毁时清除:

public class CachingMovieLister {

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

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

Note

有关组合各种生命周期机制的效果的详细信息,请参见组合生命周期机制

1.10. Classpath 扫描和托管组件

本章中的大多数示例都使用 XML 来指定在 Spring 容器中生成每个BeanDefinition的配置元数据。上一节(基于注解的容器配置)演示了如何通过源级注解提供许多配置元数据。但是,即使在这些示例中,“基本” bean 定义也已在 XML 文件中明确定义,而注解仅驱动依赖项注入。本节介绍了通过扫描 Classpath 来隐式检测候选组件的选项。候选组件是与过滤条件匹配的类,并在容器中注册了相应的 Bean 定义。这消除了使用 XML 进行 bean 注册的需要。相反,您可以使用 注解(例如@Component),AspectJ 类型表达式或您自己的自定义过滤条件来选择哪些类已向容器注册了 bean 定义。

Note

从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多功能是核心 Spring Framework 的一部分。这使您可以使用 Java 而不是使用传统的 XML 文件来定义 bean。请查看@Configuration@Bean@Import@DependsOn注解,以获取有关如何使用这些新功能的示例。

1.10.1. @Component 和其他构造型 注解

@Repository注解 是满足存储库的角色或构造型(也称为数据访问对象或 DAO)的任何类的标记。如Exception Translation中所述,此标记的用途是自动翻译异常。

Spring 提供了进一步的构造型 注解:@Component@Service@Controller@Component是任何 SpringManagement 的组件的通用构造型。 @Repository@Service@Controller@Component的特化,用于更具体的用例(分别在持久层,服务层和表示层中)。因此,您可以使用@Component注解 组件类,但是通过使用@Repository@Service@Controller注解 组件类,则您的类更适合于通过工具进行处理或与方面相关联。例如,这些构造型注解成为切入点的理想目标。 @Repository@Service@Controller在 Spring 框架的 Future 发行版中还可包含其他语义。因此,如果在服务层使用@Component@Service之间进行选择,则@Service显然是更好的选择。同样,如前所述,@Repository已被支持作为持久层中自动异常转换的标记。

1.10.2. 使用元注解和组合 注解

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component (1)
public @interface Service {

    // ....
}

您还可以结合使用元注解来创建“组合 注解”。例如,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 {

    /**
     * Alias for {@link Scope#proxyMode}.
     * <p>Defaults to {@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 页面。

1.10.3. 自动检测类并注册 Bean 定义

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

@Service
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}
@Repository
public class JpaMovieFinder implements MovieFinder {
    // implementation elided for clarity
}

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

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

Note

为简洁起见,前面的示例可能使用了注解的value属性(即@ComponentScan("org.example"))。

以下替代方法使用 XML:

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

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

</beans>

Tip

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

Note

扫描 Classpath 包需要在 Classpath 中存在相应的目录条目。使用 Ant 构建 JAR 时,请确保未激活 JAR 任务的仅文件开关。另外,在某些环境中(例如,JDK 1.7.0_45 及更高版本上的独立应用程序(在清单中要求“受信任的库”设置”),见http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources),可能不会基于安全策略公开 Classpath 目录。

在 JDK 9 的模块路径(Jigsaw)上,Spring 的 Classpath 扫描通常可以按预期进行。但是,请确保将组件类导出到module-infoDescriptors 中。如果您希望 Spring 调用类的非公共成员,请确保它们已“打开”(即,它们在module-infoDescriptors 中使用opens声明而不是exports声明)。

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

Note

您可以通过包含注解设置属性false来禁用AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor的注册。

1.10.4. 使用过滤器自定义扫描

默认情况下,唯一检测到的候选组件是用@Component@Repository@Service@Controller注解 的类或本身用@Component注解 的定制 注解。但是,您可以通过应用自定义过滤器来修改和扩展此行为。将它们添加为@ComponentScan注解的includeFiltersexcludeFilters参数(或component-scan元素的include-filterexclude-filter子元素)。每个过滤器元素都需要typeexpression属性。下表描述了过滤选项:

表 5.过滤器类型

Filter Type Example Expression Description
annotation (default) org.example.SomeAnnotation 在目标组件的类型级别上存在的 注解。
assignable org.example.SomeClass 目标组件可分配给(扩展或实现)的类(或接口)。
aspectj org.example..*Service+ 目标组件要匹配的 AspectJ 类型表达式。
regex org\.example\.Default.* 要与目标组件类名称匹配的正则表达式。
custom org.example.MyTypeFilter org.springframework.core.type .TypeFilter接口的自定义实现。

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

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

Note

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

1.10.5. 在组件中定义 Bean 元数据

Spring 组件还可以将 bean 定义元数据贡献给容器。您可以使用与@Configuration带注解的类中定义 Bean 元数据相同的@Bean注解 来执行此操作。以下示例显示了如何执行此操作:

@Component
public class FactoryMethodComponent {

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

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

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

Tip

除了用于组件初始化的角色外,还可以将@Lazy注解 放置在标有@Autowired@Inject的注入点上。在这种情况下,它导致注入了惰性解析代理。

如前所述,支持自动连线的字段和方法,并自动支持@Bean方法的自动装配。以下示例显示了如何执行此操作:

@Component
public class FactoryMethodComponent {

    private static int i;

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

    // use of a custom qualifier and autowiring of method parameters
    @Bean
    protected TestBean protectedInstance(
            @Qualifier("public") TestBean spouse,
            @Value("#{privateInstance.age}") String country) {
        TestBean tb = new TestBean("protectedInstance", 1);
        tb.setSpouse(spouse);
        tb.setCountry(country);
        return tb;
    }

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

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

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

从 Spring Framework 4.3 开始,您还可以声明类型为InjectionPoint(或其更具体的子类:DependencyDescriptor)的工厂方法参数,以访问触发当前 bean 创建的请求注入点。注意,这仅适用于实际创建 bean 实例,而不适用于注入现有实例。因此,此功能对原型范围的 bean 最有意义。对于其他作用域,factory 方法仅在给定作用域中看到触发创建新 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类中相应方法的处理方式不同。不同之处在于,CGLIB 并未增强@Component类来拦截方法和字段的调用。 CGLIB 代理是调用@Configuration类中@Bean方法中的方法或字段中的字段的方法,用于创建 Bean 元数据引用以协作对象。此类方法不是用普通的 Java 语义调用的,而是通过容器进行的,以提供通常的生命周期 Management 和 Spring bean 的代理,即使通过编程调用@Bean方法引用其他 bean 时也是如此。相反,在普通@Component类内的@Bean方法中调用方法或字段具有标准 Java 语义,而无需特殊的 CGLIB 处理或其他约束。

Note

您可以将@Bean方法声明为static,从而允许在不将其包含的配置类创建为实例的情况下调用它们。在定义后处理器 Bean(例如,类型BeanFactoryPostProcessorBeanPostProcessor)时,这特别有意义,因为此类 Bean 在容器生命周期的早期进行了初始化,并且应避免在那时触发配置的其他部分。

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

@Bean方法的 Java 语言可见性不会对 Spring 容器中的最终 bean 定义产生直接影响。您可以随意声明自己的工厂方法,以适合非@Configuration类,也可以随处声明静态方法。但是,@Configuration类中的常规@Bean方法必须是可重写的—即,不得将它们声明为privatefinal

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

最后,单个类可以为同一个 bean 保留多个@Bean方法,这取决于在运行时可用的依赖关系,从而可以使用多个工厂方法。这与在其他配置方案中选择“最贪婪”的构造函数或工厂方法的算法相同:在构造时将选择具有最大可满足依赖关系数量的变量,类似于容器在多个@Autowired构造函数之间进行选择的方式。

1.10.6. 命名自动检测的组件

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

如果这样的注解不包含名称value或任何其他检测到的组件(例如,由自定义过滤器发现的组件),则缺省 bean 名称生成器将返回不使用大写字母的非限定类名称。例如,如果检测到以下组件类,则名称将为myMovieListermovieFinderImpl

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

Note

如果不想依赖默认的 Bean 命名策略,则可以提供自定义 Bean 命名策略。首先,实现BeanNameGenerator接口,并确保包括默认的 no-arg 构造函数。然后,在配置扫描器时提供完全限定的类名,如以下示例注解和 Bean 定义所示:

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

通常,请考虑在其他组件可能对其进行显式引用时,使用注解指定名称。另一方面,只要容器负责接线,自动生成的名称就足够了。

1.10.7. 提供自动检测组件的范围

通常,与 SpringManagement 的组件一样,自动检测到的组件的默认且最常见的范围是singleton。但是,有时您需要由@Scope注解 指定的其他范围。您可以在注解中提供范围的名称,如以下示例所示:

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

Note

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

有关特定于 Web 的作用域的详细信息,例如 Spring 上下文中的“请求”或“会话”,请参见请求,会话,应用程序和 WebSocket 范围。与这些范围的预构建注解一样,您也可以使用 Spring 的元注解方法来编写自己的作用域 注解:例如,使用@Scope("prototype")进行元注解的自定义 注解,也可能会声明自定义范围代理模式。

Note

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

@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    ...
}
<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

使用某些非单作用域时,可能有必要为作用域对象生成代理。推理在范围 bean 作为依赖项中描述。为此,在 component-scan 元素上可以使用 scoped-proxy 属性。三个可能的值是:nointerfacestargetClass。例如,以下配置产生标准的 JDK 动态代理:

@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
    ...
}
<beans>
    <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>

1.10.8. 提供带注解的限定符元数据

使用限定符对基于注解的自动装配进行微调中讨论了@Qualifier注解。该部分中的示例演示了@Qualifier注解和自定义限定符注解的使用,以在解析自动装配候选时提供细粒度的控制。因为这些示例是基于 XML bean 定义的,所以通过使用 XML 中bean元素的qualifiermeta子元素,在候选 bean 定义上提供了限定符元数据。当依靠 Classpath 扫描来自动检测组件时,可以在候选类上为限定符元数据提供类型级别的 注解。下面的三个示例演示了此技术:

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

Note

与大多数基于注解的替代方法一样,请记住,注解 元数据绑定到类定义本身,而 XML 的使用允许相同类型的多个 bean 提供其限定符元数据的变体,因为该元数据是按-instance 而不是按类。

1.10.9. 生成候选组件的索引

尽管 Classpath 扫描非常快,但可以通过在编译时创建候选静态列表来提高大型应用程序的启动性能。在这种模式下,应用程序的所有模块都必须使用此机制,因为当ApplicationContext检测到这样的索引时,它将自动使用它而不是扫描 Classpath。

要生成索引,请向每个包含组件的模块添加附加依赖关系,这些组件是组件扫描指令的目标。以下示例显示了如何使用 Maven 进行操作:

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

以下示例显示了如何使用 Gradle 进行操作:

dependencies {
    compileOnly("org.springframework:spring-context-indexer:5.1.3.RELEASE")
}

该过程将生成 jar 文件中包含的META-INF/spring.components文件。

Note

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

Tip

当在 Classpath 上找到META-INF/spring.components时,索引将自动启用。如果某些库(或用例)的索引部分可用,但无法为整个应用程序构建,则可以通过将spring.index.ignore设置为true来回退到常规的 Classpath 安排(好像根本没有索引)。系统属性或 Classpath 根目录下的spring.properties文件中。

1.11. 使用 JSR 330 标准 注解

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

Note

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

<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>

1.11.1. @Inject 和@Named 的依赖注入

可以使用@javax.inject.Inject代替@Autowired,如下所示:

import javax.inject.Inject;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

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

@Autowired一样,您可以在字段级别,方法级别和构造函数参数级别使用@Inject。此外,您可以将注入点声明为Provider,以允许按需访问范围较小的 bean,或者通过Provider.get()调用来懒惰地访问其他 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没有required属性。以下一对示例显示了如何使用@Inject@Nullable

public class SimpleMovieLister {

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

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

1.11.2. @Named 和@ManagedBean:@Component注解 的标准等效项

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

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

@Named("movieListener")  // @ManagedBean("movieListener") could be used as well
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  {
    ...
}

Note

@Component相反,JSR-330 @Named和 JSR-250 ManagedBean注解 是不可组合的。您应该使用 Spring 的构造型模型来构建自定义组件 注解。

1.11.3. JSR-330 标准注解的局限性

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

表 6. Spring 组件模型元素与 JSR-330 变体

Spring javax.inject.* javax.inject 限制/注解
@Autowired @Inject @Inject没有“必需”属性。可以与 Java 8 的Optional一起使用。
@Component @Named/@ManagedBean JSR-330 不提供可组合的模型,仅提供一种识别命名组件的方法。
@Scope("singleton") @Singleton JSR-330 的默认范围类似于 Spring 的prototype。但是,为了使其与 Spring 的常规默认设置保持一致,默认情况下,在 Spring 容器中声明的 JSR-330 bean 为singleton。为了使用singleton以外的范围,您应该使用 Spring 的@Scope注解。 javax.inject还提供@Scope注解。但是,此仅用于创建自己的 注解。
@Qualifier @ Qualifier/@ Named javax.inject.Qualifier只是用于构建自定义限定符的元 注解。可以通过javax.inject.Named关联具体的String限定词(如带有值的 Spring 的@Qualifier)。
@Value - no equivalent
@Required - no equivalent
@Lazy - no equivalent
ObjectFactory Provider javax.inject.Provider是 Spring 的ObjectFactory的直接替代方法,只是使用较短的get()方法名。它也可以与 Spring 的@Autowired或未注解的构造函数和 setter 方法结合使用。

1.12. 基于 Java 的容器配置

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

1.12.1. 基本概念:@Bean 和@Configuration

Spring 的新 Java 配置支持中的主要工件是@Configuration注解 的类和@Bean注解 的方法。

@Bean注解 用于指示方法实例化,配置和初始化要由 Spring IoC 容器 Management 的新对象。对于熟悉 Spring 的<beans/> XML 配置的人来说,@Bean注解 与<bean/>元素具有相同的作用。您可以对任何 Spring @Component使用@Bean注解 的方法。但是,它们最常与@Configuration bean 一起使用。

@Configuration注解 类表示该类的主要目的是作为 Bean 定义的来源。此外,@Configuration类通过调用同一类中的其他@Bean方法来定义 Bean 间的依赖关系。最简单的@Configuration类的内容如下:

@Configuration
public class AppConfig {

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

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

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

完整的@Configuration 与“精简” @Bean 模式?

当在未使用@Configuration注解 的类中声明@Bean方法时,它们被称为以“精简”模式进行处理。在@Component或什至在普通的旧类中声明的 Bean 方法被认为是“精简版”,其中包含类的主要目的不同,而@Bean方法在那里具有某种优势。例如,服务组件可以通过每个适用的组件类上的附加@Bean方法向容器公开 Management 视图。在这种情况下,@Bean方法是一种通用的工厂方法机制。

与完整@Configuration不同,lite @Bean方法无法声明 Bean 之间的依赖关系。取而代之的是,它们在其包含组件的内部状态上运行,并且可以选择地在它们可以声明的参数上运行。因此,此类@Bean方法不应调用其他@Bean方法。实际上,每个此类方法仅是用于特定 bean 引用的工厂方法,而没有任何特殊的运行时语义。这里的积极副作用是,不必在运行时应用 CGLIB 子类,因此在类设计方面没有任何限制(即,包含类可能为final等)。

在常见情况下,将在@Configuration类中声明@Bean方法,以确保始终使用“完全”模式,因此跨方法引用将重定向到容器的生命周期 Management。这样可以防止通过常规 Java 调用意外地调用同一@Bean方法,从而有助于减少在“精简”模式下运行时难以追查的细微错误。

以下各节将对@Bean@Configuration注解 进行深入讨论。但是,首先,我们介绍了通过基于 Java 的配置使用创建 spring 容器的各种方法。

1.12.2. 使用 AnnotationConfigApplicationContext 实例化 Spring 容器

以下各节记录了 Spring 3.0 中引入的 Spring 的AnnotationConfigApplicationContext。这种通用的ApplicationContext实现不仅可以接受@Configuration类作为 Importing,而且还可以接受普通@Component类和带有 JSR-330 元数据注解的类。

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

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

Simple Construction

与实例化ClassPathXmlApplicationContext时将 Spring XML 文件用作 Importing 的方式几乎相同,实例化AnnotationConfigApplicationContext时可以将@Configuration类用作 Importing。如下面的示例所示,这允许完全不使用 XML 来使用 Spring 容器:

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

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

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

前面的示例假定MyServiceImplDependency1Dependency2使用 Spring 依赖项注入 注解,例如@Autowired

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

您可以使用 no-arg 构造函数实例化AnnotationConfigApplicationContext,然后使用register()方法对其进行配置。以编程方式构建AnnotationConfigApplicationContext时,此方法特别有用。以下示例显示了如何执行此操作:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
使用 scan(String ...)启用组件扫描

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

@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig  {
    ...
}

Tip

有经验的 Spring 用户可能熟悉 Spring 的context:名称空间中的等效 XML 声明,如以下示例所示:

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

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

Note

请记住,@Configuration类与@Componentmeta-annotated,因此它们是组件扫描的候选对象。在前面的示例中,假定在com.acme包(或下面的任何包)中声明了AppConfig,则在对scan()的调用期间将其拾取。在refresh()上,将处理其所有@Bean方法并将其注册为容器内的 bean 定义。

通过 AnnotationConfigWebApplicationContext 支持 Web 应用程序

AnnotationConfigWebApplicationContext可提供AnnotationConfigApplicationContextWebApplicationContext变体。在配置 Spring ContextLoaderListener servlet 侦听器,Spring MVC DispatcherServlet等时,可以使用此实现。以下web.xml片段配置了典型的 Spring MVC Web 应用程序(请注意contextClass context-param 和 init-param 的使用):

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

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

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

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

    <!-- map all requests for /app/* to the dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>

1.12.3. 使用@Bean注解

@Bean是方法级 注解,是 XML <bean/>元素的直接类似物。注解 支持<bean/>提供的某些属性,例如:* init-method * destroy-method * autowiring * name

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

声明一个 Bean

要声明 bean,可以使用@Bean注解 对方法进行 注解。您可以使用此方法在指定为该方法的返回值的类型的ApplicationContext内注册 bean 定义。默认情况下,Bean 名称与方法名称相同。以下示例显示了@Bean方法声明:

@Configuration
public class AppConfig {

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

您还可以使用接口(或 Base Class)返回类型声明@Bean方法,如以下示例所示:

@Configuration
public class AppConfig {

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

但是,这将提前类型预测的可见性限制为指定的接口类型(TransferService)。然后,使用只对容器知道一次的完整类型(TransferServiceImpl),实例化受影响的单例 bean。非惰性单例 bean 根据其声明 Sequences 实例化,因此您可能会看到不同的类型匹配结果,具体取决于另一个组件何时尝试通过未声明的类型进行匹配(例如@Autowired TransferServiceImpl,该实例仅在实例化transferService bean 时才解析.)。

Tip

如果您pass 语句的服务接口一致地引用类型,则@Bean返回类型可以安全地加入该设计决策。但是,对于实现多个接口的组件或由其实现类型潜在引用的组件,声明可能的最具体的返回类型(至少与引用您的 bean 的注入点所要求的具体类型一样)更为安全。

Bean Dependencies

带有@Bean注解 的方法可以具有任意数量的参数,这些参数描述构建该 bean 所需的依赖关系。例如,如果我们的TransferService要求AccountRepository,则可以使用方法参数实现该依赖关系,如以下示例所示:

@Configuration
public class AppConfig {

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

解析机制与基于构造函数的依赖注入几乎相同。有关更多详细信息,请参见相关部分

接收生命周期回调

任何使用@Bean注解 定义的类都支持常规的生命周期回调,并且可以使用 JSR-250 中的@PostConstruct@PreDestroy注解。有关更多详细信息,请参见JSR-250 annotations

还完全支持常规的 Spring lifecycle回调。如果 bean 实现InitializingBeanDisposableBeanLifecycle,则容器将调用它们各自的方法。

也完全支持*Aware接口的标准集合(例如BeanFactoryAwareBeanNameAwareMessageSourceAwareApplicationContextAware等)。

@Bean注解 支持指定任意的初始化和销毁回调方法,就像bean元素上的 Spring XML 的init-methoddestroy-method属性一样,如以下示例所示:

public class BeanOne {

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

public class BeanTwo {

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

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public BeanOne beanOne() {
        return new BeanOne();
    }

    @Bean(destroyMethod = "cleanup")
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

Note

默认情况下,使用 Java 配置定义的具有公共closeshutdown方法的 bean 会自动通过销毁回调注册。如果您有一个公共的closeshutdown方法,并且您不希望在容器关闭时调用它,则可以将@Bean(destroyMethod="")添加到 bean 定义中以禁用默认的(inferred)模式。

默认情况下,您可能要对通过 JNDI 获取的资源执行此操作,因为其生命周期是在应用程序外部进行 Management 的。特别是,请确保始终对DataSource进行操作,因为这在 Java EE 应用程序服务器上是有问题的。

以下示例显示了如何防止DataSource的自动销毁回调:

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

另外,对于@Bean方法,通常使用程序化 JNDI 查找,方法是使用 Spring 的JndiTemplateJndiLocatorDelegate帮助器,或者直接使用 JNDI InitialContext用法,而不使用JndiObjectFactoryBean变体(这将迫使您将返回类型声明为FactoryBean类型,而不是实际的目标。类型,使其更难以在打算引用此处提供的资源的其他@Bean方法中用于交叉引用调用。

对于前面注解中的示例中的BeanOne,在构造期间直接调用init()方法同样有效,如以下示例所示:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        BeanOne beanOne = new BeanOne();
        beanOne.init();
        return beanOne;
    }

    // ...
}

Tip

当您直接使用 Java 工作时,您可以对对象执行任何操作,而不必总是依赖于容器生命周期。

指定 Bean 范围

Spring 包含@Scope注解,以便您可以指定 bean 的范围。

使用@Scope 注解

您可以指定使用@Bean注解 定义的 bean 应该具有特定范围。您可以使用Bean Scopes部分中指定的任何标准范围。

默认范围是singleton,但是您可以使用@Scope注解覆盖它,如以下示例所示:

@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
}
@Scope 和 scoped-proxy

Spring 提供了一种通过scoped proxies处理范围内的依赖项的便捷方法。使用 XML 配置时创建此类代理的最简单方法是<aop:scoped-proxy/>元素。使用@Scope注解 在 Java 中配置 bean 可以提供与proxyMode属性等效的支持。缺省值为无代理(ScopedProxyMode.NO),但是您可以指定ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACES

如果使用 Java 从 XML 参考文档(请参阅scoped proxies)将作用域代理示例移植到我们的@Bean,则它类似于以下内容:

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

@Bean
public Service userService() {
    UserService service = new SimpleUserService();
    // a reference to the proxied userPreferences bean
    service.setUserPreferences(userPreferences());
    return service;
}
自定义 Bean 命名

默认情况下,配置类使用@Bean方法的名称作为结果 bean 的名称。但是,可以使用name属性覆盖此功能,如以下示例所示:

@Configuration
public class AppConfig {

    @Bean(name = "myThing")
    public Thing thing() {
        return new Thing();
    }
}
Bean Aliasing

Naming Beans中所述,有时希望为单个 bean 提供多个名称,否则称为 bean 别名。 @Bean注解的name属性为此目的接受一个 String 数组。以下示例说明如何为 bean 设置多个别名:

@Configuration
public class AppConfig {

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

有时,提供有关 bean 的更详细的文本描述会很有帮助。当出于监视目的而暴露(可能通过 JMX)bean 时,这尤其有用。

要将说明添加到@Bean,可以使用@Description注解,如以下示例所示:

@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Thing thing() {
        return new Thing();
    }
}

1.12.4. 使用@Configuration 注解

@Configuration是类级别的 注解,指示对象是 Bean 定义的源。 @Configuration类通过公共@Bean带注解的方法声明 Bean。对@Configuration类的@Bean方法的调用也可以用于定义 Bean 间的依赖关系。有关一般介绍,请参见基本概念:@Bean 和@Configuration

注入 Bean 间的依赖关系

当 bean 相互依赖时,表示这种依赖关系就像让一个 bean 方法调用另一个一样简单,如以下示例所示:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

在前面的示例中,beanOne通过构造函数注入接收对beanTwo的引用。

Note

仅当在@Configuration类中声明@Bean方法时,此声明 bean 间依赖性的方法才有效。您不能通过使用普通@Component类来声明 Bean 间的依赖关系。

查找方法注入

如前所述,查找方法注入是您不应该使用的高级功能。在单例作用域的 bean 依赖于原型作用域的 bean 的情况下,这很有用。将 Java 用于这种类型的配置为实现此模式提供了自然的方法。以下示例显示如何使用查找方法注入:

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

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

通过使用 Java 配置,您可以创建CommandManager的子类,在该子类中,抽象createCommand()方法被覆盖,从而可以查找新的(原型)命令对象。以下示例显示了如何执行此操作:

@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
    AsyncCommand command = new AsyncCommand();
    // inject dependencies here as required
    return command;
}

@Bean
public CommandManager commandManager() {
    // return new anonymous implementation of CommandManager with command() overridden
    // to return a new prototype Command object
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}
有关基于 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 中,实例化的 bean 默认具有singleton范围。这就是神奇的地方:所有@Configuration类在启动时都使用CGLIB子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存(作用域)的 bean。

Note

根据 bean 的范围,行为可能有所不同。我们在这里谈论单例。

Note

从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的 Classpath 中,因为 CGLIB 类已经在org.springframework.cglib下重新打包并直接包含在 spring-core JAR 中。

Tip

由于 CGLIB 在启动时会动态添加功能,因此存在一些限制。特别是,配置类不能是最终的。但是,从 4.3 版本开始,配置类中允许使用任何构造函数,包括对默认注入使用@Autowired或单个非默认构造函数声明。

如果您希望避免任何 CGLIB 施加的限制,请考虑在非@Configuration类(例如,在普通@Component类上)声明@Bean方法。然后,不会拦截@Bean方法之间的跨方法调用,因此您必须专门依赖那里的构造函数或方法级别的依赖项注入。

1.12.5. 组成基于 Java 的配置

Spring 的基于 Java 的配置功能使您可以编写注解,这可以降低配置的复杂性。

使用@导入 注解

就像 Spring XML 文件中使用<import/>元素来帮助模块化配置一样,@Import注解 允许从另一个配置类中加载@Bean定义,如以下示例所示:

@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }
}

现在,无需在实例化上下文时同时指定ConfigA.classConfigB.class,只需显式提供ConfigB,如以下示例所示:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // now both beans A and B will be available...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

这种方法简化了容器的实例化,因为只需要处理一个类,而不是要求您在构造过程中记住大量的@Configuration类。

Tip

从 Spring Framework 4.2 开始,@Import还支持引用常规组件类,类似于AnnotationConfigApplicationContext.register方法。如果要通过使用一些配置类作为入口点来显式定义所有组件,从而避免组件扫描,则此功能特别有用。

注入对导入的@Bean 定义的依赖关系

前面的示例有效,但过于简单。在大多数实际情况下,Bean 在配置类之间相互依赖。使用 XML 时,这不是问题,因为不涉及任何编译器,并且您可以声明ref="someBean"并信任 Spring 在容器初始化期间进行处理。使用@Configuration类时,Java 编译器会在配置模型上施加约束,因为对其他 bean 的引用必须是有效的 Java 语法。

幸运的是,解决这个问题很简单。与我们已经讨论过了一样,@Bean方法可以具有任意数量的描述 Bean 依赖关系的参数。考虑以下具有多个@Configuration类的更实际的场景,每个类均取决于其他类中声明的 bean:

@Configuration
public class ServiceConfig {

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

@Configuration
public class RepositoryConfig {

    @Bean
    public AccountRepository accountRepository(DataSource dataSource) {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

还有另一种方法可以达到相同的结果。请记住,@Configuration类最终仅是容器中的另一个 bean:这意味着它们可以利用@Autowired@Value注入以及与任何其他 bean 相同的其他功能。

Warning

确保以这种方式注入的依赖项只是最简单的一种。 @Configuration类是在上下文初始化期间非常早地处理的,并且强制以这种方式注入依赖项可能导致意外的早期初始化。如上例所示,请尽可能使用基于参数的注入。

另外,要特别注意BeanPostProcessorBeanFactoryPostProcessor@Bean的定义。通常应将它们声明为static @Bean方法,而不触发其包含的配置类的实例化。否则,@Autowired@Value不适用于配置类本身,因为它太早被创建为 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;

    @Autowired
    public RepositoryConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

Tip

从 Spring Framework 4.3 开始,仅支持@Configuration类中的构造方法注入。还要注意,如果目标 bean 仅定义一个构造函数,则无需指定@Autowired。在前面的示例中,RepositoryConfig构造函数上不需要@Autowired

完全合格的 importbean,便于导航

在前面的场景中,使用@Autowired可以很好地工作并提供所需的模块化,但是确切地确定自动装配的 Bean 定义在何处声明仍然有些模棱两可。例如,当开发人员查看ServiceConfig时,您如何确切知道@Autowired AccountRepository bean 的声明位置?它在代码中不是明确的,这可能很好。请记住,Spring 工具套件提供的工具可以呈现图形,显示所有连线的方式,这可能就是您所需要的。另外,您的 Java IDE 可以轻松找到AccountRepository类型的所有声明和使用,并快速向您显示返回该类型的@Bean方法的位置。

如果这种歧义是不可接受的,并且您希望从 IDE 内直接从一个@Configuration类导航到另一个@Configuration类,请考虑自动装配配置类本身。以下示例显示了如何执行此操作:

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // navigate 'through' the config class to the @Bean method!
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

在上述情况下,定义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})  // import the concrete config!
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return DataSource
    }

}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

现在ServiceConfig与具体的DefaultRepositoryConfig松散耦合,并且内置的 IDE 工具仍然有用:您可以轻松地获得RepositoryConfig实现的类型层次结构。这样,对@Configuration类及其依赖项进行导航变得与导航基于接口的代码的通常过程没有什么不同。

Tip

如果要影响某些 Bean 的启动创建 Sequences,请考虑将其中一些声明为@Lazy(用于首次访问而不是在启动时创建)或@DependsOn声明其他某些 Bean(确保在当前 Bean 之前创建了特定的其他 Bean) ,而不是后者的直接依赖项所暗示的含义)。

有条件地包含@Configuration 类或@Bean 方法

基于某些任意系统状态,有条件地启用或禁用完整的@Configuration类甚至单个@Bean方法通常很有用。一个常见的示例是仅在 Spring Environment中启用了特定概要文件时才使用@Profile注解 来激活 bean(有关详细信息,请参见Bean 定义配置文件)。

@Profile注解 实际上是通过使用更灵活的称为@Conditional的注解来实现的。 @Conditional注解指示在注册@Bean之前应参考的org.springframework.context.annotation.Condition特定实现。

Condition接口的实现提供了一个matches(…)方法,该方法返回truefalse。例如,以下清单显示了用于@Profile的实际Condition实现:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() != null) {
        // Read the @Profile annotation attributes
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
    }
    return true;
}

有关更多详细信息,请参见@Conditional javadoc。

结合 Java 和 XML 配置

Spring 的@Configuration类支持并非旨在 100%完全替代 Spring XML。某些工具(例如 Spring XML 名称空间)仍然是配置容器的理想方法。在使用 XML 方便或有必要的情况下,您可以选择:通过使用ClassPathXmlApplicationContext以“以 XML 为中心”的方式实例化容器,或通过使用AnnotationConfigApplicationContext和以“以 Java 为中心”的方式实例化容器。 @ImportResource注解 以根据需要导入 XML。

以 XML 为中心的@Configuration 类的使用

最好从 XML 引导 Spring 容器,并以即席方式包含@Configuration类。例如,在使用 Spring XML 的大型现有代码库中,根据需要创建@Configuration类并从现有 XML 文件中包含它们很容易。在本节的后面,我们将介绍在这种“以 XML 为中心”的情况下使用@Configuration类的选项。

@Configuration类声明为纯 Spring <bean/>元素

请记住,@Configuration类最终是容器中的 bean 定义。在本系列示例中,我们创建一个名为AppConfig@Configuration类,并将其作为<bean/>定义包含在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>
    <!-- enable processing of annotations such as @Autowired and @Configuration -->
    <context:annotation-config/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="com.acme.AppConfig"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

以下示例显示了一个可能的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);
    // ...
}

Note

system-test-config.xml文件中,AppConfig <bean/>没有声明id元素。尽管这样做是可以接受的,但是由于没有其他 bean 曾经引用过它,因此这是不必要的,并且不太可能通过名称从容器中显式获取。类似地,DataSource bean 只能按类型自动装配,因此并不需要严格要求显式 bean id

使用\ <>选择@Configuration个类

因为@Configuration使用@Component进行元 注解,所以@Configuration注解 的类自动成为组件扫描的候选对象。使用与上一个示例中描述的场景相同的场景,我们可以重新定义system-test-config.xml以利用组件扫描的优势。请注意,在这种情况下,我们无需显式声明<context:annotation-config/>,因为<context:component-scan/>启用相同的功能。

以下示例显示了修改后的system-test-config.xml文件:

<beans>
    <!-- picks up and registers AppConfig as a bean definition -->
    <context:component-scan base-package="com.acme"/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
@Configuration 以类为中心的 XML 与@ImportResource 的使用

@Configuration类是配置容器的主要机制的应用程序中,仍然可能有必要至少使用一些 XML。在这些情况下,您可以使用@ImportResource并仅定义所需的 XML。这样做实现了“以 Java 为中心”的方法来配置容器,并将 XML 保持在最低限度。以下示例(包括配置类,定义 Bean 的 XML 文件,属性文件和main类)显示了如何使用@ImportResource注解 来实现按需使用 XML 的“以 Java 为中心”的配置:

@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);
    // ...
}

1.13. 环境抽象

Environment接口是集成在容器中的抽象,用于对应用程序环境的两个关键方面进行建模:profilesproperties

概要文件是仅在给定概要文件处于活动状态时才向容器注册的 Bean 定义的命名逻辑组。可以将 Bean 分配给概要文件,无论是以 XML 定义还是带有 注解。 Environment对象与配置文件相关的作用是确定当前哪些配置文件(如果有)处于活动状态,以及默认情况下哪些配置文件(如果有)应处于活动状态。

属性在几乎所有应用程序中都起着重要作用,并且可能源自多种来源:属性文件,JVM 系统属性,系统环境变量,JNDI,Servlet 上下文参数,即席Properties对象,Map对象等等。 Environment对象相对于属性的作用是为用户提供方便的服务界面,用于配置属性源并从中解析属性。

1.13.1. Bean 定义配置文件

Bean 定义配置文件在核心容器中提供了一种机制,该机制允许在不同环境中注册不同的 Bean。 “环境”一词对不同的用户可能具有不同的含义,并且此功能可以帮助解决许多用例,包括:

考虑实际应用中需要DataSource的第一个用例。在测试环境中,配置可能类似于以下内容:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

现在,假设该应用程序的数据源已在生产应用程序服务器的 JNDI 目录中注册,请考虑如何将该应用程序部署到 QA 或生产环境中。现在,我们的dataSource 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 定义配置文件是一项核心容器功能,可提供此问题的解决方案。

如果我们概括前面特定于环境的 Bean 定义示例中所示的用例,那么最终需要在某些上下文中而不是在其他上下文中注册某些 Bean 定义。您可能会说您要在情况 A 中注册一个特定的 bean 定义配置文件,在情况 B 中注册一个不同的配置文件。我们首先更新配置以反映这种需求。

Using @Profile

@Profile注解 使您可以指示一个或多个指定配置文件处于活动状态时有资格注册的组件。使用前面的示例,我们可以如下重写dataSource配置:

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

Note

如前所述,对于@Bean方法,通常选择使用程序化 JNDI 查找,方法是使用 Spring 的JndiTemplate/JndiLocatorDelegate帮助器或前面显示的直接 JNDI InitialContext用法,而不使用JndiObjectFactoryBean变体,这将迫使您将返回类型声明为FactoryBean类型。

配置文件字符串可以包含简单的配置文件名称(例如production)或配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如production & us-east)。概要文件表达式中支持以下运算符:

Note

不使用括号不能混合使用&|运算符。例如,production & us-east | eu-central不是有效的表达式。它必须表示为production & (us-east | eu-central)

您可以将@Profile用作meta-annotation,以创建自定义的合成 注解。以下示例定义了一个自定义@Production注解,您可以将其用作@Profile("production")的替代品:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

Tip

如果@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") (1)
    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") (2)
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

Note

对于@Bean方法上的@Profile,可能适用特殊情况:对于具有相同 Java 方法名称的@Bean方法重载(类似于构造函数重载),必须在所有重载方法上一致声明@Profile条件。如果条件不一致,则仅重载方法中第一个声明的条件很重要。因此,@Profile不能用于选择具有特定自变量签名的重载方法。在创建时,相同 bean 的所有工厂方法之间的解析都遵循 Spring 的构造函数解析算法。

如果要定义具有不同概要文件条件的备用 Bean,请使用@Bean name 属性使用不同的 Java 方法名称来指向相同的 Bean 名称,如前面的示例所示。如果参数签名都相同(例如,所有变体都具有 no-arg 工厂方法),则这是首先在有效 Java 类中表示这种排列的唯一方法(因为只能有一个特定名称和参数签名的方法)。

XML Bean 定义配置文件

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="...">

    <!-- other bean definitions -->

    <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已被限制为仅允许这些元素作为文件中的最后一个元素。这应该有助于提供灵 Active,而不会引起 XML 文件混乱。

Note

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="...">

<!-- other bean definitions -->

<beans profile="production">
<beans profile="us-east">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
</beans>

在前面的示例中,如果productionus-east配置文件都处于活动状态,则dataSource bean 被公开。

激活 Profile

现在,我们已经更新了配置,我们仍然需要指示 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"
Default Profile

默认配置文件表示默认情况下启用的配置文件。考虑以下示例:

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

如果没有任何配置文件处于活动状态,则创建dataSource。您可以看到这是为一个或多个 bean 提供默认定义的一种方法。如果启用了任何配置文件,则默认配置文件将不适用。

您可以通过在Environment上使用setDefaultProfiles()或pass 语句spring.profiles.default属性来更改默认配置文件的名称。

1.13.2. PropertySource 抽象

Spring 的Environment抽象提供了对属性源的可配置层次结构的搜索操作。考虑以下清单:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);

在前面的代码片段中,我们看到了一种询问 Spring 是否为当前环境定义my-property属性的高级方法。为了回答这个问题,Environment对象对一组PropertySource对象执行搜索。 PropertySource是对任何键-值对源的简单抽象,而 Spring 的StandardEnvironment配置了两个 PropertySource 对象-一个代表 JVM 系统属性集(System.getProperties())和一个代表系统环境变量集(System.getenv())。

Note

这些默认属性源针对StandardEnvironment存在,供独立应用程序使用。 StandardServletEnvironment填充了其他默认属性源,包括 servlet 配置和 servlet 上下文参数。它可以选择启用JndiPropertySource。有关详细信息,请参见 javadoc。

具体来说,当您使用StandardEnvironment时,如果在运行时存在my-property系统属性或my-propertyi环境变量,则对env.containsProperty("my-property")的调用将返回 true。

Tip

执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用env.getProperty("my-property")的过程中在两个地方都同时设置了my-property属性,则系统属性值“ wins”并返回。请注意,属性值不会合并,而是会被前面的条目完全覆盖。

对于常见的StandardServletEnvironment,完整层次结构如下,最高优先级条目位于顶部:

  • ServletConfig 参数(如果适用,例如,在DispatcherServlet上下文的情况下)

  • ServletContext 参数(web.xml 上下文参数条目)

  • JNDI 环境变量(java:comp/env/个条目)

  • JVM 系统属性(-D个命令行参数)

  • JVM 系统环境(os 环境变量)

最重要的是,整个机制是可配置的。也许您具有要集成到此搜索中的自定义属性源。为此,请实现并实例化自己的PropertySource并将其添加到当前EnvironmentPropertySources集合中。以下示例显示了如何执行此操作:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在前面的代码中,在搜索中添加了具有最高优先级的MyPropertySource。如果它包含my-property属性,则会检测到并返回该属性,而支持其他PropertySource中的任何my-property属性。 MutablePropertySources API 公开了许多方法,这些方法可以精确地控制属性源集。

1.13.3. 使用@PropertySource

@PropertySource注解 为将PropertySource添加到 Spring 的Environment提供了一种方便的声明性机制。

给定名为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存在于已注册的属性源之一(例如,系统属性或环境变量)中,则占位符将解析为相应的值。如果不是,则使用default/path作为默认值。如果未指定默认值并且无法解析属性,则会引发IllegalArgumentException

Note

根据 Java 8 约定,@PropertySource注解 是可重复的。但是,所有此类@PropertySource注解都需要在同一级别上声明,可以直接在配置类上声明,也可以在同一自定义注解中声明为元注解。不建议将直接注解和元注解混合使用,因为直接注解会有效地覆盖元 注解。

1.13.4. 声明中的占位符解析

从历史上看,元素中占位符的值只能根据 JVM 系统属性或环境变量来解析。这已不再是这种情况。由于Environment抽象集成在整个容器中,因此很容易通过它路由占位符的解析。这意味着您可以按照自己喜欢的任何方式配置解析过程。您可以更改搜索系统属性和环境变量的优先级,也可以完全删除它们。您还可以根据需要将自己的属性源添加到组合中。

具体来说,只要在Environment中可用,以下语句无论在customer属性的定义位置如何都有效:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>

1.14. 注册一个 LoadTimeWeaver

LoadTimeWeaver被 Spring 使用,以在将类加载到 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 Framework 中使用 AspectJ 进行加载时编织

1.15. ApplicationContext 的其他功能

chapter introduction中所讨论,org.springframework.beans.factory包提供了用于 Management 和操作 bean 的基本功能,包括以编程方式。 org.springframework.context包添加了ApplicationContext接口,该接口扩展了BeanFactory接口,并扩展了其他接口以提供更多面向应用程序框架的样式的附加功能。许多人以完全声明性的方式使用ApplicationContext,甚至没有以编程方式创建它,而是依靠诸如ContextLoader之类的支持类来自动实例化ApplicationContext作为 Java EE Web 应用程序正常启动过程的一部分。

为了以更面向框架的方式增强BeanFactory功能,上下文包还提供以下功能:

1.15.1. 使用 MessageSource 进行国际化

ApplicationContext接口扩展了名为MessageSource的接口,因此提供了国际化(“ i18n”)功能。 Spring 还提供了HierarchicalMessageSource接口,该接口可以分层解析消息。这些接口一起提供了 Spring 影响消息解析的基础。这些接口上定义的方法包括:

加载ApplicationContext时,它将自动搜索上下文中定义的MessageSource bean。 Bean 必须具有名称messageSource。如果找到了这样的 bean,则对先前方法的所有调用都将委派给消息源。如果找不到消息源,则ApplicationContext尝试查找包含同名 bean 的父对象。如果是这样,它将使用该 bean 作为MessageSource。如果ApplicationContext找不到任何消息源,则实例化一个空的DelegatingMessageSource以便能够接受对上面定义的方法的调用。

Spring 提供了两个MessageSource实现ResourceBundleMessageSourceStaticMessageSource。两者都实现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>

该示例假定您在 Classpath 中定义了三个名为formatexceptionswindows的资源包。任何解析消息的请求都通过 JDK 标准的ResourceBundle对象解析消息来处理。就本示例而言,假定上述两个资源束文件的内容如下:

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

下一个示例显示了执行MessageSource功能的程序。请记住,所有ApplicationContext实现也是MessageSource实现,因此可以转换为MessageSource接口。

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

以上程序的结果输出如下:

Alligators rock!

总而言之,MessageSource是在名为beans.xml的文件中定义的,该文件位于 Classpath 的根目录下。 messageSource bean 定义通过其basenames属性引用了许多资源包。列表中传递给basenames属性的三个文件以 Classpath 的根文件形式存在,分别称为format.propertiesexceptions.propertieswindows.properties

下一个示例显示了传递给消息查找的参数。这些参数将转换为String对象,并插入到查找消息中的占位符中。

<beans>

    <!-- this MessageSource is being used in a web application -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <!-- lets inject the above MessageSource into this 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", null);
        System.out.println(message);
    }
}

调用execute()方法得到的结果如下:

The userDao argument is required.

关于国际化(“ i18n”),Spring 的各种MessageSource实现遵循与标准 JDK ResourceBundle相同的语言环境解析和后备规则。简而言之,并 continue 前面定义的示例messageSource,如果要根据英国(en-GB)语言环境解析消息,则可以分别创建名为format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties的文件。

通常,语言环境解析由应用程序的周围环境 Management。在以下示例中,手动指定了针对其解析(英国)消息的语言环境:

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

运行上述程序的结果输出如下:

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

您还可以使用MessageSourceAware界面获取对已定义的任何MessageSource的引用。创建和配置 Bean 时,在实现MessageSourceAware接口的ApplicationContext中定义的任何 Bean 都会被注入应用程序上下文的MessageSource

Note

作为ResourceBundleMessageSource的替代方法,Spring 提供了ReloadableResourceBundleMessageSource类。此变体支持相同的 Binding 文件格式,但比基于标准 JDK 的ResourceBundleMessageSource实现更灵活。特别是,它允许从任何 Spring 资源位置(不仅从 Classpath)读取文件,并支持热重载 Binding 属性文件(同时在它们之间进行有效缓存)。有关详细信息,请参见ReloadableResourceBundleMessageSource javadoc。

1.15.2. 标准和自定义事件

ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。如果将实现ApplicationListener接口的 bean 部署到上下文中,则每次将ApplicationEvent发布到ApplicationContext时,都会通知该 bean。本质上,这是标准的 Observer 设计模式。

Tip

从 Spring 4.2 开始,事件基础结构得到了显着改进,并提供了annotation-based model以及发布任何任意事件的能力(即,对象不一定从ApplicationEvent扩展)。发布此类对象后,我们会为您包装一个事件。

下表描述了 Spring 提供的标准事件:

表 7.内置事件

Event Explanation
ContextRefreshedEvent 在初始化或刷新ApplicationContext时发布(例如,通过使用ConfigurableApplicationContext接口上的refresh()方法)。在这里,“已初始化”是指所有 Bean 都已加载,检测到并激活了后处理器 Bean,已预先实例化单例,并且已准备好使用ApplicationContext对象。只要尚未关闭上下文,只要选定的ApplicationContext实际上支持这种“热”刷新,就可以多次触发刷新。例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。
ContextStartedEvent ConfigurableApplicationContext界面上使用start()方法启动ApplicationContext时发布。在这里,“启动”是指所有Lifecycle bean 都收到一个明确的启动 signal。通常,此 signal 用于在显式停止后重新启动 Bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。
ContextStoppedEvent ConfigurableApplicationContext接口上使用stop()方法停止ApplicationContext时发布。此处,“已停止”表示所有Lifecycle bean 都收到一个明确的停止 signal。停止的上下文可以通过start()调用重新启动。
ContextClosedEvent ConfigurableApplicationContext接口上使用close()方法关闭ApplicationContext时发布。此处,“封闭”表示所有单例 bean 都被破坏。封闭的情境到了生命的尽头。无法刷新或重新启动。
RequestHandledEvent 一个特定于 Web 的事件,告诉所有 Bean HTTP 请求已得到服务。请求完成后,将发布此事件。此事件仅适用于使用 Spring 的DispatcherServlet的 Web 应用程序。

您还可以创建和发布自己的自定义事件。以下示例显示了一个简单的类,该类扩展了 Spring 的ApplicationEventBase Class:

public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlackListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

要发布自定义ApplicationEvent,请在ApplicationEventPublisher上调用publishEvent()方法。通常,这是通过创建一个实现ApplicationEventPublisherAware的类并将其注册为 Spring bean 来完成的。以下示例显示了此类:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blackList;
    private ApplicationEventPublisher publisher;

    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blackList.contains(address)) {
            publisher.publishEvent(new BlackListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在配置时,Spring 容器检测到EmailService实现ApplicationEventPublisherAware并自动调用setApplicationEventPublisher()。实际上,传入的参数是 Spring 容器本身。您正在通过其ApplicationEventPublisher接口与应用程序上下文进行交互。

要接收自定义ApplicationEvent,您可以创建一个实现ApplicationListener的类并将其注册为 Spring Bean。以下示例显示了此类:

public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

    private String notificationAddress;

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

    public void onApplicationEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意,ApplicationListener通常用您的自定义事件的类型(上一示例中的BlackListEvent)进行参数化。这意味着onApplicationEvent()方法可以保持类型安全,从而避免了向下转换的任何需要。您可以根据需要注册任意数量的事件侦听器,但是请注意,默认情况下,事件侦听器会同步接收事件。这意味着publishEvent()方法将阻塞,直到所有侦听器都已完成对事件的处理为止。这种同步和单线程方法的一个优点是,当侦听器收到事件时,如果有可用的事务上下文,它将在发布者的事务上下文内部进行操作。如果需要其他用于事件发布的策略,请参见 Spring 的ApplicationEventMulticaster接口的 javadoc。

以下示例显示了用于注册和配置上述每个类的 Bean 定义:

<bean id="emailService" class="example.EmailService">
    <property name="blackList">
        <list>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
        </list>
    </property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
    <property name="notificationAddress" value="[emailprotected]"/>
</bean>

将所有内容放在一起,当调用emailService bean 的sendEmail()方法时,如果有任何电子邮件消息应列入黑名单,则会发布BlackListEvent类型的自定义事件。 blackListNotifier bean 注册为ApplicationListener并接收BlackListEvent,此时它可以通知适当的参与者。

Note

Spring 的事件机制旨在在同一应用程序上下文内在 Spring bean 之间进行简单的通信。但是,对于更复杂的企业集成需求,单独维护的Spring Integration项目为构建基于众所周知的 Spring 编程模型的事件驱动的轻量级pattern-oriented体系结构提供了完整的支持。

基于注解的事件侦听器

从 Spring 4.2 开始,您可以使用EventListener注解在托管 Bean 的任何公共方法上注册事件侦听器。 BlackListNotifier可以重写如下:

public class BlackListNotifier {

    private String notificationAddress;

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

    @EventListener
    public void processBlackListEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

方法签名再次声明其侦听的事件类型,但是这次使用灵活的名称并且没有实现特定的侦听器接口。只要实际事件类型在其实现层次结构中解析您的通用参数,也可以通过通用类型来缩小事件类型。

如果您的方法应该侦听多个事件,或者您要完全不使用任何参数来定义它,则事件类型也可以在注解本身上指定。以下示例显示了如何执行此操作:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    ...
}

也可以通过使用定义SpEL expression的注解的condition属性来添加其他运行时过滤,该属性应匹配以针对特定事件实际调用该方法。

以下示例显示了仅当事件的content属性等于my-event时,才可以重写我们的通知程序以进行调用:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlackListEvent(BlackListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

每个SpEL表达式都针对专用上下文进行评估。下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:

表 8. Event SpEL 可用的元数据

Name Location Description Example
Event root object 实际的ApplicationEvent #root.event
Arguments array root object 用于调用目标的参数(作为数组)。 #root.args[0]
Argument name evaluation context 任何方法参数的名称。如果由于某种原因名称不可用(例如,因为没有调试信息),则参数名称也可以在#a<#arg>下获得,其中#arg代表参数索引(从 0 开始)。 #blEvent#a0(您也可以使用#p0#p<#arg>表示法作为别名)

请注意,即使您的方法签名实际上引用了已发布的任意对象,#root.event也使您可以访问基础事件。

如果由于处理另一个事件而需要发布一个事件,则可以更改方法签名以返回应发布的事件,如以下示例所示:

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

Note

asynchronous listeners不支持此功能。

此新方法为上述方法处理的每个BlackListEvent发布一个新的ListUpdateEvent。如果您需要发布多个事件,则可以返回Collection事件。

Asynchronous Listeners

如果希望特定的侦听器异步处理事件,则可以重用常规@Async 支持。以下示例显示了如何执行此操作:

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
    // BlackListEvent is processed in a separate thread
}

使用异步事件时,请注意以下限制:

Ordering Listeners

如果需要先调用一个侦听器,则可以将@Order注解添加到方法声明中,如以下示例所示:

@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress...
}
Generic Events

您还可以使用泛型来进一步定义事件的结构。考虑使用EntityCreatedEvent<T>,其中T是已创建的实际实体的类型。例如,您可以创建以下侦听器定义以仅接收_4 的EntityCreatedEvent

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    ...
}

由于类型擦除,只有在触发的事件解析了事件侦听器所依据的通用参数(即诸如class 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()));
    }
}

Tip

这不仅适用于ApplicationEvent,而且适用于您作为事件发送的任何任意对象。

1.15.3. 方便地访问低级资源

为了最佳使用和理解应用程序上下文,您应该熟悉 Spring 的Resource抽象,如Resources中所述。

应用程序上下文是ResourceLoader,可用于加载Resource个对象。 Resource本质上是 JDK java.net.URL类的功能更丰富的版本。实际上,Resource的实现在适当的地方包装了java.net.URL的实例。 Resource可以从几乎任何位置以透明方式获取低级资源,包括从 Classpath,文件系统位置,可使用标准 URL 描述的任何位置以及其他一些变体。如果资源位置字符串是没有任何特殊前缀的简单路径,则这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。

您可以配置部署到应用程序上下文中的 Bean,以实现特殊的回调接口ResourceLoaderAware,以便在初始化时使用应用程序上下文本身作为ResourceLoader进行自动回调。您还可以公开Resource类型的属性,以用于访问静态资源。它们像其他任何属性一样注入其中。您可以将这些Resource属性指定为简单的String路径,并在部署 bean 时依靠特殊的 JavaBean PropertyEditor(由上下文自动注册)将这些文本字符串转换为实际的Resource对象。

提供给ApplicationContext构造函数的一个或多个位置路径实际上是资源字符串,并且根据特定的上下文实现以简单的形式对其进行了适当处理。例如ClassPathXmlApplicationContext将简单的位置路径视为 Classpath 位置。您也可以使用带有特殊前缀的位置路径(资源字符串)来强制从 Classpath 或 URL 中加载定义,而不管实际的上下文类型如何。

1.15.4. Web 应用程序的便捷 ApplicationContext 实例化

您可以使用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作为默认值。当参数确实存在时,侦听器将使用 sched 义的定界符(逗号,分号和空格)来分隔String,并将这些值用作搜索应用程序上下文的位置。还支持 Ant 风格的路径模式。示例为/WEB-INF/*Context.xml(对于名称以Context.xml结尾并且位于WEB-INF目录中的所有文件)和/WEB-INF/**/*Context.xml(对于WEB-INF的任何子目录中的所有此类文件)。

1.15.5. 将 Spring ApplicationContext 部署为 Java EE RAR 文件

可以将 Spring ApplicationContext部署为 RAR 文件,并将上下文及其所有必需的 Bean 类和库 JAR 封装在 Java EE RAR 部署单元中。这等效于引导独立的ApplicationContext(仅托管在 Java EE 环境中)能够访问 Java EE 服务器功能。对于部署无头 WAR 文件的情况,RAR 部署是一种更自然的选择-实际上,这种 WAR 文件没有任何 HTTP 入口点,仅用于在 Java EE 环境中引导 Spring ApplicationContext

对于不需要 HTTP 入口点而仅由消息端点和计划的作业组成的应用程序上下文,RAR 部署是理想的选择。在这样的上下文中,Bean 可以使用应用程序服务器资源,例如 JTA 事务 Management 器,与 JNDI 绑定的 JDBC DataSource实例和 JMS ConnectionFactory实例,还可以在平台的 JMX 服务器上注册-整个过程都通过 Spring 的标准事务 Management 以及 JNDI 和 JMX 支持工具进行。应用程序组件还可以通过 Spring 的TaskExecutor抽象与应用程序服务器的 JCA WorkManager进行交互。

有关 RAR 部署中涉及的配置详细信息,请参见SpringContextResourceAdapter类的 javadoc。

对于将 Spring ApplicationContext 作为 Java EE RAR 文件的简单部署:

Note

此类 RAR 部署单元通常是独立的。它们不会将组件暴露给外界,甚至不会暴露给同一应用程序的其他模块。与基于 RAR 的ApplicationContext的交互通常是通过与其他模块共享的 JMS 目标进行的。例如,基于 RAR 的ApplicationContext还可以计划一些作业或对文件系统(或类似文件)中的新文件做出反应。如果需要允许从外部进行同步访问,则可以(例如)导出 RMI 端点,该端点可以由同一台计算机上的其他应用程序模块使用。

1.16. bean 工厂

BeanFactory API 为 Spring 的 IoC 功能提供了基础。它的特定 Contract 主要用于与 Spring 的其他部分以及相关的第三方框架集成,并且其DefaultListableBeanFactory实现是更高级别GenericApplicationContext容器中的关键委托。

BeanFactory和相关接口(例如BeanFactoryAwareInitializingBeanDisposableBean)是其他框架组件的重要集成点。通过不需要任何注解甚至反射,它们可以在容器及其组件之间进行非常有效的交互。应用程序级 Bean 可以使用相同的回调接口,但通常更喜欢通过注解或通过程序配置进行声明式依赖注入。

请注意,核心BeanFactory API 级别及其DefaultListableBeanFactory实现不对配置格式或要使用的任何组件注解进行假设。所有这些风味都是通过 extensions(例如XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor)引入的,并以共享的BeanDefinition对象作为核心元数据表示形式进行操作。这就是使 Spring 的容器如此灵活和可扩展的本质。

1.16.1. BeanFactory 或 ApplicationContext?

本节说明BeanFactoryApplicationContext容器级别之间的区别以及对引导的影响。

除非有充分的理由,否则应使用ApplicationContext,除非GenericApplicationContext及其子类AnnotationConfigApplicationContext作为自定义引导的常见实现,否则应使用它们。对于所有常见目的,这些都是 Spring 核心容器的主要入口点:加载配置文件,触发 Classpath 扫描,以编程方式注册 Bean 定义和带注解的类,以及(从 5.0 版本开始)注册功能性 Bean 定义。

因为ApplicationContext包含BeanFactory的所有功能,所以通常建议在纯BeanFactory上使用,除非需要完全控制 Bean 处理的情况。在ApplicationContext(例如GenericApplicationContext实现)内,按惯例(即,按 Bean 名称或 Bean 类型(尤其是后处理器))检测到几种 Bean,而普通的DefaultListableBeanFactory则与任何特殊的 Bean 无关。

对于许多扩展的容器功能(例如注解处理和 AOP 代理),BeanPostProcessor 扩展点是必不可少的。如果仅使用普通DefaultListableBeanFactory,则默认情况下不会检测到此类后处理器并将其激活。这种情况可能令人困惑,因为您的 bean 配置实际上并没有错。而是在这种情况下,需要通过其他设置完全引导容器。

下表列出了BeanFactoryApplicationContext接口和实现提供的功能。

表 9.功能列表

Feature BeanFactory ApplicationContext
Bean instantiation/wiring Yes Yes
集成生命周期 Management No Yes
自动BeanPostProcessor注册 No Yes
自动BeanFactoryPostProcessor注册 No Yes
方便的MessageSource访问权限(用于内部化) No Yes
内置的ApplicationEvent发布机制 No Yes

要使用DefaultListableBeanFactory显式注册 bean 后处理器,您需要以编程方式调用addBeanPostProcessor,如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions

// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

要将BeanFactoryPostProcessor应用于普通DefaultListableBeanFactory,需要调用其postProcessBeanFactory方法,如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

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

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

在这两种情况下,显式的注册步骤都是不方便的,这就是为什么在 Spring 支持的应用程序中,各种ApplicationContext变体优于普通DefaultListableBeanFactory的原因,尤其是在典型企业设置中依靠BeanFactoryPostProcessorBeanPostProcessor实例扩展容器功能时。

Note

AnnotationConfigApplicationContext已注册了所有通用注解后处理器,并且可以通过诸如@EnableTransactionManagement之类的配置注解在幕后引入其他处理器。在 Spring 基于注解的配置模型的抽象级别上,bean 后处理器的概念仅是内部容器详细信息。

2. Resources

本章介绍了 Spring 如何处理资源以及如何在 Spring 中使用资源。它包括以下主题:

2.1. Introduction

不幸的是,Java 的标准java.net.URL类和用于各种 URL 前缀的标准处理程序不足以满足所有对低级资源的访问。例如,没有标准化的URL实现可用于访问需要从 Classpath 或相对于ServletContext获得的资源。尽管可以为专用的URL前缀注册新的处理程序(类似于现有的针对http:的前缀的处理程序),但这通常相当复杂,并且URL接口仍然缺少某些理想的功能,例如用于检查是否存在URL的方法。指向的资源。

2.2. 资源接口

Spring 的Resource接口旨在成为一种功能更强大的接口,用于抽象化对低级资源的访问。以下清单显示了Resource接口定义:

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isOpen();

    URL getURL() throws IOException;

    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();

}

Resource接口的定义所示,它扩展了InputStreamSource接口。以下清单显示了InputStreamSource接口的定义:

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}

Resource界面中一些最重要的方法是:

其他方法可让您获得代表资源的实际URLFile对象(如果基础实现兼容并且支持该功能)。

当需要资源时,Spring 本身广泛使用Resource抽象作为许多方法签名中的参数类型。某些 Spring API 中的其他方法(例如,各种ApplicationContext实现的构造函数)采用String,其以无装饰或简单的形式用于创建适合于该上下文实现的Resource,或者通过String路径上的特殊前缀,让调用者指定必须创建并使用特定的Resource实现。

虽然Resource接口在 Spring 和 Spring 上经常使用,但在您自己的代码中单独用作通用实用工具类来访问资源实际上非常有用,即使您的代码不知道或不关心其他任何代码 Spring 的一部分。虽然这会将您的代码耦合到 Spring,但实际上仅将其耦合到这套 Util 小类,它们可以更强大地替代URL,并且可以认为等同于您将为此使用的任何其他库。

Note

Resource抽象不能代替功能。它尽可能地包装它。例如,一个UrlResource包裹一个 URL,并使用包裹的URL来完成其工作。

2.3. 内置资源实现

Spring 包含以下Resource实现:

2.3.1. UrlResource

UrlResource包装java.net.URL,并且可用于访问通常可通过 URL 访问的任何对象,例如文件,HTTP 目标,FTP 目标等。所有 URL 都具有标准化的String表示形式,以便使用适当的标准化前缀来指示另一种 URL 类型。这包括file:用于访问文件系统路径,http:用于通过 HTTP 协议访问资源,ftp:用于通过 FTP 访问资源,以及其他。

Java 代码通过显式使用UrlResource构造函数来创建UrlResource,但是在调用带有String自变量表示路径的 API 方法时,通常会隐式创建UrlResource。对于后一种情况,JavaBeans PropertyEditor最终决定要创建哪种类型的Resource。如果路径字符串包含众所周知的前缀(例如classpath:),则会为该前缀创建一个适当的专用Resource。但是,如果无法识别前缀,则假定该字符串是标准 URL 字符串并创建UrlResource

2.3.2. ClassPathResource

此类表示应从 Classpath 获取的资源。它使用线程上下文类加载器,给定的类加载器或给定的类来加载资源。

如果 Classpath 资源驻留在文件系统中,而不是驻留在 jar 中并且尚未(通过 servlet 引擎或任何环境将其扩展到)文件系统的 Classpath 资源不驻留在文件系统中,则此Resource实现支持以java.io.File解析。为了解决这个问题,各种Resource实现始终支持将分辨率作为java.net.URL

Java 代码通过显式使用ClassPathResource构造函数来创建ClassPathResource,但是在调用带有String自变量表示路径的 API 方法时,通常会隐式创建ClassPathResource。对于后一种情况,JavaBeans PropertyEditor识别字符串路径上的特殊前缀classpath:,并在这种情况下创建ClassPathResource

2.3.3. FileSystemResource

这是java.io.Filejava.nio.file.Path句柄的Resource实现。它支持FileURL分辨率。

2.3.4. ServletContextResource

这是ServletContext资源的Resource实现,用于解释相关 Web 应用程序根目录中的相对路径。

它始终支持流访问和 URL 访问,但仅在扩展 Web 应用程序 Files 并且资源实际位于文件系统上时才允许java.io.File访问。它是在文件系统上扩展还是在文件系统上进行扩展,或者直接从 JAR 或其他类似数据库(可以想到的)访问,实际上取决于 Servlet 容器。

2.3.5. InputStreamResource

InputStreamResource是给定InputStreamResource实现。仅当没有特定的Resource实现适用时才应使用它。特别是,在可能的情况下,最好使用ByteArrayResource或任何基于文件的Resource实现。

与其他Resource实现相反,这是一个已经打开的资源的 Descriptors。因此,它从isOpen()返回true。如果您需要将资源 Descriptors 保留在某个地方,或者需要多次读取流,请不要使用它。

2.3.6. ByteArrayResource

这是给定字节数组的Resource实现。它为给定的字节数组创建一个ByteArrayInputStream

这对于从任何给定的字节数组加载内容而无需使用一次性InputStreamResource很有用。

2.4. 资源加载器

ResourceLoader接口旨在由可以返回(即加载)Resource实例的对象实现。以下清单显示了ResourceLoader接口定义:

public interface ResourceLoader {

    Resource getResource(String location);

}

所有应用程序上下文都实现ResourceLoader接口。因此,所有应用程序上下文都可用于获取Resource个实例。

当您在特定的应用程序上下文上调用getResource()时,并且指定的位置路径没有特定的前缀时,您将获得适合该特定应用程序上下文的Resource类型。例如,假设针对ClassPathXmlApplicationContext实例执行了以下代码段:

Resource template = ctx.getResource("some/resource/path/myTemplate.txt");

针对ClassPathXmlApplicationContext,该代码返回ClassPathResource。如果对FileSystemXmlApplicationContext实例执行了相同的方法,它将返回FileSystemResource。对于WebApplicationContext,它将返回ServletContextResource。类似地,它将为每个上下文返回适当的对象。

结果,您可以以适合特定应用程序上下文的方式加载资源。

另一方面,您也可以通过指定特殊的classpath:前缀来强制使用ClassPathResource,而与应用程序上下文类型无关,如下例所示:

Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");

同样,您可以通过指定任何标准java.net.URL前缀来强制使用UrlResource。以下一对示例使用filehttp前缀:

Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt");

下表总结了将String个对象转换为Resource个对象的策略:

表 10.资源字符串

Prefix Example Explanation
classpath: classpath:com/myapp/config.xml 从 Classpath 加载。
file: file:///data/config.xml 从文件系统作为URL加载。另请参见FileSystemResource Caveats
http: http://myserver/logo.png 载入为URL
(none) /data/config.xml 取决于基础的ApplicationContext

2.5. ResourceLoaderAware 接口

ResourceLoaderAware接口是一个特殊的标记接口,用于标识期望提供ResourceLoader参考的对象。以下清单显示了ResourceLoaderAware接口的定义:

public interface ResourceLoaderAware {

    void setResourceLoader(ResourceLoader resourceLoader);
}

当一个类实现ResourceLoaderAware并部署到应用程序上下文中(作为 Spring 托管 bean)时,该类被应用程序上下文识别为ResourceLoaderAware。然后,应用程序上下文调用setResourceLoader(ResourceLoader),将其自身作为参数(请记住,Spring 中的所有应用程序上下文都实现ResourceLoader接口)。

由于ApplicationContextResourceLoader,因此 bean 也可以实现ApplicationContextAware接口并直接使用提供的应用程序上下文来加载资源。但是,通常,如果需要的话,最好使用专用的ResourceLoader接口。该代码将仅耦合到资源加载接口(可以视为 Util 接口),而不耦合到整个 Spring ApplicationContext接口。

从 Spring 2.5 开始,您可以依靠ResourceLoader的自动装配来替代实现ResourceLoaderAware接口。现在,“传统” constructorbyType自动装配模式(如Autowiring Collaborators中所述)能够分别为构造函数参数或 setter 方法参数提供ResourceLoader类型的依赖项。为了获得更大的灵 Active(包括自动装配字段和多个参数方法的能力),请考虑使用基于注解的自动装配功能。在这种情况下,只要有问题的字段,构造函数或方法带有@Autowired注解,就将ResourceLoader自动连接到期望ResourceLoader类型的字段,构造函数参数或方法参数中。有关更多信息,请参见Using @Autowired

2.6. 资源依赖

如果 Bean 本身将通过某种动态过程来确定并提供资源路径,那么对于 Bean 来说,使用ResourceLoader接口加载资源可能是有意义的。例如,考虑加载某种模板,其中所需的特定资源取决于用户的角色。如果资源是静态的,则有必要完全消除对ResourceLoader接口的使用,让 Bean 公开所需的Resource属性,并期望将其注入其中。

然后注入这些属性很简单,因为所有应用程序上下文都注册并使用了特殊的 JavaBeans PropertyEditor,可以将String路径转换为Resource对象。因此,如果myBean具有类型为Resource的模板属性,则可以为该资源配置一个简单的字符串,如以下示例所示:

<bean id="myBean" class="...">
    <property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

请注意,资源路径没有前缀。因此,由于应用程序上下文本身将用作ResourceLoader,因此根据上下文的确切类型,资源本身是通过ClassPathResourceFileSystemResourceServletContextResource加载的。

如果需要强制使用特定的Resource类型,则可以使用前缀。以下两个示例显示了如何强制ClassPathResourceUrlResource(后者用于访问文件系统文件):

<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>

2.7. 应用程序上下文和资源路径

本节介绍如何使用资源创建应用程序上下文,包括使用 XML 的快捷方式,如何使用通配符以及其他详细信息。

2.7.1. 构造应用程序上下文

应用程序上下文构造函数(针对特定的应用程序上下文类型)通常采用字符串或字符串数组作为资源的位置路径,例如构成上下文定义的 XML 文件。

当这样的位置路径没有前缀时,从该路径构建并用于加载 Bean 定义的特定Resource类型取决于特定的应用程序上下文,并且适用于特定的应用程序上下文。例如,考虑以下示例,该示例创建一个ClassPathXmlApplicationContext

ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

Bean 定义是从 Classpath 加载的,因为使用了ClassPathResource。但是,请考虑以下示例,该示例创建FileSystemXmlApplicationContext

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/appContext.xml");

现在,bean 定义是从文件系统位置(在这种情况下,是相对于当前工作目录)加载的。

请注意,在位置路径上使用特殊的 Classpath 前缀或标准 URL 前缀会覆盖为加载定义而创建的默认类型Resource。考虑以下示例:

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");

使用FileSystemXmlApplicationContext从 Classpath 中加载 bean 定义。但是,它仍然是FileSystemXmlApplicationContext。如果随后将其用作ResourceLoader,则所有未前缀的路径仍将视为文件系统路径。

构造 ClassPathXmlApplicationContext 实例—快捷方式

ClassPathXmlApplicationContext公开了许多构造函数以启用方便的实例化。基本思想是,您只能提供一个字符串数组,该字符串数组仅包含 XML 文件本身的文件名(不包含前导路径信息),还提供ClassClassPathXmlApplicationContext然后从提供的类中导出路径信息。

请考虑以下目录布局:

com/
  foo/
    services.xml
    daos.xml
    MessengerService.class

以下示例显示了如何实例化由名为services.xmldaos.xml(位于 Classpath 中)的文件中定义的 bean 组成的ClassPathXmlApplicationContext实例:

ApplicationContext ctx = new ClassPathXmlApplicationContext(
    new String[] {"services.xml", "daos.xml"}, MessengerService.class);

有关各种构造函数的详细信息,请参见ClassPathXmlApplicationContext javadoc。

2.7.2. 应用程序上下文构造函数资源路径中的通配符

应用程序上下文构造函数值中的资源路径可以是简单路径(如前所述),每个路径都具有到目标Resource的一对一 Map,或者可以包含特殊的“ classpath *:”前缀或内部 Ant。样式的正则表达式(通过使用 Spring 的PathMatcherUtil 进行匹配)。后者都是有效的通配符。

这种机制的一种用途是当您需要进行组件样式的应用程序组装时。所有组件都可以将上下文定义片段“发布”到一个众所周知的位置路径,并且当使用前缀classpath*:的相同路径创建最终应用程序上下文时,所有组件片段都会被自动拾取。

请注意,此通配符特定于在应用程序上下文构造函数中使用资源路径(或在直接使用PathMatcherUtil 类层次结构时使用),并在构造时解决。它与Resource类型本身无关。您不能使用classpath*:前缀构造实际的Resource,因为资源一次仅指向一个资源。

Ant-style Patterns

路径位置可以包含 Ant 样式的模式,如以下示例所示:

/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml

当路径位置包含 Ant 样式的模式时,解析程序将遵循更复杂的过程来尝试解析通配符。它为到达最后一个非通配符段的路径生成一个Resource,并从中获取 URL。如果此 URL 不是jar: URL 或特定于容器的变体(例如 WebLogic 中的zip:,WebSphere 中的wsjar等),则从中获得java.io.File并将其用于遍历文件系统来解析通配符。对于 jar URL,解析器可以从中获取java.net.JarURLConnection或手动解析 jar URL,然后遍历 jar 文件的内容来解析通配符。

对便携性的影响

如果指定的路径已经是一个文件 URL(由于基ResourceLoader是一个文件系统,则是隐式的,或者是明确的),则保证通配符可以完全可移植的方式工作。

如果指定的路径是 Classpath 位置,则解析器必须通过进行Classloader.getResource()调用来获取最后一个非通配符路径段 URL。由于这只是路径的一个节点(而不是末尾的文件),因此实际上(在ClassLoader javadoc 中)未定义到底返回了哪种 URL。实际上,它始终是代表目录的java.io.File(Classpath 资源解析为文件系统位置)或某种 jar URL(Classpath 资源解析为 jar 位置)。尽管如此,此操作仍存在可移植性问题。

如果为最后一个非通配符段获取了 jar URL,则解析程序必须能够从中获取java.net.JarURLConnection或手动解析 jar URL,以便能够遍历 jar 的内容并解析通配符。这在大多数环境中确实有效,但在其他环境中则无效,因此我们强烈建议您在依赖特定环境之前,对来自 jars 的资源的通配符解析进行彻底测试。

Classpath*:前缀

构造基于 XML 的应用程序上下文时,位置字符串可以使用特殊的classpath*:前缀,如以下示例所示:

ApplicationContext ctx =
    new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

这个特殊的前缀指定必须获取与给定名称匹配的所有 Classpath 资源(内部,这实际上是通过调用ClassLoader.getResources(…)发生的),然后合并以形成最终的应用程序上下文定义。

Note

通配符 Classpath 依赖于基础类加载器的getResources()方法。由于当今大多数应用程序服务器提供其自己的类加载器实现,因此行为可能有所不同,尤其是在处理 jar 文件时。检查classpath*是否有效的简单测试是使用类加载器从 ClasspathgetClass().getClassLoader().getResources("<someFileInsideTheJar>")的 jar 中加载文件。尝试对具有相同名称但位于两个不同位置的文件进行此测试。如果返回了不合适的结果,请检查应用程序服务器文档中可能影响类加载器行为的设置。

您还可以在其余的位置路径(例如classpath*:META-INF/*-beans.xml)中将classpath*:前缀与PathMatcher模式结合使用。在这种情况下,解析策略非常简单:在最后一个非通配符路径段上使用ClassLoader.getResources()调用,以获取类加载器层次结构中的所有匹配资源,然后从每个资源中分离出与上述相同的PathMatcher解析策略:用于通配符子路径。

与通配符有关的其他说明

请注意,当classpath*:与 Ant 样式的模式结合使用时,除非模式文件实际存在于目标文件系统中,否则至少在模式启动之前,它至少与一个根目录可靠地配合使用。这意味着诸如classpath*:*.xml之类的模式可能不会从 jar 文件的根目录检索文件,而只会从扩展目录的根目录检索文件。

Spring 检索 Classpath 条目的能力源于 JDK 的ClassLoader.getResources()方法,该方法仅返回文件系统位置中的空字符串(指示可能要搜索的根目录)。 Spring 也会在 jar 文件中评估URLClassLoader运行时配置和java.class.path清单,但这不能保证会导致可移植行为。

Note

扫描 Classpath 包需要在 Classpath 中存在相应的目录条目。使用 Ant 构建 JAR 时,请勿激活 JAR 任务的仅文件开关。另外,在某些环境中,Classpath 目录可能不会基于安全策略公开-例如,在 JDK 1.7.0_45 及更高版本上的独立应用程序(要求在清单中设置“受信任的库”.请参阅http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在 JDK 9 的模块路径(Jigsaw)上,Spring 的 Classpath 扫描通常可以按预期进行。强烈建议在此处将资源放入专用目录中,以避免在搜索 jar 文件根级别时遇到上述可移植性问题。

如果要搜索的根包在多个 Classpath 位置中可用,则不能保证具有classpath:资源的 Ant 样式模式会找到匹配的资源。考虑以下资源位置示例:

com/mycompany/package1/service-context.xml

现在考虑某人可能用来尝试找到该文件的 Ant 样式的路径:

classpath:com/mycompany/**/service-context.xml

这样的资源可能只在一个位置,但是当使用诸如前面示例的路径尝试解析它时,解析器将根据getResource("com/mycompany");返回的(第一个)URL 进行处理。如果此基本包节点存在于多个类加载器位置,则实际的最终资源可能不存在。因此,在这种情况下,您应该更喜欢使用具有相同 Ant 样式模式的classpath*:,该模式将搜索包含根包的所有 Classpath 位置。

2.7.3. FileSystemResource 警告

未连接到FileSystemApplicationContextFileSystemResource(也就是说,当FileSystemApplicationContext不是实际的ResourceLoader时)将按您期望的那样处理绝对路径和相对路径。相对路径是相对于当前工作目录的,而绝对路径是相对于文件系统的根的。

但是,出于向后兼容性(历史)的原因,当FileSystemApplicationContextResourceLoader时,情况会有所变化。 FileSystemApplicationContext强制所有附加的FileSystemResource实例将所有位置路径都视为相对位置,无论它们是否以前斜杠开头。实际上,这意味着以下示例是等效的:

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/context.xml");
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("/conf/context.xml");

以下示例也是等效的(即使它们有所不同也有意义,因为一种情况是相对的,另一种情况是绝对的):

FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");

实际上,如果您需要真实的绝对文件系统路径,则应避免将绝对路径与FileSystemResourceFileSystemXmlApplicationContext一起使用,而应通过使用file: URL 前缀来强制使用UrlResource。以下示例显示了如何执行此操作:

// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("file:///conf/context.xml");

3.验证,数据绑定和类型转换

考虑将验证作为业务逻辑是有利有弊,Spring 提供了一种验证(和数据绑定)设计,但并不排除其中任何一个。具体来说,验证不应与网络层绑定,并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring 提供了一个Validator接口,该接口既基本又可以在应用程序的每一层使用。

数据绑定对于使用户 Importing 动态绑定到应用程序(或用于处理用户 Importing 的任何对象)域模型很有用。 Spring 提供了恰当地命名为DataBinder的名称。 ValidatorDataBinder组成了validation程序包,该程序包主要用于但不限于 MVC 框架。

BeanWrapper是 Spring 框架中的一个基本概念,在很多地方都使用过。但是,您可能不需要直接使用BeanWrapper。但是,由于这是参考文档,因此我们认为可能需要进行一些解释。我们将在本章中解释BeanWrapper,因为如果您将要使用它,则在尝试将数据绑定到对象时最有可能使用它。

Spring 的DataBinder和较低级别的BeanWrapper都使用PropertyEditorSupport实现来解析和格式化属性值。 PropertyEditorPropertyEditorSupport接口是 JavaBeans 规范的一部分,本章还将对此进行说明。 Spring 3 引入了core.convert软件包,该软件包提供了常规的类型转换工具,以及用于格式化 UI 字段值的高级“格式”软件包。您可以将这些软件包用作PropertyEditorSupport实现的更简单替代方案。本章还将对它们进行讨论。

JSR-303/JSR-349 Bean 验证

从 4.0 版本开始,Spring 框架支持 Bean 验证 1.0(JSR-303)和 Bean 验证 1.1(JSR-349),以支持设置并使其适应 Spring 的Validator接口。

应用程序可以选择一次全局启用 Bean 验证(如Spring Validation中所述),并将其专用于所有验证需求。

应用程序还可以为每个DataBinder实例注册其他 Spring Validator实例,如配置一个 DataBinder中所述。这对于在不使用注解的情况下插入验证逻辑可能很有用。

3.1. 使用 Spring 的 Validator 接口进行验证

Spring 具有Validator接口,可用于验证对象。 Validator接口通过使用Errors对象工作,因此验证器可以在验证时向Errors对象报告验证失败。

考虑以下小型数据对象的示例:

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

下一个示例通过实现org.springframework.validation.Validator接口的以下两种方法来提供Person类的验证行为:

实现Validator非常简单,尤其是当您知道 Spring 框架还提供的ValidationUtils helper 类时。以下示例为Person实例实现Validator

public class PersonValidator implements Validator {

    /**
     * This Validator validates *only* Person instances
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

ValidationUtils类上的static rejectIfEmpty(..)方法用于拒绝name属性(如果它是null或空字符串)。看看ValidationUtils javadoc,看看它提供了除先前显示的示例之外的功能。

虽然可以实现单个Validator类来验证丰富对象中的每个嵌套对象,但是最好将每个嵌套类的验证逻辑封装在自己的Validator实现中。一个“丰富”对象的简单示例是一个Customer,它由两个String属性(名字和第二个名称)和一个复杂的Address对象组成。 Address对象可以独立于Customer对象使用,因此已实现了不同的AddressValidator。如果希望CustomerValidator重用AddressValidator类中包含的逻辑而不求助于复制和粘贴,则可以在CustomerValidator中依赖注入或实例化AddressValidator,如以下示例所示:

public class CustomerValidator implements Validator {

    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if (addressValidator == null) {
            throw new IllegalArgumentException("The supplied [Validator] is " +
                "required and must not be null.");
        }
        if (!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("The supplied [Validator] must " +
                "support the validation of [Address] instances.");
        }
        this.addressValidator = addressValidator;
    }

    /**
     * This Validator validates Customer instances, and any subclasses of Customer too
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
        Customer customer = (Customer) target;
        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

验证错误会报告给传递给验证器的Errors对象。对于 Spring Web MVC,可以使用<spring:bind/>标签检查错误消息,但是也可以自己检查Errors对象。有关其提供的方法的更多信息,请参见javadoc

3.2. 将代码解析为错误消息

我们介绍了数据绑定和验证。本节介绍与验证错误相对应的输出消息。在preceding section所示的示例中,我们拒绝了nameage字段。如果要使用MessageSource输出错误消息,可以使用拒绝字段时提供的错误代码(在这种情况下为“名称”和“年龄”)进行输出。当您从Errors接口调用(直接或间接使用ValidationUtils类)rejectValue或其他reject方法时,基础实现不仅会注册您传入的代码,还会注册许多其他错误代码。 MessageCodesResolver确定Errors接口寄存器的错误代码。默认情况下,使用DefaultMessageCodesResolver,例如,它不仅使用您提供的代码注册一条消息,而且还注册包含您传递给 reject 方法的字段名称的消息。因此,如果您使用rejectValue("age", "too.darn.old")拒绝字段,除了too.darn.old代码,Spring 还会注册too.darn.old.agetoo.darn.old.age.int(第一个包含字段名称,第二个包含字段类型)。这样做是为了方便开发人员在定位错误消息时提供帮助。

有关MessageCodesResolver和默认策略的更多信息,可以分别在MessageCodesResolverDefaultMessageCodesResolver的 javadoc 中找到。

3.3. Bean 操作和 BeanWrapper

org.springframework.beans软件包遵循 JavaBeans 标准。 JavaBean 是具有默认无参数构造函数的类,并且遵循命名约定,其中(例如)名为bingoMadness的属性将具有 setter 方法setBingoMadness(..)和 getter 方法getBingoMadness()。有关 JavaBean 和规范的更多信息,请参见javabeans

Bean 包中的一个非常重要的类是BeanWrapper接口及其相应的实现(BeanWrapperImpl)。正如从 Javadoc 引用的那样,BeanWrapper提供了以下功能:设置和获取属性值(单独或批量),获取属性 Descriptors 以及查询属性以确定它们是否可读或可写。此外,BeanWrapper还支持嵌套属性,从而可以将子属性上的属性设置为无限深度。 BeanWrapper还支持添加标准 JavaBean PropertyChangeListenersVetoableChangeListeners的能力,而无需在目标类中支持代码。最后但并非最不重要的一点是,BeanWrapper支持设置索引属性。 BeanWrapper通常不直接由应用程序代码使用,而由DataBinderBeanFactory使用。

BeanWrapper的工作方式部分地由其名称表示:它包装一个 bean 以对该 bean 执行操作,例如设置和检索属性。

3.3.1. 设置和获取基本和嵌套属性

设置和获取属性的方法是使用setPropertyValuesetPropertyValuesgetPropertyValuegetPropertyValues方法,这些方法带有一些重载的变体。 Springs javadoc 更详细地描述了它们。 JavaBeans 规范具有用于指示对象属性的约定。下表显示了这些约定的一些示例:

表 11.属性示例

Expression Explanation
name 表示与nameisName()setName(..)方法相对应的属性name
account.name 表示与getAccount().setName()getAccount().getName()方法相对应的属性account的嵌套属性name
account[2] 指示索引属性account的* third *元素。索引属性可以是arraylist或其他自然排序的集合。
account[COMPANYNAME] 表示由account Map属性的COMPANYNAME键索引的 Map 条目的值。

(如果您不打算直接使用BeanWrapper,那么下一部分对您而言并不重要.如果仅使用DataBinderBeanFactory及其默认实现,则应跳到关于 PropertyEditors 的部分。)

以下两个示例类使用BeanWrapper来获取和设置属性:

public class Company {

    private String name;
    private Employee managingDirector;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Employee getManagingDirector() {
        return this.managingDirector;
    }

    public void setManagingDirector(Employee managingDirector) {
        this.managingDirector = managingDirector;
    }
}
public class Employee {

    private String name;

    private float salary;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getSalary() {
        return salary;
    }

    public void setSalary(float salary) {
        this.salary = salary;
    }
}

以下代码段显示了如何检索和操纵实例化的CompaniesEmployees的某些属性的一些示例:

BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");

3.3.2. 内置的 PropertyEditor 实现

Spring 使用PropertyEditor的概念来实现ObjectString之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,Date可以以人类可读的方式表示(如String'2007-14-09'),而我们仍然可以将人类可读的形式转换回原始日期(或者更好的是,转换以人类可读的形式 Importing 的任何日期)回到Date个对象)。通过注册java.beans.PropertyEditor类型的自定义编辑器可以实现此行为。在BeanWrapper上或在特定的 IoC 容器中注册自定义编辑器(如上一章所述),使它具有如何将属性转换为所需类型的知识。有关PropertyEditor的更多信息,请参见Oracle 的 java.beans 包的 javadoc

在 Spring 中使用属性编辑的几个示例:

Spring 具有许多内置的PropertyEditor实现,以简化生活。它们都位于org.springframework.beans.propertyeditors包中。默认情况下,大多数(但不是全部,如下表所示)由BeanWrapperImpl注册。如果可以通过某种方式配置属性编辑器,则仍可以注册自己的变体以覆盖默认变体。下表描述了 Spring 提供的各种PropertyEditor实现:

表 12.内置PropertyEditor实现

Class Explanation
ByteArrayPropertyEditor 字节数组的编辑器。将字符串转换为其相应的字节表示形式。默认情况下由BeanWrapperImpl注册。
ClassEditor 将表示类的字符串解析为实际类,反之亦然。如果找不到类,则会抛出IllegalArgumentException。默认情况下,由BeanWrapperImpl注册。
CustomBooleanEditor Boolean个属性的可定制属性编辑器。默认情况下,由BeanWrapperImpl注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomCollectionEditor 集合的属性编辑器,可将任何源Collection转换为给定目标Collection类型。
CustomDateEditor java.util.Date的可定制属性编辑器,支持定制DateFormat。默认未注册。必须根据需要以适当的格式进行用户注册。
CustomNumberEditor 任何Number子类(例如IntegerLongFloatDouble)的可定制属性编辑器。默认情况下,由BeanWrapperImpl注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。
FileEditor 将字符串解析为java.io.File个对象。默认情况下,由BeanWrapperImpl注册。
InputStreamEditor 单向属性编辑器,它可以获取字符串并产生(通过中间的ResourceEditorResource)InputStream,以便可以将InputStream属性直接设置为字符串。请注意,默认用法不会为您关闭InputStream。默认情况下,由BeanWrapperImpl注册。
LocaleEditor 可以将字符串解析为Locale对象,反之亦然(字符串格式为[country][variant],与LocaletoString()方法相同)。默认情况下,由BeanWrapperImpl注册。
PatternEditor 可以将字符串解析为java.util.regex.Pattern个对象,反之亦然。
PropertiesEditor 可以将字符串(格式为java.util.Properties类的 javadoc 中定义的格式)转换为Properties对象。默认情况下,由BeanWrapperImpl注册。
StringTrimmerEditor 修剪字符串的属性编辑器。 (可选)允许将空字符串转换为null值。默认情况下未注册—必须是用户注册的。
URLEditor 可以将 URL 的字符串表示形式解析为实际的URL对象。默认情况下,由BeanWrapperImpl注册。

Spring 使用java.beans.PropertyEditorManager来设置可能需要的属性编辑器的搜索路径。搜索路径还包括sun.bean.editors,其中sun.bean.editors包括针对FontColor等类型和大多数基本类型的PropertyEditor实现。还要注意,如果标准 JavaBeans 基础结构与它们处理的类在相同的程序包中并且与该类具有相同的名称并附加了Editor,则自动发现PropertyEditor类(无需显式注册它们)。例如,可能具有以下类和包结构,足以识别SomethingEditor类并将其用作Something类型的属性的PropertyEditor

com
  chank
    pop
      Something
      SomethingEditor // the PropertyEditor for the Something class

请注意,您也可以在此处使用标准的BeanInfo JavaBeans 机制(在某种程度上已描述here)。下面的示例使用BeanInfo机制使用关联类的属性显式注册一个或多个PropertyEditor实例:

com
  chank
    pop
      Something
      SomethingBeanInfo // the BeanInfo for the Something class

所引用的SomethingBeanInfo类的以下 Java 源代码将CustomNumberEditorSomething类的age属性相关联:

public class SomethingBeanInfo extends SimpleBeanInfo {

    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}
注册其他自定义 PropertyEditor 实现

当将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditor实现将这些字符串转换为属性的复杂类型。 Spring 预注册了许多自定义PropertyEditor实现(例如,将表示为字符串的类名转换为Class对象)。另外,Java 的标准 JavaBeans PropertyEditor查找机制允许为类的PropertyEditor适当命名,并与它提供支持的类放在同一包中,以便可以自动找到它。

如果需要注册其他自定义PropertyEditors,则可以使用几种机制。最手动的方法(通常不方便或不建议使用)是使用ConfigurableBeanFactory接口的registerCustomEditor()方法(假设您有BeanFactory引用)。另一种(稍微方便些)的机制是使用称为CustomEditorConfigurer的特殊 bean 工厂后处理器。尽管您可以将 Bean 工厂后处理器与BeanFactory实现一起使用,但是CustomEditorConfigurer具有嵌套的属性设置,因此我们强烈建议您将CustomEditorConfigurerApplicationContext一起使用,在这里您可以以与其他任何 Bean 类似的方式部署它,并且可以将其放置在自动检测并应用。

请注意,所有 bean 工厂和应用程序上下文通过使用BeanWrapper来处理属性转换,都会自动使用许多内置的属性编辑器。 BeanWrapper寄存器的标准属性编辑器在上一节中列出。此外,ApplicationContexts还重写或添加其他编辑器,以适合于特定应用程序上下文类型的方式处理资源查找。

标准 JavaBeans PropertyEditor实例用于将以字符串表示的属性值转换为该属性的实际复杂类型。您可以使用 Bean 工厂后处理程序CustomEditorConfigurer来方便地向ApplicationContext添加对其他PropertyEditor实例的支持。

考虑以下示例,该示例定义了一个名为ExoticType的用户类和另一个名为DependsOnExoticType的类,该类需要将ExoticType设置为属性:

package example;

public class ExoticType {

    private String name;

    public ExoticType(String name) {
        this.name = name;
    }
}

public class DependsOnExoticType {

    private ExoticType type;

    public void setType(ExoticType type) {
        this.type = type;
    }
}

正确设置之后,我们希望能够将 type 属性分配为字符串,PropertyEditor会将其转换为实际的ExoticType实例。以下 bean 定义显示了如何构建这种关系:

<bean id="sample" class="example.DependsOnExoticType">
    <property name="type" value="aNameForExoticType"/>
</bean>

PropertyEditor的实现可能类似于以下内容:

// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}

最后,以下示例显示了如何使用CustomEditorConfigurerApplicationContext注册新的PropertyEditor,然后便可以根据需要使用它了:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
        </map>
    </property>
</bean>
Using PropertyEditorRegistrar

向 Spring 容器注册属性编辑器的另一种机制是创建并使用PropertyEditorRegistrar。当需要在几种不同情况下使用同一组属性编辑器时,此接口特别有用。您可以编写相应的注册商,并在每种情况下重复使用它。 PropertyEditorRegistrar实例与名为PropertyEditorRegistry的接口配合使用,该接口由 Spring BeanWrapper(和DataBinder)实现。 PropertyEditorRegistrar实例与CustomEditorConfigurer(描述为here)结合使用时特别方便,后者公开了称为setPropertyEditorRegistrars(..)的属性。以这种方式添加到CustomEditorConfigurerPropertyEditorRegistrar实例可以轻松地与DataBinder和 Spring MVC 控制器共享。此外,它避免了在自定义编辑器上进行同步的需要:PropertyEditorRegistrar有望为每次 bean 创建尝试创建新的PropertyEditor实例。

以下示例显示了如何创建自己的PropertyEditorRegistrar实现:

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // it is expected that new PropertyEditor instances are created
        registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

        // you could register as many custom property editors as are required here...
    }
}

另请参见org.springframework.beans.support.ResourceEditorRegistrar以获取示例PropertyEditorRegistrar的实现。请注意,在实现registerCustomEditors(..)方法时,它如何创建每个属性编辑器的新实例。

下一个示例显示了如何配置CustomEditorConfigurer并将CustomPropertyEditorRegistrar实例注入其中:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>

<bean id="customPropertyEditorRegistrar"
    class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>

最后(对于使用Spring 的 MVC Web 框架的用户而言,这与本章的重点有所偏离),将PropertyEditorRegistrars与数据绑定Controllers(例如SimpleFormController)结合使用会非常方便。下面的示例在initBinder(..)方法的实现中使用PropertyEditorRegistrar

public final class RegisterUserController extends SimpleFormController {

    private final PropertyEditorRegistrar customPropertyEditorRegistrar;

    public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
        this.customPropertyEditorRegistrar = propertyEditorRegistrar;
    }

    protected void initBinder(HttpServletRequest request,
            ServletRequestDataBinder binder) throws Exception {
        this.customPropertyEditorRegistrar.registerCustomEditors(binder);
    }

    // other methods to do with registering a User
}

这种PropertyEditor注册样式可以导致代码简洁(initBinder(..)的实现只有一行长),并且可以将通用的PropertyEditor注册代码封装在一个类中,然后根据需要在多个Controllers之间共享。

3.4. Spring 类型转换

Spring 3 引入了core.convert软件包,该软件包提供了常规的类型转换系统。该系统定义了一个用于实现类型转换逻辑的 SPI 和一个用于在运行时执行类型转换的 API。在 Spring 容器中,您可以将此系统用作PropertyEditor实现的替代方法,以将外部化的 bean 属性值字符串转换为所需的属性类型。您还可以在应用程序中需要类型转换的任何地方使用公共 API。

3.4.1. 转换器 SPI

如以下接口定义所示,用于实现类型转换逻辑的 SPI 非常简单且具有强类型。

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
}

要创建自己的转换器,请实现Converter界面,并将S作为要转换的类型,并将T参数化为要转换的类型。如果需要将S的集合或数组转换为T的数组或集合,您也可以透明地应用此类转换器,前提是还已注册了委派的数组或集合转换器(默认情况下DefaultConversionService这样做)。

对于每次convert(S)的调用,保证源参数不为 null。如果转换失败,您的Converter可能会引发任何未经检查的异常。具体来说,它应该抛出IllegalArgumentException以报告无效的源值。注意确保Converter实现是线程安全的。

为方便起见,在core.convert.support包中提供了几种转换器实现。这些包括从字符串到数字以及其他常见类型的转换器。下面的清单显示了StringToInteger类,这是一个典型的Converter实现:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

3.4.2. 使用 ConverterFactory

当需要集中整个类层次结构的转换逻辑时(例如,从 String 转换为 java.lang.Enum 对象时),可以实现ConverterFactory,如以下示例所示:

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

参数化 S 为您要转换的类型,参数化 R 为定义可以转换为的类的“范围”的基本类型。然后实现 getConverter(Class),其中 T 是 R 的子类。

StringToEnum ConverterFactory为例:

package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }

    private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}

3.4.3. 使用 GenericConverter

当您需要复杂的Converter实现时,请考虑使用GenericConverter界面。 GenericConverter具有比Converter更灵活但类型不那么强的签名,支持GenericConverter在多种源类型和目标类型之间进行转换。另外,GenericConverter提供了实现转换逻辑时可以使用的源字段和目标字段上下文。这种上下文允许类型转换由字段注解或在字段签名上声明的通用信息驱动。以下清单显示了GenericConverter的接口定义:

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

要实现GenericConverter,让getConvertibleTypes()返回支持的源→目标类型对。然后实现convert(Object, TypeDescriptor, TypeDescriptor)以包含您的转换逻辑。源TypeDescriptor提供对包含正在转换的值的源字段的访问。目标TypeDescriptor提供对要设置转换值的目标字段的访问。

GenericConverter的一个很好的例子是在 Java 数组和集合之间进行转换的转换器。这样的ArrayToCollectionConverter会检查声明目标集合类型的字段以解析集合的元素类型。这样就可以在将集合设置到目标字段上之前,将源数组中的每个元素转换为集合元素类型。

Note

由于GenericConverter是更复杂的 SPI 接口,因此仅应在需要时使用它。支持ConverterConverterFactory以满足基本类型转换需求。

Using ConditionalGenericConverter

有时,您希望Converter仅在满足特定条件的情况下运行。例如,您可能只想在目标字段上存在特定注解时才运行Converter,或者仅在目标类上定义了特定方法(例如static valueOf方法)时才运行ConverterConditionalGenericConverterGenericConverterConditionalConverter接口的并集,可用于定义以下自定义匹配条件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

ConditionalGenericConverter的一个很好的例子是EntityConverter,它在持久性实体标识符和实体引用之间转换。仅当目标实体类型声明静态查找器方法(例如findAccount(Long))时,此类EntityConverter才可能匹配。您可以在matches(TypeDescriptor, TypeDescriptor)的实现中执行这种 finder 方法检查。

3.4.4. ConversionService API

ConversionService定义了一个统一的 API,用于在运行时执行类型转换逻辑。转换器通常在以下外观界面后面执行:

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

大多数ConversionService实现还实现ConverterRegistry,该ConverterRegistry提供了用于注册转换器的 SPI。在内部,ConversionService实现委派其注册的转换器执行类型转换逻辑。

core.convert.support软件包中提供了一种强大的ConversionService实现。 GenericConversionService是适用于大多数环境的通用实现。 ConversionServiceFactory提供了一个方便的工厂来创建通用的ConversionService配置。

3.4.5. 配置 ConversionService

ConversionService是 Stateless 对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,通常为每个 Spring 容器(或ApplicationContext)配置一个ConversionService实例。 Spring 需要ConversionService并在框架需要执行类型转换时使用它。您也可以将此ConversionService注入到您的任何 bean 中并直接调用它。

Note

如果没有ConversionService向 Spring 注册,则使用基于PropertyEditor的原始系统。

要向 Spring 注册默认的ConversionService,请添加以下 Bean 定义,并将idconversionService

<bean id="conversionService"
    class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默认的ConversionService可以在字符串,数字,枚举,集合,Map 和其他常见类型之间进行转换。要用您自己的自定义转换器补充或覆盖默认转换器,请设置converters属性。属性值可以实现ConverterConverterFactoryGenericConverter接口中的任何一个。

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>

在 Spring MVC 应用程序中通常使用ConversionService。请参阅 Spring MVC 章节中的转换和格式化

在某些情况下,您可能希望在转换过程中应用格式设置。有关使用FormattingConversionServiceFactoryBean的详细信息,请参见FormatterRegistry SPI

3.4.6. 以编程方式使用 ConversionService

要以编程方式使用ConversionService实例,可以像对其他任何 bean 一样注入对该实例的引用。以下示例显示了如何执行此操作:

@Service
public class MyService {

    @Autowired
    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void doIt() {
        this.conversionService.convert(...)
    }
}

对于大多数使用情况,可以使用指定targetTypeconvert方法,但不适用于更复杂的类型,例如参数化元素的集合。例如,如果要以编程方式将IntegerList转换为StringList,则需要提供源类型和目标类型的正式定义。

幸运的是,TypeDescriptor提供了各种选项来使操作变得如此简单,如以下示例所示:

DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ....
cs.convert(input,
    TypeDescriptor.forObject(input), // List<Integer> type descriptor
    TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

请注意,DefaultConversionService自动注册适用于大多数环境的转换器。这包括集合转换器,标量转换器和基本的ObjectString转换器。您可以使用DefaultConversionService类上的静态addDefaultConverters方法将相同的转换器注册到任何ConverterRegistry中。

值类型的转换器可重用于数组和集合,因此,无需使用特定的转换器即可将SCollection转换为TCollection,前提是需要标准的集合处理。

3.5. Spring 字段格式

如上一节所述,core.convert是通用类型转换系统。它提供了统一的ConversionService API 以及强类型的Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。 Spring 容器使用此系统绑定 bean 属性值。此外,Spring Expression Language(SpEL)和DataBinder都使用此系统绑定字段值。例如,当 SpEL 需要将Short强制转换为Long才能完成expression.setValue(Object bean, Object value)尝试时,core.convert系统将执行强制转换。

现在考虑典型 Client 端环境(例如 Web 或桌面应用程序)的类型转换要求。在这种环境中,通常您会从String转换为支持 Client 端回发过程,而从String转换为支持视图渲染过程。另外,您通常需要本地化String值。更为通用的core.convert Converter SPI 不能直接解决此类格式化要求。为了直接解决这些问题,Spring 3 引入了一个方便的Formatter SPI,它为 Client 端环境提供了一种简单且健壮的替代PropertyEditor实现的方法。

通常,当您需要实现通用类型转换逻辑时(例如,在java.util.Datejava.lang.Long之间进行转换),可以使用Converter SPI。在 Client 端环境(例如 Web 应用程序)中工作并且需要解析和打印本地化的字段值时,可以使用Formatter SPI。 ConversionService为两个 SPI 提供统一的类型转换 API。

3.5.1. 格式化器 SPI

用于实现字段格式逻辑的Formatter SPI 非常简单且类型严格。以下清单显示了Formatter接口定义:

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter来自PrinterParser构件块接口。以下清单显示了这两个接口的定义:

public interface Printer<T> {

    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;

public interface Parser<T> {

    T parse(String clientValue, Locale locale) throws ParseException;
}

要创建自己的Formatter,请实现前面显示的Formatter接口。将T参数化为您希望格式化的对象类型(例如java.util.Date)。实现print()操作以打印T的实例以在 Client 端语言环境中显示。实现parse()操作,以从 Client 端语言环境返回的格式化表示形式解析T的实例。如果解析尝试失败,则您的Formatter应该抛出ParseExceptionIllegalArgumentException。注意确保Formatter实现是线程安全的。

为了方便起见,format子软件包提供了Formatter个实现。 number包提供NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter来格式化使用java.text.NumberFormatjava.lang.Number对象。 datetime包提供了DateFormatter,用于将java.util.Date对象格式化为java.text.DateFormatdatetime.joda包基于Joda-Time library提供了全面的日期时间格式支持。

以下DateFormatter是示例Formatter的实现:

package org.springframework.format.datetime;

public final class DateFormatter implements Formatter<Date> {

    private String pattern;

    public DateFormatter(String pattern) {
        this.pattern = pattern;
    }

    public String print(Date date, Locale locale) {
        if (date == null) {
            return "";
        }
        return getDateFormat(locale).format(date);
    }

    public Date parse(String formatted, Locale locale) throws ParseException {
        if (formatted.length() == 0) {
            return null;
        }
        return getDateFormat(locale).parse(formatted);
    }

    protected DateFormat getDateFormat(Locale locale) {
        DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
        dateFormat.setLenient(false);
        return dateFormat;
    }
}

Spring 小组欢迎社区推动的Formatter贡献,请参阅jira.spring.io做出贡献。

3.5.2.注解驱动的格式

可以通过字段类型或注解配置字段格式。要将注解绑定到Formatter,请实现AnnotationFormatterFactory。以下清单显示了AnnotationFormatterFactory接口的定义:

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);
}

要创建一个实现:。将 A 参数化为您希望与格式逻辑关联的字段annotationType,例如org.springframework.format.annotation.DateTimeFormat。 。让getFieldTypes()返回可以在其上使用注解的字段类型。 。让getPrinter()返回Printer以打印带注解的字段的值。 。让getParser()返回Parser来解析带注解字段的clientValue

下面的示例AnnotationFormatterFactory实现将@NumberFormat注解 绑定到格式化程序,以指定数字样式或模式:

public final class NumberFormatAnnotationFormatterFactory
        implements AnnotationFormatterFactory<NumberFormat> {

    public Set<Class<?>> getFieldTypes() {
        return new HashSet<Class<?>>(asList(new Class<?>[] {
            Short.class, Integer.class, Long.class, Float.class,
            Double.class, BigDecimal.class, BigInteger.class }));
    }

    public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
        if (!annotation.pattern().isEmpty()) {
            return new NumberStyleFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentStyleFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyStyleFormatter();
            } else {
                return new NumberStyleFormatter();
            }
        }
    }
}

要触发格式,可以使用@NumberFormat注解 字段,如以下示例所示:

public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}
格式 注解API

org.springframework.format.annotation软件包中存在可移植格式 注解API。您可以使用@NumberFormat格式化 java.lang.Number 字段,使用@DateTimeFormat格式化java.util.Datejava.util.Calendarjava.util.Long或 Joda-Time 字段。

下面的示例使用@DateTimeFormat格式化java.util.Date作为 ISO 日期(yyyy-MM-dd):

public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}

3.5.3. FormatterRegistry SPI

FormatterRegistry是用于注册格式器和转换器的 SPI。 FormattingConversionService是适用于大多数环境的FormatterRegistry的实现。您可以使用FormattingConversionServiceFactoryBean以编程方式或声明方式将此实现配置为 Spring bean。因为此实现还实现了ConversionService,所以您可以直接配置它以与 Spring 的DataBinder和 Spring Expression Language(SpEL)一起使用。

以下清单显示了FormatterRegistry SPI:

package org.springframework.format;

public interface FormatterRegistry extends ConverterRegistry {

    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

    void addFormatterForFieldType(Formatter<?> formatter);

    void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);
}

如前面的清单所示,您可以按字段类型或注解注册格式化程序。

FormatterRegistry SPI 使您可以集中配置格式设置规则,而不必在控制器之间复制此类配置。例如,您可能需要强制以某种方式设置所有日期字段的格式或以某种方式设置带有特定注解的字段的格式。使用共享的FormatterRegistry,您一次定义这些规则,并在需要格式化时将它们应用。

3.5.4. FormatterRegistrar SPI

FormatterRegistrar是用于通过 FormatterRegistry 注册格式器和转换器的 SPI。以下清单显示了其接口定义:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

为给定的格式类别(例如日期格式)注册多个相关的转换器和格式器时,FormatterRegistrar很有用。在声明式注册不充分的情况下它也很有用。例如,当格式化程序需要在不同于其自己的<T>的特定字段类型下进行索引时,或者在注册Printer/Parser对时。下一节将提供有关转换器和格式化程序注册的更多信息。

3.5.5. 在 Spring MVC 中配置格式

请参阅 Spring MVC 章节中的转换和格式化

3.6. 配置全局日期和时间格式

默认情况下,未使用@DateTimeFormat注解 的日期和时间字段是使用DateFormat.SHORT样式从字符串转换的。如果愿意,可以通过定义自己的全局格式来更改此设置。

为此,您需要确保 Spring 不注册默认格式器。相反,您应该手动注册所有格式化程序。使用org.springframework.format.datetime.joda.JodaTimeFormatterRegistrarorg.springframework.format.datetime.DateFormatterRegistrar类,具体取决于您使用的是 Joda-Time 库。

例如,以下 Java 配置注册全局yyyyMMdd格式(此示例不依赖于 Joda-Time 库):

@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

        // Use the DefaultFormattingConversionService but do not register defaults
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

        // Ensure @NumberFormat is still supported
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

        // Register date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}

如果您喜欢基于 XML 的配置,则可以使用FormattingConversionServiceFactoryBean。以下示例显示了如何执行此操作(这次使用 Joda Time):

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

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="registerDefaultFormatters" value="false" />
        <property name="formatters">
            <set>
                <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
                    <property name="dateFormatter">
                        <bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
                            <property name="pattern" value="yyyyMMdd"/>
                        </bean>
                    </property>
                </bean>
            </set>
        </property>
    </bean>
</beans>

Note

Joda-Time 提供了单独的不同类型来表示datetimedate-time值。 JodaTimeFormatterRegistrardateFormattertimeFormatterdateTimeFormatter属性应用于为每种类型配置不同的格式。 DateTimeFormatterFactoryBean提供了一种创建格式化程序的便捷方法。

Note

如果您使用 Spring MVC,请记住显式配置所使用的转换服务。对于基于 Java 的@Configuration,这意味着扩展WebMvcConfigurationSupport类并覆盖mvcConversionService()方法。对于 XML,应使用mvc:annotation-driven元素的conversion-service属性。有关详情,请参见转换和格式化

3.7. Spring 验证

Spring 3 对其验证支持进行了一些增强。首先,完全支持 JSR-303 Bean 验证 API。其次,当以编程方式使用时,Spring 的DataBinder可以验证对象并绑定到它们。第三,Spring MVC 支持以声明方式验证@ControllerImporting。

3.7.1. JSR-303 Bean 验证 API 概述

JSR-303 标准化了 Java 平台的验证约束声明和元数据。通过使用此 API,您可以使用声明性验证约束来注解域模型属性,并且运行时会强制执行它们。您可以使用许多内置约束。您还可以定义自己的自定义约束。

考虑以下示例,该示例显示了具有两个属性的简单PersonForm模型:

public class PersonForm {
    private String name;
    private int age;
}

JSR-303 允许您针对此类属性定义声明性验证约束,如以下示例所示:

public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}

当 JSR-303 验证程序验证此类的实例时,将强制执行这些约束。

有关 JSR-303 和 JSR-349 的一般信息,请参见Bean 验证网站。有关默认参考实现的特定功能的信息,请参见Hibernate Validator文档。要学习如何将 bean 验证提供程序设置为 Spring bean,请 continue 阅读。

3.7.2. 配置 Bean 验证提供程序

Spring 提供了对 Bean 验证 API 的全面支持。这包括对将 JSR-303 或 JSR-349 Bean 验证提供程序引导为 Spring Bean 的便捷支持。这样,您就可以在应用程序中需要验证的地方注入javax.validation.ValidatorFactoryjavax.validation.Validator

您可以使用LocalValidatorFactoryBean将默认的 Validator 配置为 Spring Bean,如以下示例所示:

<bean id="validator"
    class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

前面示例中的基本配置触发 Bean 验证以使用其默认引导机制进行初始化。诸如 Hibernate Validator 之类的 JSR-303 或 JSR-349 提供程序应预期存在于 Classpath 中并被自动检测到。

注入验证器

LocalValidatorFactoryBean实现javax.validation.ValidatorFactoryjavax.validation.Validator以及 Spring 的org.springframework.validation.Validator。您可以将对这些接口之一的引用注入需要调用验证逻辑的 bean 中。

如果您希望直接使用 Bean Validation API,则可以插入对javax.validation.Validator的引用,如以下示例所示:

import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

如果您的 bean 需要使用 Spring Validation API,则可以注入对org.springframework.validation.Validator的引用,如以下示例所示:

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}
配置自定义约束

每个 bean 验证约束由两部分组成:* @Constraint注解,用于声明约束及其可配置属性。 * javax.validation.ConstraintValidator接口的实现,用于实现约束的行为。

要将声明与实现相关联,每个@Constraint注解都引用一个对应的ConstraintValidator实现类。在运行时,当域模型中遇到约束注解时,ConstraintValidatorFactory实例化引用的实现。

默认情况下,LocalValidatorFactoryBean配置使用 Spring 创建ConstraintValidator实例的SpringConstraintValidatorFactory。这使您的自定义ConstraintValidators像任何其他 Spring bean 一样受益于依赖项注入。

以下示例显示了一个自定义@Constraint声明,后跟一个关联的ConstraintValidator实现,该实现使用 Spring 进行依赖项注入:

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    ...
}

如前面的示例所示,ConstraintValidator实现可以像其他任何 Spring bean 一样具有其依赖关系@Autowired

Spring 驱动的方法验证

您可以通过MethodValidationPostProcessor bean 定义将 Bean Validation 1.1(以及作为自定义扩展,还包括 Hibernate Validator 4.3)支持的方法验证功能通过MethodValidationPostProcessor bean 定义集成到 Spring 上下文中,如下所示:

<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

为了有资格通过 Spring 驱动的方法验证,所有目标类都需要使用 Spring 的@Validated注解 进行 注解。 (可选地,您也可以声明要使用的验证组.)有关 Hibernate Validator 和 Bean Validation 1.1 提供程序的设置详细信息,请参见MethodValidationPostProcessor javadoc。

其他配置选项

在大多数情况下,默认的LocalValidatorFactoryBean配置就足够了。从消息插值到遍历解析,有许多用于各种 Bean 验证构造的配置选项。有关这些选项的更多信息,请参见LocalValidatorFactoryBean javadoc。

3.7.3. 配置一个 DataBinder

从 Spring 3 开始,您可以使用Validator配置DataBinder实例。配置完成后,您可以通过调用binder.validate()来调用Validator。任何验证Errors都会自动添加到 Binder 的BindingResult中。

下面的示例显示在绑定到目标对象之后,如何以编程方式使用DataBinder来调用验证逻辑:

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// bind to the target object
binder.bind(propertyValues);

// validate the target object
binder.validate();

// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

您还可以通过dataBinder.addValidatorsdataBinder.replaceValidators配置具有多个Validator实例的DataBinder。当将全局配置的 bean 验证与在 DataBinder 实例上本地配置的 Spring Validator结合使用时,这很有用。参见[validation-mvc-configuring]

3.7.4. Spring MVC 3 验证

请参阅 Spring MVC 章节中的Validation

4. Spring 表达式语言(SpEL)

Spring 表达式语言(简称“ SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于 Unified EL,但提供了其他功能,最著名的是方法调用和基本的字符串模板功能。

尽管还有其他几种 Java 表达式语言可用-OGNL,MVEL 和 JBoss EL,仅举几例-Spring 表达式语言的创建是为了向 Spring 社区提供一种受良好支持的表达式语言,该语言可用于以下版本中的所有产品 Spring 投资组合。它的语言功能由 Spring 产品组合中的项目要求所驱动,包括基于 Eclipse 的 Spring Tool Suite 中代码完成支持的工具要求。也就是说,SpEL 基于与技术无关的 API,如果需要,可以将其他表达语言实现集成在一起。

虽然 SpEL 是 Spring 产品组合中表达评估的基础,但它并不直接与 Spring 绑定,可以独立使用。为了自成一体,本章中的许多示例都将 SpEL 当作一种独立的表达语言来使用。这需要创建一些自举基础结构类,例如解析器。大多数 Spring 用户不需要处理这种基础结构,而只能编写表达式字符串进行评估。这种典型用法的一个示例是将 SpEL 集成到创建 XML 或基于注解的 Bean 定义中,如表达式支持,用于定义 bean 定义所示。

本章介绍了表达语言,其 API 和语言语法的功能。在许多地方,InventorSociety类用作表达式评估的目标对象。这些类声明和用于填充它们的数据在本章末尾列出。

表达式语言支持以下功能:

4.1. Evaluation

本节介绍 SpEL 接口的简单用法及其表达语言。完整的语言参考可以在Language Reference中找到。

以下代码介绍了 SpEL API 来评估 Literals 字符串表达式Hello World

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();

您最可能使用的 SpEL 类和接口位于org.springframework.expression包及其子包中,例如spel.support

ExpressionParser接口负责解析表达式字符串。在前面的示例中,表达式字符串是由周围的单引号表示的字符串 Literals。 Expression接口负责评估先前定义的表达式字符串。分别调用parser.parseExpressionexp.getValue时,可以引发两个异常ParseExceptionEvaluationException

SpEL 支持多种功能,例如调用方法,访问属性和调用构造函数。

在以下方法调用示例中,我们在字符串 Literals 上调用concat方法:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();

以下调用 JavaBean 属性的示例将调用String属性Bytes

ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();

SpEL 还通过使用标准的点符号(例如prop1.prop2.prop3)和属性值的设置来支持嵌套属性。也可以访问公共字段。下面的示例演示如何使用点表示法获取 Literals 的长度:

ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1)
int length = (Integer) exp.getValue();

可以调用 String 的构造函数,而不是使用字符串 Literals,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);

注意使用通用方法:public <T> T getValue(Class<T> desiredResultType)。使用此方法无需将表达式的值强制转换为所需的结果类型。如果无法将值强制转换为T类型或无法使用已注册的类型转换器进行转换,则将引发EvaluationException

SpEL 的更常见用法是提供一个表达式字符串,该字符串针对特定对象实例(称为根对象)进行评估。下面的示例演示如何从Inventor类的实例检索name属性或创建布尔条件:

// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression("name"); (1)
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"

exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true

4.1.1. 了解评估上下文

评估表达式以解析属性,方法或字段并帮助执行类型转换时,使用EvaluationContext接口。 Spring 提供了两种实现。

SimpleEvaluationContext设计为仅支持 SpEL 语言语法的一部分。它不包括 Java 类型引用,构造函数和 Bean 引用。它还要求您明确选择对表达式中的属性和方法的支持级别。默认情况下,create()静态工厂方法仅启用对属性的读取访问。您还可以获取构建器来配置所需的确切支持级别,并针对以下一种或某些组合:

Type Conversion

默认情况下,SpEL 使用 Spring core(org.springframework.core.convert.ConversionService)中可用的转换服务。此转换服务随附有许多用于常见转换的内置转换器,但它也是完全可扩展的,因此您可以在类型之间添加自定义转换。此外,它是泛型感知的。这意味着,当您在表达式中使用泛型类型时,SpEL 会尝试进行转换以维护遇到的任何对象的类型正确性。

在实践中这意味着什么?假设使用setValue()的赋值被用来设置List属性。该属性的类型实际上是List<Boolean>。 SpEL 意识到列表中的元素需要先转换为Boolean,然后才能放入其中。以下示例显示了如何执行此操作:

class Simple {
    public List<Boolean> booleanList = new ArrayList<Boolean>();
}

Simple simple = new Simple();
simple.booleanList.add(true);

EvaluationContext context = SimpleEvaluationContext().forReadOnlyDataBinding().build();

// false is passed in here as a string. SpEL and the conversion service
// correctly recognize that it needs to be a Boolean and convert it
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");

// b is false
Boolean b = simple.booleanList.get(0);

4.1.2. 解析器配置

可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)配置 SpEL 表达式解析器。配置对象控制某些表达式组件的行为。例如,如果您索引到数组或集合中并且指定索引处的元素是null,则可以自动创建该元素。当使用由属性引用链组成的表达式时,这很有用。如果索引到数组或列表中并指定的索引超出了数组或列表当前大小的末尾,则可以自动增长数组或列表以容纳该索引。下面的示例演示如何自动增加列表:

class Demo {
    public List<String> list;
}

// Turn on:
// - auto null reference initialization
// - auto collection growing
SpelParserConfiguration config = new SpelParserConfiguration(true,true);

ExpressionParser parser = new SpelExpressionParser(config);

Expression expression = parser.parseExpression("list[3]");

Demo demo = new Demo();

Object o = expression.getValue(demo);

// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String

4.1.3. SpEL 编译

Spring Framework 4.1 包含一个基本的表达式编译器。通常对表达式进行解释,这样可以在评估过程中提供很大的动态灵 Active,但不能提供最佳性能。对于偶尔使用表达式,这很好,但是,当与其他组件(如 Spring Integration)一起使用时,性能可能非常重要,并且不需要动态性。

SpEL 编译器旨在满足这一需求。在评估期间,编译器会生成一个真实的 Java 类,该类体现了表达式行为,并使用该类来实现更快的表达式评估。由于缺少在表达式周围 Importing 内容的信息,因此编译器在执行编译时会使用在表达式的解释求值过程中收集的信息。例如,它不仅仅从表达式中就知道属性引用的类型,而是在第一次解释求值时就知道它是什么。当然,如果各种表达式元素的类型随时间变化,则基于此信息进行编译可能会在以后引起麻烦。因此,编译最适合类型信息在重复求值时不会改变的表达式。

考虑以下基本表达式:

someArray[0].someProperty.someOtherProperty < 0.1

因为前面的表达式涉及数组访问,一些属性取消引用和数字运算,所以性能提升可能非常明显。在一个示例中,进行了 50000 次迭代的微基准测试,使用解释器评估需要 75 毫秒,而使用表达式的编译版本仅需要 3 毫秒。

Compiler Configuration

默认情况下不打开编译器,但是您可以通过两种不同的方式之一来打开它。当 SpEL 用法嵌入到另一个组件中时,可以使用解析器配置过程(discussed earlier)或使用系统属性来将其打开。本节讨论这两个选项。

编译器可以在org.springframework.expression.spel.SpelCompilerMode枚举中捕获的三种模式之一进行操作。模式如下:

之所以存在IMMEDIATE模式,是因为MIXED模式可能会导致具有副作用的表达式出现问题。如果已编译的表达式在部分成功后就崩溃了,则它可能已经完成了影响系统状态的操作。如果发生这种情况,调用者可能不希望它在解释模式下静默地重新运行,因为表达式的一部分可能运行了两次。

选择模式后,使用SpelParserConfiguration配置解析器。以下示例显示了如何执行此操作:

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
    this.getClass().getClassLoader());

SpelExpressionParser parser = new SpelExpressionParser(config);

Expression expr = parser.parseExpression("payload");

MyMessage message = new MyMessage();

Object payload = expr.getValue(message);

当指定编译器模式时,还可以指定一个类加载器(允许传递 null)。编译的表达式是在提供的任何子类加载器中定义的。重要的是要确保,如果指定了类加载器,则它可以查看表达式评估过程中涉及的所有类型。如果未指定类加载器,则使用默认的类加载器(通常是在表达式求值期间运行的线程的上下文类加载器)。

第二种配置编译器的方法是将 SpEL 嵌入到其他组件中,并且可能无法通过配置对象进行配置。在这些情况下,可以使用系统属性。您可以将spring.expression.compiler.mode属性设置为SpelCompilerMode枚举值之一(offimmediatemixed)。

Compiler Limitations

从 Spring Framework 4.1 开始,已经有了基本的编译框架。但是,该框架尚不支持编译每种表达式。最初的重点是可能在性能关键型上下文中使用的通用表达式。目前无法编译以下类型的表达式:

将来会编译更多类型的表达。

4.2. Bean 定义中的表达式

您可以将 SpEL 表达式与基于 XML 或基于注解的配置元数据一起使用,以定义BeanDefinition实例。在这两种情况下,定义表达式的语法均为#{ <expression string> }形式。

4.2.1. XML 配置

可以使用表达式来设置属性或构造函数的参数值,如以下示例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

systemProperties变量是 sched 义的,因此您可以在表达式中使用它,如以下示例所示:

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

    <!-- other properties -->
</bean>

请注意,在这种情况下,您不必在 sched 义变量前加上#符号。

您还可以按名称引用其他 bean 属性,如以下示例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>

    <!-- other properties -->
</bean>

4.2.2.注解配置

若要指定默认值,可以在字段,方法以及方法或构造函数参数上放置@Value注解。

下面的示例设置字段变量的默认值:

public static class FieldValueTestBean

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }

}

以下示例显示了等效的但使用属性设置器方法的示例:

public static class PropertyValueTestBean

    private String defaultLocale;

    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }

}

自动装配的方法和构造函数也可以使用@Value注解,如以下示例所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;
    private String defaultLocale;

    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }

    // ...
}
public class MovieRecommender {

    private String defaultLocale;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }

    // ...
}

4.3. 语言参考

本节描述了 Spring Expression Language 的工作方式。它涵盖以下主题:

4.3.1. Literals 表达

支持的 Literals 表达式的类型为字符串,数值(int,实数,十六进制),布尔值和 null。字符串由单引号引起来。要将单引号本身放在字符串中,请使用两个单引号字符。

以下清单显示了 Literals 的简单用法。通常,它们不是像这样孤立地使用,而是作为更复杂的表达式的一部分使用-例如,在逻辑比较运算符的一侧使用 Literals。

ExpressionParser parser = new SpelExpressionParser();

// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

数字支持使用负号,指数符号和小数点。默认情况下,使用 Double.parseDouble()解析实数。

4.3.2. 属性,数组,列表,Map 和索引器

使用属性引用进行导航很容易。为此,请使用句点来指示嵌套的属性值。 Inventor类的实例pupintesla填充了示例中使用的类部分中列出的数据。要向下导航并获取特斯拉的出生年份和普平的出生城市,我们使用以下表达式:

// evals to 1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);

属性名称的首字母允许不区分大小写。数组和列表的内容通过使用方括号表示法获得,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// Inventions Array

// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
        context, tesla, String.class);

// Members List

// evaluates to "Nikola Tesla"
String name = parser.parseExpression("Members[0].Name").getValue(
        context, ieee, String.class);

// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
        context, ieee, String.class);

通过在方括号内指定 Literals 键值可以获取 Map 的内容。在下面的示例中,由于OfficersMap 的键是字符串,因此我们可以指定字符串 Literals:

// Officer's Dictionary

Inventor pupin = parser.parseExpression("Officers['president']").getValue(
        societyContext, Inventor.class);

// evaluates to "Idvor"
String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
        societyContext, String.class);

// setting values
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
        societyContext, "Croatia");

4.3.3. 内联列表

您可以使用{}表示法直接在表达式中表达列表。

// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);

List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);

{}本身表示一个空列表。出于性能原因,如果列表本身完全由固定的 Literals 组成,则会创建一个常量列表来表示表达式(而不是在每次求值时都构建一个新列表)。

4.3.4. 内联 Map

您也可以使用{key:value}表示法在表达式中直接表达 Map。以下示例显示了如何执行此操作:

// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);

Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);

{:}本身就是一张空的 Map。出于性能原因,如果 Map 表本身由固定的 Literals 或其他嵌套的常量结构(列表或 Map 表)组成,则会创建一个常量 Map 表来表示该表达式(而不是在每次求值时都构建一个新的 Map 表)。Map 键的引用是可选的。上面的示例不使用带引号的键。

4.3.5. 阵列构造

您可以使用熟悉的 Java 语法来构建数组,可以选择提供一个初始化程序以在构造时填充该数组。以下示例显示了如何执行此操作:

int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);

// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

构造多维数组时,当前无法提供初始化程序。

4.3.6. Methods

您可以使用典型的 Java 编程语法来调用方法。您还可以在 Literals 上调用方法。还支持变量参数。下面的示例演示如何调用方法:

// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);

// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
        societyContext, Boolean.class);

4.3.7. Operators

Spring Expression Language 支持以下几种运算符:

Relational Operators

使用标准运算符表示法支持关系运算符(等于,不等于,小于,小于或等于,大于和大于或等于)。以下清单显示了一些运算符示例:

// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);

// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

Note

null的大于和小于比较遵循一个简单的规则:null被视为无(不是零)。结果,任何其他值始终大于null(X > null始终为true),并且其他任何值都不小于零(X < null总是false)。

如果您更喜欢数字比较,请避免基于数字的null比较,而反对与零的比较(例如X > 0X < 0)。

除了标准的关系运算符外,SpEL 还支持instanceof和基于正则表达式的matches运算符。以下清单显示了两个示例:

// evaluates to false
boolean falseValue = parser.parseExpression(
        "'xyz' instanceof T(Integer)").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression(
        "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

//evaluates to false
boolean falseValue = parser.parseExpression(
        "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

Warning

请注意基本类型,因为它们会立即被包装为包装类型,因此,按预期,1 instanceof T(int)的值为false,而1 instanceof T(Integer)的值为true

每个符号运算符也可以指定为纯字母等效项。这样可以避免使用的符号对于嵌入表达式的文档类型具有特殊含义的问题(例如在 XML 文档中)。等效的 Literals 是:

所有的文本运算符都不区分大小写。

Logical Operators

SpEL 支持以下逻辑运算符:

下面的示例显示如何使用逻辑运算符

// -- AND --

// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- OR --

// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- NOT --

// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);

// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
Mathematical Operators

您可以在数字和字符串上使用加法运算符。您只能对数字使用减法,乘法和除法运算符。您还可以使用模数(%)和指数幂(^)运算符。强制执行标准运算符优先级。以下示例显示了正在使用的 math 运算符:

// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class);  // 2

String testString = parser.parseExpression(
        "'test' + ' ' + 'string'").getValue(String.class);  // 'test string'

// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class);  // 4

double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class);  // -9000

// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class);  // 6

double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class);  // 24.0

// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class);  // -2

double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class);  // 1.0

// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class);  // 3

int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class);  // 1

// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class);  // -21
赋值运算符

要设置属性,请使用赋值运算符(=)。这通常在对setValue的调用中完成,但也可以在对getValue的调用中完成。下面的清单显示了使用赋值运算符的两种方法:

Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

parser.parseExpression("Name").setValue(context, inventor, "Aleksandar Seovic");

// alternatively
String aleks = parser.parseExpression(
        "Name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);

4.3.8. Types

您可以使用特殊的T运算符来指定java.lang.Class(类型)的实例。静态方法也可以通过使用此运算符来调用。 StandardEvaluationContext使用TypeLocator查找类型,而StandardTypeLocator(可以替换)是在了解java.lang程序包的情况下构建的。这意味着对java.lang中的类型的T()引用不需要完全限定,但所有其他类型引用都必须是完全限定的。下面的示例演示如何使用T运算符:

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);

Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);

boolean trueValue = parser.parseExpression(
        "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
        .getValue(Boolean.class);

4.3.9. Constructors

您可以使用new运算符来调用构造函数。除了基本类型(intfloat等等)和 String 之外,您都应使用完全限定的类名。下面的示例演示如何使用new运算符调用构造函数:

Inventor einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor.class);

//create new inventor instance within add method of List
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))").getValue(societyContext);

4.3.10. Variables

您可以使用#variableName语法在表达式中引用变量。通过在EvaluationContext实现上使用setVariable方法来设置变量。以下示例显示了如何使用变量:

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName())  // "Mike Tesla"
#this 和#root 变量

始终定义#this变量,该变量指向当前评估对象(针对不合格的引用,将对其进行解析)。始终定义#root变量,并引用根上下文对象。尽管#this可能随表达式的组成部分的求值而变化,但#root始终引用根。以下示例显示了如何使用#this#root变量:

// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);

// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);

4.3.11. Functions

您可以通过注册可以在表达式字符串中调用的用户定义函数来扩展 SpEL。该功能通过EvaluationContext注册。下面的示例演示如何注册用户定义的函数:

Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

例如,考虑以下用于反转字符串的 Util 方法:

public abstract class StringUtils {

    public static String reverseString(String input) {
        StringBuilder backwards = new StringBuilder(input.length());
        for (int i = 0; i < input.length(); i++)
            backwards.append(input.charAt(input.length() - 1 - i));
        }
        return backwards.toString();
    }
}

然后,您可以注册并使用前面的方法,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
        StringUtils.class.getDeclaredMethod("reverseString", String.class));

String helloWorldReversed = parser.parseExpression(
        "#reverseString('hello')").getValue(context, String.class);

4.3.12. Bean 参考

如果评估上下文已使用 bean 解析器配置,则可以使用@符号从表达式中查找 bean。以下示例显示了如何执行此操作:

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);

要访问工厂 bean 本身,应改为在 Bean 名称前加上&符号。以下示例显示了如何执行此操作:

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);

4.3.13. 三元运算符(If-Then-Else)

您可以使用三元运算符在表达式内部执行 if-then-else 条件逻辑。以下清单显示了一个最小的示例:

String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);

在这种情况下,布尔值false导致返回字符串值'falseExp'。一个更现实的示例如下:

parser.parseExpression("Name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");

expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
        "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";

String queryResultString = parser.parseExpression(expression)
        .getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

有关三元运算符的更短语法,请参阅关于 Elvis 运算符的下一部分。

4.3.14. Elvisoperator

Elvis 运算符是三元运算符语法的简化,并且以Groovy语言使用。使用三元运算符语法,通常必须将变量重复两次,如以下示例所示:

String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

取而代之的是,您可以使用 Elvis 运算符(其命名类似于 Elvis 的发型)。以下示例显示了如何使用 Elvis 运算符:

ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(String.class);
System.out.println(name);  // 'Unknown'

以下清单显示了一个更复杂的示例:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Nikola Tesla

tesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Elvis Presley

Note

您可以使用 Elvis 运算符在表达式中应用默认值。以下示例显示了如何在@Value表达式中使用 Elvis 运算符:

@Value("#{systemProperties['pop3.port'] ?: 25}")

如果定义了系统属性pop3.port,否则将注入 25.

4.3.15. 安全导航操作员

安全导航操作符用于避免NullPointerException,并且来自Groovy语言。通常,当您引用一个对象时,可能需要在访问该对象的方法或属性之前验证其是否为 null。为了避免这种情况,安全导航运算符返回 null 而不是引发异常。下面的示例演示如何使用安全导航操作符:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));

String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // Smiljan

tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // null - does not throw NullPointerException!!!

4.3.16. collections 选择

选择是一种强大的表达语言功能,可让您通过从源集合中选择条目来将其转换为另一个集合。

选择使用.?[selectionExpression]的语法。它过滤集合并返回一个包含原始元素子集的新集合。例如,通过选择,我们可以轻松地获得塞尔维亚发明者的列表,如以下示例所示:

List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "Members.?[Nationality == 'Serbian']").getValue(societyContext);

在列表和 Map 上都可以选择。对于列表,将针对每个单独的列表元素评估选择标准。针对 Map,针对每个 Map 条目(Java 类型Map.Entry的对象)评估选择标准。每个 Map 条目都有其键和值,可作为属性进行访问以供选择。

以下表达式返回一个新 Map,该 Map 由原始 Map 中条目值小于 27 的那些元素组成:

Map newMap = parser.parseExpression("map.?[value<27]").getValue();

除了返回所有选定的元素外,您只能检索第一个或最后一个值。要获得与选择匹配的第一个条目,语法为.^[selectionExpression]。要获得最后的匹配选择,语法为.$[selectionExpression]

4.3.17. 集合投影

投影使集合可以驱动子表达式的求值,结果是一个新的集合。投影的语法为.![projectionExpression]。例如,假设我们有一个发明家列表,但想要他们出生的城市的列表。实际上,我们希望为发明人列表中的每个条目评估“ placeOfBirth.city”。下面的示例使用投影来做到这一点:

// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");

您还可以使用 Map 来驱动投影,在这种情况下,将根据 Map 中的每个条目(以 Java Map.Entry表示)来评估投影表达式。跨 Map 的投影结果是一个列表,其中包含针对每个 Map 条目的投影表达式的评估。

4.3.18. 表达式模板

表达式模板允许将 Literals 文本与一个或多个评估块混合。每个评估块均以您可以定义的前缀和后缀字符分隔。常见的选择是使用#{ }作为分隔符,如以下示例所示:

String randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"

通过将 Literals 文本'random number is '与计算#{ }定界符内的表达式的结果(在这种情况下,是调用random()方法的结果)进行连接来评估字符串。 parseExpression()方法的第二个参数的类型为ParserContextParserContext接口用于影响表达式的解析方式,以支持表达式模板功能。 TemplateParserContext的定义如下:

public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return "#{";
    }

    public String getExpressionSuffix() {
        return "}";
    }

    public boolean isTemplate() {
        return true;
    }
}

4.4. 示例中使用的类

本节列出了本章示例中使用的类。

例子 1. Inventor.java

package org.spring.samples.spel.inventor;

import java.util.Date;
import java.util.GregorianCalendar;

public class Inventor {

    private String name;
    private String nationality;
    private String[] inventions;
    private Date birthdate;
    private PlaceOfBirth placeOfBirth;

    public Inventor(String name, String nationality) {
        GregorianCalendar c= new GregorianCalendar();
        this.name = name;
        this.nationality = nationality;
        this.birthdate = c.getTime();
    }

    public Inventor(String name, Date birthdate, String nationality) {
        this.name = name;
        this.nationality = nationality;
        this.birthdate = birthdate;
    }

    public Inventor() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNationality() {
        return nationality;
    }

    public void setNationality(String nationality) {
        this.nationality = nationality;
    }

    public Date getBirthdate() {
        return birthdate;
    }

    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
    }

    public PlaceOfBirth getPlaceOfBirth() {
        return placeOfBirth;
    }

    public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
        this.placeOfBirth = placeOfBirth;
    }

    public void setInventions(String[] inventions) {
        this.inventions = inventions;
    }

    public String[] getInventions() {
        return inventions;
    }
}

例子 2. PlaceOfBirth.java

package org.spring.samples.spel.inventor;

public class PlaceOfBirth {

    private String city;
    private String country;

    public PlaceOfBirth(String city) {
        this.city=city;
    }

    public PlaceOfBirth(String city, String country) {
        this(city);
        this.country = country;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String s) {
        this.city = s;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

}

例子 3. Society.java

package org.spring.samples.spel.inventor;

import java.util.*;

public class Society {

    private String name;

    public static String Advisors = "advisors";
    public static String President = "president";

    private List<Inventor> members = new ArrayList<Inventor>();
    private Map officers = new HashMap();

    public List getMembers() {
        return members;
    }

    public Map getOfficers() {
        return officers;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isMember(String name) {
        for (Inventor inventor : members) {
            if (inventor.getName().equals(name)) {
                return true;
            }
        }
        return false;
    }

}

5.使用 Spring 进行面向方面的编程

面向方面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。 OOP 中模块化的关键单元是类,而在 AOP 中模块化是方面。方面使关注点(例如事务 Management)的模块化可以跨越多种类型和对象。 (这种关注在 AOP 文献中通常被称为“跨领域”关注.)

Spring 的关键组件之一是 AOP 框架。尽管 Spring IoC 容器不依赖于 AOP(这意味着您不需要使用 AOP),但 AOP 是对 Spring IoC 的补充,以提供功能非常强大的中间件解决方案。

Spring 2.0 AOP

Spring 2.0 引入了一种使用schema-based approach@AspectJ 注解样式来编写自定义方面的更简单,更强大的方法。这两种样式都提供了完全类型化的建议,并使用了 AspectJ 切入点语言,同时仍使用 Spring AOP 进行编织。

本章讨论基于 Spring 2.0 模式和基于@AspectJ 的 AOP 支持。 接下来的章节中讨论了 Spring 1.2 应用程序中常见的较低级别的 AOP 支持。

AOP 在 Spring Framework 中用于:

Note

如果您只对通用声明性服务或其他预打包的声明性中间件服务(例如池)感兴趣,则无需直接使用 Spring AOP,并且可以跳过本章的大部分内容。

5.1. AOP 概念

让我们首先定义一些重要的 AOP 概念和术语。这些术语不是特定于 Spring 的。不幸的是,AOP 术语并不是特别直观。但是,如果 Spring 使用其自己的术语,那将更加令人困惑。

Spring AOP 包括以下类型的建议:

围绕建议是最通用的建议。由于 Spring AOP 与 AspectJ 一样,提供了各种建议类型,因此我们建议您使用功能最弱的建议类型,以实现所需的行为。例如,如果您只需要使用方法的返回值更新缓存,则最好使用返回后的建议而不是周围的建议,尽管周围的建议可以完成相同的事情。使用最具体的建议类型可以提供更简单的编程模型,并减少出错的可能性。例如,您不需要在用于周围建议的JoinPoint上调用proceed()方法,因此,您不会失败。

在 Spring 2.0 中,所有建议参数都是静态类型的,因此您可以使用适当类型(例如,从方法执行返回值的类型)而不是Object数组的建议参数。

切入点匹配的连接点的概念是 AOP 的关键,它与仅提供拦截功能的旧技术有所不同。切入点使建议的目标独立于面向对象的层次结构。例如,您可以将提供声明性事务 Management 的环绕建议应用于跨越多个对象(例如服务层中的所有业务操作)的一组方法。

5.2. SpringAOP 能力和目标

Spring AOP 是用纯 Java 实现的。不需要特殊的编译过程。 Spring AOP 不需要控制类加载器的层次结构,因此适合在 Servlet 容器或应用程序服务器中使用。

Spring AOP 当前仅支持方法执行连接点(建议在 Spring Bean 上执行方法)。尽管可以在不破坏核心 Spring AOP API 的情况下添加对字段拦截的支持,但并未实现字段拦截。如果需要建议字段访问和更新连接点,请考虑使用诸如 AspectJ 之类的语言。

Spring AOP 的 AOP 方法不同于大多数其他 AOP 框架。目的不是提供最完整的 AOP 实现(尽管 Spring AOP 相当强大)。相反,其目的是在 AOP 实现和 Spring IoC 之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。

因此,例如,通常将 Spring Framework 的 AOP 功能与 Spring IoC 容器结合使用。通过使用常规 bean 定义语法来配置方面(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现的关键区别。使用 Spring AOP 无法轻松或高效地完成某些事情,例如建议非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ 是最佳选择。但是,我们的经验是,Spring AOP 为企业 Java 应用程序中适合 AOP 的大多数问题提供了出色的解决方案。

Spring AOP 从未努力与 AspectJ 竞争以提供全面的 AOP 解决方案。我们认为,基于代理的框架(如 Spring AOP)和成熟的框架(如 AspectJ)都是有价值的,它们是互补的,而不是竞争。 Spring 将 AspectsJ 无缝集成了 Spring AOP 和 IoC,以在基于 Spring 的一致应用程序架构中支持 AOP 的所有使用。这种集成不会影响 Spring AOP API 或 AOP Alliance API。 Spring AOP 仍然向后兼容。有关 Spring AOP API 的讨论,请参见接下来的章节

Note

Spring 框架的中心宗旨之一是非侵入性。这是一个想法,不应强迫您将特定于框架的类和接口引入业务或域模型。但是,在某些地方,Spring Framework 确实为您提供了将特定于 Spring Framework 的依赖项引入代码库的选项。提供此类选项的理由是,在某些情况下,以这种方式阅读或编码某些特定功能可能会变得更加容易。但是,Spring 框架(几乎)总是为您提供选择:您可以自由地就哪个选项最适合您的特定用例或场景做出明智的决定。

与本章相关的一种选择是选择哪种 AOP 框架(以及哪种 AOP 样式)。您可以选择 AspectJ 和/或 Spring AOP。您还可以选择@AspectJ注解 样式方法或 Spring XML 配置样式方法。本章选择首先介绍@AspectJ 风格的方法这一事实不应被视为表明 Spring 团队比 Spring XML 配置风格更喜欢@AspectJ注解 风格的方法。

有关每种样式的“为什么和为什么”的更完整讨论,请参见选择要使用的 AOP 声明样式

5.3. AOP 代理

Spring AOP 默认将标准 JDK 动态代理用于 AOP 代理。这使得可以代理任何接口(或一组接口)。

Spring AOP 也可以使用 CGLIB 代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在某些情况下(可能极少发生),您需要建议未在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法,则可以使用强制使用 CGLIB

掌握 Spring AOP 是基于代理的这一事实非常重要。有关此实现细节实际含义的彻底检查,请参见了解 AOP 代理

5.4. @AspectJ 支持

@AspectJ 是一种将方面声明为带有注解的常规 Java 类的样式。 @AspectJ 样式是AspectJ project作为 AspectJ 5 版本的一部分引入的。 Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ 5 相同的 注解。但是,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

Note

使用 AspectJ 编译器和 weaver 可以使用完整的 AspectJ 语言,并将在在 Spring 应用程序中使用 AspectJ中进行讨论。

5.4.1. 启用@AspectJ 支持

要在 Spring 配置中使用@AspectJ 方面,您需要启用 Spring 支持以基于@AspectJ 方面配置 Spring AOP,并根据这些方面是否建议对它们进行自动代理。通过自动代理,我们的意思是,如果 Spring 确定一个或多个方面建议一个 bean,它会自动为该 bean 生成一个代理来拦截方法调用并确保按需执行建议。

可以使用 XML 或 Java 样式的配置来启用@AspectJ 支持。无论哪种情况,都需要确保 AspectJ 的aspectjweaver.jar库位于应用程序的 Classpath(版本 1.8 或更高版本)上。该库在 AspectJ 发行版的lib目录中或从 Maven Central 存储库中可用。

通过 Java 配置启用@AspectJ 支持

要使用 Java @Configuration启用@AspectJ 支持,请添加@EnableAspectJAutoProxy注解,如以下示例所示:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
通过 XML 配置启用@AspectJ 支持

要通过基于 XML 的配置启用@AspectJ 支持,请使用aop:aspectj-autoproxy元素,如以下示例所示:

<aop:aspectj-autoproxy/>

假设您使用基于 XML 模式的配置中所述的架构支持。有关如何在aop名称空间中导入标签的信息,请参见AOP 模式

5.4.2. 声明一个方面

启用@AspectJ 支持后,Spring 会自动检测到在应用程序上下文中使用@AspectJ 方面(具有@Aspect注解)的类定义的任何 bean,并用于配置 Spring AOP。接下来的两个示例显示了一个不太有用的方面所需的最小定义。

两个示例中的第一个示例显示了应用程序上下文中的常规 bean 定义,该定义指向具有@Aspect注解的 bean 类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
</bean>

这两个示例中的第二个示例显示了NotVeryUsefulAspect类定义,该类定义带有org.aspectj.lang.annotation.Aspect注解;

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

方面(带有@Aspect注解 的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点,建议和引入(类型间)声明。

Autodetecting aspects through component scanning

您可以将方面类注册为 Spring XML 配置中的常规 bean,也可以通过 Classpath 扫描来自动检测它们-与其他任何 SpringManagement 的 bean 一样。但是,请注意,@Aspect注解 不足以在 Classpath 中进行自动检测。为此,您需要添加一个单独的@Component注解(或者,或者,按照 Spring 的组件扫描程序的规则,有条件的自定义构造型注解)。

Advising aspects with other aspects?

在 Spring AOP 中,方面本身不能成为其他方面的建议目标。类上的@Aspect注解 将其标记为一个方面,因此将其从自动代理中排除。

5.4.3. 声明切入点

切入点确定了感兴趣的连接点,从而使我们能够控制执行建议的时间。 Spring AOP 仅支持 Spring Bean 的方法执行连接点,因此您可以将切入点视为与 Spring Bean 上的方法执行相匹配。切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该切入点表达式准确确定我们感兴趣的方法执行。在 AOP 的@AspectJ 注解样式中,常规方法定义提供了切入点签名。 ,并通过使用@Pointcut注解 指示切入点表达式(用作切入点签名的方法必须具有void返回类型)。

一个示例可能有助于使切入点签名和切入点表达式之间的区别变得清晰。下面的示例定义一个名为anyOldTransfer的切入点,该切入点与任何名为transfer的方法的执行相匹配:

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

形成@Pointcut注解的值的切入点表达式是一个常规的 AspectJ 5 切入点表达式。有关 AspectJ 的切入点语言的完整讨论,请参见AspectJ 编程指南(以及 extensionsAspectJ 5 开发人员的笔记本)或有关 AspectJ 的其中一本书(例如 Colyer 等人的* Eclipse AspectJ AspectJ in Action *),由 Ramnivas Laddad)。

支持的切入点指示符

Spring AOP 支持以下在切入点表达式中使用的 AspectJ 切入点指示符(PCD):

其他切入点类型

完整的 AspectJ 切入点语言支持 Spring 中不支持的其他切入点指示符:callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@this@withincode。在 Spring AOP 解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException

Spring AOP 支持的切入点指示符集合可能会在将来的版本中扩展,以支持更多的 AspectJ 切入点指示符。

因为 Spring AOP 将匹配限制为仅方法执行连接点,所以前面对切入点指示符的讨论所提供的定义比 AspectJ 编程指南中的定义要窄。此外,AspectJ 本身具有基于类型的语义,并且在执行连接点处thistarget引用同一对象:执行该方法的对象。 Spring AOP 是基于代理的系统,可区分代理对象本身(绑定到this)和代理后面的目标对象(绑定到target)。

Note

由于 Spring 的 AOP 框架基于代理的性质,因此根据定义,不会拦截目标对象内的调用。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB 时,将拦截代理上的公共方法和受保护方法(甚至在必要时对程序包可见的方法)。但是,通常应通过公共签名设计通过代理进行的常见交互。

请注意,切入点定义通常与任何拦截方法匹配。如果严格地将切入点设置为仅公开使用,即使在 CGLIB 代理方案中通过代理存在潜在的非公开交互作用,也需要相应地进行定义。

如果您的拦截需要在目标类中包括方法调用甚至构造函数,请考虑使用 Spring 驱动的原生 AspectJ 编织而不是 Spring 的基于代理的 AOP 框架。这构成了具有不同 Feature 的 AOP 使用模式,因此请确保在做出决定之前先熟悉编织。

Spring AOP 还支持名为bean的附加 PCD。使用 PCD,可以将连接点的匹配限制为特定的命名 Spring Bean 或一组命名 Spring Bean(使用通配符时)。 bean PCD 具有以下形式:

bean(idOrNameOfBean)

idOrNameOfBean令牌可以是任何 Spring bean 的名称。提供了使用*字符的有限通配符支持,因此,如果为 Spring bean 构建了一些命名约定,则可以编写bean PCD 表达式来选择它们。与其他切入点指示符一样,bean PCD 也可以与&&(和),||(或)和!(否定)运算符一起使用。

Note

bean PCD 仅在 Spring AOP 中受支持,而在本机 AspectJ 编织中不受支持。它是 AspectJ 定义的标准 PCD 的特定于 Spring 的扩展,因此,不适用于@Aspect模型中声明的方面。

bean PCD 在实例级别(基于 Spring bean 名称概念构建)而不是仅在类型级别(基于编织的 AOP 受其限制)上运行。基于实例的切入点指示符是 Spring 基于代理的 AOP 框架及其与 Spring bean 工厂的紧密集成的一种特殊功能,在该工厂中自然而直接地通过名称来标识特定的 bean。

组合切入点表达式

您可以组合切入点表达式,可以使用&&, ||!进行组合。您也可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} (1)

@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {} (2)

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} (3)

最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如先前所示。按名称引用切入点时,将应用常规的 Java 可见性规则(您可以看到相同类型的私有切入点,层次结构中受保护的切入点,任何位置的公共切入点,等等)。可见性不影响切入点匹配。

共享通用切入点定义

在使用企业应用程序时,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。我们建议为此定义一个“ SystemArchitecture”方面,以捕获常见的切入点表达式。这样的方面通常类似于以下示例:

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.someapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.someapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.service..*)")
    public void inServiceLayer() {}

    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.someapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
     * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}

    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

您可以在需要切入点表达式的任何地方引用在这样的方面中定义的切入点。例如,要使服务层具有事务性,您可以编写以下内容:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

基于架构的 AOP 支持中讨论了<aop:config><aop:advisor>元素。Transaction 元素在Transaction Management中讨论。

Examples

Spring AOP 用户可能最常使用execution切入点指示符。执行表达式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)

除了返回类型模式(前面的代码段中为ret-type-pattern),名称模式和参数模式以外的所有其他部分都是可选的。返回类型模式确定该方法的返回类型必须是什么才能使连接点匹配。 *最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,完全合格的类型名称才匹配。名称模式与方法名称匹配。您可以将*通配符用作名称模式的全部或一部分。如果指定了声明类型模式,请在其末尾添加.并将其连接到名称模式组件。参数模式稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量(零个或多个)的参数。 (*)模式与采用任何类型的一个参数的方法匹配。 (*,String)与采用两个参数的方法匹配。第一个可以是任何类型,而第二个必须是String。有关更多信息,请查阅 AspectJ 编程指南的Language Semantics部分。

以下示例显示了一些常用的切入点表达式:

execution(public * *(..))
execution(* set*(..))
execution(* com.xyz.service.AccountService.*(..))
execution(* com.xyz.service.*.*(..))
execution(* com.xyz.service..*.*(..))
within(com.xyz.service.*)
within(com.xyz.service..*)
this(com.xyz.service.AccountService)

Note

“ this”通常以绑定形式使用。有关如何在建议正文中使代理对象可用的信息,请参阅Declaring Advice上的部分。

target(com.xyz.service.AccountService)

Note

“目标”通常以绑定形式使用。有关如何使建议对象在建议正文中可用的信息,请参见Declaring Advice部分。

args(java.io.Serializable)

Note

“ args”更通常以绑定形式使用。有关如何使方法参数在建议正文中可用的信息,请参见Declaring Advice部分。

请注意,此示例中给出的切入点不同于execution(* *(java.io.Serializable))。如果在运行时传递的参数为Serializable,则 args 版本匹配,如果方法签名声明单个类型为Serializable的参数,则执行版本匹配。

@target(org.springframework.transaction.annotation.Transactional)

Note

您也可以在绑定形式中使用“ @target”。有关如何使注解对象在建议正文中可用的信息,请参见Declaring Advice部分。

@within(org.springframework.transaction.annotation.Transactional)

Note

您也可以在绑定形式中使用“ @within”。有关如何使注解对象在建议正文中可用的信息,请参见Declaring Advice部分。

@annotation(org.springframework.transaction.annotation.Transactional)

Note

您也可以在绑定形式中使用“ @annotation”。有关如何使注解对象在建议正文中可用的信息,请参见Declaring Advice部分。

@args(com.xyz.security.Classified)

Note

您也可以在绑定形式中使用“ @args”。请参见Declaring Advice部分,如何在建议正文中使注解对象可用。

bean(tradeService)
bean(*Service)
编写好的切入点

在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个昂贵的过程。 (动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中进行测试以确定在运行代码时是否存在实际匹配)。首次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点以 DNF(析取范式)重写,并且对切入点的组件进行排序,以便首先检查那些较便宜的组件。这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何 Sequences 提供它们。

但是,AspectJ 只能使用所告诉的内容。为了获得最佳的匹配性能,您应该考虑他们试图达到的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三类之一:同类,作用域和上下文:

编写正确的切入点至少应包括前两种类型(种类和作用域)。您可以包括上下文指示符以根据连接点上下文进行匹配,也可以绑定该上下文以在建议中使用。仅提供同类的标识符或仅提供上下文的标识符是可行的,但是由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指定者的匹配非常快,使用它们的使用意味着 AspectJ 可以非常迅速地消除不应进一步处理的连接点组。一个好的切入点应该始终包括一个切入点。

5.4.4. 宣告建议

建议与切入点表达式关联,并且在切入点匹配的方法执行之前,之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。

Before Advice

您可以使用@Before注解 在方面中在建议之前声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

如果使用就地切入点表达式,则可以将前面的示例重写为以下示例:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

}
返回建议后

返回建议后,当匹配的方法执行正常返回时,运行建议。您可以使用@AfterReturning注解进行声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

Note

您可以在同一方面内拥有多个建议声明(以及其他成员)。在这些示例中,我们仅显示单个建议声明,以集中每个建议的效果。

有时,您需要在建议正文中访问返回的实际值。您可以使用@AfterReturning的形式绑定返回值以获取该访问权限,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

returning属性中使用的名称必须与 advice 方法中的参数名称相对应。当方法执行返回时,该返回值将作为相应的参数值传递到通知方法。 returning子句还将匹配仅限制为返回指定类型值(在这种情况下为Object,该值与任何返回值匹配)的那些方法执行。

请注意,返回建议后使用时,不可能返回完全不同的参考。

提出建议后

抛出建议后,当匹配的方法执行通过抛出异常退出时运行建议。您可以使用@AfterThrowing注解进行声明,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

}

通常,您希望通知仅在引发给定类型的异常时才运行,并且您通常还需要访问通知正文中的异常。您可以使用throwing属性来限制匹配(如果需要)(否则,请使用Throwable作为异常类型),并将抛出的异常绑定到 advice 参数。以下示例显示了如何执行此操作:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

throwing属性中使用的名称必须与 advice 方法中的参数名称相对应。当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给通知方法。 throwing子句还将匹配仅限制为抛出指定类型(在本例中为DataAccessException)的异常的方法执行。

(最后)建议后

当匹配的方法执行退出时,通知(最终)运行。通过使用@After注解 进行声明。之后必须准备处理正常和异常返回条件的建议。它通常用于释放资源和类似目的。以下示例显示了最终建议后的用法:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

}
Around Advice

最后一种建议是围绕建议。围绕建议在匹配方法的执行过程中“围绕”运行。它有机会在方法执行之前和之后进行工作,并确定何时,如何以及什至根本不执行该方法。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用绕行建议。始终使用最不符合要求的建议形式(即,在建议可以使用之前,不要在建议周围使用)。

周围的建议通过使用@Around注解 来声明。咨询方法的第一个参数必须为ProceedingJoinPoint类型。在建议的正文中,在ProceedingJoinPoint上调用proceed()会使底层方法执行。 proceed方法也可以传入Object[]。数组中的值用作方法执行时的参数。

Note

对于由 AspectJ 编译器编译的周围建议,使用Object[]调用时proceed的行为与proceed的行为稍有不同。对于使用传统的 AspectJ 语言编写的环绕通知,传递给proceed的参数数量必须与传递给环绕通知的参数数量(而不是基础连接点采用的参数数量)相匹配,并且传递给值的值必须与给定的参数位置会取代该值绑定到的实体的连接点处的原始值(不要担心,如果这现在没有意义)。 Spring 采取的方法更简单,并且更适合其基于代理的,仅执行的语义。如果您编译为 Spring 编写的@AspectJ 方面,并将proceed与 AspectJ 编译器和 weaver 的参数一起使用,则只需要意识到这种区别。有一种方法可以编写在 Spring AOP 和 AspectJ 之间 100%兼容的方面,这在以下关于建议参数的部分中进行了讨论。

以下示例显示了如何使用周围建议:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}

周围建议返回的值是该方法的调用者看到的返回值。例如,如果一个简单的缓存方面有一个值,则可以从缓存中返回一个值;如果没有,则调用proceed()。请注意,proceed可能在周围建议的正文中被调用一次,多次或完全不被调用。所有这些都是合法的。

Advice Parameters

Spring 提供了完全类型化的建议,这意味着您可以在建议签名中声明所需的参数(如我们先前在返回和抛出示例中所见),而不是一直使用Object[]数组。我们将在本节的后面部分介绍如何使参数和其他上下文值可用于建议主体。首先,我们看一下如何编写通用建议,以了解该建议当前建议的方法。

访问当前的 JoinPoint

任何通知方法都可以将类型org.aspectj.lang.JoinPoint的参数声明为第一个参数(请注意,在周围的通知中必须声明类型JoinPoint的子类ProceedingJoinPoint的第一个参数。JoinPoint接口提供了许多有用的方法:

有关详情,请参见javadoc

将参数传递给建议

我们已经看到了如何绑定返回的值或异常值(在返回之后和引发建议之后使用)。要使参数值可用于建议正文,可以使用args的绑定形式。如果在 args 表达式中使用参数名称代替类型名称,则在调用建议时会将相应参数的值作为参数值传递。一个例子应该使这一点更清楚。假设您要建议执行以Account对象作为第一个参数的 DAO 操作,并且您需要访问建议正文中的帐户。您可以编写以下内容:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

切入点表达式的args(account,..)部分有两个作用。首先,它将匹配限制为仅方法采用至少一个参数并且传递给该参数的参数是Account的实例的方法执行。其次,它通过account参数使实际的Account对象可用于建议。

编写此文件的另一种方法是声明一个切入点,当切入点Account对象值与连接点匹配时,该切入点“提供”,然后从建议中引用命名切入点。如下所示:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

有关更多详细信息,请参见 AspectJ 编程指南。

代理对象(this),目标对象(target)和 注解(@within@target@annotation@args)都可以以类似的方式绑定。接下来的两个示例显示如何匹配使用@Auditable注解注解 的方法的执行并提取审核代码:

这两个示例中的第一个显示了@Auditable注解的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

两个示例中的第二个示例显示与@Auditable方法的执行相匹配的建议:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}
建议参数和泛型

Spring AOP 可以处理类声明和方法参数中使用的泛型。假设您具有如下通用类型:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

您可以通过在要拦截方法的参数类型中键入 advice 参数,将方法类型的拦截限制为某些参数类型:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

这种方法不适用于通用集合。因此,您不能按以下方式定义切入点:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

为了使这项工作有效,我们将不得不检查集合中的每个元素,这是不合理的,因为我们也无法决定通常如何处理null值。要实现类似目的,您必须将参数键入Collection<?>并手动检查元素的类型。

确定参数名称

通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。通过 Java 反射无法获得参数名称,因此 Spring AOP 使用以下策略来确定参数名称:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

如果第一个参数是JoinPointProceedingJoinPointJoinPoint.StaticPart类型,则可以从argNames属性的值中省略参数的名称。例如,如果您修改前面的建议以接收连接点对象,则argNames属性不需要包括它:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

对于不收集任何其他连接点上下文的建议实例,对JoinPointProceedingJoinPointJoinPoint.StaticPart类型的第一个参数进行特殊处理特别方便。在这种情况下,您可以省略argNames属性。例如,以下建议无需声明argNames属性:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

Note

如果即使没有调试信息,AspectJ 编译器(ajc)都已编译@AspectJ 方面,则无需添加argNames属性,因为编译器会保留所需的信息。

处理参数

前面我们提到过,我们将描述如何使用在 Spring AOP 和 AspectJ 上始终有效的参数编写proceed调用。解决方案是确保建议签名按 Sequences 绑定每个方法参数。以下示例显示了如何执行此操作:

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}

在许多情况下,无论如何都要进行此绑定(如上例所示)。

Advice Ordering

当多条建议都希望在同一连接点上运行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定建议执行的 Sequences。优先级最高的建议首先“在途中”运行(因此,给定两条优先建议,则优先级最高的建议首先运行)。从连接点“出路”中,优先级最高的建议将最后运行(因此,给定两条后置通知,优先级最高的建议将第二次运行)。

当在不同方面定义的两条建议都需要在同一连接点上运行时,除非另行指定,否则执行 Sequences 是不确定的。您可以通过指定优先级来控制执行 Sequences。通过在 Aspect 类中实现org.springframework.core.Ordered接口或使用Order注解对其进行 注解,可以通过普通的 Spring 方法来完成。给定两个方面,从Ordered.getValue()返回较低值(或注解值)的方面具有较高的优先级。

当在相同方面定义的两条建议都需要在同一连接点上运行时,其 Sequences 是未定义的(因为无法通过反射来获取 javac 编译类的声明 Sequences)。考虑将这些建议方法折叠成每个方面类中每个连接点的一个建议方法,或将建议重构为单独的方面类,您可以在方面级别进行 Order。

5.4.5. Introductions

简介(在 AspectJ 中称为类型间声明)使方面可以声明建议对象实现给定的接口,并代表那些对象提供该接口的实现。

您可以使用@DeclareParents注解 进行介绍。此注解用于声明匹配类型具有新的父代(因此而得名)。例如,在给定名为UsageTracked的接口和该接口名为DefaultUsageTracked的实现的情况下,以下方面声明服务接口的所有实现者也都实现UsageTracked接口(例如,通过 JMX 公开统计信息):

@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

要实现的接口由带注解的字段的类型确定。 @DeclareParents注解的value属性是 AspectJ 类型的模式。任何匹配类型的 bean 都实现UsageTracked接口。请注意,在前面示例的建议中,服务 Bean 可以直接用作UsageTracked接口的实现。如果以编程方式访问 bean,则应编写以下内容:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

5.4.6. 方面实例化模型

Note

这是一个高级主题。如果您刚开始使用 AOP,则可以放心地跳过它,直到以后。

默认情况下,应用程序上下文中每个方面都有一个实例。 AspectJ 将此称为单例实例化模型。可以使用备用生命周期来定义方面。 Spring 支持 AspectJ 的perthispertarget实例化模型(当前不支持percflow, percflowbelow,pertypewithin)。

您可以通过在@Aspect注解中指定perthis子句来声明perthis方面。考虑以下示例:

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {

    private int someState;

    @Before(com.xyz.myapp.SystemArchitecture.businessService())
    public void recordServiceUsage() {
        // ...
    }

}

在前面的示例中,'perthis'子句的作用是为每个执行业务服务的唯一服务对象(每个与切入点表达式匹配的联接点绑定到“ this”的唯一对象)创建一个方面实例。方面实例是在服务对象上首次调用方法时创建的。当服务对象超出范围时,方面将超出范围。在创建方面实例之前,其中的任何建议都不会执行。创建方面实例后,在其中声明的建议将在匹配的连接点处执行,但仅当服务对象是与此方面相关联的对象时才执行。有关per子句的更多信息,请参见 AspectJ 编程指南。

pertarget实例化模型的工作方式与perthis完全相同,但是它为匹配的连接点处的每个唯一目标对象创建一个方面实例。

5.4.7. AOP 示例

既然您已经了解了所有组成部分是如何工作的,那么我们可以将它们放在一起做一些有用的事情。

有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务服务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作以避免 Client 端看到PessimisticLockingFailureException。这项要求清楚地跨越了服务层中的多个服务,因此非常适合通过一个方面实施。

因为我们想重试该操作,所以我们需要使用“周围”建议,以便我们可以多次调用proceed。以下清单显示了基本方面的实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

请注意,方面实现了Ordered接口,因此我们可以将方面的优先级设置为高于事务建议(每次重试时都希望有新的事务)。 maxRetriesorder属性均由 Spring 配置。主要动作发生在doConcurrentOperation周围建议中。请注意,目前,我们将重试逻辑应用于每个businessService()。我们尝试 continue,如果失败并失败了PessimisticLockingFailureException,我们将重试,除非我们用尽了所有的重试尝试。

相应的 Spring 配置如下:

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

为了优化方面,使其仅重试幂等操作,我们可以定义以下Idempotent注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

然后,我们可以使用注解来注解服务操作的实现。方面的更改仅重试幂等操作涉及精简切入点表达式,以便只有@Idempotent个操作匹配,如下所示:

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    ...
}

5.5. 基于架构的 AOP 支持

如果您更喜欢基于 XML 的格式,Spring 还支持使用新的aop名称空间标签定义方面。支持与使用@AspectJ 样式时完全相同的切入点表达式和建议类型。因此,在本节中,我们将重点放在新语法上,并使 Reader 参考上一节(@AspectJ support)中的讨论,以了解编写切入点表达式和建议参数的绑定。

要使用本节中描述的 aop 名称空间标签,您需要导入spring-aop模式,如基于 XML 模式的配置中所述。有关如何在aop名称空间中导入标签的信息,请参见AOP 模式

在您的 Spring 配置中,所有方面和顾问元素都必须放在<aop:config>元素内(在应用程序上下文配置中可以有多个<aop:config>元素)。 <aop:config>元素可以包含切入点,顾问和方面元素(请注意,这些元素必须按此 Sequences 声明)。

Warning

<aop:config>样式的配置大量使用了 Spring 的auto-proxying机制。如果您已经通过使用BeanNameAutoProxyCreator或类似方法来使用显式自动代理,则可能会导致问题(例如未编制建议)。推荐的用法模式是仅使用<aop:config>样式或仅AutoProxyCreator样式,并且不要混合使用。

5.5.1. 声明一个方面

使用模式支持时,方面是在 Spring 应用程序上下文中定义为 Bean 的常规 Java 对象。状态和行为在对象的字段和方法中捕获,切入点和建议信息在 XML 中捕获。

您可以使用\ <>元素声明一个方面,并使用ref属性引用该支持 bean,如以下示例所示:

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

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

支持方面(在这种情况下为aBean)的 bean 当然可以像配置任何其他 Spring bean 一样进行配置并注入依赖项。

5.5.2. 声明切入点

您可以在<aop:config>元素中声明命名的切入点,从而使切入点定义在多个方面和顾问程序之间共享。

可以定义代表服务层中任何业务服务的执行的切入点:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

注意,切入点表达式本身使用的是与@AspectJ support中所述的 AspectJ 切入点表达式语言。如果使用基于架构的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下:

<aop:config>

    <aop:pointcut id="businessService"
        expression="com.xyz.myapp.SystemArchitecture.businessService()"/>

</aop:config>

假定您具有共享通用切入点定义中所述的SystemArchitecture外观。

然后,在方面中声明切入点与声明顶级切入点非常相似,如以下示例所示:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        ...

    </aop:aspect>

</aop:config>

与@AspectJ 方面几乎相同,使用基于架构的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点收集this对象作为连接点上下文,并将其传递给建议:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) &amp;&amp; this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...

    </aop:aspect>

</aop:config>

必须声明通知,以通过包含匹配名称的参数来接收收集的连接点上下文,如下所示:

public void monitor(Object service) {
    ...
}

组合切入点子表达式时,XML 文档中的&&很尴尬,因此可以分别使用andornot关键字代替&&||!。例如,上一个切入点可以更好地编写如下:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service..(..)) and this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>
</aop:config>

请注意,以这种方式定义的切入点由其 XML id引用,并且不能用作命名切入点以形成复合切入点。因此,基于架构的定义样式中的命名切入点支持比@AspectJ 样式所提供的更受限制。

5.5.3. 宣告建议

基于模式的 AOP 支持使用与@AspectJ 样式相同的五种建议,并且它们具有完全相同的语义。

Before Advice

在运行匹配的方法之前,建议运行之前。使用\ <>元素在<aop:aspect>内部声明它,如以下示例所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

在这里,dataAccessOperation是在最高(<aop:config>)级别定义的切入点的id。要定义切入点内联,请用pointcut属性替换pointcut-ref属性,如下所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
        method="doAccessCheck"/>

    ...

</aop:aspect>

正如我们在@AspectJ 样式的讨论中所指出的那样,使用命名的切入点可以显着提高代码的可读性。

method属性标识提供建议正文的方法(doAccessCheck)。必须为包含建议的 Aspect 元素所引用的 bean 定义此方法。在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用 Aspect Bean 上的doAccessCheck方法。

返回建议后

返回的建议在匹配的方法执行正常完成时运行。在<aop:aspect>内部以与建议之前相同的方式声明它。以下示例显示了如何声明它:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

与@AspectJ 样式一样,您可以在建议正文中获取返回值。为此,使用 returning 属性指定返回值应传递到的参数的名称,如以下示例所示:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

doAccessCheck方法必须声明一个名为retVal的参数。该参数的类型以与@AfterReturning相同的方式约束匹配。例如,您可以声明方法签名,如下所示:

public void doAccessCheck(Object retVal) {...
提出建议后

抛出建议后,当匹配的方法执行通过抛出异常退出时执行建议。通过使用掷后元素在<aop:aspect>内部声明它,如以下示例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

与@AspectJ 样式一样,您可以在通知正文中获取引发的异常。为此,请使用 throwing 属性指定异常应传递到的参数的名称,如以下示例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

doRecoveryActions方法必须声明一个名为dataAccessEx的参数。该参数的类型以与@AfterThrowing相同的方式约束匹配。例如,方法签名可以声明如下:

public void doRecoveryActions(DataAccessException dataAccessEx) {...
(最后)建议后

无论最终如何执行匹配的方法,建议(最终)都会运行。您可以使用after元素对其进行声明,如以下示例所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>
Around Advice

最后一种建议是围绕建议。围绕建议在匹配的方法执行过程中“围绕”运行。它有机会在方法执行之前和之后进行工作,并确定何时,如何以及什至根本不执行该方法。周围建议通常用于以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态。始终使用最不强大的建议形式,以满足您的要求。如果建议可以完成这项工作,请不要在建议周围使用。

您可以使用aop:around元素在建议周围进行声明。咨询方法的第一个参数必须为ProceedingJoinPoint类型。在建议的正文中,在ProceedingJoinPoint上调用proceed()会导致基础方法执行。 proceed方法也可以用Object[]调用。数组中的值用作方法执行时的参数。有关使用Object[]调用proceed的说明,请参见Around Advice。以下示例显示了如何在 XML 中围绕建议进行声明:

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...

</aop:aspect>

doBasicProfiling通知的实现可以与@AspectJ 示例完全相同(当然要减去 注解),如以下示例所示:

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}
Advice Parameters

基于架构的声明样式以与@AspectJ 支持相同的方式支持完全类型的建议,即通过名称与建议方法参数匹配切入点参数。有关详情,请参见Advice Parameters。如果您希望显式指定建议方法的参数名称(不依赖于先前描述的检测策略),则可以通过使用建议元素的arg-names属性来实现,该属性与argNames属性的处理方式相同。建议 注解(如确定参数名称中所述)。以下示例显示如何在 XML 中指定参数名称:

<aop:before
    pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
    method="audit"
    arg-names="auditable"/>

arg-names属性接受逗号分隔的参数名称列表。

以下基于 XSD 的方法中涉及程度稍高的示例显示了一些与一些强类型参数结合使用的建议:

package x.y.service;

public interface PersonService {

    Person getPerson(String personName, int age);
}

public class DefaultFooService implements FooService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}

接下来是方面。请注意,profile(..)方法接受许多强类型的参数,第一个恰好是用于进行方法调用的连接点。此参数的存在表明profile(..)用作around建议,如以下示例所示:

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class SimpleProfiler {

    public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
        StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
        try {
            clock.start(call.toShortString());
            return call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
    }
}

最后,以下示例 XML 配置影响了特定连接点的上述建议的执行:

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

    <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
    <bean id="personService" class="x.y.service.DefaultPersonService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomePersonServiceMethod"
                expression="execution(* x.y.service.PersonService.getPerson(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
                method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>

考虑以下驱动程序脚本:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        PersonService person = (PersonService) ctx.getBean("personService");
        person.getPerson("Pengo", 12);
    }
}

有了这样的 Boot 类,我们将在标准输出上获得类似于以下内容的输出:

StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)
Advice Ordering

当需要在同一连接点(执行方法)上执行多个建议时,排序规则如Advice Ordering中所述。方面之间的优先级是通过将Order注解添加到支持方面的 Bean 或通过使 Bean 实现Ordered接口来确定的。

5.5.4. Introductions

简介(在 AspectJ 中称为类型间声明)使方面可以声明建议的对象实现给定的接口,并代表那些对象提供该接口的实现。

您可以使用aop:aspect内的aop:declare-parents元素进行介绍。您可以使用aop:declare-parents元素来声明匹配类型具有新的父代(因此而得名)。例如,给定名为UsageTracked的接口和该名为DefaultUsageTracked的接口的实现,以下方面声明服务接口的所有实现者也都实现UsageTracked接口。 (例如,为了通过 JMX 公开统计信息.)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.SystemArchitecture.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

支持usageTracking bean 的类将包含以下方法:

public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

要实现的接口由implement-interface属性确定。 types-matching属性的值是 AspectJ 类型的模式。任何匹配类型的 bean 都实现UsageTracked接口。请注意,在前面示例的建议中,服务 Bean 可以直接用作UsageTracked接口的实现。要以编程方式访问 bean,可以编写以下代码:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

5.5.5. 方面实例化模型

模式定义方面唯一受支持的实例化模型是单例模型。在将来的版本中可能会支持其他实例化模型。

5.5.6. Advisors

“顾问”的概念来自 Spring 中定义的 AOP 支持,并且在 AspectJ 中没有直接等效的概念。顾问就像一个独立的小方面,只有一条建议。通知本身由 bean 表示,并且必须实现Spring 的建议类型中描述的建议接口之一。顾问可以利用 AspectJ 切入点表达式。

Spring 通过<aop:advisor>元素支持顾问程序概念。您通常会看到它与事务建议结合使用,事务建议在 Spring 中也有自己的名称空间支持。以下示例显示顾问程序:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor
        pointcut-ref="businessService"
        advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

除了前面的示例中使用的pointcut-ref属性,还可以使用pointcut属性来内联定义切入点表达式。

要定义顾问程序的优先级,以便该建议书可以参与 Order,请使用order属性来定义顾问程序的Ordered值。

5.5.7. AOP 模式示例

本节显示了使用模式支持重写时来自AOP 示例的并发锁定失败重试示例的外观。

有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务服务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作以避免 Client 端看到PessimisticLockingFailureException。这项要求清楚地跨越了服务层中的多个服务,因此非常适合通过一个方面实施。

因为我们想重试该操作,所以我们需要使用“周围”建议,以便我们可以多次调用proceed。以下清单显示了基本方面的实现(这是使用模式支持的常规 Java 类):

public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

请注意,方面实现了Ordered接口,因此我们可以将方面的优先级设置为高于事务建议(每次重试时都希望有新的事务)。 maxRetriesorder属性均由 Spring 配置。主要动作发生在doConcurrentOperation周围建议方法中。我们尝试 continue。如果我们失败了PessimisticLockingFailureException,我们将重试,除非我们用尽了所有的重试尝试。

Note

该类与@AspectJ 示例中使用的类相同,但是除去了 注解。

相应的 Spring 配置如下:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

请注意,目前我们假设所有业务服务都是幂等的。如果不是这种情况,我们可以通过引入Idempotent注解 并使用该注解来注解服务操作的实现,来改进方面,使其仅重试 true 的幂等操作,如以下示例所示:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

方面的更改仅重试幂等操作涉及精简切入点表达式,以便只有@Idempotent个操作匹配,如下所示:

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

5.6. 选择要使用的 AOP 声明样式

一旦确定方面是实现给定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ 以及在 Aspect 语言(代码)样式,@ AspectJ 注解样式或 Spring XML 样式之间做出选择?这些决定受许多因素影响,包括应用程序需求,开发工具以及团队对 AOP 的熟悉程度。

5.6.1. Spring AOP 还是 Full AspectJ?

使用最简单的方法即可。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/编织器。如果您只需要建议在 Spring bean 上执行操作,则 Spring AOP 是正确的选择。如果您需要建议不受 Spring 容器 Management 的对象(通常是域对象),则需要使用 AspectJ。如果您希望建议除简单方法执行以外的连接点(例如,字段 get 或设置连接点等),则还需要使用 AspectJ。

使用 AspectJ 时,可以选择 AspectJ 语言语法(也称为“代码样式”)或@AspectJ注解 样式。显然,如果您不使用 Java 5,那么将为您做出选择:使用代码样式。如果方面在您的设计中起着重要作用,并且您能够将AspectJ 开发工具(AJDT)插件用于 Eclipse,则 AspectJ 语言语法是首选。它更干净,更简单,因为该语言是专为编写方面而设计的。如果您不使用 Eclipse 或只有少数几个方面在您的应用程序中不起作用,那么您可能要考虑使用@AspectJ 样式,在 IDE 中坚持常规 Java 编译,并向其中添加方面编织阶段您的构建脚本。

5.6.2. @AspectJ 或 Spring AOP 的 XML?

如果选择使用 Spring AOP,则可以选择@AspectJ 或 XML 样式。有各种折衷考虑。

XML 样式可能是现有 Spring 用户最熟悉的,并且得到了 true 的 POJO 的支持。当使用 AOP 作为配置企业服务的工具时,XML 可能是一个不错的选择(一个很好的测试是您是否将切入点表达式视为配置的一部分,您可能希望独立更改)。使用 XML 样式,可以说从您的配置中可以更清楚地了解系统中存在哪些方面。

XML 样式有两个缺点。首先,它没有完全将要解决的需求的实现封装在一个地方。 DRY 原则说,系统中的任何知识都应该有单一,明确,Authority 的表示形式。当使用 XML 样式时,关于如何实现需求的知识会在配置文件中的后备 bean 类的声明和 XML 中分散。当您使用@AspectJ 样式时,此信息将封装在一个单独的模块中:方面。其次,与@AspectJ 样式相比,XML 样式在表达能力上有更多限制:仅支持“单例”方面实例化模型,并且无法组合以 XML 声明的命名切入点。例如,使用@AspectJ 样式,您可以编写如下内容:

@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

在 XML 样式中,您可以声明前两个切入点:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML 方法的缺点是无法通过组合这些定义来定义accountPropertyAccess切入点。

@AspectJ 样式支持其他实例化模型和更丰富的切入点组合。它具有将方面保持为模块化单元的优势。它还具有的优点是,Spring AOP 和 AspectJ 都可以理解@AspectJ 方面。因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,则可以轻松地迁移到基于 AspectJ 的方法。总而言之,只要您拥有比简单地配置企业服务更多的功能,Spring 团队就会喜欢@AspectJ 样式。

5.7. 混合方面类型

通过使用自动代理支持,模式定义的<aop:aspect>方面,<aop:advisor>声明的顾问程序,甚至是在同一配置中使用 Spring 1.2 样式定义的代理和拦截器,完全可以混合使用@AspectJ 样式的方面。所有这些都是通过使用相同的基础支持机制实现的,并且可以毫无困难地共存。

5.8. 代理机制

Spring AOP 使用 JDK 动态代理或 CGLIB 创建给定目标对象的代理。 (只要有选择,首选 JDK 动态代理)。

如果要代理的目标对象实现至少一个接口,则使用 JDK 动态代理。代理了由目标类型实现的所有接口。如果目标对象未实现任何接口,则将创建 CGLIB 代理。

如果要强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅是由其接口实现的方法),都可以这样做。但是,您应该考虑以下问题:

要强制使用 CGLIB 代理,请将<aop:config>元素的proxy-target-class属性的值设置为 true,如下所示:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用@AspectJ 自动代理支持时强制 CGLIB 代理,请将<aop:aspectj-autoproxy>元素的proxy-target-class属性设置为true,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true"/>

Note

多个<aop:config/>节在运行时折叠到一个统一的自动代理创建器中,该创建器将应用<aop:config/>节中的任何(通常来自不同 XML bean 定义文件)指定的* strong *代理设置。这也适用于<tx:annotation-driven/><aop:aspectj-autoproxy/>元素。

明确地说,在<tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/>元素上使用proxy-target-class="true"会强制对所有三个元素*使用 CGLIB 代理。

5.8.1. 了解 AOP 代理

Spring AOP 是基于代理的。在编写自己的方面或使用 Spring Framework 随附的任何基于 Spring AOP 的方面之前,掌握最后一条语句实际含义的语义至关重要。

首先考虑您有一个普通的,未经代理的,没有什么特别的,直接的对象引用的情况,如以下代码片段所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图和清单所示:

public class Main {

    public static void main(String[] args) {

        Pojo pojo = new SimplePojo();

        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

当 Client 端代码具有的引用是代理时,情况会稍有变化。考虑以下图表和代码片段:

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

这里要理解的关键是,Main类的main(..)方法内部的 Client 端代码具有对代理的引用。这意味着该对象引用上的方法调用是代理上的调用。结果,代理可以委派给与该特定方法调用相关的所有拦截器(建议)。但是,一旦调用最终到达目标对象(在这种情况下为SimplePojo,则为this.bar()this.foo()),则将针对this引用而不是对this引用调用它可能对其自身进行的任何方法调用。代理。这具有重要的意义。这意味着自调用不会导致与方法调用相关的建议得到执行的机会。

好吧,那么该怎么办?最好的方法(此处宽松地使用术语“最好”)是重构代码,以免发生自调用。这确实需要您做一些工作,但这是最好的,侵入性最小的方法。下一种方法绝对可怕,我们正要指出这一点,恰恰是因为它是如此可怕。您可以(对我们来说是痛苦的)完全将类中的逻辑与 Spring AOP 绑定在一起,如以下示例所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

这将您的代码完全耦合到 Spring AOP,并且使类本身意识到在 AOP 上下文中使用它这一事实,而 AOP 却是事实。创建代理时,还需要一些其他配置,如以下示例所示:

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.adddInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

最后,必须注意,AspectJ 没有此自调用问题,因为它不是基于代理的 AOP 框架。

5.9. 以编程方式创建@AspectJ 代理

除了使用<aop:config><aop:aspectj-autoproxy>声明配置中的各个方面外,还可以通过编程方式创建建议目标对象的代理。有关 Spring 的 AOP API 的完整详细信息,请参见next chapter。在这里,我们要重点介绍通过使用@AspectJ 方面自动创建代理的功能。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory类为一个或多个@AspectJ 方面建议的目标对象创建代理。此类的基本用法非常简单,如以下示例所示:

// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

有关更多信息,请参见javadoc

5.10. 在 Spring 应用程序中使用 AspectJ

到目前为止,本章介绍的所有内容都是纯 Spring AOP。在本节中,我们将研究如果您的需求超出了 Spring AOP 所提供的功能,那么如何使用 AspectJ 编译器或 weaver 代替 Spring AOP 或除 Spring AOP 之外使用。

Spring 附带了一个小的 AspectJ 方面库,该库在您的发行版中可以作为spring-aspects.jar独立使用。您需要将其添加到 Classpath 中才能使用其中的方面。 使用 AspectJ 通过 Spring 依赖注入域对象AspectJ 的其他 Spring 方面讨论了该库的内容以及如何使用它。 使用 Spring IoC 配置 AspectJ Aspects讨论如何依赖注入使用 AspectJ 编译器编织的 AspectJ 方面。最后,在 Spring Framework 中使用 AspectJ 进行加载时编织介绍了使用 AspectJ 的 Spring 应用程序的加载时编织。

5.10.1. 使用 AspectJ 通过 Spring 依赖注入域对象

Spring 容器实例化并配置在您的应用程序上下文中定义的 bean。给定包含要应用的配置的 Bean 定义的名称,也可以要求 Bean 工厂配置预先存在的对象。 spring-aspects.jar包含注解驱动的方面,该方面利用此功能允许任何对象的依赖项注入。该支架旨在用于在任何容器的控制范围之外创建的对象。域对象通常属于此类,因为它们通常是通过new运算符或通过 ORM 工具以数据库查询的方式通过程序创建的。

@Configurable注解 将一个类标记为符合 Spring 驱动的配置。在最简单的情况下,您可以将其纯粹用作标记 注解,如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

当以这种方式用作标记接口时,Spring 通过使用具有与完全限定类型名称(com.xyz.myapp.domain.Account)同名的 bean 定义(通常为原型作用域)来配置带注解类型的新实例(在本例中为Account)。 。由于 Bean 的默认名称是其类型的完全限定名称,因此声明原型定义的便捷方法是省略id属性,如以下示例所示:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要显式指定要使用的原型 bean 定义的名称,则可以直接在注解中这样做,如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring 现在查找名为account的 bean 定义,并将其用作配置新Account实例的定义。

您也可以使用自动装配来避免完全指定专用的 bean 定义。要让 Spring 应用自动装配,请使用@Configurable注解的autowire属性。您可以分别按类型或名称指定@Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME自动布线。或者,从 Spring 2.5 开始,最好在字段或方法级别使用@Autowired@Inject@Configurable bean 指定显式的,注解 驱动的依赖项注入(有关更多详细信息,请参见基于注解的容器配置)。

最后,您可以使用dependencyCheck属性(例如@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))为新创建和配置的对象中的对象引用启用 Spring 依赖项检查。如果此属性设置为true,则 Spring 在配置后验证是否已设置所有属性(不是基元或集合)。

请注意,单独使用注解不会执行任何操作。注解 中存在的是spring-aspects.jar中的AnnotationBeanConfigurerAspect。从本质上讲,方面说:“在从带有@Configurable注解 的类型的新对象的初始化返回之后,根据注解的属性使用 Spring 配置新创建的对象”。在这种情况下,“初始化”是指新实例化的对象(例如,用new运算符实例化的对象)以及正在反序列化(例如,通过readResolve())的Serializable对象。

Note

上段中的关键短语之一是“本质上”。对于大多数情况,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”是指在构造对象之后注入依赖项。这意味着该依赖项不可在类的构造函数体中使用。如果要在构造函数主体执行之前注入依赖项,从而可以在构造函数主体中使用这些依赖项,则需要在@Configurable声明中定义此变量,如下所示:

@Configurable(preConstruction=true)

您可以在AspectJ 编程指南的 AspectJ 在本附录中中找到有关各种切入点类型的语言语义的更多信息。

为此,必须将带注解的类型与 AspectJ 编织器编织在一起。您可以使用构建时 Ant 或 Maven 任务来执行此操作(例如,参见__),也可以使用加载时编织(请参见在 Spring Framework 中使用 AspectJ 进行加载时编织)。 AnnotationBeanConfigurerAspect本身需要由 Spring 配置(以获取对将用于配置新对象的 Bean 工厂的引用)。如果使用基于 Java 的配置,则可以将@EnableSpringConfigured添加到任何@Configuration类中,如下所示:

@Configuration
@EnableSpringConfigured
public class AppConfig {

}

如果您喜欢基于 XML 的配置,Spring context namespace定义了一个方便的context:spring-configured元素,您可以按以下方式使用它:

<context:spring-configured/>

在配置方面之前创建的@Configurable个对象的实例导致向调试日志发出一条消息,并且未进行任何对象配置。一个示例可能是 Spring 配置中的 bean,当它由 Spring 初始化时会创建域对象。在这种情况下,可以使用depends-on bean 属性来手动指定该 bean 取决于配置方面。下面的示例演示如何使用depends-on属性:

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>

Note

除非您真的想在运行时依赖它的语义,否则不要通过 bean configurer 方面激活@Configurable处理。特别是,请确保不要在已通过容器注册为常规 Spring bean 的 bean 类上使用@Configurable。这样做将导致两次初始化,一次是通过容器,一次是通过方面。

单元测试@Configurable 对象

@Configurable支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果 AspectJ 尚未编织@Configurable类型,则注解在单元测试期间不起作用。您可以在被测对象中设置模拟或存根属性引用,然后照常进行。如果@Configurable类型是 AspectJ 编织的,您仍然可以像往常一样在容器外部进行单元测试,但是每次构造@Configurable对象时,都会看到一条警告消息,指示该对象尚未由 Spring 配置。

处理多个应用程序上下文

用于实现@Configurable支持的AnnotationBeanConfigurerAspect是 AspectJ 单例方面。单例方面的范围与static成员的范围相同:每个类加载器都有一个方面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在哪里定义@EnableSpringConfigured bean 以及在哪里将spring-aspects.jar放在 Classpath 上。

考虑一个典型的 Spring Web 应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用的业务服务,支持那些服务所需的一切,以及每个 Servlet 的一个子应用程序上下文(其中包含该 Servlet 的特定定义)。所有这些上下文共存于相同的类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能保留对其中一个的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured bean。这定义了您可能想注入域对象的服务。结果是,您无法使用@Configurable 机制来配置域对象,该域对象引用的是在子(特定于 servlet 的)上下文中定义的 bean 的引用(无论如何,这可能不是您想做的事情)。

在同一容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序使用自己的类加载器(例如,将spring-aspects.jar放在'WEB-INF/lib'中)加载spring-aspects.jar中的类型。如果spring-aspects.jar仅添加到容器级的 Classpath 中(并因此由共享的父类加载器加载),则所有 Web 应用程序都共享相同的方面实例(这可能不是您想要的)。

5.10.2. AspectJ 的其他 Spring 方面

除了@Configurable方面之外,spring-aspects.jar还包含一个 AspectJ 方面,您可以使用它来驱动 Spring 的事务 Management,该事务 Management 使用@Transactional注解 进行注解的类型和方法。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。

解释@Transactional注解 的方面是AnnotationTransactionAspect。使用此方面时,必须注解实现类(或该类中的方法或两者),而不是注解该类所实现的接口(如果有)。 AspectJ 遵循 Java 的规则,即不继承接口上的 注解。

类上的@Transactional注解 指定用于执行该类中任何公共操作的默认事务语义。

类内方法上的@Transactional注解 将覆盖类 注解(如果存在)给出的默认事务语义。可以标注任何可见性的方法,包括私有方法。直接注解非公共方法是执行此类方法而获得事务划分的唯一方法。

Tip

从 Spring Framework 4.2 开始,spring-aspects提供了类似的方面,为标准javax.transaction.Transactional注解 提供了完全相同的功能。检查JtaAnnotationTransactionAspect了解更多详细信息。

对于希望使用 Spring 配置和事务 Management 支持但又不想(或不能)使用注解的 AspectJ 程序员,spring-aspects.jar还包含abstract个方面,您可以扩展它们以提供自己的切入点定义。有关更多信息,请参见AbstractBeanConfigurerAspectAbstractTransactionAspect方面的资源。例如,以下摘录显示了如何编写方面来使用与完全限定的类名匹配的原型 Bean 定义来配置域模型中定义的对象的所有实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

    public DomainObjectConfiguration() {
        setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
    }

    // the creation of a new bean (any object in the domain model)
    protected pointcut beanCreation(Object beanInstance) :
        initialization(new(..)) &&
        SystemArchitecture.inDomainModel() &&
        this(beanInstance);

}

5.10.3. 使用 Spring IoC 配置 AspectJ Aspects

当您将 AspectJ 方面与 Spring 应用程序一起使用时,既自然又希望能够使用 Spring 配置这些方面。 AspectJ 运行时本身负责方面的创建,并且通过 Spring 配置 AspectJ 创建的方面的方法取决于方面所使用的 AspectJ 实例化模型(per-xxx子句)。

AspectJ 的大多数方面都是单例方面。这些方面的配置很容易。您可以创建一个正常引用外观类型并包含factory-method="aspectOf" bean 属性的 bean 定义。这可以确保 Spring 通过向 AspectJ 索要长宽比实例,而不是尝试自己创建实例来获得长宽比实例。下面的示例演示如何使用factory-method="aspectOf"属性:

<bean id="profiler" class="com.xyz.profiler.Profiler"
        factory-method="aspectOf"> (1)

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

非单一方面很难配置。但是,可以通过创建原型 bean 定义并使用spring-aspects.jar@Configurable支持来配置方面实例(一旦 AspectJ 运行时创建了 bean)来实现。

如果您有一些要与 AspectJ 编织的@AspectJ 方面(例如,对域模型类型使用加载时编织)以及要与 Spring AOP 一起使用的其他@AspectJ 方面,那么这些方面都已在 Spring 中配置,您需要告诉 Spring AOP @AspectJ 自动代理支持,应使用配置中定义的@AspectJ 方面的确切子集进行自动代理。您可以使用<aop:aspectj-autoproxy/>声明中的一个或多个<include/>元素来完成此操作。每个<include/>元素都指定一个名称模式,并且只有名称与至少一个模式匹配的 bean 才可用于 Spring AOP 自动代理配置。以下示例显示了如何使用<include/>元素:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

Note

不要被<aop:aspectj-autoproxy/>元素的名称所迷惑。使用它可以创建 Spring AOP 代理。此处使用了@AspectJ 样式的声明,但是不涉及 AspectJ 运行时。

5.10.4. 在 Spring Framework 中使用 AspectJ 进行加载时编织

加载时编织(LTW)是指在将 AspectJ 方面加载到应用程序的类文件中时将它们编织到 Java 虚拟机(JVM)中的过程。本部分的重点是在 Spring 框架的特定上下文中配置和使用 LTW。本节不是 LTW 的一般介绍。有关 LTW 的详细信息以及仅使用 AspectJ 配置 LTW(完全不涉及 Spring)的详细信息,请参见AspectJ 开发环境指南的 LTW 部分

Spring 框架为 AspectJ LTW 带来的价值在于能够对编织过程进行更精细的控制。 “香草” AspectJ LTW 通过使用 Java(5)代理来实现,该代理通过在启动 JVM 时指定 VM 参数来打开。因此,它是一个 JVM 范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用了 Spring 的 LTW 允许您以ClassLoader为基础打开 LTW,它的粒度更细,并且在“单 JVM-多应用程序”环境中(例如在典型的应用程序服务器中发现)更有意义。环境)。

此外,在某些环境中,此支持可实现加载时编织,而无需对添加-javaagent:path/to/aspectjweaver.jar-javaagent:path/to/org.springframework.instrument-{version}.jar(以前称为spring-agent.jar)所需的应用服务器的启动脚本进行任何修改。开发人员可以修改构成应用程序上下文的一个或多个文件,以实现加载时编织,而不必依赖通常负责部署配置(例如启动脚本)的 Management 员。

现在,销售工作已经结束,让我们首先浏览一个使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中引入的元素。有关完整示例,请参见Petclinicsample 申请

第一个例子

假设您是一位负责诊断系统中某些性能问题的原因的应用程序开发人员。与其使用分析工具,不如使用一个简单的分析方面,使我们能够快速获得一些性能 Metrics。然后,我们可以立即在该特定区域应用更细粒度的分析工具。

Note

此处提供的示例使用 XML 配置。您还可以将Java configuration配置和使用@AspectJ。具体来说,您可以使用@EnableLoadTimeWeaving注解 替代<context:load-time-weaver/>(有关详细信息,请参见below)。

下面的示例显示了配置方面,它不是花哨的-它是基于时间的探查器,它使用@AspectJ 样式的方面声明:

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}

我们还需要创建一个META-INF/aop.xml文件,以通知 AspectJ 编织者我们要将ProfilingAspect编织到类中。此文件约定,即在 JavaClasspath 上名为META-INF/aop.xml的文件,是标准 AspectJ。下面的示例显示aop.xml文件:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

    <weaver>
        <!-- only weave classes in our application-specific packages -->
        <include within="foo.*"/>
    </weaver>

    <aspects>
        <!-- weave in just this aspect -->
        <aspect name="foo.ProfilingAspect"/>
    </aspects>

</aspectj>

现在,我们可以 continue 进行配置中特定于 Spring 的部分。我们需要配置一个LoadTimeWeaver(稍后说明)。此加载时织布器是必不可少的组件,负责将一个或多个META-INF/aop.xml文件中的方面配置编织到应用程序的类中。好处是,它不需要很多配置(您可以指定一些其他选项,但是稍后会详细介绍),如以下示例所示:

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

    <!-- a service object; we will be profiling its methods -->
    <bean id="entitlementCalculationService"
            class="foo.StubEntitlementCalculationService"/>

    <!-- this switches on the load-time weaving -->
    <context:load-time-weaver/>
</beans>

现在,所有必需的构件(方面,META-INF/aop.xml文件和 Spring 配置)都就位了,我们可以使用main(..)方法创建以下驱动程序类,以演示实际的 LTW:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService
            = (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

        // the profiling aspect is 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

我们还有最后一件事要做。本节的引言确实说过,可以使用 Spring 以ClassLoader为基础选择性地打开 LTW,这是事实。但是,在此示例中,我们使用 Java 代理(Spring 随附)打开 LTW。我们使用以下命令运行前面显示的Main类:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent是用于指定和启用代理来检测在 JVM 上运行的程序的标志。 Spring 框架附带了这样的代理InstrumentationSavingAgent,该代理打包在spring-instrument.jar中,在上一示例中,该代理作为-javaagent自变量的值提供。

Main程序的执行输出类似于下一个示例。 (我在calculateEntitlement()实现中引入了Thread.sleep(..)语句,以便探查器实际上捕获的不是 0 毫秒(01234毫秒不是 AOP 引入的开销)。以下清单显示了运行探查器时得到的输出:

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

由于此 LTW 是通过使用成熟的 AspectJ 来实现的,因此我们不仅限于建议 Spring Bean。 Main程序的以下细微变化会产生相同的结果:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {

        new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
            new StubEntitlementCalculationService();

        // the profiling aspect will be 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

注意,在前面的程序中,我们如何引导 Spring 容器,然后完全在 Spring 上下文之外创建StubEntitlementCalculationService的新实例。剖析建议仍会被应用。

诚然,这个例子很简单。但是,在前面的示例中已经介绍了 Spring 对 LTW 支持的基础,本节的其余部分详细解释了每一位配置和用法的“原因”。

Note

在此示例中使用的ProfilingAspect可能是基本的,但很有用。这是开发时方面的一个很好的示例,开发人员可以在开发过程中使用它,然后轻松地将其从部署到 UAT 或 Producing 的应用程序构建中排除。

Aspects

您在 LTW 中使用的方面必须是 AspectJ 方面。您可以使用 AspectJ 语言本身来编写它们,也可以使用@AspectJ 风格来编写方面。这样,您的方面就是有效的 AspectJ 和 Spring AOP 方面。此外,编译的方面类需要在 Classpath 上可用。

'META-INF/aop.xml'

通过使用 JavaClasspath 上的一个或多个META-INF/aop.xml文件(直接或通常在 jar 文件中)来配置 AspectJ LTW 基础结构。

该文件的结构和内容在 LTW 部分AspectJ 参考文档中详细介绍。由于 aop.xml 文件是 100%AspectJ,因此在此不再赘述。

必需的库(JARS)

至少,您需要以下库来使用 Spring Framework 对 AspectJ LTW 的支持:

如果您使用Spring 提供的代理程序可实现检测,则还需要:

Spring Configuration

Spring 的 LTW 支持中的关键组件是LoadTimeWeaver接口(在org.springframework.instrument.classloading包中),以及 Spring 发行版附带的众多实现。 LoadTimeWeaver负责在运行时将一个或多个java.lang.instrument.ClassFileTransformers添加到ClassLoader,这为各种有趣的应用程序打开了大门,其中之一恰好是方面的 LTW。

Tip

如果您不熟悉运行时类文件转换的概念,请在 continue 之前先查看java.lang.instrument软件包的 javadoc API 文档。虽然该文档并不全面,但是至少您可以看到关键的接口和类(在您阅读本节时作为参考)。

为特定的ApplicationContext配置LoadTimeWeaver就像添加一行一样容易。 (请注意,您几乎肯定需要使用ApplicationContext作为您的 Spring 容器—通常,BeanFactory是不够的,因为 LTW 支持使用BeanFactoryPostProcessors.)

要启用 Spring Framework 的 LTW 支持,您需要配置LoadTimeWeaver,通常通过使用@EnableLoadTimeWeaving注解来完成,如下所示:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {

}

另外,如果您更喜欢基于 XML 的配置,请使用<context:load-time-weaver/>元素。请注意,该元素是在context名称空间中定义的。以下示例显示了如何使用<context:load-time-weaver/>

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

    <context:load-time-weaver/>

</beans>

前面的配置会自动为您定义并注册许多 LTW 特定的基础结构 Bean,例如LoadTimeWeaverAspectJWeavingEnabler。缺省LoadTimeWeaverDefaultContextLoadTimeWeaver类,它将尝试装饰自动检测到的LoadTimeWeaver。 “自动检测到”的LoadTimeWeaver的确切类型取决于您的运行时环境。下表总结了各种LoadTimeWeaver实现:

*表 13. DefaultContextLoadTimeWeaver LoadTimeWeavers *

Runtime Environment LoadTimeWeaver实施
在 Oracle 的WebLogic中运行 WebLogicLoadTimeWeaver
在 Oracle 的GlassFish中运行 GlassFishLoadTimeWeaver
Apache Tomcat中运行 TomcatLoadTimeWeaver
在 Red Hat 的JBoss ASWildFly中运行 JBossLoadTimeWeaver
在 IBM 的WebSphere中运行 WebSphereLoadTimeWeaver
JVM 从 Spring InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar)开始 InstrumentationLoadTimeWeaver
回退,期望基础 ClassLoader 遵循通用约定(例如适用于TomcatInstrumentableClassLoaderResin) ReflectiveLoadTimeWeaver

请注意,该表仅列出了使用DefaultContextLoadTimeWeaver时自动检测到的LoadTimeWeavers。您可以确切指定要使用的LoadTimeWeaver实现。

要使用 Java 配置指定特定的LoadTimeWeaver,请实现LoadTimeWeavingConfigurer接口并覆盖getLoadTimeWeaver()方法。以下示例指定ReflectiveLoadTimeWeaver

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}

如果使用基于 XML 的配置,则可以将完全限定的类名指定为<context:load-time-weaver/>元素上的weaver-class属性的值。同样,以下示例指定ReflectiveLoadTimeWeaver

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

    <context:load-time-weaver
            weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>

以后可以使用众所周知的名称loadTimeWeaver从 Spring 容器中检索由配置定义和注册的LoadTimeWeaver。请记住,LoadTimeWeaver仅作为 Spring 的 LTW 基础结构添加一个或多个ClassFileTransformers的机制而存在。执行 LTW 的实际ClassFileTransformerClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime程序包)类。有关更多详细信息,请参见ClassPreProcessorAgentAdapter类的类级 javadoc,因为实际上如何实现编织的细节不在本文档的讨论范围之内。

还需要讨论配置的最后一个属性:aspectjWeaving属性(如果使用 XML,则为aspectj-weaving)。此属性控制是否启用 LTW。它接受三个可能的值之一,如果属性不存在,则默认值为autodetect。下表总结了三个可能的值:

表 14. AspectJ 编织属性值

Annotation Value XML Value Explanation
ENABLED on AspectJ 正在编织,并且在加载时适当地编织了方面。
DISABLED off LTW 已关闭。加载时不会编织任何方面。
AUTODETECT autodetect 如果 Spring LTW 基础结构可以找到至少一个META-INF/aop.xml文件,则表示 AspectJ 编织已开始。否则,它关闭。这是默认值。
Environment-specific Configuration

最后一部分包含在应用程序服务器和 Web 容器等环境中使用 Spring 的 LTW 支持时所需的所有其他设置和配置。

Tomcat

从历史上看,Apache Tomcat的默认类加载器不支持类转换,因此 Spring 提供了增强的实现来满足此需求。名为TomcatInstrumentableClassLoader的加载程序可在 Tomcat 6.0 及更高版本上运行。

Tip

不要在 Tomcat 8.0 及更高版本上定义TomcatInstrumentableClassLoader。相反,让 Spring 通过TomcatLoadTimeWeaver策略自动使用 Tomcat 的新本机InstrumentableClassLoader工具。

如果仍然需要使用TomcatInstrumentableClassLoader,则可以为每个 Web 应用程序分别进行注册,如下所示:

<Context path="/myWebApp" docBase="/my/webApp/location">
    <Loader
        loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
</Context>

Apache Tomcat 6.0 支持多个上下文位置:

为了提高效率,我们建议使用嵌入式的逐个 Web 应用程序配置样式,因为它只影响使用自定义类加载器的应用程序,并且不需要对服务器配置进行任何更改。有关可用上下文位置的更多详细信息,请参见 Tomcat 6.0.x documentation

或者,考虑使用 Spring 提供的通用 VM 代理,该代理在 Tomcat 的启动脚本中指定(本节前面已描述)。这使得检测对所有已部署的 Web 应用程序均可用,无论它们运行在ClassLoader上。

WebLogic,WebSphere,Resin,GlassFish 和 JBoss

最新版本的 WebLogic Server(版本 10 和更高版本),IBM WebSphere Application Server(版本 7 和更高版本),Resin(版本 3.1 和更高版本)以及 JBoss(版本 6.x 或更高版本)提供了ClassLoader并能够进行本地检测。 Spring 的本地 LTW 利用此类 ClassLoader 实现来实现 AspectJ 编织。您可以通过激活加载时编织来启用 LTW,如described earlier。具体来说,您无需修改启动脚本即可添加-javaagent:path/to/spring-instrument.jar

请注意,具有 GlassFish 工具功能的ClassLoader仅在其 EAR 环境中可用。对于 GlassFish Web 应用程序,请遵循 Tomcat 设置说明outlined earlier

请注意,在 JBoss 6.x 上,您需要禁用应用服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是将名为WEB-INF/jboss-scanning.xml的文件添加到您的工件中,其中包含以下内容:

<scanning xmlns="urn:jboss:scanning:1.0"/>
通用 Java 应用程序

在不支持或现有LoadTimeWeaver实现不支持的环境中需要类检测时,JDK 代理可以是唯一的解决方案。对于这种情况,Spring 提供了InstrumentationLoadTimeWeaver,这需要一个 Spring 特定(但非常通用)的 VM 代理org.springframework.instrument-{version}.jar(以前称为spring-agent.jar)。

要使用它,您必须通过提供以下 JVM 选项来使用 Spring 代理启动虚拟机:

-javaagent:/path/to/org.springframework.instrument-{version}.jar

请注意,这需要修改 VM 启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的操作策略)。此外,JDK 代理会检测整个 VM,这可能会很昂贵。

出于性能原因,我们建议仅在目标环境(例如Jetty)没有(或不支持)专用 LTW 的情况下才使用此配置。

5.11. 更多资源

可以在AspectJ website上找到有关 AspectJ 的更多信息。

强烈推荐 Ramnivas Laddad(Manning,2009)出版的《 AspectJ in Action *》第二版。本书的重点是 AspectJ,但在一定程度上探讨了许多通用的 AOP 主题。

6. Spring AOP API

上一章使用@AspectJ 和基于模式的方面定义描述了 Spring 对 AOP 的支持。在本章中,我们讨论较低级别的 Spring AOP API 和通常在 Spring 1.2 应用程序中使用的 AOP 支持。对于新应用程序,我们建议使用上一章中介绍的 Spring 2.0 和更高版本的 AOP 支持。但是,当您使用现有应用程序(或阅读书籍和文章)时,可能会遇到 Spring 1.2 样式的示例。 Spring 5 仍然与 Spring 1.2 向后兼容,Spring 5 完全支持本章中描述的所有内容。

6.1. Spring 中的 Pointcut API

本节描述了 Spring 如何处理关键切入点概念。

6.1.1. Concepts

Spring 的切入点模型使切入点重用不受建议类型的影响。您可以使用相同的切入点来定位不同的建议。

org.springframework.aop.Pointcut界面是中央界面,用于将建议定向到特定的类和方法。完整的界面如下:

public interface Pointcut {

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();

}

Pointcut接口分为两部分,可以重用类和方法匹配的部分以及细粒度的合成操作(例如与另一个方法匹配器执行“联合”)。

ClassFilter接口用于将切入点限制为给定的一组目标类。如果matches()方法始终返回 true,则匹配所有目标类。以下清单显示了ClassFilter接口定义:

public interface ClassFilter {

    boolean matches(Class clazz);
}

MethodMatcher界面通常更重要。完整的界面如下:

public interface MethodMatcher {

    boolean matches(Method m, Class targetClass);

    boolean isRuntime();

    boolean matches(Method m, Class targetClass, Object[] args);
}

matches(Method, Class)方法用于测试此切入点是否与目标类上的给定方法匹配。创建 AOP 代理时可以执行此评估,以避免需要对每个方法调用进行测试。如果给定方法的两个参数的matches方法返回true,而用于 MethodMatcher 的isRuntime()的方法返回true,则在每次方法调用时都会调用三个参数的 match 方法。这样,切入点就可以在执行目标建议之前立即查看传递给方法调用的参数。

大多数MethodMatcher实现是静态的,这意味着它们的isRuntime()方法返回false。在这种情况下,永远不会调用三参数matches方法。

Tip

如果可能,请尝试使切入点成为静态,从而在创建 AOP 代理时允许 AOP 框架缓存切入点评估的结果。

6.1.2. 切入点的操作

Spring 支持切入点上的操作(特别是联合和相交)。

联合表示两个切入点都匹配的方法。交集是指两个切入点都匹配的方法。联合通常更有用。您可以使用org.springframework.aop.support.Pointcuts类中的静态方法或同一包中的ComposablePointcut类来编写切入点。但是,使用 AspectJ 切入点表达式通常是一种更简单的方法。

6.1.3. AspectJ 表达切入点

从 2.0 开始,Spring 使用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一个切入点,该切入点使用 AspectJ 提供的库来解析 AspectJ 切入点表达式字符串。

有关支持的 AspectJ 切入点 Primitives 的讨论,请参见previous chapter

6.1.4. 便捷切入点实现

Spring 提供了几种方便的切入点实现。您可以直接使用其中一些。其他的则应归入特定于应用程序的切入点中。

Static Pointcuts

静态切入点是基于方法和目标类的,不能考虑方法的参数。静态切入点足以满足大多数用途,并且最好。首次调用方法时,Spring 只能评估一次静态切入点。之后,无需在每次方法调用时再次评估切入点。

本节的其余部分描述了 Spring 附带的一些静态切入点实现。

正则表达式切入点

指定静态切入点的一种明显方法是正则表达式。除了 Spring 之外,还有几个 AOP 框架使之成为可能。 org.springframework.aop.support.JdkRegexpMethodPointcut是通用正则表达式切入点,它使用 JDK 中的正则表达式支持。

使用JdkRegexpMethodPointcut类,可以提供模式字符串列表。如果其中任何一个匹配,则切入点的值为true。 (因此,结果实际上是这些切入点的并集.)

以下示例显示了如何使用JdkRegexpMethodPointcut

<bean id="settersAndAbsquatulatePointcut"
        class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

Spring 提供了一个名为RegexpMethodPointcutAdvisor的便捷类,它使我们也可以引用Advice(请记住Advice可以是拦截器,而不是建议,引发建议等)。在后台,Spring 使用JdkRegexpMethodPointcut。使用RegexpMethodPointcutAdvisor简化了接线,因为一个 bean 封装了切入点和建议,如以下示例所示:

<bean id="settersAndAbsquatulateAdvisor"
        class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="advice">
        <ref bean="beanNameOfAopAllianceInterceptor"/>
    </property>
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

您可以将RegexpMethodPointcutAdvisor与任何Advice类型一起使用。

Attribute-driven Pointcuts

静态切入点的一种重要类型是元数据驱动的切入点。这使用元数据属性的值(通常是源级别的元数据)。

Dynamic pointcuts

动态切入点比静态切入点更昂贵。它们考虑了方法参数以及静态信息。这意味着必须在每次方法调用时对它们进行评估,并且由于参数会有所不同,因此无法缓存结果。

主要示例是control flow切入点。

控制流切入点

Spring 控制流切入点在概念上类似于 AspectJ cflow切入点,但功能较弱。 (当前无法指定一个切入点在与另一个切入点匹配的连接点下执行.)控制流切入点与当前调用堆栈匹配。例如,如果连接点是由com.mycompany.web包中的方法或SomeCaller类调用的,则可能会触发。通过使用org.springframework.aop.support.ControlFlowPointcut类指定控制流切入点。

Note

与其他动态切入点相比,控制流切入点在运行时进行评估要昂贵得多。在 Java 1.4 中,成本大约是其他动态切入点的五倍。

6.1.5. 切入点超类

Spring 提供了有用的切入点超类,以帮助您实现自己的切入点。

因为静态切入点最有用,所以您可能应该子类StaticMethodMatcherPointcut。这仅需要实现一个抽象方法(尽管您可以覆盖其他方法以自定义行为)。以下示例显示了如何对StaticMethodMatcherPointcut进行子类化:

class TestStaticPointcut extends StaticMethodMatcherPointcut {

    public boolean matches(Method m, Class targetClass) {
        // return true if custom criteria match
    }
}

动态切入点也有超类。

在 Spring 1.0 RC2 及更高版本中,您可以将自定义切入点与任何建议类型一起使用。

6.1.6. 自定义切入点

由于 Spring AOP 中的切入点是 Java 类,而不是语言功能(如 AspectJ),因此可以声明自定义切入点,无论是静态还是动态。 Spring 中的自定义切入点可以任意复杂。但是,如果可以的话,我们建议使用 AspectJ 切入点表达语言。

Note

更高版本的 Spring 可能提供对 JAC offered 提供的“语义切入点”的支持,例如,“更改目标对象中实例变量的所有方法”。

6.2. Spring 咨询 API

现在,我们可以检查 Spring AOP 如何处理建议。

6.2.1. 咨询生命周期

每个建议都是一个 Spring bean。建议实例可以在所有建议对象之间共享,或者对于每个建议对象都是唯一的。这对应于每个类或每个实例的建议。

每班建议最常用。适用于一般建议,例如 Transaction 顾问。这些不依赖于代理对象的状态或添加新状态。它们仅作用于方法和参数。

每个实例的建议都适合引入,以支持混合。在这种情况下,建议将状态添加到代理对象。

您可以在同一 AOP 代理中混合使用共享建议和基于实例的建议。

6.2.2. Spring 的建议类型

Spring 提供了几种建议类型,并且可以扩展以支持任意建议类型。本节介绍基本概念和标准建议类型。

围绕建议进行拦截

Spring 中最基本的建议类型是围绕建议的拦截。

对于使用方法拦截的建议,Spring 符合 AOP Alliance接口。实现MethodInterceptor并围绕建议实现的类也应实现以下接口:

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke()方法的MethodInvocation参数公开了正在调用的方法,目标连接点,AOP 代理以及该方法的参数。 invoke()方法应返回调用的结果:连接点的返回值。

以下示例显示了一个简单的MethodInterceptor实现:

public class DebugInterceptor implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before: invocation=[" + invocation + "]");
        Object rval = invocation.proceed();
        System.out.println("Invocation returned");
        return rval;
    }
}

请注意对MethodInvocationproceed()方法的调用。这沿着拦截器链向下到达连接点。大多数拦截器都调用此方法并返回其返回值。但是,MethodInterceptor就像任何周围的建议一样,可以返回不同的值或引发异常,而不是调用 proceed 方法。但是,您没有充分的理由就不想这样做。

Note

MethodInterceptor实现提供与其他符合 AOP Alliance 要求的 AOP 实现的互操作性。本节其余部分讨论的其他建议类型将实现常见的 AOP 概念,但以特定于 Spring 的方式。尽管使用最具体的建议类型有一个优势,但是如果您可能想在另一个 AOP 框架中运行方面,请坚持使用MethodInterceptor。请注意,切入点当前无法在框架之间互操作,并且 AOP Alliance 当前未定义切入点接口。

Before Advice

一种更简单的建议类型是事前建议。不需要MethodInvocation对象,因为它仅在进入方法之前被调用。

先行建议的主要优点是无需调用proceed()方法,因此,不会无意中未能沿拦截器链 continue 前进。

以下清单显示了MethodBeforeAdvice界面:

public interface MethodBeforeAdvice extends BeforeAdvice {

    void before(Method m, Object[] args, Object target) throws Throwable;
}

(尽管通常的对象适用于字段拦截,并且 Spring 不太可能实现它,但 Spring 的 API 设计允许先于字段咨询.)

请注意,返回类型为void。通知可以在联接点执行之前插入自定义行为,但不能更改返回值。如果之前的建议引发异常,它将中止拦截器链的进一步执行。异常会传播回拦截器链。如果未选中它或在调用的方法的签名上,则将其直接传递给 Client 端。否则,它将被 AOP 代理包装在未经检查的异常中。

以下示例显示了 Spring 中的 before 建议,该建议计算所有方法调用:

public class CountingBeforeAdvice implements MethodBeforeAdvice {

    private int count;

    public void before(Method m, Object[] args, Object target) throws Throwable {
        ++count;
    }

    public int getCount() {
        return count;
    }
}

Tip

在将建议与任何切入点一起使用之前。

Throws Advice

如果联接点引发异常,则在联接点返回之后调用引发通知。 Spring 提供类型化的抛出建议。请注意,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一个标签接口,用于标识给定对象实现了一个或多个类型化的 throws 通知方法。这些应采用以下形式:

afterThrowing([Method, args, target], subclassOfThrowable)

仅最后一个参数是必需的。方法签名可以具有一个或四个参数,具体取决于建议方法是否对该方法和参数感兴趣。接下来的两个清单显示了类,它们是引发建议的示例。

如果抛出RemoteException(包括来自子类),则调用以下建议:

public class RemoteThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
}

与前面的建议不同,下一个示例声明四个参数,以便可以访问被调用的方法,方法参数和目标对象。如果抛出ServletException,则调用以下建议:

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

最后一个示例说明如何在处理RemoteExceptionServletException的单个类中使用这两种方法。可以将任意数量的引发建议方法组合到一个类中。以下清单显示了最后一个示例:

public static class CombinedThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

Note

如果 throws-advice 方法本身引发异常,则它将覆盖原始异常(也就是说,它将更改引发给用户的异常)。重写异常通常是 RuntimeException,它与任何方法签名都兼容。但是,如果 throws-advice 方法抛出一个已检查的异常,则它必须与目标方法的已声明异常匹配,因此在某种程度上与特定的目标方法签名耦合。 请勿抛出与目标方法签名不兼容的未声明检查异常!

Tip

抛出建议可以与任何切入点一起使用。

返回建议后

在 Spring 中返回通知后,必须实现org.springframework.aop.AfterReturningAdvice接口,以下清单显示了该接口:

public interface AfterReturningAdvice extends Advice {

    void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable;
}

After After Returning 建议可以访问返回值(它不能修改),调用的方法,方法的参数和目标。

返回建议后的以下内容将计数所有未引发异常的成功方法调用:

public class CountingAfterReturningAdvice implements AfterReturningAdvice {

    private int count;

    public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable {
        ++count;
    }

    public int getCount() {
        return count;
    }
}

该建议不会更改执行路径。如果抛出异常,则会将其抛出拦截器链,而不是返回值。

Tip

返回后,建议可以与任何切入点一起使用。

Introduction Advice

Spring 将介绍建议视为一种特殊的拦截建议。

简介需要IntroductionAdvisorIntroductionInterceptor来实现以下接口:

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

从 AOP Alliance MethodInterceptor接口继承的invoke()方法必须实现介绍。也就是说,如果被调用的方法在引入的接口上,则引入拦截器负责处理方法调用-它不能调用proceed()

简介建议不能与任何切入点一起使用,因为它仅适用于类,而不适用于方法级别。您只能通过IntroductionAdvisor使用介绍建议,该建议具有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

    ClassFilter getClassFilter();

    void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

    Class[] getInterfaces();
}

没有MethodMatcher,因此也没有Pointcut与介绍建议相关联。只有类过滤是合乎逻辑的。

getInterfaces()方法返回此顾问程序引入的接口。

在内部使用validateInterfaces()方法来查看引入的接口是否可以由配置的IntroductionInterceptor实现。

考虑一下 Spring 测试套件中的一个示例,并假设我们想为一个或多个对象引入以下接口:

public interface Lockable {
    void lock();
    void unlock();
    boolean locked();
}

这说明了混合。我们希望能够将建议对象强制转换为Lockable,无论它们的类型如何,并调用锁定和解锁方法。如果调用lock()方法,则希望所有的 setter 方法都抛出LockedException。因此,我们可以添加一个方面,使对象在不了解对象的情况下不可变:AOP 的一个很好的例子。

首先,我们需要一个IntroductionInterceptor来完成繁重的工作。在这种情况下,我们扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor便利类。我们可以直接实现IntroductionInterceptor,但是在大多数情况下最好使用DelegatingIntroductionInterceptor

DelegatingIntroductionInterceptor的设计宗旨是将引入的接口委派给引入接口的实际实现,从而隐藏使用拦截的方式。您可以使用构造函数参数将委托设置为任何对象。默认委托(使用无参数构造函数时)为this。因此,在下一个示例中,委托是DelegatingIntroductionInterceptorLockMixin子类。给定一个委托(默认情况下为自身),DelegatingIntroductionInterceptor实例将查找由委托实现的所有接口(IntroductionInterceptor除外),并支持针对其中任何接口的介绍。诸如LockMixin之类的子类可以调用suppressInterface(Class intf)方法来禁止不应公开的接口。但是,无论IntroductionInterceptor准备支持多少个接口,IntroductionAdvisor使用的控件都会控制实际公开哪些接口。引入的接口隐藏了目标对同一接口的任何实现。

因此,LockMixin扩展了DelegatingIntroductionInterceptor并实现了Lockable本身。超类会自动选择支持Lockable进行介绍,因此我们无需指定。我们可以通过这种方式引入任意数量的接口。

请注意locked实例变量的使用。这有效地将附加状态添加到目标对象中保存的状态。

下面的示例显示示例LockMixin类:

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

    private boolean locked;

    public void lock() {
        this.locked = true;
    }

    public void unlock() {
        this.locked = false;
    }

    public boolean locked() {
        return this.locked;
    }

    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
            throw new LockedException();
        }
        return super.invoke(invocation);
    }

}

通常,您无需覆盖invoke()方法。通常,DelegatingIntroductionInterceptor实现(如果引入了delegate方法,则调用delegate方法,否则 continue 向连接点前进)。在当前情况下,我们需要添加一个检查:如果处于锁定模式,则不能调用任何 setter 方法。

所需的简介仅需要保存一个不同的LockMixin实例并指定所引入的接口(在这种情况下,只需Lockable)。一个更复杂的示例可能引用了引入拦截器(将被定义为原型)。在这种情况下,没有与LockMixin相关的配置,因此我们使用new创建它。以下示例显示了LockMixinAdvisor类:

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

    public LockMixinAdvisor() {
        super(new LockMixin(), Lockable.class);
    }
}

我们可以非常简单地应用此顾问程序,因为它不需要配置。 (但是,如果没有IntroductionAdvisor,就不可能使用IntroductionInterceptor.)像通常的介绍一样,顾问程序必须是按实例的,因为它是有状态的。对于每个建议对象,我们需要LockMixinAdvisor的不同实例,因此需要LockMixin。顾问程序包含建议对象状态的一部分。

我们可以像其他任何顾问一样,通过使用Advised.addAdvisor()方法或 XML 配置中的(推荐方式)以编程方式应用此顾问。下面讨论的所有代理创建选择,包括“自动代理创建器”,都可以正确处理介绍和有状态的混合。

6.3. Spring 的 Advisor API

在 Spring 中,顾问程序是一个方面,其中仅包含与切入点表达式关联的单个建议对象。

除了介绍的特殊情况外,任何顾问都可以与任何建议一起使用。 org.springframework.aop.support.DefaultPointcutAdvisor是最常用的顾问类。可以与MethodInterceptorBeforeAdviceThrowsAdvice一起使用。

可以在同一 AOP 代理中的 Spring 中混合使用顾问和建议类型。例如,您可以在一个代理配置中使用围绕建议的拦截,抛出建议以及在建议之前。 Spring 自动创建必要的拦截器链。

6.4. 使用 ProxyFactoryBean 创建 AOP 代理

如果将 Spring IoC 容器(ApplicationContextBeanFactory)用于业务对象(应该是!),则要使用 Spring 的 AOP FactoryBean实现之一。 (请记住,工厂 bean 引入了一个间接层,允许它创建不同类型的对象.)

Note

Spring AOP 支持还在后台使用了工厂 bean。

在 Spring 中创建 AOP 代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。这样可以完全控制切入点,任何适用的建议及其 Sequences。但是,如果您不需要这样的控制,则有一些更简单的选项是可取的。

6.4.1. Basics

像其他 Spring FactoryBean实现一样,ProxyFactoryBean引入了一个间接级别。如果定义一个名为fooProxyFactoryBean,则引用foo的对象看不到ProxyFactoryBean实例本身,而是看到通过ProxyFactoryBean中的getObject()方法的实现创建的对象。此方法创建一个包装目标对象的 AOP 代理。

使用ProxyFactoryBean或另一个 IoC 感知类创建 AOP 代理的最重要好处之一是,IoC 也可以 Management 建议和切入点。这是一项强大的功能,可实现某些其他 AOP 框架难以实现的方法。例如,受益于依赖注入提供的所有可插入性,建议本身可以引用应用程序对象(目标之外,目标应该在任何 AOP 框架中可用)。

6.4.2. JavaBean 属性

与 Spring 提供的大多数FactoryBean实现一样,ProxyFactoryBean类本身就是 JavaBean。其属性用于:

一些关键属性是从org.springframework.aop.framework.ProxyConfig(Spring 中所有 AOP 代理工厂的超类)继承的。这些关键属性包括:

ProxyFactoryBean特有的其他属性包括:

名称是当前工厂中的 bean 名称,包括祖先工厂中的 bean 名称。您不能在这里提及 bean 引用,因为这样做会导致ProxyFactoryBean忽略建议的单例设置。

您可以在拦截器名称后加上星号(*)。这样做会导致应用所有顾问 Bean,其名称以要应用星号的部分开头。您可以在使用“全局”顾问中找到使用此功能的示例。

6.4.3. 基于 JDK 和 CGLIB 的代理

本部分是有关ProxyFactoryBean如何选择为特定目标对象(将被代理)创建基于 JDK 的代理或基于 CGLIB 的代理的 Authority 性文档。

Note

在 Spring 的 1.2.x 版和 2.0 版之间,ProxyFactoryBean创建基于 JDK 或 CGLIB 的代理的行为发生了变化。 ProxyFactoryBean现在在自动检测接口方面表现出与TransactionProxyFactoryBean类类似的语义。

如果要代理的目标对象的类(以下简称为目标类)没有实现任何接口,则将创建基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,并且没有接口意味着甚至无法进行 JDK 代理。您可以插入目标 bean 并通过设置interceptorNames属性来指定拦截器列表。请注意,即使ProxyFactoryBeanproxyTargetClass属性已设置为false,也会创建基于 CGLIB 的代理。 (这样做没有任何意义,最好将其从 Bean 定义中删除,因为它充其量是多余的,并且在最糟的情况下会造成混淆.)

如果目标类实现一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean的配置。

如果ProxyFactoryBeanproxyTargetClass属性已设置为true,则会创建基于 CGLIB 的代理。这是有道理的,并且符合最小惊讶原则。即使已将ProxyFactoryBeanproxyInterfaces属性设置为一个或多个完全限定的接口名称,但proxyTargetClass属性设置为true的事实也会导致基于 CGLIB 的代理生效。

如果ProxyFactoryBeanproxyInterfaces属性已设置为一个或多个完全限定的接口名称,则将创建基于 JDK 的代理。创建的代理实现proxyInterfaces属性中指定的所有接口。如果目标类恰好实现了比proxyInterfaces属性中指定的接口更多的接口,那就很好了,但是这些附加接口不会由返回的代理实现。

如果尚未设置ProxyFactoryBeanproxyInterfaces属性,但是目标类确实实现了一个(或多个)接口,则ProxyFactoryBean自动检测到目标类实际上确实实现了至少一个接口以及基于 JDK 的代理被构建。实际代理的接口是目标类实现的所有接口。实际上,这与向proxyInterfaces属性提供目标类实现的每个接口的列表相同。但是,它的工作量大大减少,而且不容易出现印刷错误。

6.4.4. 代理接口

考虑一个实际的ProxyFactoryBean的简单示例。此示例涉及:

以下清单显示了示例:

<bean id="personTarget" class="com.mycompany.PersonImpl">
    <property name="name" value="Tony"/>
    <property name="age" value="51"/>
</bean>

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>

<bean id="person"
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>

    <property name="target" ref="personTarget"/>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>

请注意,interceptorNames属性具有String列表,其中包含当前工厂中的拦截器或顾问程序的 Bean 名称。您可以在返回之前,之后使用顾问程序,拦截器并引发建议对象。顾问的 Sequences 很重要。

Note

您可能想知道为什么列表不包含 bean 引用。这样做的原因是,如果ProxyFactoryBean的 singleton 属性设置为false,则它必须能够返回独立的代理实例。如果任何顾问本身就是原型,则需要返回一个独立的实例,因此必须能够从工厂获得原型的实例。保持引用是不够的。

可以使用前面显示的person bean 定义代替Person实现,如下所示:

Person person = (Person) factory.getBean("person");

与普通 Java 对象一样,在同一 IoC 上下文中的其他 bean 可以表达对此的强类型依赖性。以下示例显示了如何执行此操作:

<bean id="personUser" class="com.mycompany.PersonUser">
    <property name="person"><ref bean="person"/></property>
</bean>

在此示例中,PersonUser类公开了Person类型的属性。就其而言,可以透明地使用 AOP 代理代替“真实的”人实现。但是,其类将是动态代理类。可以将其强制转换为Advised接口(稍后讨论)。

您可以使用匿名内部 bean 隐藏目标和代理之间的区别。只有ProxyFactoryBean定义不同。该建议仅出于完整性考虑。以下示例显示如何使用匿名内部 bean:

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>
    <!-- Use inner bean, not local reference to target -->
    <property name="target">
        <bean class="com.mycompany.PersonImpl">
            <property name="name" value="Tony"/>
            <property name="age" value="51"/>
        </bean>
    </property>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>

使用匿名内部 bean 的优点是只有一个类型为Person的对象。如果我们希望防止应用程序上下文的用户获取对未建议对象的引用,或者需要避免使用 Spring IoC 自动装配的任何歧义,这将非常有用。可以说,还有一个优点是ProxyFactoryBean定义是独立的。但是,有时能够从工厂获得未经建议的目标实际上可能是一个优势(例如,在某些测试方案中)。

6.4.5. 代理类

如果您需要代理一类,而不是一个或多个接口,该怎么办?

想象一下,在我们之前的示例中,没有Person接口。我们需要建议一个名为Person的类,该类未实现任何业务接口。在这种情况下,您可以将 Spring 配置为使用 CGLIB 代理而不是动态代理。为此,请将前面显示的ProxyFactoryBeanproxyTargetClass属性设置为true。尽管最好对接口而不是对类进行编程,但是在处理遗留代码时,建议未实现接口的类的功能可能会很有用。 (通常,Spring 并不是规定性的.虽然可以轻松地应用良好实践,但可以避免强制采用特定方法.)

如果需要,即使您有接口,也可以在任何情况下强制使用 CGLIB。

CGLIB 代理通过在运行时生成目标类的子类来工作。 Spring 配置此生成的子类以将方法调用委托给原始目标。子类用于实现 Decorator 模式,并编织在建议中。

CGLIB 代理通常应对用户透明。但是,有一些问题要考虑:

CGLIB 代理和动态代理之间几乎没有性能差异。从 Spring 1.0 开始,动态代理要快一些。但是,将来可能会改变。在这种情况下,性能不应作为决定性的考虑因素。

6.4.6. 使用“全局”顾问

通过在拦截器名称后附加一个星号,所有具有与该星号之前的部分匹配的 Bean 名称的顾问程序都将添加到顾问程序链中。如果您需要添加标准的“全局”顾问程序集,这可能会派上用场。以下示例定义了两个全局顾问程序:

<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="service"/>
    <property name="interceptorNames">
        <list>
            <value>global*</value>
        </list>
    </property>
</bean>

<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>

6.5. 简洁的代理定义

特别是在定义事务代理时,您可能会得到许多类似的代理定义。使用父子 bean 定义和子 bean 定义以及内部 bean 定义可以使代理定义更加简洁明了。

首先,我们为代理创建父模板,bean 定义,如下所示:

<bean id="txProxyTemplate" abstract="true"
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager" ref="transactionManager"/>
    <property name="transactionAttributes">
        <props>
            <prop key="*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

它本身从未实例化,因此实际上可能是不完整的。然后,每个需要创建的代理都是一个子 bean 定义,它将代理的目标包装为内部 bean 定义,因为无论如何该目标都不会单独使用。以下示例显示了这样的子 bean:

<bean id="myService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MyServiceImpl">
        </bean>
    </property>
</bean>

您可以从父模板覆盖属性。在以下示例中,我们将覆盖事务传播设置:

<bean id="mySpecialService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MySpecialServiceImpl">
        </bean>
    </property>
    <property name="transactionAttributes">
        <props>
            <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="store*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

请注意,在父 bean 的示例中,我们通过将abstract属性设置为true来明确标记父 bean 定义为抽象,如previously所述,因此它实际上可能没有实例化。默认情况下,应用程序上下文(但不是简单的 bean 工厂)会预先实例化所有单例。因此,重要的是(至少对于单例 bean),如果您有一个(父)bean 定义仅打算用作模板,并且此定义指定了一个类,则必须确保将abstract属性设置为true。否则,应用程序上下文实际上会尝试对其进行实例化。

6.6. 使用 ProxyFactory 以编程方式创建 AOP 代理

使用 Spring 以编程方式创建 AOP 代理很容易。这使您可以使用 Spring AOP,而无需依赖 Spring IoC。

由目标对象实现的接口将被自动代理。以下清单显示了使用一个拦截器和一个顾问程序为目标对象创建代理的过程:

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

第一步是构造类型为org.springframework.aop.framework.ProxyFactory的对象。您可以使用目标对象创建此对象,如前面的示例中所示,或指定要在备用构造函数中代理的接口。

您可以添加建议(使用拦截器作为一种特殊的建议),建议程序,或同时添加两者,并在ProxyFactory的生命周期内对其进行操作。如果添加IntroductionInterceptionAroundAdvisor,则可以使代理实现其他接口。

ProxyFactory(从AdvisedSupport继承)上还有便捷的方法,可让您添加其他建议类型,例如 before 并引发建议。 AdvisedSupportProxyFactoryProxyFactoryBean的超类。

Tip

在大多数应用程序中,将 AOP 代理创建与 IoC 框架集成在一起是最佳实践。通常,建议您使用 AOP 从 Java 代码外部化配置。

6.7. 操作建议对象

无论创建 AOP 代理,都可以使用org.springframework.aop.framework.Advised界面对其进行操作。任何 AOP 代理都可以强制转换为该接口,无论它实现了哪个其他接口。该界面包括以下方法:

Advisor[] getAdvisors();

void addAdvice(Advice advice) throws AopConfigException;

void addAdvice(int pos, Advice advice) throws AopConfigException;

void addAdvisor(Advisor advisor) throws AopConfigException;

void addAdvisor(int pos, Advisor advisor) throws AopConfigException;

int indexOf(Advisor advisor);

boolean removeAdvisor(Advisor advisor) throws AopConfigException;

void removeAdvisor(int index) throws AopConfigException;

boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;

boolean isFrozen();

getAdvisors()方法针对已添加到工厂的每个顾问程序,拦截器或其他建议类型返回Advisor。如果添加了Advisor,则在此索引处返回的顾问程序就是您添加的对象。如果添加了拦截器或其他建议类型,Spring 会将其包装在带有指向始终返回true的切入点的顾问程序中。因此,如果添加了MethodInterceptor,则为此索引返回的顾问程序是DefaultPointcutAdvisor,它返回MethodInterceptor以及与所有类和方法匹配的切入点。

addAdvisor()方法可用于添加任何Advisor。通常,拥有切入点和建议的顾问是通用的DefaultPointcutAdvisor,您可以将其与任何建议或切入点一起使用(但不能用于介绍)。

默认情况下,即使已创建代理,也可以添加或删除顾问程序或拦截器。唯一的限制是不可能添加或删除介绍顾问,因为工厂中的现有代理不会显示界面更改。 (您可以从工厂获取新的代理来避免此问题.)

以下示例显示了将 AOP 代理投射到Advised接口并检查和处理其建议:

Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");

// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());

// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));

assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);

Note

尽管无疑存在合法的使用案例,但是否建议(无双关语)修改 Producing 的业务对象的建议值得怀疑。但是,它在开发中(例如在测试中)非常有用。有时我们发现以拦截器或其他建议的形式添加测试代码,并进入我们要测试的方法调用中非常有用。 (例如,建议可以进入为该方法创建的事务内部,也许可以在将事务标记为回滚之前运行 SQL 以检查数据库是否已正确更新.)

根据创建代理的方式,通常可以设置frozen标志。在这种情况下,Advised isFrozen()方法返回true,而通过添加或删除来修改建议的任何尝试都将导致AopConfigException。冻结建议对象状态的功能在某些情况下很有用(例如,防止调用代码删除安全拦截器)。如果已知不需要修改运行时建议,则在 Spring 1.1 中也可以使用它来进行积极的优化。

6.8. 使用“自动代理”功能

到目前为止,我们已经考虑过使用ProxyFactoryBean或类似的工厂 bean 来显式创建 AOP 代理。

Spring 还允许我们使用“自动代理” Bean 定义,该定义可以自动代理选定的 Bean 定义。这是在 Spring 的“ bean 后处理器”基础结构上构建的,该基础结构允许在容器加载时修改任何 bean 定义。

在此模型中,您在 XML bean 定义文件中设置了一些特殊的 bean 定义,以配置自动代理基础结构。这使您可以声明有资格进行自动代理的目标。您无需使用ProxyFactoryBean

有两种方法可以做到这一点:

6.8.1. 自动代理 Bean 定义

本节介绍了org.springframework.aop.framework.autoproxy软件包提供的自动代理创建者。

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator类是BeanPostProcessor,它会自动为名称与 Literals 值或通配符匹配的 bean 创建 AOP 代理。以下示例显示了如何创建BeanNameAutoProxyCreator bean:

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="jdk*,onlyJdk"/>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>

ProxyFactoryBean一样,有interceptorNames属性而不是拦截器列表,以允许原型顾问程序具有正确的行为。命名为“拦截器”的可以是顾问或任何建议类型。

通常,与自动代理一样,使用BeanNameAutoProxyCreator的要点是将相同的配置一致地应用于多个对象,并且配置量最少。将声明式事务应用于多个对象是一种流行的选择。

名称匹配的 Bean 定义(例如上例中的jdkMyBeanonlyJdk)是带有目标类的普通旧 Bean 定义。 BeanNameAutoProxyCreator自动创建一个 AOP 代理。相同的建议适用于所有匹配的 bean。注意,如果使用了顾问程序(而不是前面示例中的拦截器),则切入点可能会不同地应用于不同的 bean。

DefaultAdvisorAutoProxyCreator

一个更通用,功能更强大的自动代理创建者是DefaultAdvisorAutoProxyCreator。这可以在当前上下文中自动应用合格的顾问程序,而无需在自动代理顾问程序的 Bean 定义中包括特定的 Bean 名称。它具有与BeanNameAutoProxyCreator相同的一致配置和避免重复的优点。

使用此机制涉及:

DefaultAdvisorAutoProxyCreator自动评估每个顾问中包含的切入点,以查看它应应用于每个业务对象的建议(如果有)(例如示例中的businessObject1businessObject2)。

这意味着可以将任意数量的顾问程序自动应用于每个业务对象。如果在任何顾问程序中没有切入点与业务对象中的任何方法匹配,则该对象不会被代理。当为新的业务对象添加 Bean 定义时,如有必要,它们会自动被代理。

通常,自动代理的优点是使调用者或依赖者无法获得不建议的对象。在此ApplicationContext上调用getBean("businessObject1")将返回 AOP 代理,而不是目标业务对象。 (前面显示的“ inner bean”惯用语也提供了这一好处.)

以下示例创建一个DefaultAdvisorAutoProxyCreator bean 和本节中讨论的其他元素:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
    <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>

<bean id="businessObject1" class="com.mycompany.BusinessObject1">
    <!-- Properties omitted -->
</bean>

<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>

如果要将相同的建议一致地应用于许多业务对象,则DefaultAdvisorAutoProxyCreator非常有用。基础结构定义到位后,您可以添加新的业务对象,而无需包括特定的代理配置。您还可以轻松地添加其他方面(例如,跟踪或性能监视方面),而对配置的更改最少。

DefaultAdvisorAutoProxyCreator支持过滤(通过使用命名约定,以便仅评估某些顾问程序,从而允许在同一工厂中使用多个不同配置的 AdvisorAutoProxyCreators)和排序。如果存在问题,顾问可以实现org.springframework.core.Ordered接口以确保正确的排序。前面示例中使用的TransactionAttributeSourceAdvisor具有可配置的 Sequences 值。默认设置为无序。

6.9. 使用 TargetSource 实现

Spring 提供了org.springframework.aop.TargetSource接口中表示的TargetSource的概念。该接口负责返回实现连接点的“目标对象”。每次 AOP 代理处理方法调用时,都会向TargetSource实现请求目标实例。

使用 Spring AOP 的开发人员通常不需要直接使用TargetSource实现,但这提供了支持池化,热插拔和其他复杂目标的强大方法。例如,池TargetSource可以通过使用池来 Management 实例,从而为每次调用返回不同的目标实例。

如果未指定TargetSource,则使用默认实现包装本地对象。每次调用都会返回相同的目标(与您期望的一样)。

本节的其余部分描述了 Spring 随附的标准目标源以及如何使用它们。

Tip

使用自定义目标源时,目标通常需要是原型而不是单例 bean 定义。这样,Spring 可以在需要时创建一个新的目标实例。

6.9.1. 可热交换的目标源

org.springframework.aop.target.HotSwappableTargetSource的存在是为了允许 AOP 代理服务器的目标切换,同时允许呼叫者保留对其的引用。

更改目标源的目标会立即生效。 HotSwappableTargetSource是线程安全的。

您可以使用 HotSwappableTargetSource 上的swap()方法更改目标,如以下示例所示:

HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);

以下示例显示了必需的 XML 定义:

<bean id="initialTarget" class="mycompany.OldTarget"/>

<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
    <constructor-arg ref="initialTarget"/>
</bean>

<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource" ref="swapper"/>
</bean>

前面的swap()调用更改了可交换 bean 的目标。拥有对该 bean 的引用的 Client 端不知道更改,但立即开始达到新目标。

尽管此示例未添加任何建议(使用TargetSource无需添加建议),但任何TargetSource均可与任意建议结合使用。

6.9.2. 汇集目标源

使用池目标源提供了与 Stateless 会话 EJB 相似的编程模型,在 Stateless 会话 EJB 中,维护了相同实例的池,方法调用将释放池中的对象。

Spring 池和 SLSB 池之间的关键区别在于,Spring 池可以应用于任何 POJO。通常,与 Spring 一样,可以以非侵入性方式应用此服务。

Spring 提供了对 Commons Pool 2.2 的支持,该池提供了相当有效的池实现。您需要在应用程序的 Classpath 上使用commons-pool Jar 才能使用此功能。您还可以将org.springframework.aop.target.AbstractPoolingTargetSource子类化以支持任何其他池化 API。

Note

还支持 Commons Pool 1.5,但从 Spring Framework 4.2 开始不推荐使用。

以下清单显示了一个示例配置:

<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
        scope="prototype">
    ... properties omitted
</bean>

<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
    <property name="maxSize" value="25"/>
</bean>

<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource" ref="poolTargetSource"/>
    <property name="interceptorNames" value="myInterceptor"/>
</bean>

请注意,目标对象(上例中为businessObjectTarget)必须是原型。这使PoolingTargetSource实现可以创建目标的新实例以根据需要扩展池。有关其属性的信息,请参见AbstractPoolingTargetSource 的 javadoc和您希望使用的具体子类。 maxSize是最基本的,并且始终保证存在。

在这种情况下,myInterceptor是需要在同一 IoC 上下文中定义的拦截器的名称。但是,您无需指定拦截器即可使用池。如果只希望池化而没有其他建议,则根本不要设置interceptorNames属性。

您可以将 Spring 配置为能够将任何池化对象投射到org.springframework.aop.target.PoolingConfig接口,该接口通过介绍来公开有关池的配置和当前大小的信息。您需要定义类似于以下内容的顾问程序:

<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="poolTargetSource"/>
    <property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>

通过在AbstractPoolingTargetSource类上调用便捷方法(因此使用MethodInvokingFactoryBean)可获得此顾问程序。该顾问的名称(此处为poolConfigAdvisor)必须位于公开池对象的ProxyFactoryBean中的拦截器名称列表中。

演员表的定义如下:

PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());

Note

通常不需要合并 Stateless 服务对象。我们不认为它应该是默认选择,因为大多数 Stateless 对象自然是线程安全的,并且如果缓存了资源,实例池会成问题。

通过使用自动代理,可以简化池。您可以设置任何自动代理创建者使用的TargetSource实现。

6.9.3. 原型目标源

设置“原型”目标源类似于设置池TargetSource。在这种情况下,每次方法调用都会创建目标的新实例。尽管在现代 JVM 中创建新对象的成本并不高,但是连接新对象(满足其 IoC 依赖关系)的成本可能会更高。因此,没有充分的理由就不应使用此方法。

为此,您可以按如下所示修改poolTargetSource定义(为清楚起见,我们也更改了名称):

<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
    <property name="targetBeanName" ref="businessObjectTarget"/>
</bean>

唯一的属性是目标 Bean 的名称。 TargetSource实现中使用继承来确保命名的一致性。与池化目标源一样,目标 Bean 必须是原型 Bean 定义。

6.9.4. ThreadLocal 目标源

如果您需要为每个传入请求(每个线程)创建一个对象,则ThreadLocal目标源很有用。 ThreadLocal的概念提供了 JDK 范围的功能,可以透明地将资源与线程一起存储。设置ThreadLocalTargetSource与其他类型的目标源所说明的几乎相同,如以下示例所示:

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
</bean>

Note

ThreadLocal实例在多线程和多类加载器环境中使用不正确时会遇到严重问题(可能导致内存泄漏)。您应该始终考虑将 threadlocal 包装在其他一些类中,并且切勿直接使用ThreadLocal本身(包装类中除外)。另外,您应始终记住正确设置和取消设置线程本地资源(在后者中仅涉及对ThreadLocal.set(null)的调用)。在任何情况下都应进行取消设置,因为不取消设置可能会导致出现问题。 Spring 的ThreadLocal支持为您做到了这一点,应该始终考虑使用ThreadLocal实例,而无需其他适当的处理代码。

6.10. 定义新的建议类型

Spring AOP 被设计为可扩展的。尽管目前在内部使用拦截实现策略,但是除了在建议周围,在建议之前,抛出建议和返回建议之后进行拦截之外,还可以支持任意建议类型。

org.springframework.aop.framework.adapter软件包是 SPI 软件包,可在不更改核心框架的情况下添加对新的自定义建议类型的支持。自定义Advice类型的唯一限制是它必须实现org.aopalliance.aop.Advice标记接口。

有关更多信息,请参见org.springframework.aop.framework.adapter javadoc。

7. Null-safety

尽管 Java 不允许您使用其类型系统来表示空安全性,但 Spring Framework 现在在org.springframework.lang包中提供了以下 注解,以使您可以声明 API 和字段的空性:

Spring Framework 自身利用了这些 注解,但是它们也可以在任何基于 Spring 的 Java 项目中使用,以声明 null 安全的 API 和可选的 null 安全的字段。尚不支持泛型类型参数,varargs 和数组元素的可空性,但应在即将发布的版本中使用它们,有关最新信息,请参见SPR-15942。可以在 Spring Framework 版本之间(包括次要版本)对可空性声明进行微调。方法主体内部使用的类型的可空性超出了此功能的范围。

Note

像 Reactor 或 Spring Data 这样的库提供使用此功能的空安全 API。

7.1. 用例

除了为 Spring Framework API 可空性提供显式声明之外,IDE(例如 IDEA 或 Eclipse)还可以使用这些注解来提供与空安全性相关的有用警告,从而避免在运行时出现NullPointerException

由于 Kotlin 本机支持null-safety,因此它们还用于使 Kotlin 项目中的 Spring API 空安全。 Kotlin 支持文档中提供了更多详细信息。

7.2. JSR 305 元 注解

Spring注解 使用JSR 305注解(休眠但分布广泛的 JSR)进行元 注解。 JSR 305 元注解使工具供应商(如 IDEA 或 Kotlin)以通用方式提供了空安全性支持,而无需对 Spring注解 进行硬编码支持。

既不需要也不建议在项目 Classpath 中添加 JSR 305 依赖项以利用 Spring 空安全 API。只有诸如在其代码库中使用空安全注解的基于 Spring 的库之类的项目才应添加具有compileOnly Gradle 配置或 Maven provided范围的com.google.code.findbugs:jsr305:3.0.2以避免编译警告。

8.数据缓冲区和编解码器

Java NIO 提供了ByteBuffer,但是许多库在顶部构建了自己的字节缓冲区 API,特别是对于网络操作,其中重用缓冲区和/或使用直接缓冲区对于性能有好处。例如,Netty 具有ByteBuf层次结构,Undertow 使用 XNIO,Jetty 使用具有要释放的回调的池字节缓冲区,依此类推。 spring-core模块提供了一组抽象,可与各种字节缓冲区 API 配合使用,如下所示:

8.1. DataBufferFactory

DataBufferFactory用于通过以下两种方式之一创建数据缓冲区:

请注意,WebFlux 应用程序不会直接创建DataBufferFactory,而是通过 Client 端的ServerHttpResponseClientHttpRequest访问它。工厂的类型取决于基础 Client 端或服务器,例如NettyDataBufferFactory用于 Reactor Netty,DefaultDataBufferFactory用于其他。

8.2. DataBuffer

DataBuffer界面提供与java.nio.ByteBuffer类似的操作,但还带来了一些其他好处,其中一些是受 Netty ByteBuf启发的。以下是部分好处清单:

8.3. PooledDataBuffer

如 Javadoc ByteBuffer中所述,字节缓冲区可以是直接的也可以是非直接的。直接缓冲区可以驻留在 Java 堆之外,从而无需复制本机 I/O 操作。这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但是创建和释放它们也更加昂贵,这导致了缓冲池的想法。

PooledDataBufferDataBuffer的扩展,有助于进行引用计数,这对于字节缓冲区池至关重要。它是如何工作的?分配PooledDataBuffer时,参考计数为 1.调用retain()会使计数递增,而调用release()会使计数递减。只要计数大于 0,就保证不会释放缓冲区。当计数减少到 0 时,可以释放池中的缓冲区,这实际上意味着将为缓冲区保留的内存返回到内存池。

请注意,与其直接在PooledDataBuffer上进行操作,不如在大多数情况下,最好使用DataBufferUtils中的便捷方法,仅当它是PooledDataBuffer的实例时,才对DataBuffer应用发布或保留。

8.4. DataBufferUtils

DataBufferUtils提供了许多 Util 方法来对数据缓冲区进行操作:

8.5. Codecs

org.springframework.core.codec软件包提供以下策略接口:

spring-core模块提供byte[]ByteBufferDataBufferResourceString编码器和解码器实现。 spring-web模块添加了 Jackson JSON,Jackson Smile,JAXB2,协议缓冲区以及其他编码器和解码器。请参阅“ WebFlux”部分中的Codecs

8.6. 使用 DataBuffer

使用数据缓冲区时,必须特别小心以确保释放缓冲区,因为它们可能是pooled。我们将使用编解码器来说明其工作原理,但这些概念会更普遍地应用。让我们看看编解码器必须在内部执行哪些操作来 Management 数据缓冲区。

Decoder是创建高级对象之前最后读取 Importing 数据缓冲区的方法,因此它必须按以下方式释放它们:

请注意,DataBufferUtils#join提供了一种安全有效的方法来将数据缓冲区流聚合到单个数据缓冲区中。同样,skipUntilByteCounttakeUntilByteCount是供解码器使用的其他安全方法。

Encoder分配其他人必须读取(和释放)的数据缓冲区。因此,Encoder没什么事要做。但是,如果在向缓冲区填充数据时发生序列化错误,则Encoder必须小心释放数据缓冲区。例如:

DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
    // serialize and populate buffer..
    release = false;
}
finally {
    if (release) {
        DataBufferUtils.release(buffer);
    }
}
return buffer;

Encoder的使用者负责释放其接收的数据缓冲区。在 WebFlux 应用程序中,Encoder的输出用于写入 HTTP 服务器响应或 Client 端 HTTP 请求,在这种情况下,释放数据缓冲区是代码写入服务器响应或 Client 端的责任。请求。

请注意,在 Netty 上运行时,解决缓冲区泄漏问题有调试选项。

9. Appendix

9.1. XML 模式

附录的此部分列出了与核心容器相关的 XML 模式。

9.1.1. 实用模式

顾名思义,util标签处理常见的 Util 配置问题,例如配置集合,引用常量等。要在util模式中使用标签,您需要在 Spring XML 配置文件的顶部具有以下序言(代码段中的文本引用了正确的模式,以便您可以使用util名称空间中的标签):

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

        <!-- bean definitions here -->
</beans>
Using <util:constant/>

考虑以下 bean 定义:

<bean id="..." class="...">
    <property name="isolation">
        <bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
                class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
    </property>
</bean>

前面的配置使用 Spring FactoryBean实现(FieldRetrievingFactoryBean)将 Bean 上isolation属性的值设置为java.sql.Connection.TRANSACTION_SERIALIZABLE常量的值。这一切都很好,但是很冗长,并且(不必要地)将 Spring 的内部管道暴露给最终用户。

以下基于 XML Schema 的版本更加简洁,清楚地表达了开发人员的意图(“注入此常量值”),并且读起来更好:

<bean id="..." class="...">
    <property name="isolation">
        <util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
    </property>
</bean>
根据字段值设置 Bean 属性或构造函数参数

FieldRetrievingFactoryBeanFactoryBean,它检索static或非静态字段值。它通常用于检索public static final常量,然后可用于为另一个 bean 设置属性值或构造函数参数。

下面的示例显示如何通过使用staticField属性来显示static字段:

<bean id="myField"
        class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
    <property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>

还有一个便利用法表格,其中将static字段指定为 bean 名称,如以下示例所示:

<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
        class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>

这的确意味着 Bean id不再是任何选择(因此,引用它的其他任何 Bean 也必须使用这个较长的名称),但是这种形式的定义非常简洁,可以很方便地用作内部对象。 bean,因为不必为 bean 引用指定id,如以下示例所示:

<bean id="..." class="...">
    <property name="isolation">
        <bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
                class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
    </property>
</bean>

您还可以访问另一个 bean 的非静态(实例)字段,如FieldRetrievingFactoryBean类的 API 文档中所述。

在 Spring 中,很容易将枚举值作为属性或构造函数参数注入到 bean 中。实际上,您不必做任何事情或不了解 Spring 内部知识(甚至不必了解诸如FieldRetrievingFactoryBean之类的类)。以下示例枚举显示了注入枚举值的难易程度:

package javax.persistence;

public enum PersistenceContextType {

    TRANSACTION,
    EXTENDED
}

现在考虑以下类型为PersistenceContextType的 setter 和相应的 bean 定义:

package example;

public class Client {

    private PersistenceContextType persistenceContextType;

    public void setPersistenceContextType(PersistenceContextType type) {
        this.persistenceContextType = type;
    }
}
<bean class="example.Client">
    <property name="persistenceContextType" value="TRANSACTION"/>
</bean>
Using <util:property-path/>

考虑以下示例:

<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<bean id="testBean.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>

前面的配置使用 Spring FactoryBean实现(PropertyPathFactoryBean)创建一个名为testBean.age的 Bean(类型int),该 Bean 的值等于testBean bean 的age属性。

现在考虑以下示例,该示例添加了一个<util:property-path/>元素:

<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>

<property-path/>元素的path属性的值遵循beanName.beanProperty的形式。在这种情况下,它将获取名为testBean的 bean 的age属性。该age属性的值为10

使用\ <>设置 Bean 属性或构造函数参数

PropertyPathFactoryBeanFactoryBean,它评估给定目标对象上的属性路径。可以直接指定目标对象,也可以通过 bean 名称指定目标对象。然后,您可以在另一个 bean 定义中将此值用作属性值或构造函数参数。

以下示例按名称显示了针对另一个 bean 的路径:

// target bean to be referenced by name
<bean id="person" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

// results in 11, which is the value of property 'spouse.age' of bean 'person'
<bean id="theAge"
        class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
    <property name="targetBeanName" value="person"/>
    <property name="propertyPath" value="spouse.age"/>
</bean>

在以下示例中,针对内部 bean 评估路径:

<!-- results in 12, which is the value of property 'age' of the inner bean -->
<bean id="theAge"
        class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
    <property name="targetObject">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="12"/>
        </bean>
    </property>
    <property name="propertyPath" value="age"/>
</bean>

还有一种快捷方式,其中 Bean 名称是属性路径。以下示例显示了快捷方式表格:

<!-- results in 10, which is the value of property 'age' of bean 'person' -->
<bean id="person.age"
        class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>

这种形式的确意味着在 Bean 名称中别无选择。对它的任何引用也必须使用相同的id,即路径。如果用作内部 bean,则根本不需要引用它,如以下示例所示:

<bean id="..." class="...">
    <property name="age">
        <bean id="person.age"
                class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
    </property>
</bean>

您可以在实际定义中专门设置结果类型。对于大多数用例来说,这不是必需的,但有时可能会有用。有关此功能的更多信息,请参见 javadoc。

Using <util:properties/>

考虑以下示例:

<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<bean id="jdbcConfiguration" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="location" value="classpath:com/foo/jdbc-production.properties"/>
</bean>

前面的配置使用 Spring FactoryBean实现(PropertiesFactoryBean)来实例化具有从提供的Resource位置加载的值的java.util.Properties实例)。

以下示例使用util:properties元素进行更简洁的表示:

<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<util:properties id="jdbcConfiguration" location="classpath:com/foo/jdbc-production.properties"/>
Using <util:list/>

考虑以下示例:

<!-- creates a java.util.List instance with values loaded from the supplied 'sourceList' -->
<bean id="emails" class="org.springframework.beans.factory.config.ListFactoryBean">
    <property name="sourceList">
        <list>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
        </list>
    </property>
</bean>

前面的配置使用 Spring FactoryBean实现(ListFactoryBean)创建java.util.List实例,并使用从提供的sourceList中获取的值对其进行初始化。

以下示例使用<util:list/>元素进行更简洁的表示:

<!-- creates a java.util.List instance with the supplied values -->
<util:list id="emails">
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
</util:list>

您还可以使用<util:list/>元素上的list-class属性来显式控制实例化和填充的List的确切类型。例如,如果我们确实需要实例化java.util.LinkedList,则可以使用以下配置:

<util:list id="emails" list-class="java.util.LinkedList">
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
    <value>d'[emailprotected]</value>
</util:list>

如果没有提供list-class属性,则容器选择List实现。

Using <util:map/>

考虑以下示例:

<!-- creates a java.util.Map instance with values loaded from the supplied 'sourceMap' -->
<bean id="emails" class="org.springframework.beans.factory.config.MapFactoryBean">
    <property name="sourceMap">
        <map>
            <entry key="pechorin" value="[emailprotected]"/>
            <entry key="raskolnikov" value="[emailprotected]"/>
            <entry key="stavrogin" value="[emailprotected]"/>
            <entry key="porfiry" value="[emailprotected]"/>
        </map>
    </property>
</bean>

前面的配置使用 Spring FactoryBean实现(MapFactoryBean)来创建java.util.Map实例,该实例使用从提供的'sourceMap'中获取的键值对进行初始化。

以下示例使用<util:map/>元素进行更简洁的表示:

<!-- creates a java.util.Map instance with the supplied key-value pairs -->
<util:map id="emails">
    <entry key="pechorin" value="[emailprotected]"/>
    <entry key="raskolnikov" value="[emailprotected]"/>
    <entry key="stavrogin" value="[emailprotected]"/>
    <entry key="porfiry" value="[emailprotected]"/>
</util:map>

您还可以使用<util:map/>元素上的'map-class'属性来显式控制实例化和填充的Map的确切类型。例如,如果我们确实需要实例化java.util.TreeMap,则可以使用以下配置:

<util:map id="emails" map-class="java.util.TreeMap">
    <entry key="pechorin" value="[emailprotected]"/>
    <entry key="raskolnikov" value="[emailprotected]"/>
    <entry key="stavrogin" value="[emailprotected]"/>
    <entry key="porfiry" value="[emailprotected]"/>
</util:map>

如果没有提供'map-class'属性,则容器选择Map实现。

Using <util:set/>

考虑以下示例:

<!-- creates a java.util.Set instance with values loaded from the supplied 'sourceSet' -->
<bean id="emails" class="org.springframework.beans.factory.config.SetFactoryBean">
    <property name="sourceSet">
        <set>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
        </set>
    </property>
</bean>

前面的配置使用 Spring FactoryBean实现(SetFactoryBean)来创建java.util.Set实例,该实例使用从提供的sourceSet中获取的值进行初始化。

以下示例使用<util:set/>元素进行更简洁的表示:

<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
</util:set>

您还可以使用<util:set/>元素上的set-class属性来显式控制实例化和填充的Set的确切类型。例如,如果我们确实需要实例化java.util.TreeSet,则可以使用以下配置:

<util:set id="emails" set-class="java.util.TreeSet">
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
    <value>[emailprotected]</value>
</util:set>

如果没有提供set-class属性,则容器选择Set实现。

9.1.2. aop 模式

aop标签处理在 Spring 中配置 AOP 的所有事情,包括 Spring 自己的基于代理的 AOP 框架以及 Spring 与 AspectJ AOP 框架的集成。这些标签在标题为Spring 面向方面的编程的章节中全面介绍。

为了完整起见,要在aop模式中使用标签,您需要在 Spring XML 配置文件的顶部具有以下前导(代码段中的文本引用了正确的模式,以便aop名称空间中的标签为提供给您):

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

    <!-- bean definitions here -->
</beans>

9.1.3. 上下文模式

context标签处理与管道相关的ApplicationContext配置,即通常不是对最终用户重要的 bean,而是在 Spring 中完成大量“艰巨”工作的 bean,例如BeanfactoryPostProcessors。以下代码段引用了正确的架构,以便您可以使用context名称空间中的元素:

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

    <!-- bean definitions here -->
</beans>
Using <property-placeholder/>

此元素激活${…}占位符的替换,这些占位符针对指定的属性文件(作为Spring 资源位置)解析。此元素是为您设置PropertyPlaceholderConfigurer的便捷机制。如果您需要对PropertyPlaceholderConfigurer的更多控制,则可以自己明确定义。

Using <annotation-config/>

此元素激活 Spring 基础结构以检测 Bean 类中的 注解:

或者,您可以选择为这些注解显式激活单独的BeanPostProcessors

Note

该元素不会激活对 Spring 的@Transactional注解 的处理。您可以为此使用<tx:annotation-driven/>元素。

Using <component-scan/>

基于注解的容器配置中对此元素进行了详细说明。

Using <load-time-weaver/>

在 Spring Framework 中使用 AspectJ 进行加载时编织中对此元素进行了详细说明。

Using <spring-configured/>

使用 AspectJ 通过 Spring 依赖注入域对象中对此元素进行了详细说明。

Using <mbean-export/>

配置基于注解的 MBean 导出中对此元素进行了详细说明。

9.1.4. Bean 模式

最后但并非最不重要的一点是,我们在beans模式中具有元素。自框架诞生之初,这些元素就一直出现在 Spring。此处未显示beans模式中各种元素的示例,因为它们在依赖关系和配置详细中(并且实际上在整个chapter中)已进行了全面介绍。

请注意,您可以将零个或多个键值对添加到<bean/> XML 定义中。使用此额外的元数据进行的操作(如果有的话)完全取决于您自己的自定义逻辑(因此,通常只有在您按照标题为XML 模式创作的附录中所述编写自己的自定义元素时才能使用)。

以下示例在周围的<bean/>上下文中显示了<meta/>元素(请注意,由于没有任何逻辑来解释它,因此元数据实际上是毫无用处的)。

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

    <bean id="foo" class="x.y.Foo">
        <meta key="cacheName" value="foo"/> (1)
        <property name="name" value="Rick"/>
    </bean>

</beans>

在前面的示例中,您可以假设存在一些逻辑,这些逻辑消耗了 bean 的定义,并构建了一些使用提供的元数据的缓存基础结构。

9.2. XML 模式创作

从 2.0 版开始,Spring 提供了一种机制,可以将基于架构的扩展添加到基本 Spring XML 格式中,以定义和配置 bean。本节介绍如何编写自己的自定义 XML Bean 定义解析器,以及如何将此类解析器集成到 Spring IoC 容器中。

为了方便使用架构感知的 XML 编辑器编写配置文件,Spring 的可扩展 XML 配置机制基于 XML Schema。如果您不熟悉标准 Spring 发行版随附的 Spring 当前的 XML 配置扩展,则应首先阅读名为[xsd-config]的附录。

要创建新的 XML 配置扩展,请执行以下操作:

对于一个统一的示例,我们创建一个 XML 扩展(一个自定义 XML 元素),该扩展使我们可以配置SimpleDateFormat类型的对象(来自java.text包)。完成后,我们将能够如下定义SimpleDateFormat类型的 bean 定义:

<myns:dateformat id="dateFormat"
    pattern="yyyy-MM-dd HH:mm"
    lenient="true"/>

(我们将在本附录后面提供更多详细的示例.第一个简单示例的目的是引导您完成制作自定义扩展程序的基本步骤.)

9.2.1. 编写架构

创建用于 Spring 的 IoC 容器的 XML 配置扩展首先要编写 XML Schema 来描述扩展。对于我们的示例,我们使用以下架构来配置SimpleDateFormat对象:

<!-- myns.xsd (inside package org/springframework/samples/xml) -->

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.com/schema/myns"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:beans="http://www.springframework.org/schema/beans"
        targetNamespace="http://www.mycompany.com/schema/myns"
        elementFormDefault="qualified"
        attributeFormDefault="unqualified">

    <xsd:import namespace="http://www.springframework.org/schema/beans"/>

    <xsd:element name="dateformat">
        <xsd:complexType>
            <xsd:complexContent>
                <xsd:extension base="beans:identifiedType"> (1)
                    <xsd:attribute name="lenient" type="xsd:boolean"/>
                    <xsd:attribute name="pattern" type="xsd:string" use="required"/>
                </xsd:extension>
            </xsd:complexContent>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>

前面的架构使我们可以使用<myns:dateformat/>元素直接在 XML 应用程序上下文文件中配置SimpleDateFormat对象,如以下示例所示:

<myns:dateformat id="dateFormat"
    pattern="yyyy-MM-dd HH:mm"
    lenient="true"/>

请注意,在创建基础结构类之后,上述 XML 片段与以下 XML 片段基本相同:

<bean id="dateFormat" class="java.text.SimpleDateFormat">
    <constructor-arg value="yyyy-HH-dd HH:mm"/>
    <property name="lenient" value="true"/>
</bean>

前面两个片段中的第二个片段在容器中创建了一个 bean(以类型SimpleDateFormat的名称dateFormat标识),并设置了两个属性。

Note

创建配置格式的基于模式的方法允许与具有模式识别 XML 编辑器的 IDE 紧密集成。通过使用正确编写的架构,可以使用自动完成功能来让用户在枚举中定义的多个配置选项之间进行选择。

9.2.2. 编码 NamespaceHandler

除了模式,我们还需要一个NamespaceHandler来解析 Spring 在解析配置文件时遇到的该特定名称空间的所有元素。对于此示例,NamespaceHandler应该负责myns:dateformat元素的解析。

NamespaceHandler界面具有三种方法:

尽管您可以为整个名称空间编写自己的NamespaceHandler(并因此提供解析名称空间中每个元素的代码),但是通常情况下,Spring XML 配置文件中的每个顶级 XML 元素都产生一个 bean 定义(例如在我们的示例中,单个<myns:dateformat/>元素导致单个SimpleDateFormat bean 定义)。 Spring 提供了许多支持这种情况的便利类。在下面的示例中,我们使用NamespaceHandlerSupport类:

package org.springframework.samples.xml;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class MyNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
    }

}

您可能会注意到,此类中实际上没有很多解析逻辑。的确,NamespaceHandlerSupport类具有内置的委托概念。它支持注册任意数量的BeanDefinitionParser实例,当需要解析其命名空间中的元素时,可以委托该实例注册。这种清晰的关注点分离使NamespaceHandler处理其命名空间中所有自定义元素的解析编排,同时委派BeanDefinitionParsers来完成 XML 解析的繁琐工作。这意味着每个BeanDefinitionParser仅包含解析单个自定义元素的逻辑,正如我们在下一步中看到的那样。

9.2.3. 使用 BeanDefinitionParser

如果NamespaceHandler遇到 Map 到特定 bean 定义解析器(在这种情况下为dateformat)的 XML 元素,则使用BeanDefinitionParser。换句话说,BeanDefinitionParser负责解析模式中定义的一个不同的顶级 XML 元素。在解析器中,我们可以访问 XML 元素(因此也可以访问其子元素),以便我们可以解析自定义 XML 内容,如以下示例所示:

package org.springframework.samples.xml;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

import java.text.SimpleDateFormat;

public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { (1)

    protected Class getBeanClass(Element element) {
        return SimpleDateFormat.class; (2)
    }

    protected void doParse(Element element, BeanDefinitionBuilder bean) {
        // this will never be null since the schema explicitly requires that a value be supplied
        String pattern = element.getAttribute("pattern");
        bean.addConstructorArg(pattern);

        // this however is an optional property
        String lenient = element.getAttribute("lenient");
        if (StringUtils.hasText(lenient)) {
            bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
        }
    }

}

在这种简单的情况下,这就是我们要做的全部。 BeanDefinition的创建由AbstractSingleBeanDefinitionParser超类处理,bean 定义的唯一标识符的提取和设置也是如此。

9.2.4. 注册处理程序和架构

编码完成。剩下要做的就是让 Spring XML 解析基础结构了解我们的自定义元素。为此,我们在两个特殊用途的属性文件中注册了自定义namespaceHandler和自定义 XSD 文件。这些属性文件都放置在应用程序的META-INF目录中,例如,可以与二进制类一起分发到 JAR 文件中。 Spring XML 解析基础结构通过使用这些特殊的属性文件来自动选择您的新扩展,以下两部分将详细介绍其格式。

Writing META-INF/spring.handlers

名为spring.handlers的属性文件包含 XML 模式 URI 到名称空间处理程序类的 Map。对于我们的示例,我们需要编写以下内容:

http\://www.mycompany.com/schema/myns=org.springframework.samples.xml.MyNamespaceHandler

(:字符是 Java 属性格式的有效分隔符,因此 URI 中的:字符需要用反斜杠转义.)

键值对的第一部分(键)是与您的自定义名称空间扩展关联的 URI,并且需要与您的自定义 XSD 架构中指定的targetNamespace属性值完全匹配。

Writing 'META-INF/spring.schemas'

名为spring.schemas的属性文件包含 XML 架构位置(与架构声明一起引用,在使用该架构作为xsi:schemaLocation属性的一部分的 XML 文件中)到 Classpath 资源。需要该文件来防止 Spring 绝对使用默认的EntityResolver,该默认EntityResolver需要 Internet 访问才能检索架构文件。如果您在此属性文件中指定 Map,Spring 将在 Classpath 上搜索架构(在本例中为org.springframework.samples.xml包中的myns.xsd)。以下代码段显示了我们需要为自定义架构添加的行:

http\://www.mycompany.com/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd

(请记住,必须对:字符进行转义.)

鼓励您在 Classpath 上的NamespaceHandlerBeanDefinitionParser类旁边部署 XSD 文件。

9.2.5. 在 Spring XML 配置中使用自定义扩展

使用您自己实现的定制扩展与使用 Spring 提供的“定制”扩展之一没有什么不同。以下示例在 Spring XML 配置文件中使用前面步骤中开发的自定义<dateformat/>元素:

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

    <!-- as a top-level bean -->
    <myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/> (1)

    <bean id="jobDetailTemplate" abstract="true">
        <property name="dateFormat">
            <!-- as an inner bean -->
            <myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
        </property>
    </bean>

</beans>

9.2.6. 更详细的例子

本节提供一些更详细的自定义 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:foo="http://www.foo.com/schema/component"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.foo.com/schema/component http://www.foo.com/schema/component/component.xsd">

    <foo:component id="bionic-family" name="Bionic-1">
        <foo:component name="Mother-1">
            <foo:component name="Karate-1"/>
            <foo:component name="Sport-1"/>
        </foo:component>
        <foo:component name="Rock-1"/>
    </foo:component>

</beans>

前面的配置将自定义扩展相互嵌套。 <foo:component/>元素实际配置的类是Component类(在下一个示例中显示)。注意Component类如何不公开components属性的 setter 方法。这使得很难(或几乎不可能)通过使用 setter 注入为Component类配置 bean 定义。以下清单显示了Component类:

package com.foo;

import java.util.ArrayList;
import java.util.List;

public class Component {

    private String name;
    private List<Component> components = new ArrayList<Component> ();

    // mmm, there is no setter method for the 'components'
    public void addComponent(Component component) {
        this.components.add(component);
    }

    public List<Component> getComponents() {
        return components;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

解决此问题的典型方法是创建一个自定义FactoryBean,该自定义FactoryBean公开components属性的 setter 属性。以下清单显示了这样的自定义FactoryBean

package com.foo;

import org.springframework.beans.factory.FactoryBean;

import java.util.List;

public class ComponentFactoryBean implements FactoryBean<Component> {

    private Component parent;
    private List<Component> children;

    public void setParent(Component parent) {
        this.parent = parent;
    }

    public void setChildren(List<Component> children) {
        this.children = children;
    }

    public Component getObject() throws Exception {
        if (this.children != null && this.children.size() > 0) {
            for (Component child : children) {
                this.parent.addComponent(child);
            }
        }
        return this.parent;
    }

    public Class<Component> getObjectType() {
        return Component.class;
    }

    public boolean isSingleton() {
        return true;
    }

}

这很好用,但是向最终用户暴露了很多 Spring 管道。我们要做的是编写一个自定义 extensions,以隐藏所有此 Spring 管道。如果我们坚持使用前面描述的步骤,那么我们首先创建 XSD 模式以定义自定义标记的结构,如以下清单所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.com/schema/component"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.foo.com/schema/component"
        elementFormDefault="qualified"
        attributeFormDefault="unqualified">

    <xsd:element name="component">
        <xsd:complexType>
            <xsd:choice minOccurs="0" maxOccurs="unbounded">
                <xsd:element ref="component"/>
            </xsd:choice>
            <xsd:attribute name="id" type="xsd:ID"/>
            <xsd:attribute name="name" use="required" type="xsd:string"/>
        </xsd:complexType>
    </xsd:element>

</xsd:schema>

前面描述的过程之后,我们再创建一个自定义NamespaceHandler

package com.foo;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
    }

}

接下来是自定义BeanDefinitionParser。请记住,我们正在创建BeanDefinition来描述ComponentFactoryBean。以下清单显示了我们的自定义BeanDefinitionParser

package com.foo;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import java.util.List;

public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser {

    protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
        return parseComponentElement(element);
    }

    private static AbstractBeanDefinition parseComponentElement(Element element) {
        BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class);
        factory.addPropertyValue("parent", parseComponent(element));

        List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
        if (childElements != null && childElements.size() > 0) {
            parseChildComponents(childElements, factory);
        }

        return factory.getBeanDefinition();
    }

    private static BeanDefinition parseComponent(Element element) {
        BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
        component.addPropertyValue("name", element.getAttribute("name"));
        return component.getBeanDefinition();
    }

    private static void parseChildComponents(List<Element> childElements, BeanDefinitionBuilder factory) {
        ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
        for (Element element : childElements) {
            children.add(parseComponentElement(element));
        }
        factory.addPropertyValue("children", children);
    }

}

最后,需要通过修改META-INF/spring.handlersMETA-INF/spring.schemas文件,将各种工件注册到 Spring XML 基础结构中,如下所示:

# in 'META-INF/spring.handlers'
http\://www.foo.com/schema/component=com.foo.ComponentNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.com/schema/component/component.xsd=com/foo/component.xsd
“常规”元素上的自定义属性

编写自己的自定义解析器和关联的工件并不难。但是,有时这不是正确的选择。考虑一个需要将元数据添加到已经存在的 bean 定义的场景。在这种情况下,您当然不需要编写自己的整个自定义扩展。相反,您只想向现有的 bean 定义元素添加一个附加属性。

作为另一个示例,假设您为访问集群JCache的服务对象(它不知道)定义了一个 bean 定义,并且您想确保在周围的集群中急切启动命名的 JCache 实例。以下清单显示了这样的定义:

<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
        jcache:cache-name="checking.account">
    <!-- other dependencies here... -->
</bean>

然后,当解析'jcache:cache-name'属性时,我们可以创建另一个BeanDefinition。然后,此BeanDefinition为我们初始化命名的 JCache。我们还可以为'checkingAccountService'修改现有的BeanDefinition,以便它依赖于此新的 JCache 初始化BeanDefinition。以下清单显示了我们的JCacheInitializer

package com.foo;

public class JCacheInitializer {

    private String name;

    public JCacheInitializer(String name) {
        this.name = name;
    }

    public void initialize() {
        // lots of JCache API calls to initialize the named cache...
    }

}

现在我们可以进入自定义扩展了。首先,我们需要编写描述自定义属性的 XSD 架构,如下所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.com/schema/jcache"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.foo.com/schema/jcache"
        elementFormDefault="qualified">

    <xsd:attribute name="cache-name" type="xsd:string"/>

</xsd:schema>

接下来,我们需要创建关联的NamespaceHandler,如下所示:

package com.foo;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class JCacheNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        super.registerBeanDefinitionDecoratorForAttribute("cache-name",
            new JCacheInitializingBeanDefinitionDecorator());
    }

}

接下来,我们需要创建解析器。请注意,在这种情况下,因为我们要解析 XML 属性,所以我们编写BeanDefinitionDecorator而不是BeanDefinitionParser。以下清单显示了我们的BeanDefinitionDecorator

package com.foo;

import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {

    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder,
            ParserContext ctx) {
        String initializerBeanName = registerJCacheInitializer(source, ctx);
        createDependencyOnJCacheInitializer(holder, initializerBeanName);
        return holder;
    }

    private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder,
            String initializerBeanName) {
        AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition());
        String[] dependsOn = definition.getDependsOn();
        if (dependsOn == null) {
            dependsOn = new String[]{initializerBeanName};
        } else {
            List dependencies = new ArrayList(Arrays.asList(dependsOn));
            dependencies.add(initializerBeanName);
            dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY);
        }
        definition.setDependsOn(dependsOn);
    }

    private String registerJCacheInitializer(Node source, ParserContext ctx) {
        String cacheName = ((Attr) source).getValue();
        String beanName = cacheName + "-initializer";
        if (!ctx.getRegistry().containsBeanDefinition(beanName)) {
            BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class);
            initializer.addConstructorArg(cacheName);
            ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition());
        }
        return beanName;
    }

}

最后,我们需要通过修改META-INF/spring.handlersMETA-INF/spring.schemas文件,在 Spring XML 基础结构中注册各种工件,如下所示:

# in 'META-INF/spring.handlers'
http\://www.foo.com/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.com/schema/jcache/jcache.xsd=com/foo/jcache.xsd
Home