参考文档的这一部分涵盖了 Spring 框架中绝对不可或缺的所有技术。

其中最重要的是 Spring Framework 的控制反转 (IoC) 容器。在对 Spring Framework 的 IoC 容器进行彻底处理之后,紧接着是对 Spring 的面向切面编程 (AOP) 技术的全面介绍。Spring 框架有它自己的 AOP 框架,它在概念上很容易理解,并且成功地解决了 Java 企业编程中 80% 的 AOP 需求甜蜜点。

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

1. IoC 容器

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

1.1。Spring IoC 容器和 Bean 简介

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

org.springframework.beansorg.springframework.context包是 Spring Framework 的 IoC 容器的基础。该 BeanFactory 接口提供了一种高级配置机制,能够管理任何类型的对象。 ApplicationContext 是 的子接口BeanFactory。它补充说:

  • 更容易与 Spring 的 AOP 功能集成

  • 消息资源处理(用于国际化)

  • 活动发布

  • 应用层特定上下文,例如WebApplicationContext 用于 Web 应用程序的上下文。

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

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

1.2. 容器概述

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

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

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

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

容器魔法
图 1. Spring IoC 容器

1.2.1。配置元数据

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

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

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

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

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

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

Spring 配置包含容器必须管理的至少一个,通常是多个 bean 定义。基于 XML 的配置元数据将这些 bean 配置为<bean/>顶级元素内的<beans/>元素。Java 配置通常@Bean在类中使用 -annotated 方法@Configuration

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

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

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

    <bean id="..." class="..."> (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>
1 id属性是标识单个 bean 定义的字符串。
2 class属性定义 bean 的类型并使用完全限定的类名。

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

1.2.2。实例化一个容器

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

java
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
科特林
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")

在了解了 Spring 的 IoC 容器之后,您可能想了解更多关于 Spring 的 Resource抽象(如参考资料中所述),它提供了一种方便的机制来从 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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- services -->

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

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

</beans>

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

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

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

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

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

</beans>

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

编写基于 XML 的配置元数据

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

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

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

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

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

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

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

命名空间本身提供了导入指令功能。Spring 提供的一系列 XML 命名空间中提供了超出普通 bean 定义的更多配置特性——例如,thecontextutil命名空间。

Groovy Bean 定义 DSL

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

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

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

1.2.3。使用容器

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

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

java
// 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();
科特林
import org.springframework.beans.factory.getBean

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

// retrieve configured instance
val service = context.getBean<PetStoreService>("petStore")

// use configured instance
var userList = service.getUsernameList()

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

java
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");
科特林
val context = GenericGroovyApplicationContext("services.groovy", "daos.groovy")

最灵活的变体是GenericApplicationContext与阅读器委托结合使用——例如,与XmlBeanDefinitionReaderXML 文件结合使用,如以下示例所示:

java
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();
科特林
val context = GenericApplicationContext()
XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml")
context.refresh()

您还可以使用GroovyBeanDefinitionReaderGroovy 文件,如以下示例所示:

java
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();
科特林
val context = GenericApplicationContext()
GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy")
context.refresh()

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

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

1.3. 豆概述

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

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

  • 一个包限定的类名:通常是被定义的 bean 的实际实现类。

  • Bean 行为配置元素,用于说明 bean 在容器中的行为方式(范围、生命周期回调等)。

  • 对 bean 完成工作所需的其他 bean 的引用。这些引用也称为协作者或依赖项。

  • 要在新创建的对象中设置的其他配置设置——例如,池的大小限制或在管理连接池的 bean 中使用的连接数。

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

表 1. bean 定义
财产 解释...​

班级

实例化 Bean

姓名

命名 Bean

范围

豆范围

构造函数参数

依赖注入

特性

依赖注入

自动装配模式

自动装配协作者

惰性初始化模式

延迟初始化的 Bean

初始化方法

初始化回调

销毁方法

销毁回调

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

需要尽早注册 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类型。请注意,id容器仍然强制执行 bean 唯一性,但不再由 XML 解析器强制执行。

您不需要为 bean 提供 aname或 an id。如果您没有显式提供 nameid,则容器会为该 bean 生成一个唯一名称。但是,如果您想通过名称引用该 bean,通过使用ref元素或服务定位器样式查找,您必须提供名称。不提供名称的动机与使用内部 bean自动装配合作者有关。

Bean 命名约定

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

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

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

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

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

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

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

例如,子系统 A 的配置元数据可能引用名为 的 DataSource subsystemA-dataSource。子系统 B 的配置元数据可能会引用名称为 的 DataSource subsystemB-dataSource。在编写同时使用这两个子系统的主应用程序时,主应用程序通过名称来引用 DataSource myApp-dataSource。要让所有三个名称都引用同一个对象,您可以将以下别名定义添加到配置元数据中:

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

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

Java 配置

如果使用 Java 配置,则@Bean可以使用注解来提供别名。有关详细信息,请参阅使用@Bean注释

1.3.2. 实例化 Bean

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

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

  • 通常,在容器本身通过反射调用其构造函数直接创建 bean 的情况下,指定要构造的 bean 类,有点相当于 Java 代码中带有new操作符的情况。

  • static指定包含被调用以创建对象 的工厂方法的实际类,在不太常见的情况下,容器调用static类上的工厂方法来创建 bean。调用static工厂方法返回的对象类型可能是同一个类,也可能完全是另一个类。

嵌套类名

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

例如,如果您SomeThingcom.example包中调用了一个类,并且SomeThing该类有一个static名为 的嵌套类OtherThing,则它们可以用美元符号 ( $) 或点 ( .) 分隔。所以classbean 定义中属性的值是com.example.SomeThing$OtherThingor com.example.SomeThing.OtherThing

使用构造函数进行实例化

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

Spring IoC 容器几乎可以管理您希望它管理的任何类。它不仅限于管理真正的 JavaBean。大多数 Spring 用户更喜欢只有默认(无参数)构造函数以及根据容器中的属性建模的适当的 setter 和 getter 的实际 JavaBeans。您还可以在容器中拥有更多奇特的非 bean 样式类。例如,如果您需要使用绝对不符合 JavaBean 规范的遗留连接池,那么 Spring 也可以管理它。

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

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

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

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

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

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

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

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

下面的例子展示了一个可以与前面的 bean 定义一起工作的类:

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

    public static ClientService createInstance() {
        return clientService;
    }
}
科特林
class ClientService private constructor() {
    companion object {
        private val clientService = ClientService()
        @JvmStatic
        fun createInstance() = 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;
    }
}
科特林
class DefaultServiceLocator {
    companion object {
        private val clientService = ClientServiceImpl()
    }
    fun createClientServiceInstance(): ClientService {
        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;
    }
}
科特林
class DefaultServiceLocator {
    companion object {
        private val clientService = ClientServiceImpl()
        private val accountService = AccountServiceImpl()
    }

    fun createClientServiceInstance(): ClientService {
        return clientService
    }

    fun createAccountServiceInstance(): AccountService {
        return accountService
    }
}

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

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

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

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

1.4. 依赖项

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

1.4.1。依赖注入

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

使用 DI 原则,代码更干净,当对象具有依赖关系时,解耦更有效。该对象不查找其依赖项,也不知道依赖项的位置或类。结果,您的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,它们允许在单元测试中使用存根或模拟实现。

基于构造函数的依赖注入

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

java
public class SimpleMovieLister {

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

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

    // business logic that actually uses the injected MovieFinder is omitted...
}
科特林
// a constructor so that the Spring container can inject a MovieFinder
class SimpleMovieLister(private val movieFinder: MovieFinder) {
    // business logic that actually uses the injected MovieFinder is omitted...
}

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

构造函数参数解析

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

java
package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}
科特林
package x.y

class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree)

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

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

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

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

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

java
package examples;

public class ExampleBean {

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

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

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}
科特林
package examples

class ExampleBean(
    private val years: Int, // Number of years to calculate the Ultimate Answer
    private val ultimateAnswer: String // The Answer to Life, the Universe, and Everything
)
构造函数参数类型匹配

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

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

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

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

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

索引从 0 开始。
构造函数参数名称

您还可以使用构造函数参数名称进行值消歧,如以下示例所示:

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

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

java
package examples;

public class ExampleBean {

    // Fields omitted

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}
科特林
package examples

class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)
基于 Setter 的依赖注入

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

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

java
public class SimpleMovieLister {

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

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

    // business logic that actually uses the injected MovieFinder is omitted...
}
科特林
class SimpleMovieLister {

    // a late-initialized property so that the Spring container can inject a MovieFinder
    lateinit var movieFinder: MovieFinder

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

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

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

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

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

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

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

依赖解决过程

容器执行 bean 依赖解析如下:

  • 使用ApplicationContext描述所有 bean 的配置元数据创建和初始化。配置元数据可以由 XML、Java 代码或注释指定。

  • 对于每个 bean,其依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示(如果您使用它而不是普通的构造函数)。这些依赖关系在实际创建 bean 时提供给 bean。

  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。

  • 作为值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,例如int, long, String,boolean等。

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

循环依赖

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

例如: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。以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您会在创建这些 bean 时发现配置问题ApplicationContext,而不是稍后。您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预先实例化。

如果不存在循环依赖关系,那么当一个或多个协作 bean 被注入到依赖 bean 中时,每个协作 bean 在被注入依赖 bean 之前完全配置。这意味着,如果 bean A 依赖于 bean B,则 Spring IoC 容器在调用 bean A 上的 setter 方法之前完全配置 bean B。换句话说,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类:

java
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;
    }
}
科特林
class ExampleBean {
    lateinit var beanOne: AnotherBean
    lateinit var beanTwo: YetAnotherBean
    var i: Int = 0
}

在前面的示例中,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类:

java
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;
    }
}
科特林
class ExampleBean(
        private val beanOne: AnotherBean,
        private val beanTwo: YetAnotherBean,
        private val i: Int)

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

java
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;
    }
}
科特林
class ExampleBean private constructor() {
    companion object {
        // 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.
        @JvmStatic
        fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean {
            val eb = ExampleBean (...)
            // some other operations...
            return eb
        }
    }
}

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

1.4.2. 详细的依赖关系和配置

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

直值(基元、字符串等)

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

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

以下示例使用p-namespace进行更简洁的 XML 配置:

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

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

</beans>

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

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

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

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

Spring 容器通过使用 JavaBeans机制将<value/>元素内的文本转换为 实例。这是一个不错的捷径,也是 Spring 团队支持使用嵌套元素而不是属性样式的少数几个地方之一。java.util.PropertiesPropertyEditor<value/>value

idref元素_

元素只是将容器中另一个 bean 的(字符串值 - 不是引用)传递给or元素的一种idref防错方法 。以下示例显示了如何使用它: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 确实存在。在第二个变体中,不对传递给beantargetName属性的值执行验证。只有在实际实例化 beanclient时才会发现拼写错误(很可能是致命的结果) 。client如果client bean 是一个原型bean,那么这个拼写错误和产生的异常可能只有在容器部署很久之后才会被发现。

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

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

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

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

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

<ref bean="someBean"/>

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

<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required 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>
4.0 bean XSD 不再支持元素上的local属性ref,因为它不再提供常规bean引用的值。在升级到 4.0 架构时 更改现有ref local引用。ref bean
内豆

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

<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 的范围。

收藏品

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

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">[email protected]</prop>
            <prop key="support">[email protected]</prop>
            <prop key="development">[email protected]</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>

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

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

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

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

以下示例演示了集合合并:

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">[email protected]</prop>
                <prop key="support">[email protected]</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">[email protected]</prop>
                <prop key="support">[email protected]</prop>
            </props>
        </property>
    </bean>
<beans>

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

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

这种合并行为同样适用于<list/><map/><set/> 集合类型。在<list/>元素的特定情况下,与List集合类型相关联的语义(即ordered 值集合的概念)得到维护。父级的值在所有子级列表的值之前。对于MapSetProperties集合类型,不存在排序。Map因此,对于容器内部使用的关联、SetProperties实现类型下的集合类型,没有任何排序语义有效。

集合合并的限制

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

强类型集合

由于 Java 对泛型类型的支持,您可以使用强类型集合。也就是说,可以声明一个Collection类型,使其只能包含(例如)String元素。如果使用 Spring 将强类型依赖注入Collection到 bean 中,则可以利用 Spring 的类型转换支持,以便强类型Collection 实例的元素在添加到Collection. 以下 Java 类和 bean 定义显示了如何执行此操作:

java
public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
科特林
class SomeClass {
    lateinit var accounts: Map<String, Float>
}
<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>

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

Null 和空字符串值

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

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

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

java
exampleBean.setEmail("");
科特林
exampleBean.email = ""

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

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

上述配置等价于以下 Java 代码:

java
exampleBean.setEmail(null);
科特林
exampleBean.email = null
带有 p 命名空间的 XML 快捷方式

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

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

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

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

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

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

该示例显示了email在 bean 定义中调用的 p-namespace 中的一个属性。这告诉 Spring 包含一个属性声明。如前所述,p-namespace 没有架构定义,因此您可以将属性的名称设置为属性名称。

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

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

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

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

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

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

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

与带有 p-namespace 的 XML Shortcut类似,Spring 3.1 中引入的 c-namespace 允许内联属性来配置构造函数参数,而不是嵌套constructor-arg元素。

以下示例使用c:命名空间执行与 基于构造函数的依赖注入相同的操作:

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

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

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

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

</beans>

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

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

<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
    c:_2="[email protected]"/>
由于 XML 语法,索引表示法需要存在前导_,因为 XML 属性名称不能以数字开头(即使某些 IDE 允许)。相应的索引符号也可用于<constructor-arg>元素,但不常用,因为声明的简单顺序通常在那里就足够了。

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

复合属性名称

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

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

somethingbean 有一个fred属性,它有一个属性bob,它有一个sammy 属性,并且最终sammy属性被设置为123. 为了使它起作用,fred属性 ofsomething和 的bob属性fred不能null在构造 bean 之后。否则,NullPointerException抛出 a。

1.4.3. 使用depends-on

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

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

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

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

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
depends-on属性既可以指定初始化时间依赖项,也可以指定对应的销毁时间依赖项(仅在单例bean 的情况下)。定义与给定 bean 的关系的从属 beandepends-on首先被销毁,然后给定 bean 本身被销毁。这样,depends-on也可以控制关​​机顺序。

1.4.4。延迟初始化的 Bean

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

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

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

当前面的配置被 an 使用ApplicationContext时,lazybean 不会在ApplicationContext启动时急切地预实例化,而not.lazybean 是急切地预实例化的。

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

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

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

1.4.5。自动装配协作者

Spring 容器可以自动装配协作 bean 之间的关系。您可以通过检查ApplicationContext. 自动装配具有以下优点:

  • 自动装配可以显着减少指定属性或构造函数参数的需要。(本章其他地方讨论的 bean 模板等其他机制 在这方面也很有价值。)

  • 随着对象的发展,自动装配可以更新配置。例如,如果您需要向类添加依赖项,则可以自动满足该依赖项,而无需修改配置。因此,自动装配在开发过程中特别有用,而不会在代码库变得更加稳定时否定切换到显式装配的选项。

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

表 2. 自动装配模式
模式 解释

no

(默认)不自动装配。Bean 引用必须由ref元素定义。对于较大的部署,不建议更改默认设置,因为明确指定协作者可以提供更大的控制权和清晰度。在某种程度上,它记录了系统的结构。

byName

按属性名称自动装配。Spring 寻找与需要自动装配的属性同名的 bean。例如,如果一个 bean 定义被设置为按名称自动装配并且它包含一个master属性(即,它有一个 setMaster(..)方法),那么 Spring 会查找一个命名的 bean 定义master并使用它来设置属性。

byType

如果容器中恰好存在一个属性类型的 bean,则让属性自动装配。如果存在多个,则会抛出一个致命异常,这表明您可能不会byType对该 bean 使用自动装配。如果没有匹配的 bean,则不会发生任何事情(未设置属性)。

constructor

类似于byType但适用于构造函数参数。如果容器中没有一个构造函数参数类型的 bean,则会引发致命错误。

使用byTypeconstructor自动装配模式,您可以连线数组和类型化集合。在这种情况下,将提供容器内与预期类型匹配的所有自动装配候选者来满足依赖关系。Map如果预期的键类型是 ,您可以自动装配强类型实例String。自动装配Map 实例的值包含与预期类型匹配的所有 bean 实例,并且 Map实例的键包含相应的 bean 名称。

自动装配的限制和缺点

自动装配在项目中一致使用时效果最佳。如果一般不使用自动装配,开发人员可能会混淆使用它只装配一个或两个 bean 定义。

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

  • property和设置中的显式依赖constructor-arg项总是覆盖自动装配。您不能自动装配简单属性,例如基元、、 StringsClasses(以及此类简单属性的数组)。此限制是设计使然。

  • 自动装配不如显式装配精确。尽管如前表中所述,Spring 会小心避免猜测可能会产生意想不到的结果的歧义。Spring 管理的对象之间的关系不再明确记录。

  • 从 Spring 容器生成文档的工具可能无法使用接线信息。

  • 容器内的多个 bean 定义可能与要自动装配的 setter 方法或构造函数参数指定的类型匹配。对于数组、集合或 Map实例,这不一定是问题。但是,对于期望单个值的依赖项,这种歧义不会被任意解决。如果没有唯一的 bean 定义可用,则会引发异常。

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

  • 放弃自动装配以支持显式装配。

  • autowire-candidate通过将其属性设置为 来避免对 bean 定义进行自动装配false,如下一节所述

  • 通过将其元素的primary属性设置为 ,将单个 bean 定义指定为主要候选者 。<bean/>true

  • 使用基于注释的配置实现更细粒度的控制,如基于注释的容器配置中所述。

从自动装配中排除 Bean

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

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

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

这些技术对于您永远不想通过自动装配注入其他 bean 的 bean 很有用。这并不意味着排除的 bean 本身不能通过使用自动装配来配置。相反,bean 本身不是自动装配其他 bean 的候选对象。

1.4.6。方法注入

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

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

java
// 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;
    }
}
科特林
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple

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

class CommandManager : ApplicationContextAware {

    private lateinit var applicationContext: ApplicationContext

    fun process(commandState: Map<*, *>): Any {
        // grab a new instance of the appropriate Command
        val command = createCommand()
        // set the state on the (hopefully brand new) Command instance
        command.state = commandState
        return command.execute()
    }

    // notice the Spring API dependency!
    protected fun createCommand() =
            applicationContext.getBean("command", Command::class.java)

    override fun setApplicationContext(applicationContext: ApplicationContext) {
        this.applicationContext = applicationContext
    }
}

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

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

查找方法注入

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

  • 要使这种动态子类化起作用,Spring bean 容器子类的类不能是final,要覆盖的方法也不能final是 。

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

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

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

在前面代码片段中的类的情况下CommandManager,Spring 容器会动态覆盖createCommand() 方法的实现。该类CommandManager没有任何 Spring 依赖项,如重新设计的示例所示:

java
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();
}
科特林
package fiona.apple

// no more Spring imports!

abstract class CommandManager {

    fun process(commandState: Any): Any {
        // grab a new instance of the appropriate Command interface
        val command = createCommand()
        // set the state on the (hopefully brand new) Command instance
        command.state = commandState
        return command.execute()
    }

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

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

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

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

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

java
public abstract class CommandManager {

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

    @Lookup("myCommand")
    protected abstract Command createCommand();
}
科特林
abstract class CommandManager {

    fun process(commandState: Any): Any {
        val command = createCommand()
        command.state = commandState
        return command.execute()
    }

    @Lookup("myCommand")
    protected abstract fun createCommand(): Command
}

或者,更惯用的说法是,您可以依赖于根据查找方法声明的返回类型解析目标 bean:

java
public abstract class CommandManager {

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

    @Lookup
    protected abstract Command createCommand();
}
科特林
abstract class CommandManager {

    fun process(commandState: Any): Any {
        val command = createCommand()
        command.state = commandState
        return command.execute()
    }

    @Lookup
    protected abstract fun createCommand(): Command
}

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

访问不同范围的目标 bean 的另一种方法是ObjectFactory/ Provider注入点。请参阅Scoped Beans as Dependencies

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

任意方法替换

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

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

java
public class MyValueCalculator {

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

    // some other methods...
}
科特林
class MyValueCalculator {

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

    // some other methods...
}

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

java
/**
 * 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 ...;
    }
}
科特林
/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
class ReplacementComputeValue : MethodReplacer {

    override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
        // get the input value, work with it, and return a computed result
        val input = args[0] as String;
        ...
        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"/>

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

java.lang.String
String
Str

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

1.5。豆范围

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

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

下表描述了支持的范围:

表 3. Bean 作用域
范围 描述

单身人士

(默认)将单个 bean 定义限定为每个 Spring IoC 容器的单个对象实例。

原型

将单个 bean 定义限定为任意数量的对象实例。

要求

将单个 bean 定义限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例,该实例是在单个 bean 定义的后面创建的。仅在 Web 感知 Spring 的上下文中有效ApplicationContext

会议

将单个 bean 定义限定为 HTTP 的生命周期Session。仅在 Web 感知 Spring 的上下文中有效ApplicationContext

应用

将单个 bean 定义限定为ServletContext. 仅在 Web 感知 Spring 的上下文中有效ApplicationContext

网络套接字

将单个 bean 定义限定为WebSocket. 仅在 Web 感知 Spring 的上下文中有效ApplicationContext

从 Spring 3.0 开始,线程范围可用,但默认情况下未注册。有关详细信息,请参阅 SimpleThreadScope. 有关如何注册此或任何其他自定义范围的说明,请参阅 使用自定义范围

1.5.1。单例范围

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

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

单身人士

Spring 的单例 bean 概念不同于四人组 (GoF) 模式书中定义的单例模式。GoF 单例对对象的范围进行硬编码,以便每个 ClassLoader 创建一个且仅一个特定类的实例。Spring 单例的范围最好描述为每个容器和每个 bean。这意味着,如果您在单个 Spring 容器中为特定类定义一个 bean,则 Spring 容器会创建该 bean 定义定义的类的一个且仅一个实例。单例范围是 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 使用原型范围,对无状态 bean 使用单例范围。

下图说明了 Spring 原型范围:

原型

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

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

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

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

在某些方面,Spring 容器在原型范围 bean 方面的角色是 Javanew运算符的替代品。此后的所有生命周期管理都必须由客户处理。(有关 Spring 容器中 bean 的生命周期的详细信息,请参阅Lifecycle Callbacks。)

1.5.3. 具有原型 bean 依赖关系的单例 bean

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

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

1.5.4。请求、会话、应用程序和 WebSocket 范围

requestsession和范围仅在您使用可感知 Web 的 Spring实现(例如 application)时才可用。如果您将这些范围与常规 Spring IoC 容器(例如 )一起使用,则会抛出一个抱怨未知 bean 范围的问题。websocketApplicationContextXmlWebApplicationContextClassPathXmlApplicationContextIllegalStateException

初始 Web 配置

为了支持requestsessionapplicationwebsocket级别的 bean 范围(网络范围的 bean),在定义 bean 之前需要进行一些小的初始配置。(标准示波器不需要此初始设置:singletonprototype。)

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

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

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

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

或者,如果您的侦听器设置有问题,请考虑使用 Spring 的 RequestContextFilter. 过滤器映射取决于周围的 Web 应用程序配置,因此您必须根据需要进行更改。以下清单显示了 Web 应用程序的过滤器部分:

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

DispatcherServlet, RequestContextListener, 并且RequestContextFilter都做完全相同的事情,即将 HTTP 请求对象绑定到为该Thread请求提供服务的对象。这使得请求和会话范围的 bean 在调用链的下游可用。

请求范围

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

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

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

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

java
@RequestScope
@Component
public class LoginAction {
    // ...
}
科特林
@RequestScope
@Component
class LoginAction {
    // ...
}
会话范围

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

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

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

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

java
@SessionScope
@Component
public class UserPreferences {
    // ...
}
科特林
@SessionScope
@Component
class UserPreferences {
    // ...
}
适用范围

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

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

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

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

java
@ApplicationScope
@Component
public class AppPreferences {
    // ...
}
科特林
@ApplicationScope
@Component
class AppPreferences {
    // ...
}
WebSocket 范围

WebSocket 范围与 WebSocket 会话的生命周期相关联,适用于 STOMP over WebSocket 应用程序,请参阅 WebSocket 范围了解更多详细信息。

作用域 Bean 作为依赖项

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

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

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

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

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

调用它的 JSR-330 变体,Provider并与Provider<MyTargetBean> 声明和get()每次检索尝试的相应调用一起使用。有关整体 JSR-330 的更多详细信息,请参见此处

以下示例中的配置只有一行,但重要的是要了解其背后的“为什么”以及“如何”:

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

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- instructs the container to proxy the surrounding bean -->
        <aop:scoped-proxy/> (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>
1 定义代理的行。

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

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

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

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

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

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

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

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

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

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

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

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

有关选择基于类或基于接口的代理的更多详细信息,请参阅代理机制

1.5.5。自定义范围

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

创建自定义范围

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

Scope接口有四种方法可以从作用域中获取对象,将它们从作用域中移除,并让它们被销毁。

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

java
Object get(String name, ObjectFactory<?> objectFactory)
科特林
fun get(name: String, objectFactory: ObjectFactory<*>): Any

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

java
Object remove(String name)
科特林
fun remove(name: String): Any

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

java
void registerDestructionCallback(String name, Runnable destructionCallback)
科特林
fun registerDestructionCallback(name: String, destructionCallback: Runnable)

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

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

java
String getConversationId()
科特林
fun getConversationId(): String

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

使用自定义范围

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

java
void registerScope(String scopeName, Scope scope);
科特林
fun registerScope(scopeName: String, scope: Scope)

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

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

假设您编写了自定义Scope实现,然后按照下一个示例所示进行注册。

下一个示例使用SimpleThreadScope,它包含在 Spring 中,但默认情况下未注册。对于您自己的自定义Scope 实现,说明将是相同的。
java
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
科特林
val threadScope = SimpleThreadScope()
beanFactory.registerScope("thread", threadScope)

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

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

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

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

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

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

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

</beans>
当您放置<aop:scoped-proxy/>在实现的<bean>声明中 FactoryBean时,作用域是工厂 bean 本身,而不是从getObject().

1.6. 自定义 Bean 的性质

Spring Framework 提供了许多接口,您可以使用它们来自定义 bean 的性质。本节将它们分组如下:

1.6.1. 生命周期回调

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

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

如果您不想使用 JSR-250 注释但仍想移除耦合,请考虑init-methoddestroy-methodbean 定义元数据。

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

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

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

初始化回调

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

void afterPropertiesSet() throws Exception;

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

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

    public void init() {
        // do some initialization work
    }
}
科特林
class ExampleBean {

    fun init() {
        // do some initialization work
    }
}

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

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

    @Override
    public void afterPropertiesSet() {
        // do some initialization work
    }
}
科特林
class AnotherExampleBean : InitializingBean {

    override fun afterPropertiesSet() {
        // do some initialization work
    }
}

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

销毁回调

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

void destroy() throws Exception;

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

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

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}
科特林
class ExampleBean {

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

前面的定义与下面的定义几乎完全相同:

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

    @Override
    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}
科特林
class AnotherExampleBean : DisposableBean {

    override fun destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}

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

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

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

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

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

java
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.");
        }
    }
}
科特林
class DefaultBlogService : BlogService {

    private var blogDao: BlogDao? = null

    // this is (unsurprisingly) the initialization callback method
    fun init() {
        if (blogDao == null) {
            throw 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>

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

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

如果现有的 bean 类已经具有命名与约定不一致的回调方法,您可以通过使用 自身的init-methoddestroy-method属性指定(在 XML 中)方法名称来覆盖默认值。<bean/>

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

结合生命周期机制

从 Spring 2.5 开始,您可以通过三个选项来控制 bean 生命周期行为:

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

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

  1. 用注释的方法@PostConstruct

  2. afterPropertiesSet()InitializingBean回调接口定义

  3. 自定义配置init()方法

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

  1. 用注释的方法@PreDestroy

  2. destroy()DisposableBean回调接口定义

  3. 自定义配置destroy()方法

启动和关闭回调

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

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

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

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

请注意,LifecycleProcessor它本身就是Lifecycle 接口的扩展。它还添加了另外两种方法来对正在刷新和关闭的上下文做出反应。

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

另外,请注意,停止通知不能保证在销毁之前发出。在定期关闭时,所有Lifecyclebean 首先会在传播一般销毁回调之前收到停止通知。但是,在上下文的生命周期内进行热刷新或停止刷新尝试时,只会调用销毁方法。

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

public interface Phased {

    int getPhase();
}

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

public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);
}

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

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

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

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

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

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

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

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

java
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...
    }
}
科特林
import org.springframework.context.support.ClassPathXmlApplicationContext

fun main() {
    val ctx = 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. ApplicationContextAwareBeanNameAware

当 anApplicationContext创建一个实现 org.springframework.context.ApplicationContextAware接口的对象实例时,会为该实例提供对该 的引用ApplicationContext。以下清单显示了ApplicationContextAware接口的定义:

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

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

自动装配是获得对 ApplicationContext. 传统 constructor模式和自动装配byType模式(如Autowiring CollaboratorsApplicationContext中所述)可以分别为构造函数参数或 setter 方法参数提供类型依赖 。要获得更大的灵活性,包括自动装配字段和多个参数方法的能力,请使用基于注释的自动装配功能。如果您这样做,如果相关字段、构造函数或方法带有注释,则ApplicationContext将自动装配到期望类型的字段、构造函数参数或方法参数中。有关详细信息,请参阅 使用.ApplicationContext@Autowired@Autowired

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

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

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

1.6.3. 其他Aware接口

除了ApplicationContextAwareBeanNameAware(前面讨论)之外,Spring 提供了广泛的Aware回调接口,让 bean 向容器指示它们需要特定的基础设施依赖项。作为一般规则,名称表示依赖类型。下表总结了最重要的Aware接口:

表 4. 感知接口
姓名 注入依赖 解释...​

ApplicationContextAware

声明ApplicationContext.

ApplicationContextAwareBeanNameAware

ApplicationEventPublisherAware

封闭的事件发布者ApplicationContext

的附加功能ApplicationContext

BeanClassLoaderAware

类加载器用于加载 bean 类。

实例化 Bean

BeanFactoryAware

声明BeanFactory.

BeanFactoryAPI _

BeanNameAware

声明 bean 的名称。

ApplicationContextAwareBeanNameAware

LoadTimeWeaverAware

定义的编织器,用于在加载时处理类定义。

在 Spring 框架中使用 AspectJ 进行加载时编织

MessageSourceAware

用于解析消息的配置策略(支持参数化和国际化)。

的附加功能ApplicationContext

NotificationPublisherAware

Spring JMX 通知发布者。

通知

ResourceLoaderAware

为对资源进行低级访问而配置的加载程序。

资源

ServletConfigAware

当前ServletConfig容器在其中运行。仅在可感知网络的 Spring 中有效 ApplicationContext

SpringMVC

ServletContextAware

当前ServletContext容器在其中运行。仅在可感知网络的 Spring 中有效 ApplicationContext

SpringMVC

再次注意,使用这些接口将您的代码绑定到 Spring API,并且不遵循 Inversion of Control 样式。因此,我们建议将它们用于需要以编程方式访问容器的基础设施 bean。

1.7. Bean定义继承

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

如果您以ApplicationContext编程方式使用接口,则子 bean 定义由ChildBeanDefinition类表示。大多数用户不在此级别上与他们合作。相反,他们在诸如ClassPathXmlApplicationContext. 当您使用基于 XML 的配置元数据时,您可以通过使用属性来指示子 bean 定义parent,将父 bean 指定为该属性的值。以下示例显示了如何执行此操作:

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

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

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

子 bean 定义从父 bean 继承范围、构造函数参数值、属性值和方法覆盖,并可选择添加新值。您指定的任何范围、初始化方法、销毁方法或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 属性或getBean()使用父 bean ID 进行显式调用会返回错误。同样,容器的内部 preInstantiateSingletons()方法会忽略定义为抽象的 bean 定义。

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

1.8. 容器扩展点

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

1.8.1。通过使用自定义 BeanBeanPostProcessor

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

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

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

BeanPostProcessor实例的范围是每个容器。这仅在您使用容器层次结构时才相关。如果您BeanPostProcessor在一个容器中定义 a,它只会对该容器中的 bean 进行后处理。换句话说,在一个容器中定义的 bean 不会由BeanPostProcessor另一个容器中定义的 bean 进行后处理,即使两个容器都是同一层次结构的一部分。

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

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

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

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

以编程方式注册BeanPostProcessor实例
虽然推荐的BeanPostProcessor注册方法是通过 ApplicationContext自动检测(如前所述),但您可以ConfigurableBeanFactory使用该addBeanPostProcessor 方法以编程方式注册它们。当您需要在注册之前评估条件逻辑,甚至在层次结构中跨上下文复制 bean 后处理器时,这可能很有用。但是请注意,以BeanPostProcessor编程方式添加的实例不尊重Ordered接口。在这里,注册的顺序决定了执行的顺序。另请注意,以BeanPostProcessor编程方式注册的实例始终在通过自动检测注册的实例之前处理,无论任何显式排序如何。
BeanPostProcessor实例和 AOP 自动代理

实现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).

如果您BeanPostProcessor使用自动装配或 @Resource(可能回退到自动装配)将 bean 连接到您的 bean,则 Spring 在搜索类型匹配依赖项候选时可能会访问意外的 bean,因此,使它们没有资格进行自动代理或其他类型的 bean 发布-加工。例如,如果您有一个依赖项,@Resource其中字段或 setter 名称不直接对应于 bean 的声明名称并且没有使用 name 属性,则 Spring 会访问其他 bean 以按类型匹配它们。

以下示例展示了如何BeanPostProcessorApplicationContext.

示例:Hello World, BeanPostProcessor-style

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

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

java
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;
    }
}
科特林
import org.springframework.beans.factory.config.BeanPostProcessor

class InstantiationTracingBeanPostProcessor : BeanPostProcessor {

    // simply return the instantiated bean as-is
    override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
        return bean // we could potentially return any object reference here...
    }

    override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
        println("Bean '$beanName' created : $bean")
        return bean
    }
}

以下beans元素使用InstantiationTracingBeanPostProcessor:

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

    <lang:groovy id="messenger"
            script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
        <lang:property name="message" value="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 应用程序运行上述代码和配置:

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

public final class Boot {

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

}
科特林
import org.springframework.beans.factory.getBean

fun main() {
    val ctx = ClassPathXmlApplicationContext("scripting/beans.xml")
    val messenger = ctx.getBean<Messenger>("messenger")
    println(messenger)
}

上述应用程序的输出类似于以下内容:

Bean 'messenger' 创建:[电子邮件保护] 
[电子邮件保护]
示例:AutowiredAnnotationBeanPostProcessor

将回调接口或注释与自定义BeanPostProcessor 实现结合使用是扩展 Spring IoC 容器的常用方法。一个例子是 Spring 的AutowiredAnnotationBeanPostProcessor ——一个BeanPostProcessor随 Spring 发行版一起提供的实现,并自动连接带注释的字段、setter 方法和任意配置方法。

1.8.2. 自定义配置元数据BeanFactoryPostProcessor

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

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

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

此外,BeanFactoryPostProcessor实例的范围是每个容器。这仅在您使用容器层次结构时才相关。如果您BeanFactoryPostProcessor在一个容器中定义 a,它仅适用于该容器中的 bean 定义。一个容器中的 Bean 定义不会由BeanFactoryPostProcessor另一个容器中的实例进行后处理,即使两个容器都属于同一层次结构。

bean 工厂后处理器在 中声明时会自动运行 ApplicationContext,以便将更改应用于定义容器的配置元数据。Spring 包括许多预定义的 bean factory 后处理器,例如PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer. 您还可以使用自定义BeanFactoryPostProcessor - 例如,注册自定义属性编辑器。

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

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

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

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

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

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

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

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

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

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

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

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

PropertySourcesPlaceholderConfigurer不仅在您指定的文件中查找属性Properties 。默认情况下,如果在指定的属性文件中找不到属性,它会检查 SpringEnvironment属性和常规 JavaSystem属性。

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

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

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

如果该类在运行时无法解析为有效类,则该 bean 在即将创建时解析失败,这是在非惰性初始化 bean的preInstantiateSingletons() 阶段。ApplicationContext

示例:PropertyOverrideConfigurer

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

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

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

beanName.property=值

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

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

这个示例文件可以与一个容器定义一起使用,该容器定义包含一个名为的 bean ,该 beandataSource具有driverurl属性。

还支持复合属性名称,只要路径的每个组件(除了要覆盖的最终属性)都已经非空(可能由构造函数初始化)。在以下示例中,将 bean 的属性的sammy属性设置为标量值:bobfredtom123

汤姆.弗雷德.鲍勃.萨米=123
指定的覆盖值始终是文字值。它们不会被翻译成 bean 引用。当 XML bean 定义中的原始值指定 bean 引用时,该约定也适用。

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

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

1.8.3. 自定义实例化逻辑FactoryBean

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

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

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

  • T getObject():返回此工厂创建的对象的实例。该实例可能会被共享,具体取决于该工厂返回的是单例还是原型。

  • boolean isSingleton()true如果FactoryBean返回单例或 false其他,则返回。此方法的默认实现返回true.

  • Class<?> getObjectType():返回getObject()方法返回的对象类型,或者null如果事先不知道类型。

FactoryBeanSpring 框架中的许多地方都使用了概念和接口。FactoryBeanSpring 本身提供了超过 50 个接口的实现。

当您需要向容器询问实际FactoryBean实例本身而不是它生成的 bean时id,请在&调用. 因此,对于 带有of的给定,在容器上调用会返回 的乘积,而调用会返回 实例本身。getBean()ApplicationContextFactoryBeanidmyBeangetBean("myBean")FactoryBeangetBean("&myBean")FactoryBean

1.9。基于注解的容器配置

在配置 Spring 时,注解是否比 XML 更好?

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

无论选择如何,Spring 都可以同时适应这两种风格,甚至可以将它们混合在一起。值得指出的是,通过其JavaConfig选项,Spring 允许以非侵入性的方式使用注释,而无需触及目标组件的源代码,并且在工具方面, Spring Tools for Eclipse支持所有配置样式。

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

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

    <context:annotation-config/>

</beans>

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

<context:annotation-config/>仅在定义它的同一应用程序上下文中查找 bean 上的注释。这意味着,如果您 <context:annotation-config/>输入 a WebApplicationContextfor a DispatcherServlet,它只会检查@Autowired您的控制器中的 bean,而不是您的服务。有关详细信息,请参阅 DispatcherServlet

1.9.1。@必需的

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

java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}
科特林
class SimpleMovieLister {

    lateinit var movieFinder: MovieFinder
        @Required
        set

    // ...
}

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

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

从Spring Framework 5.1 开始正式弃用@Required注解RequiredAnnotationBeanPostProcessor,支持使用构造函数注入进行所需设置(或自定义实现InitializingBean.afterPropertiesSet() 或自定义@PostConstruct方法以及 bean 属性设置器方法)。

1.9.2。使用@Autowired

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

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

java
public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

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

    // ...
}
科特林
class MovieRecommender @Autowired constructor(
    private val customerPreferenceDao: CustomerPreferenceDao)

从 Spring Framework 4.3@Autowired开始,如果目标 bean 只定义了一个构造函数,则不再需要对此类构造函数进行注释。但是,如果有多个构造函数可用并且没有主/默认构造函数,则必须至少对其中一个构造函数进行注释,@Autowired以指示容器使用哪一个。有关详细信息,请参阅构造函数解析的讨论 。

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

java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}
科特林
class SimpleMovieLister {

    @Autowired
    lateinit var movieFinder: MovieFinder

    // ...

}

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

java
public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

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

    // ...
}
科特林
class MovieRecommender {

    private lateinit var movieCatalog: MovieCatalog

    private lateinit var customerPreferenceDao: CustomerPreferenceDao

    @Autowired
    fun prepare(movieCatalog: MovieCatalog,
                customerPreferenceDao: CustomerPreferenceDao) {
        this.movieCatalog = movieCatalog
        this.customerPreferenceDao = customerPreferenceDao
    }

    // ...
}

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

java
public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

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

    // ...
}
科特林
class MovieRecommender @Autowired constructor(
    private val customerPreferenceDao: CustomerPreferenceDao) {

    @Autowired
    private lateinit var movieCatalog: MovieCatalog

    // ...
}

确保您的目标组件(例如,MovieCatalogCustomerPreferenceDao)始终由您用于带@Autowired注释的注入点的类型声明。否则,注入可能会由于运行时出现“找不到类型匹配”错误而失败。

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

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

java
public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}
科特林
class MovieRecommender {

    @Autowired
    private lateinit var movieCatalogs: Array<MovieCatalog>

    // ...
}

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

java
public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;

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

    // ...
}
科特林
class MovieRecommender {

    @Autowired
    lateinit var movieCatalogs: Set<MovieCatalog>

    // ...
}

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

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

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

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

java
public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

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

    // ...
}
科特林
class MovieRecommender {

    @Autowired
    lateinit var movieCatalogs: Map<String, MovieCatalog>

    // ...
}

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

默认行为是将带注释的方法和字段视为指示所需的依赖项。您可以按照以下示例所示更改此行为,使框架能够通过将其标记为非必需(即,通过将required属性设置@Autowiredfalse)来跳过不可满足的注入点:

java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}
科特林
class SimpleMovieLister {

    @Autowired(required = false)
    var movieFinder: MovieFinder? = null

    // ...
}

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

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

任何给定 bean 类@Autowired构造函数只能声明其required 属性设置为true,指示构造函数在用作 Spring bean 时自动装配。因此,如果将属性保留为默认值,则只能使用 注释单个构造函数。如果多个构造函数声明了注解,它们都必须声明才能被视为自动装配的候选者(类似于requiredtrue@Autowiredrequired=falseautowire=constructor在 XML 中)。将选择在 Spring 容器中通过匹配 bean 可以满足的依赖项数量最多的构造函数。如果不能满足任何候选者,则将使用主/默认构造函数(如果存在)。类似地,如果一个类声明了多个构造函数,但没有一个用 注释@Autowired,那么将使用主/默认构造函数(如果存在)。如果一个类只声明一个构造函数开始,它总是会被使用,即使没有注释。请注意,带注释的构造函数不必是公共的。

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

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

public class SimpleMovieLister {

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

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

java
public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        ...
    }
}
科特林
class SimpleMovieLister {

    @Autowired
    var movieFinder: MovieFinder? = null

    // ...
}

您还可以@Autowired用于众所周知的可解析依赖项的接口:BeanFactoryApplicationContextEnvironmentResourceLoaderApplicationEventPublisherMessageSource. 这些接口及其扩展接口,例如ConfigurableApplicationContextor ResourcePatternResolver,会自动解析,无需特殊设置。以下示例自动装配一个ApplicationContext对象:

java
public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...
}
科特林
class MovieRecommender {

    @Autowired
    lateinit var context: ApplicationContext

    // ...
}

@Autowired@Inject和注释由 Spring 实现处理@Value。这意味着您不能在您自己的或类型(如果有)中应用这些注释。这些类型必须使用 XML 或 Spring方法明确地“连接起来”。@ResourceBeanPostProcessorBeanPostProcessorBeanFactoryPostProcessor@Bean

1.9.3。微调基于注释的自动装配@Primary

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

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

java
@Configuration
public class MovieConfiguration {

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

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

    // ...
}
科特林
@Configuration
class MovieConfiguration {

    @Bean
    @Primary
    fun firstMovieCatalog(): MovieCatalog { ... }

    @Bean
    fun secondMovieCatalog(): MovieCatalog { ... }

    // ...
}

使用上述配置,以下MovieRecommender内容与 自动装配 firstMovieCatalog

java
public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...
}
科特林
class MovieRecommender {

    @Autowired
    private lateinit var 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog" primary="true">
        <!-- 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。在最简单的情况下,这可以是一个简单的描述性值,如以下示例所示:

java
public class MovieRecommender {

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

    // ...
}
科特林
class MovieRecommender {

    @Autowired
    @Qualifier("main")
    private lateinit var movieCatalog: MovieCatalog

    // ...
}

您还可以@Qualifier在单​​个构造函数参数或方法参数上指定注释,如下例所示:

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

    // ...
}
科特林
class MovieRecommender {

    private lateinit var movieCatalog: MovieCatalog

    private lateinit var customerPreferenceDao: CustomerPreferenceDao

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier 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>
1 具有限定符值的 bean 与main具有相同值限定的构造函数参数连接。
2 具有限定符值的 bean 与action具有相同值限定的构造函数参数连接。

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

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

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

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

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

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

尝试从@Bean同一配置类上的方法注入结果实际上也是一种自引用场景。要么在实际需要的方法签名中延迟解析此类引用(与配置类中的自动装配字段相反),要么将受影响的@Bean方法声明为static,将它们与包含的配置类实例及其生命周期解耦。否则,仅在回退阶段考虑此类 bean,而将其他配置类上的匹配 bean 选为主要候选者(如果可用)。

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

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

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

    String value();
}
科特林
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Genre(val value: String)

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

java
public class MovieRecommender {

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

    private MovieCatalog comedyCatalog;

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

    // ...
}
科特林
class MovieRecommender {

    @Autowired
    @Genre("Action")
    private lateinit var actionCatalog: MovieCatalog

    private lateinit var comedyCatalog: MovieCatalog

    @Autowired
    fun setComedyCatalog(@Genre("Comedy") comedyCatalog: MovieCatalog) {
        this.comedyCatalog = comedyCatalog
    }

    // ...
}

接下来,您可以提供候选 bean 定义的信息。您可以添加 <qualifier/>标签作为标签的子元素,<bean/>然后指定typeand value以匹配您的自定义限定符注释。类型与注解的完全限定类名匹配。或者,如果不存在名称冲突的风险,为方便起见,您可以使用短类名。以下示例演示了这两种方法:

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="Genre" value="Action"/>
        <!-- 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 Scanning and Managed Components中,您可以看到基于注释的替代方法,以在 XML 中提供限定符元数据。具体来说,请参阅提供带有注释的限定符元数据

在某些情况下,使用没有值的注释可能就足够了。当注释服务于更通用的目的并且可以应用于多种不同类型的依赖项时,这可能很有用。例如,您可以提供一个离线目录,当没有可用的 Internet 连接时可以搜索该目录。首先,定义简单的注解,如下例所示:

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

}
科特林
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Offline

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

java
public class MovieRecommender {

    @Autowired
    @Offline (1)
    private MovieCatalog offlineCatalog;

    // ...
}
1 此行添加@Offline注释。
科特林
class MovieRecommender {

    @Autowired
    @Offline (1)
    private lateinit var offlineCatalog: MovieCatalog

    // ...
}
1 此行添加@Offline注释。

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

<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline"/> (1)
    <!-- inject any dependencies required by this bean -->
</bean>
1 此元素指定限定符。

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

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

    String genre();

    Format format();
}
科特林
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MovieQualifier(val genre: String, val format: Format)

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

java
public enum Format {
    VHS, DVD, BLURAY
}
科特林
enum class Format {
    VHS, DVD, BLURAY
}

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

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

    // ...
}
科特林
class MovieRecommender {

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

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

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

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

    // ...
}

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Action"/>
        </qualifier>
        <!-- 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 泛型类型作为限定的隐式形式。例如,假设您有以下配置:

java
@Configuration
public class MyConfiguration {

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

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}
科特林
@Configuration
class MyConfiguration {

    @Bean
    fun stringStore() = StringStore()

    @Bean
    fun integerStore() = IntegerStore()
}

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

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

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean
科特林
@Autowired
private lateinit var s1: Store<String> // <String> qualifier, injects the stringStore bean

@Autowired
private lateinit var s2: Store<Integer> // <Integer> qualifier, injects the integerStore bean

通用限定符也适用于自动装配列表、Map实例和数组。以下示例自动装配一个泛型List

java
// 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;
科特林
// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private lateinit var s: List<Store<Integer>>

1.9.6。使用CustomAutowireConfigurer

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

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

通过以下AutowireCandidateResolver方式确定自动接线候选者:

  • autowire-candidate每个bean定义的值

  • 元素default-autowire-candidates上可用的任何模式<beans/>

  • 注释的存在@Qualifier和任何注册的自定义注释CustomAutowireConfigurer

当多个 bean 有资格作为自动装配候选者时,“主要”的确定如下:如果候选者中恰好一个 bean 定义的primary 属性设置为true,则选择它。

1.9.7。注射用@Resource

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

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

java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") (1)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}
1 此行注入一个@Resource.
科特林
class SimpleMovieLister {

    @Resource(name="myMovieFinder") (1)
    private lateinit var movieFinder:MovieFinder
}
1 此行注入一个@Resource.

如果没有明确指定名称,则默认名称来自字段名称或 setter 方法。如果是字段,则采用字段名称。对于 setter 方法,它采用 bean 属性名称。以下示例将把名为 bean 的 beanmovieFinder注入到它的 setter 方法中:

java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}
科特林
class SimpleMovieLister {

    @Resource
    private lateinit var movieFinder: MovieFinder

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

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

因此,在以下示例中,该customerPreferenceDao字段首先查找名为“customerPreferenceDao”的 bean,然后回退到 type 的主要类型匹配 CustomerPreferenceDao

java
public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; (1)

    public MovieRecommender() {
    }

    // ...
}
1 context字段是根据已知的可解析依赖类型注入的: ApplicationContext.
科特林
class MovieRecommender {

    @Resource
    private lateinit var customerPreferenceDao: CustomerPreferenceDao


    @Resource
    private lateinit var context: ApplicationContext (1)

    // ...
}
1 context字段是根据已知的可解析依赖类型注入的: ApplicationContext.

1.9.8。使用@Value

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

java
@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name}") String catalog) {
        this.catalog = catalog;
    }
}
科特林
@Component
class MovieRecommender(@Value("\${catalog.name}") private val catalog: String)

使用以下配置:

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

以及以下application.properties文件:

catalog.name=MovieCatalog

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

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

java
@Configuration
public class AppConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun propertyPlaceholderConfigurer() = PropertySourcesPlaceholderConfigurer()
}
配置PropertySourcesPlaceholderConfigurer使用 JavaConfig 时, @Bean方法必须是static.

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

Spring Boot 默认配置一个PropertySourcesPlaceholderConfigurerbean,该 bean 将从application.propertiesapplication.yml文件中获取属性。

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

可以提供如下默认值:

java
@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
        this.catalog = catalog;
    }
}
科特林
@Component
class MovieRecommender(@Value("\${catalog.name:defaultCatalog}") private val catalog: String)

Spring在后台BeanPostProcessor使用 a来处理将值转换为目标类型的过程。如果您想为您自己的自定义类型提供转换支持,您可以提供您自己的 bean 实例,如以下示例所示:ConversionServiceString@ValueConversionService

java
@Configuration
public class AppConfig {

    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        conversionService.addConverter(new MyCustomConverter());
        return conversionService;
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun conversionService(): ConversionService {
        return DefaultFormattingConversionService().apply {
            addConverter(MyCustomConverter())
        }
    }
}

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

java
@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
        this.catalog = catalog;
    }
}
科特林
@Component
class MovieRecommender(
    @Value("#{systemProperties['user.catalog'] + 'Catalog' }") private val catalog: String)

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

java
@Component
public class MovieRecommender {

    private final Map<String, Integer> countOfMoviesPerCatalog;

    public MovieRecommender(
            @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
        this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
    }
}
科特林
@Component
class MovieRecommender(
    @Value("#{{'Thriller': 100, 'Comedy': 300}}") private val countOfMoviesPerCatalog: Map<String, Int>)

1.9.9。使用@PostConstruct@PreDestroy

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

java
public class CachingMovieLister {

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

    @PreDestroy
    public void clearMovieCache() {
        // clears the movie cache upon destruction...
    }
}
科特林
class CachingMovieLister {

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

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

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

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

1.10。类路径扫描和托管组件

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

从 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是任何 Spring 管理的组件的通用构造型。 @Repository, @Service, 和@Controller@Component针对更具体用例(分别在持久层、服务层和表示层)的特化。因此,您可以使用 注释组件类 @Component,但是通过使用 、 注释它们@Repository@Service或者@Controller 相反,您的类更适合工具处理或与方面关联。例如,这些原型注释是切入点的理想目标。@Repository, @Service, 并且@Controller还可以在 Spring 框架的未来版本中携带额外的语义。因此,如果您在使用@Component或者@Service对于你的服务层,@Service显然是更好的选择。同样,如前所述,@Repository已经支持作为持久层中自动异常转换的标记。

1.10.2。使用元注释和组合注释

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

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

    // ...
}
1 @Component原因@Service同理@Component。_
科特林
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component (1)
annotation class Service {

    // ...
}
1 @Component原因@Service同理@Component。_

您还可以组合元注释来创建“组合注释”。例如,@RestControllerSpring MVC 的注解由@Controller和 组成@ResponseBody

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

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

}
科特林
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Scope(WebApplicationContext.SCOPE_SESSION)
annotation class SessionScope(
        @get:AliasFor(annotation = Scope::class)
        val proxyMode: ScopedProxyMode = ScopedProxyMode.TARGET_CLASS
)

然后,您可以在@SessionScope不声明proxyMode如下的情况下使用:

java
@Service
@SessionScope
public class SessionScopedService {
    // ...
}
科特林
@Service
@SessionScope
class SessionScopedService {
    // ...
}

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

java
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
    // ...
}
科特林
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
class SessionScopedUserService : UserService {
    // ...
}

有关更多详细信息,请参阅 Spring Annotation Programming Model wiki 页面。

1.10.3。自动检测类和注册 Bean 定义

Spring 可以自动检测原型类并 BeanDefinition使用ApplicationContext. 例如,以下两个类有资格进行此类自动检测:

java
@Service
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}
科特林
@Service
class SimpleMovieLister(private val movieFinder: MovieFinder)
java
@Repository
public class JpaMovieFinder implements MovieFinder {
    // implementation elided for clarity
}
科特林
@Repository
class JpaMovieFinder : MovieFinder {
    // implementation elided for clarity
}

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

java
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}
科特林
@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig  {
    // ...
}
为简洁起见,前面的示例可能使用value了注解的属性(即@ComponentScan("org.example"))。

以下替代方法使用 XML:

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

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

</beans>
使用<context:component-scan>隐式启用 <context:annotation-config>. 使用时通常不需要包含该 <context:annotation-config>元素<context:component-scan>

类路径包的扫描需要类路径中存在相应的目录条目。当您使用 Ant 构建 JAR 时,请确保您没有激活 JAR 任务的仅文件开关。此外,在某些环境中,类路径目录可能不会根据安全策略公开——例如,JDK 1.7.0_45 及更高版本上的独立应用程序(需要在清单中设置“Trusted-Library”——请参阅 https://stackoverflow.com/问题/19394570/java-jre-7u45-breaks-classloader-getresources)。

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

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

您可以禁用注册AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor通过包含annotation-config值为 的属性false

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

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

表 5. 过滤器类型
过滤器类型 示例表达式 描述

注释(默认)

org.example.SomeAnnotation

在目标组件的类型级别存在元存在的注释。

可分配的

org.example.SomeClass

目标组件可分配(扩展或实现)的类(或接口)。

方面j

org.example..*Service+

要由目标组件匹配的 AspectJ 类型表达式。

正则表达式

org\.example\.Default.*

与目标组件的类名匹配的正则表达式。

风俗

org.example.MyTypeFilter

接口的自定义实现org.springframework.core.type.TypeFilter

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

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

以下清单显示了等效的 XML:

<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex"
                expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation"
                expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>
您还可以通过useDefaultFilters=false在注释上设置或use-default-filters="false"作为 <component-scan/>元素的属性提供来禁用默认过滤器。@Component这有效地禁用了对使用, @Repository, @Service, @Controller, @RestController, 或注释或元注释的类的自动检测@Configuration

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

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

java
@Component
public class FactoryMethodComponent {

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

    public void doWork() {
        // Component method implementation omitted
    }
}
科特林
@Component
class FactoryMethodComponent {

    @Bean
    @Qualifier("public")
    fun publicInstance() = TestBean("publicInstance")

    fun doWork() {
        // Component method implementation omitted
    }
}

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

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

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

java
@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);
    }
}
科特林
@Component
class FactoryMethodComponent {

    companion object {
        private var i: Int = 0
    }

    @Bean
    @Qualifier("public")
    fun publicInstance() = TestBean("publicInstance")

    // use of a custom qualifier and autowiring of method parameters
    @Bean
    protected fun protectedInstance(
            @Qualifier("public") spouse: TestBean,
            @Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply {
        this.spouse = spouse
        this.country = country
    }

    @Bean
    private fun privateInstance() = TestBean("privateInstance", i++)

    @Bean
    @RequestScope
    fun requestScopedInstance() = TestBean("requestScopedInstance", 3)
}

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

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

java
@Component
public class FactoryMethodComponent {

    @Bean @Scope("prototype")
    public TestBean prototypeInstance(InjectionPoint injectionPoint) {
        return new TestBean("prototypeInstance for " + injectionPoint.getMember());
    }
}
科特林
@Component
class FactoryMethodComponent {

    @Bean
    @Scope("prototype")
    fun prototypeInstance(injectionPoint: InjectionPoint) =
            TestBean("prototypeInstance for ${injectionPoint.member}")
}

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

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

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

方法的 Java 语言可见性@Bean不会立即影响 Spring 容器中生成的 bean 定义。您可以自由地声明您认为适合非@Configuration类的工厂方法,也可以在任何地方声明静态方法。但是,类中的常规@Bean方法@Configuration需要是可覆盖的——也就是说,它们不能被声明为privateor final

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

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

1.10.6。命名自动检测到的组件

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

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

java
@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}
科特林
@Service("myMovieLister")
class SimpleMovieLister {
    // ...
}
java
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}
科特林
@Repository
class MovieFinderImpl : MovieFinder {
    // ...
}

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

如果由于多个自动检测到的组件具有相同的非限定类名(即具有相同名称但位于不同包中的类)而遇到命名冲突,您可能需要配置BeanNameGenerator默认为生成的完全限定类名的豆名。从 Spring Framework 5.2.3 开始, FullyQualifiedAnnotationBeanNameGenerator位于包中的包 org.springframework.context.annotation可用于此类目的。
java
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    // ...
}
科特林
@Configuration
@ComponentScan(basePackages = ["org.example"], nameGenerator = MyNameGenerator::class)
class AppConfig {
    // ...
}
<beans>
    <context:component-scan base-package="org.example"
        name-generator="org.example.MyNameGenerator" />
</beans>

作为一般规则,只要其他组件可能显式引用它,请考虑使用注释指定名称。另一方面,只要容器负责接线,自动生成的名称就足够了。

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

与一般 Spring 管理的组件一样,自动检测组件的默认和最常见范围是singleton. 但是,有时您需要可以由@Scope注释指定的不同范围。您可以在注释中提供范围的名称,如以下示例所示:

java
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}
科特林
@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
    // ...
}
@Scope注释仅在具体 bean 类(用于注释组件)或工厂方法(用于@Bean方法)上进行自省。与 XML bean 定义相比,没有 bean 定义继承的概念,并且类级别的继承层次结构与元数据无关。

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

要为范围解析提供自定义策略而不是依赖基于注释的方法,您可以实现该 ScopeMetadataResolver 接口。确保包含一个默认的无参数构造函数。然后,您可以在配置扫描器时提供完全限定的类名,如以下注释和 bean 定义示例所示:
java
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    // ...
}
科特林
@Configuration
@ComponentScan(basePackages = ["org.example"], scopeResolver = MyScopeResolver::class)
class AppConfig {
    // ...
}
<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

当使用某些非单例作用域时,可能需要为作用域对象生成代理。原因在Scoped Beans as Dependencies中进行了描述。为此,component-scan 元素上提供了 scoped-proxy 属性。三个可能的值是:nointerfacestargetClass。例如,以下配置会生成标准 JDK 动态代理:

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

1.10.8。提供带有注释的限定符元数据

@Qualifier注释在Fine -tuning Annotation-based Autowiring with Qualifiers中讨论。该部分中的示例演示了使用@Qualifier注解和自定义限定符注解在解析自动装配候选时提供细粒度控制。因为这些示例基于 XML bean 定义,所以通过使用 XML 中元素的qualifierormeta 子元素在候选 bean 定义上提供限定符元数据bean。当依靠类路径扫描来自动检测组件时,您可以在候选类上为限定符元数据提供类型级别的注释。以下三个示例演示了这种技术:

java
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
科特林
@Component
@Qualifier("Action")
class ActionMovieCatalog : MovieCatalog
java
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
科特林
@Component
@Genre("Action")
class ActionMovieCatalog : MovieCatalog {
    // ...
}
java
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}
科特林
@Component
@Offline
class CachingMovieCatalog : MovieCatalog {
    // ...
}
与大多数基于注解的替代方案一样,请记住注解元数据绑定到类定义本身,而 XML 的使用允许相同类型的多个 bean 提供其限定符元数据的变体,因为元数据是根据每个-实例而不是每个类。

1.10.9。生成候选组件的索引

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

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

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

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

对于 Gradle 4.5 及更早版本,应在compileOnly 配置中声明依赖项,如以下示例所示:

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

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

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

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

在 IDE 中使用此模式时,spring-context-indexer必须将其注册为注释处理器,以确保更新候选组件时索引是最新的。
当在类路径中找到文件时,索引会自动启用META-INF/spring.components。如果索引对某些库(或用例)部分可用,但无法为整个应用程序构建,则可以通过设置spring.index.ignore为 回退到常规类路径安排(好像根本不存在索引) true,或者作为JVM 系统属性或通过 SpringProperties机制。

1.11。使用 JSR 330 标准注释

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

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

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

1.11.1。依赖注入@Inject@Named

而不是@Autowired,您可以使用@javax.inject.Inject如下:

java
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(...);
        // ...
    }
}
科特林
import javax.inject.Inject

class SimpleMovieLister {

    @Inject
    lateinit var movieFinder: MovieFinder


    fun listMovies() {
        movieFinder.findMovies(...)
        // ...
    }
}

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

java
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(...);
        // ...
    }
}
科特林
import javax.inject.Inject

class SimpleMovieLister {

    @Inject
    lateinit var movieFinder: MovieFinder


    fun listMovies() {
        movieFinder.findMovies(...)
        // ...
    }
}

如果您想为应注入的依赖项使用限定名称,则应使用@Named注解,如以下示例所示:

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

    // ...
}
科特林
import javax.inject.Inject
import javax.inject.Named

class SimpleMovieLister {

    private lateinit var movieFinder: MovieFinder

    @Inject
    fun setMovieFinder(@Named("main") movieFinder: MovieFinder) {
        this.movieFinder = movieFinder
    }

    // ...
}

@Autowired,@Inject也可以与java.util.Optionalor 一起使用@Nullable。这在这里更适用,因为@Inject没有required属性。以下一对示例展示了如何使用@Inject@Nullable

public class SimpleMovieLister {

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

    @Inject
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        // ...
    }
}
科特林
class SimpleMovieLister {

    @Inject
    var movieFinder: MovieFinder? = null
}

1.11.2。@Named和:注释@ManagedBean的标准等效项@Component

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

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

    // ...
}
科特林
import javax.inject.Inject
import javax.inject.Named

@Named("movieListener")  // @ManagedBean("movieListener") could be used as well
class SimpleMovieLister {

    @Inject
    lateinit var movieFinder: MovieFinder

    // ...
}

@Component在不指定组件名称的情况下 使用是很常见的。@Named可以以类似的方式使用,如以下示例所示:

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

@Named
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}
科特林
import javax.inject.Inject
import javax.inject.Named

@Named
class SimpleMovieLister {

    @Inject
    lateinit var movieFinder: MovieFinder

    // ...
}

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

java
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}
科特林
@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig  {
    // ...
}
与 相比@Component,JSR-330@Named和 JSR-250@ManagedBean 注释是不可组合的。您应该使用 Spring 的原型模型来构建自定义组件注释。

1.11.3。JSR-330 标准注释的限制

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

表 6. Spring 组件模型元素与 JSR-330 变体
Spring javax.inject.* javax.inject 限制/注释

@自动连线

@注入

@Inject没有“必需”属性。可以与 Java 8 一起使用Optional

@零件

@Named / @ManagedBean

JSR-330 不提供可组合模型,仅提供一种识别命名组件的方法。

@Scope("单例")

@Singleton

JSR-330 默认范围类似于 Spring 的prototype. 但是,为了保持它与 Spring 的一般默认值一致,在 Spring 容器中声明的 JSR-330 bean 是singleton默认的。为了使用 以外的范围singleton,您应该使用 Spring 的@Scope注解。javax.inject还提供了一个 @Scope注解。然而,这个仅用于创建您自己的注释。

@Qualifier

@Qualifier / @Named

javax.inject.Qualifier只是用于构建自定义限定符的元注释。具体String限定符(如@Qualifier带有值的 Spring)可以通过javax.inject.Named.

@价值

-

没有等价物

@必需的

-

没有等价物

@懒惰的

-

没有等价物

对象工厂

提供者

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 容器管理的新对象。对于熟悉 Spring 的<beans/>XML 配置的人来说,注解与元素@Bean的作用相同。<bean/>您可以将@Bean-annotated 方法与任何 Spring 一起 使用@Component。但是,它们最常与@Configuration豆类一起使用。

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

java
@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun myService(): MyService {
        return MyServiceImpl()
    }
}

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

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
完整的@Configuration 与“精简”@Bean 模式?

@Bean方法在没有用 注释的类中声明时 @Configuration,它们被称为以“精简”模式处理。在一个或什至在一个普通的旧类中声明的 Bean 方法@Component被认为是“精简”的,包含类的不同主要目的和@Bean方法是一种奖励。例如,服务组件可以通过@Bean每个适用组件类上的附加方法向容器公开管理视图。在这种情况下,@Bean方法是一种通用的工厂方法机制。

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

在常见的场景中,@Bean方法将在@Configuration类中声明,确保始终使用“完整”模式,并且跨方法引用因此被重定向到容器的生命周期管理。这可以防止 @Bean通过常规 J​​ava 调用意外调用相同的方法,这有助于减少在“精简”模式下操作时难以追踪的细微错误。

@Bean和注释将@Configuration在以下部分中深入讨论。然而,首先,我们介绍了使用基于 Java 的配置创建 Spring 容器的各种方法。

1.12.2。通过使用实例化 Spring 容器AnnotationConfigApplicationContext

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

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

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

简单的构造

与在 实例ClassPathXmlApplicationContext化. 这允许 Spring 容器的完全无 XML 使用,如以下示例所示:@ConfigurationAnnotationConfigApplicationContext

java
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
科特林
import org.springframework.beans.factory.getBean

fun main() {
    val ctx = AnnotationConfigApplicationContext(AppConfig::class.java)
    val myService = ctx.getBean<MyService>()
    myService.doStuff()
}

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

java
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
科特林
import org.springframework.beans.factory.getBean

fun main() {
    val ctx = AnnotationConfigApplicationContext(MyServiceImpl::class.java, Dependency1::class.java, Dependency2::class.java)
    val myService = ctx.getBean<MyService>()
    myService.doStuff()
}

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

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

AnnotationConfigApplicationContext您可以使用无参数构造函数实例化一个,然后使用该register()方法对其进行配置。这种方法在以编程方式构建AnnotationConfigApplicationContext. 以下示例显示了如何执行此操作:

java
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();
}
科特林
import org.springframework.beans.factory.getBean

fun main() {
    val ctx = AnnotationConfigApplicationContext()
    ctx.register(AppConfig::class.java, OtherConfig::class.java)
    ctx.register(AdditionalConfig::class.java)
    ctx.refresh()
    val myService = ctx.getBean<MyService>()
    myService.doStuff()
}
启用组件扫描scan(String…​)

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

java
@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig  {
    // ...
}
1 此注释启用组件扫描。
科特林
@Configuration
@ComponentScan(basePackages = ["com.acme"]) (1)
class AppConfig  {
    // ...
}
1 此注释启用组件扫描。

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

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

在前面的示例中,com.acme扫描包以查找任何 带@Component注释的类,并且这些类在容器中注册为 Spring bean 定义。AnnotationConfigApplicationContext公开该 scan(String…​)方法以允许相同的组件扫描功能,如以下示例所示:

java
public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}
科特林
fun main() {
    val ctx = AnnotationConfigApplicationContext()
    ctx.scan("com.acme")
    ctx.refresh()
    val myService = ctx.getBean<MyService>()
}
请记住,@Configuration类是用元注释@Component,因此它们是组件扫描的候选对象。在前面的示例中,假设AppConfigcom.acme包(或下面的任何包)中声明了 ,在调用scan(). 在 之后refresh(),它的所有@Bean 方法都被处理并注册为容器中的 bean 定义。
支持 Web 应用程序AnnotationConfigWebApplicationContext

WebApplicationContext变体AnnotationConfigApplicationContext可用于AnnotationConfigWebApplicationContext. 您可以在配置 Spring ContextLoaderListenerservlet 侦听器、Spring MVC DispatcherServlet等时使用此实现。下面的web.xml代码片段配置了一个典型的 Spring MVC Web 应用程序(注意contextClasscontext-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>
对于编程用例, aGenericWebApplicationContext可以用作AnnotationConfigWebApplicationContext. 有关详细信息,请参阅 GenericWebApplicationContext javadoc。

1.12.3。使用@Bean注解

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

您可以在-annotated 或 -annotated 类中使用@Bean注释。@Configuration@Component

声明一个 Bean

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

java
@Configuration
public class AppConfig {

    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun transferService() = TransferServiceImpl()
}

前面的配置完全等价于下面的 Spring XML:

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

这两个声明都使 beantransferService中的可用 beanApplicationContext绑定到 type 的对象实例TransferServiceImpl,如以下文本图像所示:

transferService -> com.acme.TransferServiceImpl

您还可以使用默认方法来定义 bean。这允许通过在默认方法上实现带有 bean 定义的接口来组合 bean 配置。

java
public interface BaseConfig {

    @Bean
    default TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}

@Configuration
public class AppConfig implements BaseConfig {

}

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

java
@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun transferService(): TransferService {
        return TransferServiceImpl()
    }
}

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

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

-annotated 方法可以具有@Bean任意数量的参数,这些参数描述构建该 bean 所需的依赖项。例如,如果我们TransferService 需要一个AccountRepository,我们可以使用方法参数来实现该依赖项,如以下示例所示:

java
@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun transferService(accountRepository: AccountRepository): TransferService {
        return TransferServiceImpl(accountRepository)
    }
}

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

接收生命周期回调

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

也完全支持常规的 Spring生命周期回调。如果 bean 实现InitializingBeanDisposableBeanLifecycle,则容器调用它们各自的方法。

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

@Bean注解支持指定任意初始化和销毁​​回调方法,很像 Spring XMLinit-method和元素上的destroy-method属性bean,如以下示例所示:

java
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();
    }
}
科特林
class BeanOne {

    fun init() {
        // initialization logic
    }
}

class BeanTwo {

    fun cleanup() {
        // destruction logic
    }
}

@Configuration
class AppConfig {

    @Bean(initMethod = "init")
    fun beanOne() = BeanOne()

    @Bean(destroyMethod = "cleanup")
    fun beanTwo() = BeanTwo()
}

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

默认情况下,您可能希望对使用 JNDI 获取的资源执行此操作,因为它的生命周期在应用程序之外进行管理。特别是,请确保始终为 a 执行此操作DataSource,因为已知它在 Java EE 应用程序服务器上存在问题。

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

java
@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
    return (DataSource) jndiTemplate.lookup("MyDS");
}
科特林
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
    return jndiTemplate.lookup("MyDS") as DataSource
}

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

在上述BeanOne示例的情况下,在构造期间直接调用该init() 方法同样有效,如下例所示:

java
@Configuration
public class AppConfig {

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

    // ...
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun beanOne() = BeanOne().apply {
        init()
    }

    // ...
}
当您直接在 Java 中工作时,您可以对您的对象做任何您喜欢的事情,而不必总是依赖容器生命周期。
指定 Bean 范围

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

使用@Scope注解

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

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

java
@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
}
科特林
@Configuration
class MyConfiguration {

    @Bean
    @Scope("prototype")
    fun encryptor(): Encryptor {
        // ...
    }
}
@Scopescoped-proxy

Spring 提供了一种通过 作用域代理处理作用域依赖的便捷方式。使用 XML 配置时创建此类代理的最简单方法是<aop:scoped-proxy/>元素。使用注释在 Java 中配置您的 bean提供了对属性@Scope的等效支持。proxyMode默认值为ScopedProxyMode.DEFAULT,这通常表示不应创建作用域代理,除非在组件扫描指令级别配置了不同的默认值。您可以 指定ScopedProxyMode.TARGET_CLASS或。ScopedProxyMode.INTERFACESScopedProxyMode.NO

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

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

@Bean
public Service userService() {
    UserService service = new SimpleUserService();
    // a reference to the proxied userPreferences bean
    service.setUserPreferences(userPreferences());
    return service;
}
科特林
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
fun userPreferences() = UserPreferences()

@Bean
fun userService(): Service {
    return SimpleUserService().apply {
        // a reference to the proxied userPreferences bean
        setUserPreferences(userPreferences())
    }
}
自定义 Bean 命名

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

java
@Configuration
public class AppConfig {

    @Bean("myThing")
    public Thing thing() {
        return new Thing();
    }
}
科特林
@Configuration
class AppConfig {

    @Bean("myThing")
    fun thing() = Thing()
}
Bean 别名

正如命名 Bean中所讨论的,有时需要为单个 bean 提供多个名称,也称为 bean 别名。注释的name属性@Bean 为此目的接受一个字符串数组。以下示例显示了如何为 bean 设置多个别名:

java
@Configuration
public class AppConfig {

    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}
科特林
@Configuration
class AppConfig {

    @Bean("dataSource", "subsystemA-dataSource", "subsystemB-dataSource")
    fun dataSource(): DataSource {
        // instantiate, configure and return DataSource bean...
    }
}
豆描述

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

要向 a 添加描述@Bean,您可以使用 @Description 注解,如以下示例所示:

java
@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Thing thing() {
        return new Thing();
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    fun thing() = Thing()
}

1.12.4。使用@Configuration注释

@Configuration是一个类级别的注解,表明一个对象是 bean 定义的来源。类通过-annotated 方法@Configuration声明 bean 。对类上的方法的@Bean调用也可用于定义 bean 间的依赖关系。请参阅基本概念:一般介绍。@Bean@Configuration@Bean@Configuration

注入 bean 间依赖

当 bean 相互依赖时,表达这种依赖关系就像让一个 bean 方法调用另一个方法一样简单,如以下示例所示:

java
@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun beanOne() = BeanOne(beanTwo())

    @Bean
    fun beanTwo() = BeanTwo()
}

在前面的示例中,通过构造函数注入beanOne接收对的引用。beanTwo

这种声明 bean 间依赖关系的方法仅在该@Bean方法在@Configuration类中声明时才有效。您不能使用普通@Component类来声明 bean 间的依赖关系。
查找方法注入

如前所述,查找方法注入是您应该很少使用的高级功能。在单例范围的 bean 依赖于原型范围的 bean 的情况下,它很有用。使用 Java 进行这种类型的配置为实现这种模式提供了一种自然的方式。下面的例子展示了如何使用查找方法注入:

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();
}
科特林
abstract class CommandManager {
    fun process(commandState: Any): Any {
        // grab a new instance of the appropriate Command interface
        val 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 fun createCommand(): Command
}

通过使用 Java 配置,您可以创建一个子类,CommandManager其中抽象createCommand()方法被覆盖,从而查找新的(原型)命令对象。以下示例显示了如何执行此操作:

java
@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 createCommand()
    // overridden to return a new prototype Command object
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}
科特林
@Bean
@Scope("prototype")
fun asyncCommand(): AsyncCommand {
    val command = AsyncCommand()
    // inject dependencies here as required
    return command
}

@Bean
fun commandManager(): CommandManager {
    // return new anonymous implementation of CommandManager with createCommand()
    // overridden to return a new prototype Command object
    return object : CommandManager() {
        override fun createCommand(): Command {
            return asyncCommand()
        }
    }
}
有关基于 Java 的配置如何在内部工作的更多信息

考虑下面的例子,它显示了一个@Bean被注解的方法被调用了两次:

java
@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();
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun clientService1(): ClientService {
        return ClientServiceImpl().apply {
            clientDao = clientDao()
        }
    }

    @Bean
    fun clientService2(): ClientService {
        return ClientServiceImpl().apply {
            clientDao = clientDao()
        }
    }

    @Bean
    fun clientDao(): ClientDao {
        return ClientDaoImpl()
    }
}

clientDao()已被调用一次clientService1()和一次clientService2()。由于此方法会创建一个新实例ClientDaoImpl并返回它,因此您通常会期望有两个实例(每个服务一个实例)。那肯定会有问题:在 Spring 中,实例化的 beansingleton默认有一个作用域。这就是神奇之处:所有@Configuration类在启动时都使用CGLIB. 在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存(作用域)bean。

根据 bean 的范围,行为可能会有所不同。我们在这里谈论单例。

从 Spring 3.2 开始,不再需要将 CGLIB 添加到类路径中,因为 CGLIB 类已被重新打包org.springframework.cglib并直接包含在 spring-core JAR 中。

由于 CGLIB 在启动时动态添加功能,因此存在一些限制。特别是,配置类不能是最终的。但是,从 4.3 开始,配置类上允许使用任何构造函数,包括使用 @Autowired或使用单个非默认构造函数声明进行默认注入。

如果您希望避免任何 CGLIB 强加的限制,请考虑@Bean 在非@Configuration类上声明您的方法(例如,@Component改为在普通类上)。方法之间的跨方法调用@Bean不会被拦截,因此您必须完全依赖构造函数或方法级别的依赖注入。

1.12.5。组合基于 Java 的配置

Spring 的基于 Java 的配置功能允许您编写注解,这可以降低配置的复杂性。

使用@Import注解

就像<import/>在 Spring XML 文件中使用该元素来帮助模块化配置一样,@Import注释允许@Bean从另一个配置类加载定义,如以下示例所示:

java
@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();
    }
}
科特林
@Configuration
class ConfigA {

    @Bean
    fun a() = A()
}

@Configuration
@Import(ConfigA::class)
class ConfigB {

    @Bean
    fun b() = B()
}

现在,不需要同时指定ConfigA.classConfigB.class在实例化上下文时,只ConfigB需要显式提供,如以下示例所示:

java
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);
}
科特林
import org.springframework.beans.factory.getBean

fun main() {
    val ctx = AnnotationConfigApplicationContext(ConfigB::class.java)

    // now both beans A and B will be available...
    val a = ctx.getBean<A>()
    val b = ctx.getBean<B>()
}

这种方法简化了容器的实例化,因为只需要处理一个类,而不是要求您 @Configuration在构造过程中记住可能大量的类。

从 Spring Framework 4.2 开始,@Import还支持对常规组件类的引用,类似于AnnotationConfigApplicationContext.register方法。如果您想通过使用一些配置类作为入口点来显式定义所有组件来避免组件扫描,这将特别有用。
@Bean注入对导入定义的依赖

前面的示例有效,但过于简单。在大多数实际场景中,bean 跨配置类相互依赖。使用 XML 时,这不是问题,因为不涉及编译器,您可以声明 ref="someBean"并信任 Spring 在容器初始化期间解决它。使用@Configuration类时,Java 编译器会对配置模型施加约束,因为对其他 bean 的引用必须是有效的 Java 语法。

幸运的是,解决这个问题很简单。正如我们已经讨论过的,一个@Bean方法可以有任意数量的参数来描述 bean 依赖关系。考虑以下更真实的场景,其中包含多个@Configuration 类,每个类都依赖于其他类中声明的 bean:

java
@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");
}
科特林
import org.springframework.beans.factory.getBean

@Configuration
class ServiceConfig {

    @Bean
    fun transferService(accountRepository: AccountRepository): TransferService {
        return TransferServiceImpl(accountRepository)
    }
}

@Configuration
class RepositoryConfig {

    @Bean
    fun accountRepository(dataSource: DataSource): AccountRepository {
        return JdbcAccountRepository(dataSource)
    }
}

@Configuration
@Import(ServiceConfig::class, RepositoryConfig::class)
class SystemTestConfig {

    @Bean
    fun dataSource(): DataSource {
        // return new DataSource
    }
}


fun main() {
    val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
    // everything wires up across configuration classes...
    val transferService = ctx.getBean<TransferService>()
    transferService.transfer(100.00, "A123", "C456")
}

还有另一种方法可以达到相同的结果。请记住,@Configuration类最终只是容器中的另一个 bean:这意味着它们可以 像任何其他 bean 一样利用注入和其他特性@Autowired@Value

确保您以这种方式注入的依赖项只是最简单的类型。@Configuration 类在上下文初始化期间很早就被处理,并且强制以这种方式注入依赖项可能会导致意外的早期初始化。尽可能使用基于参数的注入,如前面的示例所示。

此外,请特别注意BeanPostProcessor和的BeanFactoryPostProcessor定义@Bean。这些通常应该被声明为static @Bean方法,而不是触发它们包含的配置类的实例化。否则,@Autowired可能@Value无法在配置类本身上工作,因为可以将其创建为早于 AutowiredAnnotationBeanPostProcessor.

以下示例显示了如何将一个 bean 自动装配到另一个 bean:

java
@Configuration
public class ServiceConfig {

    @Autowired
    private AccountRepository accountRepository;

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

@Configuration
public class RepositoryConfig {

    private final DataSource dataSource;

    public RepositoryConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // 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");
}
科特林
import org.springframework.beans.factory.getBean

@Configuration
class ServiceConfig {

    @Autowired
    lateinit var accountRepository: AccountRepository

    @Bean
    fun transferService(): TransferService {
        return TransferServiceImpl(accountRepository)
    }
}

@Configuration
class RepositoryConfig(private val dataSource: DataSource) {

    @Bean
    fun accountRepository(): AccountRepository {
        return JdbcAccountRepository(dataSource)
    }
}

@Configuration
@Import(ServiceConfig::class, RepositoryConfig::class)
class SystemTestConfig {

    @Bean
    fun dataSource(): DataSource {
        // return new DataSource
    }
}

fun main() {
    val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
    // everything wires up across configuration classes...
    val transferService = ctx.getBean<TransferService>()
    transferService.transfer(100.00, "A123", "C456")
}
@Configuration仅从 Spring Framework 4.3 开始支持类中的 构造函数注入。@Autowired另请注意,如果目标 bean 仅定义一个构造函数 ,则无需指定。
完全合格的进口豆,便于导航

在前面的场景中,using@Autowired运行良好并提供了所需的模块化,但确定自动装配 bean 定义的确切声明位置仍然有些模糊。例如,作为开发人员ServiceConfig,您如何知道@Autowired AccountRepositorybean 的确切声明位置?它在代码中并不明确,这可能很好。请记住, Eclipse 的 Spring Tools提供的工具可以呈现显示所有连接方式的图形,这可能就是您所需要的。此外,您的 Java IDE 可以轻松找到该AccountRepository类型的所有声明和使用,并快速向您显示@Bean返回该类型的方法的位置。

如果这种歧义是不可接受的,并且您希望在 IDE 中从一个@Configuration类直接导航到另一个类,请考虑自动装配配置类本身。以下示例显示了如何执行此操作:

java
@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());
    }
}
科特林
@Configuration
class ServiceConfig {

    @Autowired
    private lateinit var repositoryConfig: RepositoryConfig

    @Bean
    fun transferService(): TransferService {
        // navigate 'through' the config class to the @Bean method!
        return TransferServiceImpl(repositoryConfig.accountRepository())
    }
}

在上述情况下,where AccountRepositoryis defined 是完全明确的。但是,ServiceConfig现在与RepositoryConfig. 这就是权衡。通过使用基于接口或基于抽象类的类可以在一定程度上缓解这种紧密耦合@Configuration。考虑以下示例:

java
@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");
}
科特林
import org.springframework.beans.factory.getBean

@Configuration
class ServiceConfig {

    @Autowired
    private lateinit var repositoryConfig: RepositoryConfig

    @Bean
    fun transferService(): TransferService {
        return TransferServiceImpl(repositoryConfig.accountRepository())
    }
}

@Configuration
interface RepositoryConfig {

    @Bean
    fun accountRepository(): AccountRepository
}

@Configuration
class DefaultRepositoryConfig : RepositoryConfig {

    @Bean
    fun accountRepository(): AccountRepository {
        return JdbcAccountRepository(...)
    }
}

@Configuration
@Import(ServiceConfig::class, DefaultRepositoryConfig::class)  // import the concrete config!
class SystemTestConfig {

    @Bean
    fun dataSource(): DataSource {
        // return DataSource
    }

}

fun main() {
    val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
    val transferService = ctx.getBean<TransferService>()
    transferService.transfer(100.00, "A123", "C456")
}

NowServiceConfig相对于具体的 是松散耦合的 DefaultRepositoryConfig,内置的 IDE 工具仍然有用:您可以轻松获得RepositoryConfig实现的类型层次结构。通过这种方式,导航@Configuration类及其依赖项与导航基于接口的代码的通常过程没有什么不同。

如果您想影响某些 bean 的启动创建顺序,请考虑将其中一些声明为@Lazy(用于在首次访问时创建而不是在启动时创建)或@DependsOn某些其他 bean(确保在当前 bean 之前创建特定的其他 bean,超出后者的直接依赖意味着什么)。
有条件地包含@Configuration类或@Bean方法

基于某些任意系统状态,有条件地启用或禁用完整的@Configuration类甚至单个方法通常很有用。@Bean一个常见的例子是@Profile仅当在 Spring 中启用了特定配置文件时才使用注释激活 bean Environment(有关详细信息,请参阅Bean 定义配置文件 )。

@Profile注释实际上是通过使用更灵活的注释来实现的,称为@Conditional. @Conditional注解表示 在注册org.springframework.context.annotation.Conditiona 之前应参考的具体实现@Bean

接口的实现Condition提供了一个matches(…​) 返回true或的方法false。例如,以下清单显示了 Condition用于 的实际实现@Profile

java
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // 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;
}
科特林
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
    // Read the @Profile annotation attributes
    val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name)
    if (attrs != null) {
        for (value in attrs["value"]!!) {
            if (context.environment.acceptsProfiles(Profiles.of(*value as Array<String>))) {
                return true
            }
        }
        return false
    }
    return true
}

有关更多详细信息,请参阅@Conditional javadoc。

结合 Java 和 XML 配置

Spring 的@Configuration类支持并非旨在 100% 完全替代 Spring XML。一些工具,例如 Spring XML 命名空间,仍然是配置容器的理想方式。在 XML 方便或必要的情况下,您可以选择:或者以“以 XML 为中心”的方式实例化容器,例如使用 , ClassPathXmlApplicationContext或者以“以 Java 为中心”的方式实例化它 AnnotationConfigApplicationContext@ImportResource使用根据需要导入 XML。

以 XML 为中心的@Configuration类的使用

最好从 XML 引导 Spring 容器并 @Configuration以特别的方式包含类。例如,在使用 Spring XML 的大型现有代码库中,更容易根据@Configuration需要创建类并从现有 XML 文件中包含它们。在本节的后面部分,我们将介绍@Configuration在这种“以 XML 为中心”的情况下使用类的选项。

将类声明@Configuration为普通 Spring<bean/>元素

请记住,@Configuration类最终是容器中的 bean 定义。在本系列示例中,我们创建了一个@Configuration名为的类AppConfig并将其system-test-config.xml作为<bean/>定义包含在其中。因为 <context:annotation-config/>是开启的,所以容器会识别 @Configuration注解并正确处理其中@Bean声明的方法AppConfig

以下示例显示了 Java 中的一个普通配置类:

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());
    }
}
科特林
@Configuration
class AppConfig {

    @Autowired
    private lateinit var dataSource: DataSource

    @Bean
    fun accountRepository(): AccountRepository {
        return JdbcAccountRepository(dataSource)
    }

    @Bean
    fun transferService() = 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.密码=
java
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
科特林
fun main() {
    val ctx = ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml")
    val transferService = ctx.getBean<TransferService>()
    // ...
}
system-test-config.xml文件中,AppConfig <bean/>不声明id 元素。虽然这样做是可以接受的,但这是不必要的,因为没有其他 bean 曾经引用过它,并且不太可能通过名称从容器中显式获取。类似地,DataSourcebean 仅按类型自动装配,因此id 并不严格要求显式 bean。
使用 <context:component-scan/> 拾取@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注解来实现“以 Java 为中心”的配置,该配置根据需要使用 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);
    }
}
科特林
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
class AppConfig {

    @Value("\${jdbc.url}")
    private lateinit var url: String

    @Value("\${jdbc.username}")
    private lateinit var username: String

    @Value("\${jdbc.password}")
    private lateinit var password: String

    @Bean
    fun dataSource(): DataSource {
        return 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.密码=
java
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
科特林
import org.springframework.beans.factory.getBean

fun main() {
    val ctx = AnnotationConfigApplicationContext(AppConfig::class.java)
    val transferService = ctx.getBean<TransferService>()
    // ...
}

1.13。环境抽象

Environment接口是集成在容器中的抽象,它对应用程序环境的两个关键方面进行建模:配置文件属性

配置文件是一个命名的、逻辑​​的 bean 定义组,仅当给定的配置文件处于活动状态时才向容器注册。可以将 Bean 分配给配置文件,无论是在 XML 中定义还是使用注释定义。与配置文件相关的对象的作用Environment是确定哪些配置文件(如果有)当前处于活动状态,以及哪些配置文件(如果有)默认应该是活动的。

属性在几乎所有应用程序中都发挥着重要作用,并且可能源自多种来源:属性文件、JVM 系统属性、系统环境变量、JNDI、servlet 上下文参数、ad-hocProperties对象、Map对象等。与属性相关的对象的作用Environment是为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。

1.13.1。Bean 定义配置文件

Bean 定义配置文件在核心容器中提供了一种机制,允许在不同环境中注册不同的 bean。“环境”这个词对不同的用户可能意味着不同的东西,这个功能可以帮助许多用例,包括:

  • 在开发中处理内存中的数据源,而不是在 QA 或生产中从 JNDI 中查找相同的数据源。

  • 仅在将应用程序部署到性能环境时才注册监控基础架构。

  • 为客户 A 和客户 B 部署注册定制的 bean 实现。

考虑实际应用程序中需要 DataSource. 在测试环境中,配置可能类似于以下内容:

java
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}
科特林
@Bean
fun dataSource(): DataSource {
    return EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("my-schema.sql")
            .addScript("my-test-data.sql")
            .build()
}

现在考虑如何将此应用程序部署到 QA 或生产环境中,假设应用程序的数据源已在生产应用程序服务器的 JNDI 目录中注册。我们的dataSourcebean 现在看起来像下面的清单:

java
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
科特林
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
    val ctx = InitialContext()
    return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}

问题是如何根据当前环境在使用这两种变体之间进行切换。随着时间的推移,Spring 用户已经设计了许多方法来完成这项工作,通常依赖于系统环境变量和<import/>包含令牌的 XML 语句的组合,${placeholder}这些令牌根据环境变量的值解析为正确的配置文件路径。Bean 定义概要文件是一个核心容器特性,它为这个问题提供了解决方案。

如果我们概括前面环境特定 bean 定义示例中所示的用例,我们最终需要在某些上下文中注册某些 bean 定义,而在其他上下文中则不需要。您可以说您想在情况 A 中注册特定的 bean 定义配置文件,并在情况 B 中注册不同的配置文件。我们首先更新配置以反映这种需求。

使用@Profile

@Profile 一个或多个指定的配置文件处于活动状态时,注释可让您指示组件有资格注册。使用我们前面的示例,我们可以重写dataSource配置如下:

java
@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("development")
class StandaloneDataConfig {

    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .addScript("classpath:com/bank/config/sql/test-data.sql")
                .build()
    }
}
java
@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");
    }
}
科特林
@Configuration
@Profile("production")
class JndiDataConfig {

    @Bean(destroyMethod = "")
    fun dataSource(): DataSource {
        val ctx = InitialContext()
        return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
    }
}
如前所述,对于@Bean方法,您通常选择使用程序化 JNDI 查找,通过使用 Spring 的JndiTemplate/JndiLocatorDelegate助手或InitialContext前面显示的直接 JNDI 用法,而不是JndiObjectFactoryBean 变体,这将迫使您将返回类型声明为FactoryBean类型。

配置文件字符串可能包含一个简单的配置文件名称(例如,production)或配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,production & us-east)。配置文件表达式中支持以下运算符:

  • !:配置文件的逻辑“非”

  • &:配置文件的逻辑“与”

  • |:配置文件的逻辑“或”

不能在不使用括号的情况下混合使用&and运算符。|例如, production & us-east | eu-central不是一个有效的表达式。它必须表示为 production & (us-east | eu-central)

您可以将@Profile其用作元注释以创建自定义组合注释。以下示例定义了一个自定义 @Production注释,您可以将其用作 的替代品 @Profile("production")

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
科特林
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
如果一个@Configuration类标有,则与该类关联@Profile的所有@Bean方法和 注释都将被绕过,除非一个或多个指定的配置文件处于活动状态。@Import如果一个@Component@Configuration类标有@Profile({"p1", "p2"}),则除非已激活配置文件“p1”或“p2”,否则不会注册或处理该类。如果给定配置文件以 NOT 运算符 ( ) 为前缀,!则仅当配置文件不活动时才注册带注释的元素。例如,@Profile({"p1", "!p2"})如果配置文件“p1”处于活动状态或配置文件“p2”未处于活动状态,则会发生注册。

@Profile也可以在方法级别声明为仅包含配置类的一个特定 bean(例如,对于特定 bean 的替代变体),如以下示例所示:

java
@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");
    }
}
1 standaloneDataSource方法仅在development配置文件中可用。
2 jndiDataSource方法仅在production配置文件中可用。
科特林
@Configuration
class AppConfig {

    @Bean("dataSource")
    @Profile("development") (1)
    fun standaloneDataSource(): DataSource {
        return 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)
    fun jndiDataSource() =
        InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
1 standaloneDataSource方法仅在development配置文件中可用。
2 jndiDataSource方法仅在production配置文件中可用。

使用@Profileon@Bean方法,可能会应用一种特殊情况:在@Bean相同 Java 方法名称的重载方法的情况下(类似于构造函数重载),@Profile需要在所有重载方法上一致地声明一个条件。如果条件不一致,则仅重载方法中第一个声明的条件重要。因此,@Profile不能用于选择具有特定参数签名的重载方法而不是另一个。同一 bean 的所有工厂方法之间的解析遵循 Spring 在创建时的构造函数解析算法。

如果要定义具有不同配置文件条件的备用 bean,请使用不同的 Java 方法名称,这些方法名称通过使用@Beanname 属性指向相同的 bean 名称,如前面的示例所示。如果参数签名都相同(例如,所有变体都有无参数工厂方法),这是首先在有效 Java 类中表示这种安排的唯一方法(因为只能有一个特定名称和参数签名的方法)。

XML Bean 定义配置文件

XML 对应物是元素的profile属性。<beans>我们前面的示例配置可以重写为两个 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被限制为仅允许文件中的最后一个元素。这应该有助于提供灵活性,而不会在 XML 文件中产生混乱。

XML 对应项不支持前面描述的配置文件表达式。但是,可以使用!运算符来否定配置文件。也可以通过嵌套配置文件来应用逻辑“和”,如以下示例所示:

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

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

在前面的示例中,如果和 配置文件都处于活动状态,dataSource则公开 bean 。productionus-east

激活配置文件

现在我们已经更新了配置,我们仍然需要指示 Spring 哪个配置文件处于活动状态。如果我们现在启动示例应用程序,我们会看到NoSuchBeanDefinitionException抛出异常,因为容器找不到名为 的 Spring bean dataSource

激活配置文件可以通过多种方式完成,但最直接的方式是以编程方式针对Environment可通过 ApplicationContext. 以下示例显示了如何执行此操作:

java
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
科特林
val ctx = AnnotationConfigApplicationContext().apply {
    environment.setActiveProfiles("development")
    register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
    refresh()
}

此外,您还可以通过 spring.profiles.active属性以声明方式激活配置文件,可以通过系统环境变量、JVM 系统属性、servlet 上下文参数来指定web.xml,甚至可以作为 JNDI 中的条目(参见PropertySourceAbstraction)。@ActiveProfiles在集成测试中,可以使用模块中的注释来声明活动配置文件spring-test (请参阅环境配置文件的上下文配置)。

请注意,配置文件不是“非此即彼”的命题。您可以一次激活多个配置文件。以编程方式,您可以为 setActiveProfiles()接受String…​可变参数的方法提供多个配置文件名称。以下示例激活多个配置文件:

java
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
科特林
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")

以声明方式,spring.profiles.active可以接受以逗号分隔的配置文件名称列表,如以下示例所示:

    -Dspring.profiles.active="profile1,profile2"
默认配置文件

默认配置文件表示默认启用的配置文件。考虑以下示例:

java
@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();
    }
}
科特林
@Configuration
@Profile("default")
class DefaultDataConfig {

    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .build()
    }
}

如果没有激活的配置文件,dataSource则创建。您可以将此视为一种为一个或多个 bean 提供默认定义的方法。如果启用了任何配置文件,则默认配置文件不适用。

setDefaultProfiles()您可以使用onEnvironment或以声明方式使用spring.profiles.default属性来更改默认配置文件的名称。

1.13.2。PropertySource抽象

Spring 的Environment抽象提供了对属性源的可配置层次结构的搜索操作。考虑以下清单:

java
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);
科特林
val ctx = GenericApplicationContext()
val env = ctx.environment
val containsMyProperty = env.containsProperty("my-property")
println("Does my environment contain the 'my-property' property? $containsMyProperty")

my-property在前面的代码片段中,我们看到了一种询问 Spring 是否为当前环境定义属性的高级方法。为了回答这个问题,Environment对象对一组对象执行搜索PropertySource 。APropertySource是对任何键值对源的简单抽象,SpringStandardEnvironment 配置了两个 PropertySource 对象——一个代表 JVM 系统属性集(System.getProperties()),一个代表系统环境变量集(System.getenv())。

这些默认属性源用于StandardEnvironment, 用于独立应用程序。StandardServletEnvironment 填充了其他默认属性源,包括 servlet 配置和 servlet 上下文参数。它可以选择启用JndiPropertySource. 有关详细信息,请参阅 javadoc。

具体来说,当您使用 时,如果系统属性或环境变量在运行时存在StandardEnvironment,则调用env.containsProperty("my-property") 返回 true 。my-propertymy-property

执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果my-property在调用 期间恰好在两个位置都设置了env.getProperty("my-property")属性,则系统属性值“获胜”并返回。请注意,属性值不会合并,而是完全被前面的条目覆盖。

对于 common StandardServletEnvironment,完整的层次结构如下,最高优先级的条目位于顶部:

  1. ServletConfig 参数(如果适用——例如,在DispatcherServlet上下文的情况下)

  2. ServletContext 参数(web.xml 上下文参数条目)

  3. JNDI 环境变量(java:comp/env/条目)

  4. JVM 系统属性(-D命令行参数)

  5. JVM系统环境(操作系统环境变量)

最重要的是,整个机制是可配置的。也许您有一个想要集成到此搜索中的自定义属性源。为此,请实现并实例化您自己的PropertySource并将其添加到PropertySources当前Environment. 以下示例显示了如何执行此操作:

java
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
科特林
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())

在前面的代码中,MyPropertySource已在搜索中以最高优先级添加。如果它包含一个my-property属性,则检测并返回该属性,以支持my-property任何 other 中的任何属性PropertySourceMutablePropertySources API 公开了许多允许精确操作属性源集的方法。

1.13.3。使用@PropertySource

@PropertySource 注解提供了一种方便的声明性机制,用于将 a 添加到PropertySource Spring 的Environment.

app.properties给定一个包含键值对的名为的文件testbean.name=myTestBean,以下@Configuration@PropertySource以调用testBean.getName()返回的方式使用myTestBean

java
@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;
    }
}
科特林
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
class AppConfig {

    @Autowired
    private lateinit var env: Environment

    @Bean
    fun testBean() = TestBean().apply {
        name = env.getProperty("testbean.name")!!
    }
}

资源位置中存在的任何${…​}占位符都会@PropertySource根据已针对环境注册的属性源集进行解析,如以下示例所示:

java
@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;
    }
}
科特林
@Configuration
@PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties")
class AppConfig {

    @Autowired
    private lateinit var env: Environment

    @Bean
    fun testBean() = TestBean().apply {
        name = env.getProperty("testbean.name")!!
    }
}

假设my.placeholder存在于已注册的属性源之一(例如,系统属性或环境变量)中,则占位符被解析为相应的值。如果不是,则将default/path其用作默认值。如果未指定默认值且无法解析属性, IllegalArgumentException则抛出 an。

根据@PropertySourceJava 8 约定,注释是可重复的。但是,所有此类@PropertySource注释都需要在同一级别声明,或者直接在配置类上声明,或者作为同一自定义注释中的元注释。不建议混合直接注释和元注释,因为直接注释有效地覆盖了元注释。

1.13.4。语句中的占位符解析

从历史上看,元素中占位符的值只能根据 JVM 系统属性或环境变量来解析。这已不再是这种情况。因为Environment抽象集成在整个容器中,所以很容易通过它来路由占位符的解析。这意味着您可以以任何您喜欢的方式配置解析过程。您可以更改搜索系统属性和环境变量的优先级或完全删除它们。您还可以根据需要将自己的属性源添加到组合中。

customer 具体来说,无论属性在何处定义,只要它在 中可用,以下语句都有效Environment

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>

1.14。注册一个LoadTimeWeaver

LoadTimeWeaver类加载到 Java 虚拟机 (JVM) 中时,Spring 使用它来动态转换类。

要启用加载时编织,您可以将 添加@EnableLoadTimeWeaving到您的 @Configuration类之一,如以下示例所示:

java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
科特林
@Configuration
@EnableLoadTimeWeaving
class AppConfig

或者,对于 XML 配置,您可以使用以下context:load-time-weaver元素:

<beans>
    <context:load-time-weaver/>
</beans>

一旦为 配置 ,其中的ApplicationContext任何 bean 都ApplicationContext 可以实现LoadTimeWeaverAware,从而接收对加载时编织器实例的引用。这在与Spring 的 JPA 支持结合使用时特别有用, 其中 JPA 类转换可能需要加载时编织。有关更多详细信息,请参阅 LocalContainerEntityManagerFactoryBean javadoc。有关 AspectJ 加载时编织的更多信息,请参阅Spring Framework 中使用 AspectJ 进行加载时编织

1.15。的附加功能ApplicationContext

正如在介绍章节中所讨论的,该org.springframework.beans.factory 包提供了管理和操作 bean 的基本功能,包括以编程方式。该org.springframework.context包添加了 ApplicationContext 接口,它扩展了BeanFactory接口,此外还扩展了其他接口以提供更多面向应用程序框架的样式的附加功能。许多人ApplicationContext以完全声明的方式使用它,甚至没有以编程方式创建它,而是依赖于支持类,例如在Java EE Web 应用程序的正常启动过程中ContextLoader自动实例化一个 。ApplicationContext

BeanFactory为了以更加面向框架的风格增强功能,上下文包还提供了以下功能:

  • MessageSource通过界面访问 i18n 风格的消息。

  • 通过接口访问资源,例如 URL 和文件ResourceLoader

  • ApplicationListener事件发布,即通过使用接口发布给实现接口的bean ApplicationEventPublisher

  • 加载多个(分层)上下文,通过 HierarchicalBeanFactory接口让每个上下文都集中在一个特定的层上,例如应用程序的 Web 层。

1.15.1。国际化使用MessageSource

ApplicationContext接口扩展了一个名为的接口MessageSource,因此提供了国际化(“i18n”)功能。Spring 还提供了 HierarchicalMessageSource接口,可以分层解析消息。这些接口共同提供了 Spring 影响消息解析的基础。这些接口上定义的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc): 用于从MessageSource. 如果未找到指定语言环境的消息,则使用默认消息。MessageFormat使用标准库提供的功能,传入的任何参数都将成为替换值。

  • String getMessage(String code, Object[] args, Locale loc):与上一种方法基本相同,但有一个区别:不能指定默认消息。如果找不到消息,NoSuchMessageException则抛出 a。

  • String getMessage(MessageSourceResolvable resolvable, Locale locale): 上述方法中使用的所有属性也都包装在一个名为 的类 MessageSourceResolvable中,您可以在此方法中使用该类。

加载an 时ApplicationContext,它会自动搜索MessageSource 上下文中定义的 bean。bean 必须具有名称messageSource。如果找到这样的 bean,则对前面方法的所有调用都委托给消息源。如果未找到消息源,则ApplicationContext尝试查找包含同名 bean 的父级。如果是这样,它将使用该 bean 作为MessageSource. 如果 ApplicationContext找不到任何消息源, DelegatingMessageSource则实例化一个空,以便能够接受对上述方法的调用。

Spring 提供了三个MessageSource实现ResourceBundleMessageSourceReloadableResourceBundleMessageSourceStaticMessageSource。所有这些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>

该示例假定您有三个名为 的资源包formatexceptionswindows 在您的类路径中定义。ResourceBundle任何解析消息的请求都以通过对象解析消息的 JDK 标准方式处理。出于示例的目的,假设上述两个资源包文件的内容如下:

    # 在 format.properties 中
    message=鳄鱼摇滚!
    # 在 exceptions.properties 中
    argument.required={0} 参数是必需的。

下一个示例显示了一个运行该MessageSource功能的程序。请记住,所有ApplicationContext实现也是MessageSource 实现,因此可以转换为MessageSource接口。

java
public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
    System.out.println(message);
}
科特林
fun main() {
    val resources = ClassPathXmlApplicationContext("beans.xml")
    val message = resources.getMessage("message", null, "Default", Locale.ENGLISH)
    println(message)
}

上述程序的结果输出如下:

鳄鱼摇滚!

总而言之,MessageSource定义在一个名为 的文件中beans.xml,该文件位于类路径的根目录中。bean 定义通过其属性messageSource引用了许多资源包。basenames在列表中传递给basenames属性的三个文件作为文件存在于类路径的根目录中,分别称为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>
java
public class Example {

    private MessageSource messages;

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

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", Locale.ENGLISH);
        System.out.println(message);
    }
}
科特林
    class Example {

    lateinit var messages: MessageSource

    fun execute() {
        val message = messages.getMessage("argument.required",
                arrayOf("userDao"), "Required", Locale.ENGLISH)
        println(message)
    }
}

调用该execute()方法的结果输出如下:

userDao 参数是必需的。

关于国际化(“i18n”),Spring 的各种MessageSource 实现遵循与标准 JDK 相同的语言环境解析和回退规则 ResourceBundle。简而言之,继续messageSource前面定义的示例,如果您想针对英国 ( en-GB) 语言环境解析消息,您将分别创建名为format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties的文件。

通常,区域设置解析由应用程序的周围环境管理。在以下示例中,手动指定解析(英国)消息所针对的语言环境:

# 在 exceptions_en_GB.properties
argument.required=Ebagum 小伙子,''{0}'' 参数是必需的,我说是必需的。
java
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);
}
科特林
fun main() {
    val resources = ClassPathXmlApplicationContext("beans.xml")
    val message = resources.getMessage("argument.required",
            arrayOf("userDao"), "Required", Locale.UK)
    println(message)
}

上述程序运行的结果如下:

Ebagum 小伙子,'userDao' 参数是必需的,我说是必需的。

您还可以使用该MessageSourceAware接口来获取对 MessageSource已定义的任何内容的引用。在创建和配置 bean 时,在ApplicationContext实现接口的 bean 中定义的任何 bean 都会 MessageSourceAware被注入应用程序上下文。MessageSource

因为 SpringMessageSource是基于 Java 的ResourceBundle,所以它不会合并具有相同基本名称的包,而只会使用找到的第一个包。具有相同基本名称的后续消息包将被忽略。
作为 的替代方案ResourceBundleMessageSource,Spring 提供了一个 ReloadableResourceBundleMessageSource类。此变体支持相同的捆绑文件格式,但比基于标准 JDK 的 ResourceBundleMessageSource实现更灵活。特别是,它允许从任何 Spring 资源位置(不仅从类路径)读取文件,并支持捆绑属性文件的热重载(同时在它们之间有效地缓存它们)。有关详细信息,请参阅ReloadableResourceBundleMessageSource javadoc。

1.15.2。标准和自定义事件

中的事件处理ApplicationContext是通过ApplicationEvent 类和ApplicationListener接口提供的。如果将实现 ApplicationListener接口的 bean 部署到上下文中,则每次 ApplicationEvent发布到 时ApplicationContext,都会通知该 bean。本质上,这是标准的观察者设计模式。

从 Spring 4.2 开始,事件基础结构得到了显着改进,并提供了基于注释的模型以及发布任意事件的能力(即,不一定从 扩展的对象ApplicationEvent)。当这样的对象发布时,我们会为您将其包装在一个事件中。

下表描述了 Spring 提供的标准事件:

表 7. 内置事件
事件 解释

ContextRefreshedEvent

在初始化或刷新时发布ApplicationContext(例如,通过使用接口refresh()上的方法ConfigurableApplicationContext)。这里,“初始化”意味着所有 bean 都被加载,后处理器 bean 被检测并激活,单例被预实例化,ApplicationContext对象可以使用了。只要上下文没有关闭,就可以多次触发刷新,前提是所选择的ApplicationContext实际支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但 GenericApplicationContext不支持。

ContextStartedEvent

使用接口上的方法 ApplicationContext启动时发布。在这里,“已启动”意味着所有 bean 都接收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 bean,但它也可用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。start()ConfigurableApplicationContextLifecycle

ContextStoppedEvent

使用接口上的方法 ApplicationContext停止时发布。在这里,“停止”意味着所有 的 bean 都会收到一个明确的停止信号。可以通过 调用重新启动已停止的上下文。stop()ConfigurableApplicationContextLifecyclestart()

ContextClosedEvent

ApplicationContext使用接口close()上的方法ConfigurableApplicationContext或通过 JVM 关闭挂钩关闭时发布。在这里,“关闭”意味着所有的单例 bean 都将被销毁。一旦上下文关闭,它就到了生命的尽头,无法刷新或重新启动。

RequestHandledEvent

一个特定于 Web 的事件,告诉所有 bean 一个 HTTP 请求已得到服务。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 Web 应用程序DispatcherServlet

ServletRequestHandledEvent

它的子类RequestHandledEvent添加了 Servlet 特定的上下文信息。

您还可以创建和发布自己的自定义事件。以下示例显示了一个扩展 SpringApplicationEvent基类的简单类:

java
public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}
科特林
class BlockedListEvent(source: Any,
                    val address: String,
                    val content: String) : ApplicationEvent(source)

要发布自定义ApplicationEvent,请publishEvent()ApplicationEventPublisher. 通常,这是通过创建一个实现 ApplicationEventPublisherAware并将其注册为 Spring bean 的类来完成的。下面的例子展示了这样一个类:

java
public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}
科特林
class EmailService : ApplicationEventPublisherAware {

    private lateinit var blockedList: List<String>
    private lateinit var publisher: ApplicationEventPublisher

    fun setBlockedList(blockedList: List<String>) {
        this.blockedList = blockedList
    }

    override fun setApplicationEventPublisher(publisher: ApplicationEventPublisher) {
        this.publisher = publisher
    }

    fun sendEmail(address: String, content: String) {
        if (blockedList!!.contains(address)) {
            publisher!!.publishEvent(BlockedListEvent(this, address, content))
            return
        }
        // send email...
    }
}

在配置时,Spring 容器检测到EmailService实现 ApplicationEventPublisherAware并自动调用 setApplicationEventPublisher(). 实际上,传入的参数是Spring容器本身。您正在通过其 ApplicationEventPublisher接口与应用程序上下文进行交互。

要接收 custom ApplicationEvent,您可以创建一个实现 ApplicationListener并将其注册为 Spring bean 的类。下面的例子展示了这样一个类:

java
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

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

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}
科特林
class BlockedListNotifier : ApplicationListener<BlockedListEvent> {

    lateinit var notificationAddres: String

    override fun onApplicationEvent(event: BlockedListEvent) {
        // notify appropriate parties via notificationAddress...
    }
}

请注意,ApplicationListener它通常使用自定义事件的类型进行参数化(BlockedListEvent在前面的示例中)。这意味着该 onApplicationEvent()方法可以保持类型安全,避免任何向下转换的需要。您可以根据需要注册任意数量的事件侦听器,但请注意,默认情况下,事件侦听器会同步接收事件。这意味着该publishEvent()方法会阻塞,直到所有侦听器都完成了对事件的处理。这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它会在发布者的事务上下文中运行。如果需要另一种事件发布策略,请参阅 javadoc 了解 Spring 的 ApplicationEventMulticaster接口和SimpleApplicationEventMulticaster 配置选项的实现。

以下示例显示了用于注册和配置上述每个类的 bean 定义:

<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </list>
    </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="[email protected]"/>
</bean>

总而言之,当调用 bean 的sendEmail()方法时emailService,如果有任何电子邮件消息应该被阻止, BlockedListEvent则会发布一个自定义类型的事件。blockedListNotifierbean 注册为 an ApplicationListener并接收,此时BlockedListEvent它可以通知适当的各方。

Spring 的事件机制是为同一应用程序上下文中的 Spring bean 之间的简单通信而设计的。然而,对于更复杂的企业集成需求,单独维护的 Spring Integration项目为 构建基于众所周知的 Spring 编程模型的 轻量级、面向模式、事件驱动的架构提供了完整的支持。
基于注解的事件监听器

@EventListener您可以使用注解在托管 bean 的任何方法上注册事件侦听器 。BlockedListNotifier可以改写如下:

java
public class BlockedListNotifier {

    private String notificationAddress;

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

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}
科特林
class BlockedListNotifier {

    lateinit var notificationAddress: String

    @EventListener
    fun processBlockedListEvent(event: BlockedListEvent) {
        // notify appropriate parties via notificationAddress...
    }
}

方法签名再次声明了它所侦听的事件类型,但是这一次使用了一个灵活的名称并且没有实现特定的侦听器接口。只要实际事件类型在其实现层次结构中解析您的泛型参数,也可以通过泛型来缩小事件类型。

如果您的方法应该监听多个事件,或者如果您想在没有参数的情况下定义它,也可以在注解本身上指定事件类型。以下示例显示了如何执行此操作:

java
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}
科特林
@EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class)
fun handleContextStart() {
    // ...
}

还可以通过使用condition定义SpEL表达式的注释属性添加额外的运行时过滤,表达式应该匹配以实际调用特定事件的方法。

content以下示例显示了如何重写我们的通知器以仅在事件的属性等于时才被调用 my-event

java
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}
科特林
@EventListener(condition = "#blEvent.content == 'my-event'")
fun processBlockedListEvent(blEvent: BlockedListEvent) {
    // notify appropriate parties via notificationAddress...
}

每个SpEL表达式都针对专用上下文进行评估。下表列出了对上下文可用的项目,以便您可以将它们用于条件事件处理:

表 8. 事件 SpEL 可用元数据
姓名 地点 描述 例子

事件

根对象

实际的ApplicationEvent.

#root.event或者event

参数数组

根对象

用于调用方法的参数(作为对象数组)。

#root.argsargsargs[0]访问第一个参数等。

参数名称

评估上下文

任何方法参数的名称。如果由于某种原因,名称不可用(例如,因为编译的字节码中没有调试信息),也可以使用代表参数索引的#a<#arg>语法<#arg>(从 0 开始)使用单独的参数。

#blEvent#a0(您也可以使用#p0#p<#arg>参数表示法作为别名)

请注意#root.event,即使您的方法签名实际上引用了已发布的任意对象,这也使您可以访问底层事件。

如果您需要发布一个事件作为处理另一个事件的结果,您可以更改方法签名以返回应该发布的事件,如以下示例所示:

java
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}
科特林
@EventListener
fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}
异步侦听 器不支持此功能 。

该方法为它处理的每一个 handleBlockedListEvent()发布一个新的。如果您需要发布多个事件,则可以改为返回一个或一组事件。ListUpdateEventBlockedListEventCollection

异步侦听器

如果您希望特定侦听器异步处理事件,则可以重用 常规@Async支持。以下示例显示了如何执行此操作:

java
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}
科特林
@EventListener
@Async
fun processBlockedListEvent(event: BlockedListEvent) {
    // BlockedListEvent is processed in a separate thread
}

使用异步事件时请注意以下限制:

  • 如果异步事件侦听器抛出Exception,它不会传播给调用者。有关 AsyncUncaughtExceptionHandler 更多详细信息,请参阅。

  • 异步事件侦听器方法不能通过返回值来发布后续事件。如果您需要发布另一个事件作为处理的结果,请注入一个 ApplicationEventPublisher 手动发布事件。

订购听众

如果您需要在调用另一个侦听器之前调用一个侦听器,则可以将@Order 注释添加到方法声明中,如以下示例所示:

java
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}
科特林
@EventListener
@Order(42)
fun processBlockedListEvent(event: BlockedListEvent) {
    // notify appropriate parties via notificationAddress...
}
通用事件

您还可以使用泛型来进一步定义事件的结构。考虑使用 EntityCreatedEvent<T>whereT是创建的实际实体的类型。例如,您可以创建以下侦听器定义以仅接收EntityCreatedEventa Person

java
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}
科特林
@EventListener
fun onPersonCreated(event: EntityCreatedEvent<Person>) {
    // ...
}

由于类型擦除,这仅在触发的事件解析了事件侦听器过滤的通用参数(即类似的东西 class PersonCreatedEvent extends EntityCreatedEvent<Person> { …​ })时才有效。

在某些情况下,如果所有事件都遵循相同的结构,这可能会变得非常乏味(就像前面示例中的事件一样)。在这种情况下,您可以实施ResolvableTypeProvider以引导框架超出运行时环境提供的范围。以下事件显示了如何执行此操作:

java
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()));
    }
}
科特林
class EntityCreatedEvent<T>(entity: T) : ApplicationEvent(entity), ResolvableTypeProvider {

    override fun getResolvableType(): ResolvableType? {
        return ResolvableType.forClassWithGenerics(javaClass, ResolvableType.forInstance(getSource()))
    }
}
这不仅适用于ApplicationEvent您作为事件发送的任何任意对象。

1.15.3。方便访问底层资源

为了优化使用和理解应用程序上下文,您应该熟悉 Spring 的Resource抽象,如参考资料中所述。

应用程序上下文是ResourceLoader,可用于加载Resource对象。AResource本质上是 JDKjava.net.URL类的功能更丰富的版本。事实上,在适当的地方Resource包装一个实例的实现。java.net.URLAResource可以以透明的方式从几乎任何位置获取低级资源,包括从类路径、文件系统位置、可使用标准 URL 描述的任何位置以及其他一些变体。如果资源位置字符串是没有任何特殊前缀的简单路径,则这些资源的来源是特定的并且适合于实际的应用程序上下文类型。

您可以配置部署到应用程序上下文中的 bean 来实现特殊的回调接口,ResourceLoaderAware在初始化时自动回调,应用程序上下文本身作为ResourceLoader. 您还可以公开 type 的属性,Resource用于访问静态资源。它们像任何其他属性一样被注入其中。您可以将这些Resource 属性指定为简单路径,并在部署 bean 时String依赖从这些文本字符串到实际对象的自动转换。Resource

提供给构造函数的一个或多个位置路径ApplicationContext实际上是资源字符串,并且以简单的形式,根据特定的上下文实现进行适当的处​​理。例如ClassPathXmlApplicationContext,将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或 URL 加载定义,而不管实际的上下文类型如何。

1.15.4。应用程序启动跟踪

ApplicationContext管理 Spring 应用程序的生命周期并围绕组件提供丰富的编程模型。因此,复杂的应用程序可以具有同样复杂的组件图和启动阶段。

使用特定指标跟踪应用程序启动步骤可以帮助了解在启动阶段花费的时间,但它也可以用作更好地了解整个上下文生命周期的一种方式。

AbstractApplicationContext及其子类)使用 进行检测 ApplicationStartup,它收集StartupStep有关各种启动阶段的数据:

  • 应用程序上下文生命周期(基础包扫描、配置类管理)

  • bean 生命周期(实例化、智能初始化、后处理)

  • 应用事件处理

以下是仪器仪表的示例AnnotationConfigApplicationContext

java
// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();
科特林
// create a startup step and start recording
val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan")
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages))
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages)
// end the current step
scanPackages.end()

应用程序上下文已经配备了多个步骤。记录后,可以使用特定工具收集、显示和分析这些启动步骤。有关现有启动步骤的完整列表,您可以查看 专用的附录部分

默认ApplicationStartup实现是无操作变体,以最小化开销。这意味着默认情况下在应用程序启动期间不会收集任何指标。Spring Framework 附带了一个使用 Java Flight Recorder 跟踪启动步骤的实现: FlightRecorderApplicationStartup. 要使用此变体,您必须在创建它后立即配置它的实例ApplicationContext

ApplicationStartup如果开发人员提供自己的 AbstractApplicationContext子类,或者如果他们希望收集更精确的数据,他们也可以使用基础设施。

ApplicationStartup仅在应用程序启动期间和核心容器中使用;这绝不是 Java 分析器或Micrometer等指标库的替代品。

要开始收集 custom StartupStep,组件可以ApplicationStartup 直接从应用程序上下文中获取实例,使其组件实现ApplicationStartupAware,或者在任何注入点请求ApplicationStartup类型。

"spring.*"开发人员在创建自定义启动步骤时 不应使用命名空间。这个命名空间是为内部 Spring 使用而保留的,并且可能会发生变化。

1.15.5。方便的 Web 应用程序 ApplicationContext 实例化

您可以ApplicationContext使用例如 a 以声明方式创建实例 ContextLoader。当然,您也可以ApplicationContext使用其中一种ApplicationContext实现以编程方式创建实例。

ApplicationContext您可以使用注册一个ContextLoaderListener,如以下示例所示:

<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用作默认值。当参数确实存在时,侦听String器使用预定义的分隔符(逗号、分号和空格)分隔 ,并将这些值用作搜索应用程序上下文的位置。也支持 Ant 样式的路径模式。示例是/WEB-INF/*Context.xml(对于名称以 结尾 Context.xml且驻留在WEB-INF目录中的所有文件)和/WEB-INF/**/*Context.xml (对于 的任何子目录中的所有此类文件WEB-INF)。

1.15.6。将 Spring 部署ApplicationContext为 Java EE RAR 文件

可以将 Spring 部署ApplicationContext为 RAR 文件,将上下文及其所有必需的 bean 类和库 JAR 封装在 Java EE RAR 部署单元中。ApplicationContext这相当于引导一个能够访问 Java EE 服务器设施的独立设备(仅托管在 Java EE 环境中)。RAR 部署是部署无头 WAR 文件的一种更自然的替代方案——实际上,一个没有任何 HTTP 入口点的 WAR 文件仅用于 ApplicationContext在 Java EE 环境中引导 Spring。

RAR 部署非常适合不需要 HTTP 入口点而是仅包含消息端点和计划作业的应用程序上下文。这种上下文中的 Bean 可以使用应用程序服务器资源,例如 JTA 事务管理器和 JNDI 绑定的 JDBC DataSource实例和 JMSConnectionFactory实例,还可以向平台的 JMX 服务器注册——所有这些都通过 Spring 的标准事务管理和 JNDI 和 JMX 支持工具。应用程序组件还可以WorkManager通过 Spring 的TaskExecutor抽象与应用程序服务器的 JCA 交互。

SpringContextResourceAdapter 有关RAR 部署中涉及的配置详细信息,请参阅该类的 javadoc 。

对于将 Spring ApplicationContext 部署为 Java EE RAR 文件的简单部署:

  1. 将所有应用程序类打包成一个 RAR 文件(这是一个具有不同文件扩展名的标准 JAR 文件)。

  2. 将所有必需的库 JAR 添加到 RAR 存档的根目录中。

  3. 添加 META-INF/ra.xml部署描述符(如javadoc 中SpringContextResourceAdapter所示)和相应的 Spring XML bean 定义文件(通常 META-INF/applicationContext.xml)。

  4. 将生成的 RAR 文件拖放到应用程序服务器的部署目录中。

这种 RAR 部署单元通常是独立的。它们不会将组件暴露给外部世界,甚至不会暴露给同一应用程序的其他模块。与基于 RAR 的交互ApplicationContext通常通过它与其他模块共享的 JMS 目标发生。例如,基于 RAR 的程序ApplicationContext还可以安排一些作业或对文件系统中的新文件(或类似文件)做出反应。如果它需要允许来自外部的同步访问,它可以(例如)导出 RMI 端点,这些端点可能被同一台机器上的其他应用程序模块使用。

1.16。BeanFactoryAPI _

API 为 Spring的BeanFactoryIoC 功能提供了底层基础。它的具体契约多用于与 Spring 的其他部分和相关的第三方框架的集成,它的DefaultListableBeanFactory实现是上层GenericApplicationContext容器内的关键委托。

BeanFactory和相关的接口(例如BeanFactoryAwareInitializingBeanDisposableBean)是其他框架组件的重要集成点。通过不需要任何注释甚至反射,它们允许容器与其组件之间非常有效的交互。应用程序级别的 bean 可以使用相同的回调接口,但通常更喜欢声明性依赖注入,或者通过注释或通过编程配置。

请注意,核心BeanFactoryAPI 级别及其DefaultListableBeanFactory 实现不会对要使用的配置格式或任何组件注释做出假设。所有这些风格都通过扩展(例如XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor)出现,并将共享BeanDefinition对象作为核心元数据表示进行操作。这就是使 Spring 的容器如此灵活和可扩展的本质。

1.16.1。BeanFactory还是ApplicationContext

BeanFactory本节解释了容器级别和 容器级别之间的差异ApplicationContext以及对引导的影响。

ApplicationContext除非您有充分的理由不这样做,否则 您应该使用 anGenericApplicationContext及其子类AnnotationConfigApplicationContext 作为自定义引导的常见实现。这些是 Spring 核心容器的主要入口点,用于所有常见目的:加载配置文件、触发类路径扫描、以编程方式注册 bean 定义和带注释的类,以及(从 5.0 开始)注册功能性 bean 定义。

因为 anApplicationContext包含 a 的所有功能BeanFactory,所以通常建议在 plain 上使用BeanFactory,除了需要完全控制 bean 处理的场景。在一个ApplicationContext(例如 GenericApplicationContext实现)中,按照约定(即按 bean 名称或按 bean 类型——特别是后处理器)检测几种 bean,而 plainDefaultListableBeanFactory对任何特殊 bean 是不可知的。

对于许多扩展容器特性,例如注解处理和 AOP 代理,BeanPostProcessor扩展点是必不可少的。如果您仅使用普通DefaultListableBeanFactory的,则默认情况下不会检测和激活此类后处理器。这种情况可能会令人困惑,因为您的 bean 配置实际上没有任何问题。相反,在这种情况下,需要通过额外的设置来完全引导容器。

下表列出了BeanFactoryApplicationContext接口和实现提供的功能。

表 9. 特征矩阵
特征 BeanFactory ApplicationContext

Bean实例化/接线

是的

是的

集成的生命周期管理

是的

自动BeanPostProcessor注册

是的

自动BeanFactoryPostProcessor注册

是的

方便MessageSource的访问(国际化)

是的

内置ApplicationEvent发布机制

是的

要使用 显式注册 bean 后处理器DefaultListableBeanFactory,您需要以编程方式调用addBeanPostProcessor,如以下示例所示:

java
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
科特林
val factory = DefaultListableBeanFactory()
// populate the factory with bean definitions

// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(AutowiredAnnotationBeanPostProcessor())
factory.addBeanPostProcessor(MyBeanPostProcessor())

// now start using the factory

要将 aBeanFactoryPostProcessor应用于 plain DefaultListableBeanFactory,您需要调用其postProcessBeanFactory方法,如以下示例所示:

java
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);
科特林
val factory = DefaultListableBeanFactory()
val reader = XmlBeanDefinitionReader(factory)
reader.loadBeanDefinitions(FileSystemResource("beans.xml"))

// bring in some property values from a Properties file
val cfg = PropertySourcesPlaceholderConfigurer()
cfg.setLocation(FileSystemResource("jdbc.properties"))

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

在这两种情况下,显式注册步骤都不方便,这就是为什么在 Spring 支持的应用程序中,各种ApplicationContext变体比普通的更受青睐 ,尤其是在典型企业设置中依赖实例来扩展容器功能时DefaultListableBeanFactoryBeanFactoryPostProcessorBeanPostProcessor

AnAnnotationConfigApplicationContext已注册所有常见的注释后处理器,并且可以通过配置注释引入额外的处理器,例如@EnableTransactionManagement. 在 Spring 的基于注解的配置模型的抽象级别上,bean 后处理器的概念变成了单纯的内部容器细节。

2. 资源

本章介绍 Spring 如何处理资源以及如何在 Spring 中使用资源。它包括以下主题:

2.1。介绍

不幸的是, Java 的java.net.URL各种 URL 前缀的标准类和标准处理程序不足以满足所有对低级资源的访问。例如,没有标准化的URL实现可用于访问需要从类路径或相对于 ServletContext. 虽然可以为专门的URL 前缀注册新的处理程序(类似于现有的前缀处理程序http:URL如指向。

2.2. Resource界面_

Resource位于包中的Spring接口org.springframework.core.io.旨在成为一个更强大的接口,用于抽象对低级资源的访问。以下清单提供了该Resource界面的概述。有关详细信息,请参阅 Resourcejavadoc。

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isReadable();

    boolean isOpen();

    boolean isFile();

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    ReadableByteChannel readableChannel() throws IOException;

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();
}

正如接口的定义Resource所示,它扩展了InputStreamSource 接口。以下清单显示了InputStreamSource 接口的定义:

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;
}

接口中一些最重要的方法Resource是:

  • getInputStream():定位并打开资源,返回一个InputStream用于从资源中读取。预计每次调用都会返回一个新的 InputStream. 调用者有责任关闭流。

  • exists():返回一个boolean指示此资源是否实际以物理形式存在的值。

  • isOpen():返回一个boolean指示此资源是否表示具有打开流的句柄。如果true, 则InputStream不能多次读取,必须只读取一次然后关闭以避免资源泄漏。返回false所有常用资源实现,除了InputStreamResource.

  • getDescription():返回此资源的描述,用于在使用该资源时输出错误。这通常是完全限定的文件名或资源的实际 URL。

其他方法让您获得表示资源的实际URLFile对象(如果底层实现兼容并支持该功能)。

接口的一些实现还为支持写入的资源Resource实现了扩展 接口。WritableResource

Spring 本身Resource广泛使用抽象,在需要资源时作为许多方法签名中的参数类型。某些 Spring API 中的其他方法(例如各种ApplicationContext实现的构造函数)采用 String朴素或简单的形式来创建Resource适合于该上下文实现的方法,或者通过String路径上的特殊前缀,让调用者指定特定的Resource实现必须创建和使用。

虽然Resource接口在 Spring 和 Spring 中被大量使用,但实际上在您自己的代码中将其本身用作通用实用程序类非常方便,用于访问资源,即使您的代码不知道或不关心任何其他部分Spring。虽然这会将您的代码与 Spring 耦合,但它实际上只将它耦合到这一小部分实用程序类,它可以作为更强大的替代品,URL并且可以被认为等同于您将用于此目的的任何其他库。

Resource抽象不会取代功能 。它尽可能地包裹它。例如,aUrlResource包装一个 URL 并使用被包装URL的来完成它的工作。

2.3. 内置Resource实现

Spring 包括几个内置的Resource实现:

有关 Spring 中可用实现的完整列表,请参阅javadocResource的“所有已知的实现类”部分 。Resource

2.3.1。UrlResource

UrlResource包装 ajava.net.URL并可用于访问通常可通过 URL 访问的任何对象,例如文件、HTTPS 目标、FTP 目标等。所有 URL 都有一个标准化的String表示,因此使用适当的标准化前缀来指示一个 URL 类型与另一个 URL 类型。这包括 file:访问文件系统路径、https:通过 HTTPS 协议ftp:访问资源、通过 FTP 访问资源等。

AUrlResource是由 Java 代码通过显式使用UrlResource构造函数创建的,但通常是在调用 API 方法时隐式创建的,该方法采用String 表示路径的参数。对于后一种情况,JavaBeansPropertyEditor 最终决定创建哪种类型Resource。如果路径字符串包含一个众所周知的(对属性编辑器而言)前缀(例如),它会为该前缀classpath:创建一个适当的专用化。Resource但是,如果它不能识别前缀,它会假定该字符串是标准 URL 字符串并创建一个UrlResource.

2.3.2.ClassPathResource

此类表示应从类路径获取的资源。它使用线程上下文类加载器、给定的类加载器或给定的类来加载资源。

如果类路径资源驻留在文件系统中,则此Resource实现支持解析,java.io.File但不适用于驻留在 jar 中且尚未扩展(通过 servlet 引擎或任何环境)到文件系统的类路径资源。为了解决这个问题,各种Resource实现始终支持解析为java.net.URL.

AClassPathResource是由 Java 代码通过显式使用ClassPathResource 构造函数创建的,但通常是在调用 API 方法时隐式创建的,该方法采用 String表示路径的参数。对于后一种情况,JavaBeans PropertyEditor识别字符串路径上的特殊前缀 ,classpath:并在这种情况下创建 a ClassPathResource

2.3.3。FileSystemResource

这是句柄的Resource实现。java.io.File它还支持 java.nio.file.Path句柄,应用 Spring 的标准基于字符串的路径转换,但通过java.nio.file.FilesAPI 执行所有操作。对于纯 java.nio.path.Path基于支持,请改用 a PathResourceFileSystemResource 支持分辨率为 aFile和 as a URL

2.3.4。PathResource

这是句柄的Resource实现,通过API 执行所有操作和转换。它支持解析为 a和 as a并且还实现了扩展接口。 实际上是具有不同行为的纯基于替代方案。java.nio.file.PathPathFileURLWritableResourcePathResourcejava.nio.path.PathFileSystemResourcecreateRelative

2.3.5。ServletContextResource

这是解释相关 Web 应用程序根目录中的相对路径Resource的资源的实现。ServletContext

它始终支持流访问和 URL 访问,但java.io.File仅在扩展 Web 应用程序存档且资源物理位于文件系统上时才允许访问。它是否被扩展并在文件系统上或直接从 JAR 或其他地方(如数据库)访问(这是可以想象的)实际上取决于 Servlet 容器。

2.3.6。InputStreamResource

AnInputStreamResourceResource给定 的实现InputStreamResource只有在没有特定实现适用时才应使用它。特别是,在可能的情况下,首选ByteArrayResource或任何基于文件的Resource 实现。

与其他Resource实现相比,这是一个已打开资源的描述符。因此,它trueisOpen(). 如果您需要将资源描述符保存在某处或需要多次读取流,请不要使用它。

2.3.7。ByteArrayResource

这是Resource给定字节数组的实现。它为给定的字节数组创建一个 ByteArrayInputStream

它对于从任何给定的字节数组加载内容很有用,而不必求助于单次使用InputStreamResource

2.4. ResourceLoader界面_

ResourceLoader接口旨在由可以返回(即加载)Resource实例的对象实现。以下清单显示了ResourceLoader 接口定义:

public interface ResourceLoader {

    Resource getResource(String location);

    ClassLoader getClassLoader();
}

所有应用程序上下文都实现了该ResourceLoader接口。因此,所有应用程序上下文都可以用于获取Resource实例。

当您调用getResource()特定的应用程序上下文,并且指定的位置路径没有特定前缀时,您将返回Resource适合该特定应用程序上下文的类型。例如,假设下面的代码片段是针对一个ClassPathXmlApplicationContext实例运行的:

java
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
科特林
val template = ctx.getResource("some/resource/path/myTemplate.txt")

针对 a ClassPathXmlApplicationContext,该代码返回 a ClassPathResource。如果对FileSystemXmlApplicationContext实例运行相同的方法,它将返回一个FileSystemResource. 对于 a WebApplicationContext,它将返回 a ServletContextResource。它同样会为每个上下文返回适当的对象。

因此,您可以以适合特定应用程序上下文的方式加载资源。

另一方面,您也可以ClassPathResource通过指定特殊前缀来强制使用,无论应用程序上下文类型如何classpath:,如以下示例所示:

java
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
科特林
val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt")

UrlResource同样,您可以通过指定任何标准 java.net.URL前缀来强制使用 a 。以下示例使用fileandhttps前缀:

java
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
科特林
val template = ctx.getResource("file:///some/resource/path/myTemplate.txt")
java
Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");
科特林
val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt")

下表总结了将String对象转换为Resource 对象的策略:

表 10. 资源字符串
字首 例子 解释

类路径:

classpath:com/myapp/config.xml

从类路径加载。

文件:

file:///data/config.xml

URL从文件系统加载为 a 。另请参阅FileSystemResource注意事项

HTTPS:

https://myserver/logo.png

加载为URL.

(没有任何)

/data/config.xml

取决于底层ApplicationContext

2.5. ResourcePatternResolver界面_

ResourcePatternResolver接口是接口的扩展,它ResourceLoader定义了将位置模式(例如,Ant 风格的路径模式)解析为Resource对象的策略。

public interface ResourcePatternResolver extends ResourceLoader {

    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    Resource[] getResources(String locationPattern) throws IOException;
}

从上面可以看出,这个接口还classpath*:为类路径中所有匹配的资源定义了一个特殊的资源前缀。请注意,在这种情况下,资源位置应该是没有占位符的路径,例如 classpath*:/config/beans.xml. JAR 文件或类路径中的不同目录可以包含多个具有相同路径和相同名称的文件。有关 使用资源前缀的通配符支持的更多详细信息,请参阅应用程序上下文构造函数资源路径中的通配符及其小节。classpath*:

传入的ResourceLoader(例如,通过 ResourceLoaderAware语义提供的)可以检查它是否也实现了这个扩展接口。

PathMatchingResourcePatternResolver是一个独立的实现,可在 an 外部使用ApplicationContext,也可ResourceArrayPropertyEditor用于填充Resource[]bean 属性。PathMatchingResourcePatternResolver能够将指定的资源位置路径解析为一个或多个匹配Resource对象。源路径可以是一个简单的路径,它与目标具有一对一的映射关系 Resource,或者可以包含特殊classpath*:前缀和/或内部 Ant 样式的正则表达式(使用 Spring 的 org.springframework.util.AntPathMatcher实用程序匹配)。后者都是有效的通配符。

ResourceLoader任何标准中的默认值ApplicationContext实际上都是PathMatchingResourcePatternResolver实现ResourcePatternResolver 接口的实例。ApplicationContext实例本身也是如此,它也实现了ResourcePatternResolver接口并委托给 default PathMatchingResourcePatternResolver

2.6. ResourceLoaderAware界面_

ResourceLoaderAware接口是一个特殊的回调接口,用于标识期望提供ResourceLoader引用的组件。以下清单显示了ResourceLoaderAware接口的定义:

public interface ResourceLoaderAware {

    void setResourceLoader(ResourceLoader resourceLoader);
}

当一个类实现ResourceLoaderAware并部署到应用程序上下文(作为 Spring 管理的 bean)中时,它被ResourceLoaderAware应用程序上下文识别为。然后应用程序上下文调用setResourceLoader(ResourceLoader),将自己作为参数提供(请记住,Spring 中的所有应用程序上下文都实现了该ResourceLoader接口)。

由于 anApplicationContext是 a ResourceLoader,因此 bean 也可以实现 ApplicationContextAware接口并直接使用提供的应用程序上下文来加载资源。但是,一般来说,ResourceLoader 如果您只需要专用接口,最好使用专用接口。该代码将仅与资源加载接口(可以认为是实用程序接口)耦合,而不与整个 Spring ApplicationContext接口耦合。

在应用程序组件中,您还可以依靠自动装配来ResourceLoader替代实现ResourceLoaderAware接口。传统 constructor模式和自动装配byType模式(如Autowiring Collaborators中所述)能够分别为ResourceLoader构造函数参数或 setter 方法参数提供一个。要获得更大的灵活性(包括自动装配字段和多个参数方法的能力),请考虑使用基于注释的自动装配功能。在这种情况下,只要相关字段、构造函数或方法带有注释,ResourceLoader就会自动装配到期望类型的字段、构造函数参数或方法参数中。有关详细信息,请参阅使用ResourceLoader@Autowired@Autowired.

Resource要为包含通配符或使用特殊资源前缀的资源路径 加载一个或多个对象classpath*:,请考虑将 autowired 实例 ResourcePatternResolver而不是ResourceLoader.

2.7. 资源作为依赖

如果 bean 本身要通过某种动态过程来确定和提供资源路径,那么 bean 使用ResourceLoaderor ResourcePatternResolver接口来加载资源可能是有意义的。例如,考虑加载某种模板,其中所需的特定资源取决于用户的角色。ResourceLoader如果资源是静态的,那么完全消除接口(或接口)的使用是有意义的 ResourcePatternResolver,让 bean 公开Resource它需要的属性,并期望它们被注入其中。

然后注入这些属性变得微不足道的是,所有应用程序上下文都注册并使用一个特殊的 JavaBeans PropertyEditor,它可以将String路径转换为Resource对象。例如,以下MyBean类具有template type 的属性Resource

java
package example;

public class MyBean {

    private Resource template;

    public setTemplate(Resource template) {
        this.template = template;
    }

    // ...
}
科特林
class MyBean(var template: Resource)

在 XML 配置文件中,template可以使用该资源的简单字符串配置属性,如以下示例所示:

<bean id="myBean" class="example.MyBean">
    <property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

请注意,资源路径没有前缀。因此,由于应用程序上下文本身将用作ResourceLoader,因此资源通过 a ClassPathResource、 aFileSystemResource或 a加载ServletContextResource,具体取决于应用程序上下文的确切类型。

如果需要强制使用特定Resource类型,可以使用前缀。以下两个示例展示了如何强制使用 aClassPathResource和 a UrlResource(后者用于访问文件系统中的文件):

<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>

如果MyBean该类被重构以用于注释驱动的配置,则myTemplate.txt可以将路径存储在名为的键下template.path - 例如,在 Spring 可用的属性文件中Environment(请参阅 环境抽象)。然后可以@Value 使用属性占位符通过注释引用模板路径(请参阅使用@Value)。Spring 会将模板路径的值作为字符串检索,而 specialPropertyEditor会将字符串转换Resource为要注入MyBean构造函数的对象。以下示例演示了如何实现此目的。

java
@Component
public class MyBean {

    private final Resource template;

    public MyBean(@Value("${template.path}") Resource template) {
        this.template = template;
    }

    // ...
}
科特林
@Component
class MyBean(@Value("\${template.path}") private val template: Resource)

如果我们想支持在类路径中多个位置的同一路径下发现的多个模板——例如,在类路径中的多个 jar 中——我们可以使用特殊classpath*:前缀和通配符将templates.path键定义为 classpath*:/config/templates/*.txt. 如果我们MyBean如下重新定义类,Spring 会将模板路径模式转换为Resource可以注入MyBean构造函数的对象数组。

java
@Component
public class MyBean {

    private final Resource[] templates;

    public MyBean(@Value("${templates.path}") Resource[] templates) {
        this.templates = templates;
    }

    // ...
}
科特林
@Component
class MyBean(@Value("\${templates.path}") private val templates: Resource[])

2.8. 应用程序上下文和资源路径

本节介绍如何使用资源创建应用程序上下文,包括使用 XML 的快捷方式、如何使用通配符以及其他详细信息。

2.8.1. 构建应用程序上下文

应用程序上下文构造函数(针对特定应用程序上下文类型)通常采用字符串或字符串数​​组作为资源的位置路径,例如构成上下文定义的 XML 文件。

当这样的位置路径没有前缀时,Resource从该路径构建并用于加载 bean 定义的特定类型取决于并适用于特定的应用程序上下文。例如,考虑以下示例,该示例创建一个 ClassPathXmlApplicationContext

java
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
科特林
val ctx = ClassPathXmlApplicationContext("conf/appContext.xml")

bean 定义是从类路径加载的,因为ClassPathResource使用了 a。但是,请考虑以下示例,该示例创建一个FileSystemXmlApplicationContext

java
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/appContext.xml");
科特林
val ctx = FileSystemXmlApplicationContext("conf/appContext.xml")

现在 bean 定义从文件系统位置加载(在这种情况下,相对于当前工作目录)。

请注意,classpath在位置路径上使用特殊前缀或标准 URL 前缀会覆盖默认类型Resourcecreated 以加载 bean 定义。考虑以下示例:

java
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
科特林
val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml")

使用FileSystemXmlApplicationContext从类路径加载 bean 定义。但是,它仍然是一个FileSystemXmlApplicationContext. 如果随后将其用作 ResourceLoader,则任何未加前缀的路径仍被视为文件系统路径。

构造ClassPathXmlApplicationContext实例——快捷方式

ClassPathXmlApplicationContext公开了许多构造函数以实现方便的实例化。基本思想是您可以仅提供一个字符串数组,该数组仅包含 XML 文件本身的文件名(没有前导路径信息),还可以提供一个Class. 然后ClassPathXmlApplicationContext从提供的类派生路径信息。

考虑以下目录布局:

com/
  例子/
    服务.xml
    存储库.xml
    MessengerService.class

以下示例显示了如何实例化由在名为andClassPathXmlApplicationContext的文件中定义的 bean 组成的实例(位于类路径中):services.xmlrepositories.xml

java
ApplicationContext ctx = new ClassPathXmlApplicationContext(
    new String[] {"services.xml", "repositories.xml"}, MessengerService.class);
科特林
val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java)

有关各种构造函数的详细信息,请参阅ClassPathXmlApplicationContext javadoc。

2.8.2. 应用程序上下文构造函数资源路径中的通配符

应用程序上下文构造函数值中的资源路径可能是简单路径(如前所示),每个路径都具有到目标的一对一映射Resource,或者,可能包含特殊classpath*:前缀或内部 Ant 样式模式(匹配使用 Spring 的PathMatcher实用程序)。后者都是有效的通配符。

这种机制的一个用途是当您需要进行组件式应用程序组装时。所有组件都可以将上下文定义片段发布到一个众所周知的位置路径,并且,当最终的应用程序上下文使用前缀为 的相同路径创建时 classpath*:,所有组件片段都会被自动拾取。

请注意,此通配符特定于应用程序上下文构造函数中资源路径的使用(或当您PathMatcher直接使用实用程序类层次结构时),并在构造时解析。Resource它与类型本身无关。您不能使用classpath*:前缀来构造实际的Resource,因为资源一次仅指向一个资源。

蚂蚁风格的图案

路径位置可以包含 Ant 样式的模式,如以下示例所示:

/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
文件:C:/some/path/*-context.xml
类路径:com/mycompany/**/applicationContext.xml

当路径位置包含 Ant 样式模式时,解析器会遵循更复杂的过程来尝试解析通配符。它Resource为直到最后一个非通配符段的路径生成一个,并从中获取一个 URL。如果此 URL 不是jar:URL 或特定于容器的变体(例如zip:在 WebLogic、wsjarWebSphere 等中),java.io.File则从中获取 a 并用于通过遍历文件系统来解析通配符。对于 jar URL,解析器要么从中获取 a java.net.JarURLConnection,要么手动解析 jar URL,然后遍历 jar 文件的内容来解析通配符。

对可移植性的影响

如果指定的路径已经是一个fileURL(隐含地因为基础 ResourceLoader是一个文件系统,或者显式地),通配符保证以完全可移植的方式工作。

如果指定路径是位置,则解析器必须通过调用classpath获取最后一个非通配符路径段 URL 。Classloader.getResource()由于这只是路径的一个节点(不是末尾的文件),因此实际上未定义(在 ClassLoaderjavadoc 中)在这种情况下返回的 URL 类型。在实践中,它总是java.io.File代表目录(类路径资源解析为文件系统位置)或某种 jar URL(类路径资源解析为 jar 位置)。尽管如此,此操作仍存在可移植性问题。

如果为最后一个非通配符段获取 jar URL,则解析器必须能够从中获取一个java.net.JarURLConnection或手动解析 jar URL,以便能够遍历 jar 的内容并解析通配符。这在大多数环境中都有效,但在其他环境中失败,我们强烈建议在您依赖它之前,在您的特定环境中彻底测试来自 jar 的资源的通配符解析。

classpath*:前缀_

在构建基于 XML 的应用程序上下文时,位置字符串可能会使用特殊classpath*:前缀,如以下示例所示:

java
ApplicationContext ctx =
    new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");
科特林
val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml")

这个特殊的前缀指定必须获取与给定名称匹配的所有类路径资源(在内部,这基本上是通过对 的调用发生的 ClassLoader.getResources(…​)),然后合并以形成最终的应用程序上下文定义。

通配符类路径依赖于getResources()底层的方法 ClassLoader。由于现在大多数应用程序服务器都提供自己的ClassLoader 实现,因此行为可能会有所不同,尤其是在处理 jar 文件时。检查是否classpath*有效的一个简单测试是使用ClassLoader从类路径上的 jar 中加载文件: getClass().getClassLoader().getResources("<someFileInsideTheJar>"). 尝试使用具有相同名称但位于两个不同位置的文件进行此测试 - 例如,具有相同名称和相同路径但在类路径上的不同 jar 中的文件。如果返回不适当的结果,请检查应用程序服务器文档以获取可能影响ClassLoader行为的设置。

您还可以将classpath*:前缀与PathMatcher位置路径的其余部分中的模式结合起来(例如,classpath*:META-INF/*-beans.xml)。在这种情况下,解析策略相当简单:ClassLoader.getResources()在最后一个非通配符路径段上使用调用以获取类加载器层次结构中的所有匹配资源,然后,在每个资源上,PathMatcher使用前面描述的相同解析策略通配符子路径。

与通配符有关的其他说明

请注意classpath*:,当与 Ant 样式模式结合使用时,仅在模式开始之前至少与一个根目录可靠地工作,除非实际的目标文件驻留在文件系统中。这意味着诸如此类的模式 classpath*:*.xml可能不会从 jar 文件的根目录中检索文件,而只能从扩展目录的根目录中检索文件。

Spring 检索类路径条目的能力源自 JDK 的 ClassLoader.getResources()方法,该方法仅返回空字符串的文件系统位置(指示要搜索的潜在根)。Spring 也会评估 URLClassLoader运行时配置和java.class.pathjar 文件中的清单,但这并不保证会导致可移植行为。

类路径包的扫描需要类路径中存在相应的目录条目。使用 Ant 构建 JAR 时,不要激活files-only JAR 任务的开关。此外,根据某些环境中的安全策略,类路径目录可能不会暴露——例如,JDK 1.7.0_45 及更高版本上的独立应用程序(这需要在清单中设置“可信库”。请参阅 https:// /stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常按预期工作。在这里也强烈建议将资源放入专用目录,避免上述搜索 jar 文件根级别的可移植性问题。

classpath:如果要搜索的根包在多个类路径位置中可用,则不能保证具有资源的 Ant 样式模式找到匹配的资源。考虑以下资源位置示例:

com/mycompany/package1/service-context.xml

现在考虑一个可能有人用来尝试查找该文件的 Ant 样式路径:

类路径:com/mycompany/**/service-context.xml

这样的资源可能只存在于类路径中的一个位置,但是当使用诸如前面示例之类的路径来尝试解析它时,解析器会处理getResource("com/mycompany");. 如果此基础包节点存在于多个ClassLoader位置,则所需资源可能不存在于找到的第一个位置。因此,在这种情况下,您应该更喜欢使用classpath*:相同的 Ant 样式模式,该模式搜索包含 com.mycompany基本包的所有类路径位置:classpath*:com/mycompany/**/service-context.xml.

2.8.3. FileSystemResource注意事项

FileSystemResource未附加到 a 的A FileSystemApplicationContext(即,当 aFileSystemApplicationContext不是实际的时ResourceLoader)按照您的预期处理绝对和相对路径。相对路径是相对于当前工作目录的,而绝对路径是相对于文件系统的根目录的。

然而,出于向后兼容性(历史)的原因, FileSystemApplicationContextResourceLoader. FileSystemApplicationContext强制所有附加实例将 FileSystemResource所有位置路径视为相对路径,无论它们是否以斜杠开头。实际上,这意味着以下示例是等效的:

java
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/context.xml");
科特林
val ctx = FileSystemXmlApplicationContext("conf/context.xml")
java
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("/conf/context.xml");
科特林
val ctx = FileSystemXmlApplicationContext("/conf/context.xml")

以下示例也是等价的(即使它们不同是有意义的,因为一种情况是相对的,另一种是绝对的):

java
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
科特林
val ctx: FileSystemXmlApplicationContext = ...
ctx.getResource("some/resource/path/myTemplate.txt")
java
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");
科特林
val ctx: FileSystemXmlApplicationContext = ...
ctx.getResource("/some/resource/path/myTemplate.txt")

在实践中,如果您需要真正的绝对文件系统路径,则应避免将绝对路径与FileSystemResourceor一起使用,并通过使用URL 前缀来FileSystemXmlApplicationContext强制使用 a 。以下示例显示了如何执行此操作:UrlResourcefile:

java
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
科特林
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt")
java
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("file:///conf/context.xml");
科特林
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml")

3. 验证、数据绑定和类型转换

将验证视为业务逻辑有利有弊,Spring 提供了一种验证(和数据绑定)设计,不排除其中任何一个。具体来说,验证不应该绑定到 Web 层并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring 提供了一个Validator在应用程序的每一层都基本可用的契约。

数据绑定对于让用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)很有用。Spring 提供了恰当的名称DataBinder来做到这一点。和组成包Validator, 主要用于但不限于web层。DataBindervalidation

BeanWrapper是 Spring 框架中的一个基本概念,在很多地方都有使用。但是,您可能不需要BeanWrapper 直接使用。但是,因为这是参考文档,所以我们认为可能需要一些解释。我们将BeanWrapper在本章中解释它,因为如果您要使用它,您很可能在尝试将数据绑定到对象时这样做。

SpringDataBinder和较低级别BeanWrapper都使用PropertyEditorSupport 实现来解析和格式化属性值。PropertyEditor和 类型是 JavaBeans 规范的PropertyEditorSupport一部分,本章也进行了说明。Spring 3 引入了一个core.convert提供通用类型转换工具的包,以及一个用于格式化 UI 字段值的更高级别的“格式”包。您可以将这些包用作实现的更简单的替代 PropertyEditorSupport方案。本章还将讨论它们。

Spring 通过设置基础设施和 Spring 自己的Validator合同的适配器来支持 Java Bean Validation。应用程序可以全局启用一次 Bean 验证,如Java Bean Validation中所述,并将其专门用于所有验证需求。在 web 层,应用程序可以进一步注册控制器本地 Spring Validator实例 per DataBinder,如配置 aDataBinder中所述,这对于插入自定义验证逻辑很有用。

3.1。使用 Spring 的 Validator 接口进行验证

Spring 提供了一个Validator接口,您可以使用它来验证对象。该 Validator接口通过使用一个Errors对象来工作,以便在验证时,验证器可以向该Errors对象报告验证失败。

考虑以下小数据对象的示例:

java
public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}
科特林
class Person(val name: String, val age: Int)

Person下一个示例通过实现接口的以下两个方法为类提供验证行为org.springframework.validation.Validator

  • supports(Class):这可以Validator验证提供的实例Class吗?

  • validate(Object, org.springframework.validation.Errors):验证给定对象,并在验证错误的情况下,将这些对象注册到给定Errors对象。

实现 aValidator相当简单,尤其是当您知道 ValidationUtilsSpring 框架也提供了帮助程序类时。以下示例ValidatorPerson实例实现:

java
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");
        }
    }
}
科特林
class PersonValidator : Validator {

    /**
     * This Validator validates only Person instances
     */
    override fun supports(clazz: Class<*>): Boolean {
        return Person::class.java == clazz
    }

    override fun validate(obj: Any, e: Errors) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty")
        val p = obj as Person
        if (p.age < 0) {
            e.rejectValue("age", "negativevalue")
        } else if (p.age > 110) {
            e.rejectValue("age", "too.darn.old")
        }
    }
}

类上的static rejectIfEmpty(..)方法ValidationUtils用于拒绝name属性(如果是)null或空字符串。查看 ValidationUtilsjavadoc 以了解除了前面显示的示例之外它还提供了哪些功能。

虽然实现单个Validator类来验证丰富对象中的每个嵌套对象当然是可能的,但最好将每个嵌套对象类的验证逻辑封装在其自己的Validator实现中。“丰富”对象的一个​​简单示例Customer是由两个String 属性(第一个和第二个名称)和一个复杂Address对象组成的。Address对象可以独立于Customer对象使用,因此实现了 distinct AddressValidator 。如果您希望CustomerValidator重用类中包含的逻辑AddressValidator而不使用复制和粘贴,您可以AddressValidator在您的 中进行依赖注入或实例化CustomerValidator,如以下示例所示:

java
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();
        }
    }
}
科特林
class CustomerValidator(private val addressValidator: Validator) : Validator {

    init {
        if (addressValidator == null) {
            throw IllegalArgumentException("The supplied [Validator] is required and must not be null.")
        }
        if (!addressValidator.supports(Address::class.java)) {
            throw IllegalArgumentException("The supplied [Validator] must support the validation of [Address] instances.")
        }
    }

    /*
    * This Validator validates Customer instances, and any subclasses of Customer too
    */
    override fun supports(clazz: Class<>): Boolean {
        return Customer::class.java.isAssignableFrom(clazz)
    }

    override fun validate(target: Any, errors: Errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required")
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required")
        val customer = target as Customer
        try {
            errors.pushNestedPath("address")
            ValidationUtils.invokeValidator(this.addressValidator, customer.address, errors)
        } finally {
            errors.popNestedPath()
        }
    }
}

验证错误会报告给Errors传递给验证器的对象。在 Spring Web MVC 的情况下,您可以使用<spring:bind/>标签来检查错误消息,但您也可以Errors自己检查对象。关于它提供的方法的更多信息可以在javadoc中找到。

3.2. 将代码解析为错误消息

我们涵盖了数据绑定和验证。本节介绍与验证错误相对应的输出消息。在上一节所示的示例中,我们拒绝了nameandage字段。如果我们想通过使用 a 来输出错误消息 MessageSource,我们可以使用我们在拒绝字段时提供的错误代码(在这种情况下为“姓名”和“年龄”)来完成。当您调用(直接或间接,通过使用例如ValidationUtils类)rejectValue或接口中的其他reject方法之一时Errors,底层实现不仅注册您传入的代码,而且还注册许多额外的错误代码。MessageCodesResolver 确定Errors接口寄存器的错误代码。默认情况下, DefaultMessageCodesResolver使用,它(例如)不仅使用您提供的代码注册消息,而且还注册包含您传递给拒绝方法的字段名称的消息。所以,如果你使用 拒绝一个字段rejectValue("age", "too.darn.old"),除了too.darn.old代码之外,Spring 还会注册too.darn.old.ageand too.darn.old.age.int(第一个包含字段名称,第二个包含字段类型)。这样做是为了方便开发人员定位错误消息。

有关MessageCodesResolver和 默认策略的更多信息可以分别在 和 的 javadoc 中 MessageCodesResolver找到 DefaultMessageCodesResolver

3.3. Bean 操作和BeanWrapper

org.springframework.beans包遵循 JavaBeans 标准。JavaBean 是具有默认无参数构造函数的类,并且遵循命名约定,其中(例如)命名的属性bingoMadness将具有 setter 方法setBingoMadness(..)和 getter 方法getBingoMadness()。有关 JavaBeans 和规范的更多信息,请参阅 javabeans

bean 包中一个非常重要的类是BeanWrapper接口及其相应的实现(BeanWrapperImpl)。正如从 javadoc 中引用的那样, BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符和查询属性以确定它们是可读还是可写的功能。此外,BeanWrapper还提供对嵌套属性的支持,使子属性上的属性设置可以无限深。BeanWrapper还支持添加标准 JavaBeans 的能力, PropertyChangeListenersVetoableChangeListeners无需在目标类中支持代码。最后但同样重要的是,它BeanWrapper提供了对设置索引属性的支持。BeanWrapper通常不由应用程序代码直接使用,而是由 the 和DataBindertheBeanFactory.

BeanWrapper工作方式部分由其名称表示:它包装一个 bean 以对该 bean 执行操作,例如设置和检索属性。

3.3.1。设置和获取基本和嵌套属性

setPropertyValue设置和 getPropertyValue获取属性是通过BeanWrapper. 有关详细信息,请参阅他们的 Javadoc。下表显示了这些约定的一些示例:

表 11. 属性示例
表达 解释

name

表示与or 和方法name对应的属性。getName()isName()setName(..)

account.name

指示对应于(例如)or方法name的属性的嵌套属性。accountgetAccount().setName()getAccount().getName()

account[2]

指示索引属性的第三个元素account。索引属性可以是 type arraylist或其他自然排序的集合。

account[COMPANYNAME]

指示由属性的COMPANYNAME键索引的映射条目的值。account Map

(如果您不打算直接使用 the ,下一部分对您来说并不重要BeanWrapper。如果您只使用 theDataBinder和 theBeanFactory 及其默认实现,您应该跳到关于 的 部分PropertyEditors。)

以下两个示例类使用BeanWrapper来获取和设置属性:

java
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;
    }
}
科特林
class Company {
    var name: String? = null
    var managingDirector: Employee? = null
}
java
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;
    }
}
科特林
class Employee {
    var name: String? = null
    var salary: Float? = null
}

以下代码片段显示了一些示例,说明如何检索和操作实例化Company的 s 和Employees 的某些属性:

java
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");
科特林
val company = BeanWrapperImpl(Company())
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.")
// ... can also be done like this:
val value = PropertyValue("name", "Some Company Inc.")
company.setPropertyValue(value)

// ok, let's create the director and tie it to the company:
val jim = BeanWrapperImpl(Employee())
jim.setPropertyValue("name", "Jim Stravinsky")
company.setPropertyValue("managingDirector", jim.wrappedInstance)

// retrieving the salary of the managingDirector through the company
val salary = company.getPropertyValue("managingDirector.salary") as Float?

3.3.2. 内置PropertyEditor实现

Spring 使用 a 的概念PropertyEditor来实现 an Object和 a之间的转换String。以与对象本身不同的方式表示属性可能很方便。例如,aDate 可以以人类可读的方式表示(如String: '2007-14-09'),而我们仍然可以将人类可读的形式转换回原始日期(或者,更好的是,将以人类可读的形式输入的任何日期转换回Date对象)。这种行为可以通过注册自定义类型的编辑器来实现 java.beans.PropertyEditor。在一个特定的 IoC 容器中注册自定义编辑器BeanWrapper(如前一章所述),使其了解如何将属性转换为所需的类型。有关更多信息 PropertyEditor,请参阅java.beans来自 Oracle 的软件包

在 Spring 中使用属性编辑的几个示例:

  • 在 bean 上设置属性是通过使用PropertyEditor实现来完成的。当您使用String在 XML 文件中声明的某个 bean 的属性值时,Spring(如果相应属性的设置器有Class 参数)使用ClassEditor尝试将参数解析为Class对象。

  • 在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种PropertyEditor实现来完成的,您可以在 CommandController.

Spring 有许多内置的PropertyEditor实现来简化生活。它们都位于org.springframework.beans.propertyeditors 包装中。大多数(但不是全部,如下表所示)默认情况下由 BeanWrapperImpl. 如果可以以某种方式配置属性编辑器,您仍然可以注册自己的变体来覆盖默认变体。下表描述了PropertyEditorSpring 提供的各种实现:

表 12. 内置PropertyEditor实现
班级 解释

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对象,反之亦然(字符串格式为 [language]_[country]_[variant],与 的toString()方法 相同Locale)。也接受空格作为分隔符,作为下划线的替代。默认情况下,由 注册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,其中包括PropertyEditor诸如Font、等类型的实现Color以及大多数基本类型。另请注意,标准 JavaBeans 基础结构会自动发现PropertyEditor类(无需显式注册它们),前提是它们与它们处理的类在同一个包中,并且与该类具有相同的名称,并带有Editor附加。例如,可以具有以下类和包结构,这足以使SomethingEditor该类被识别并用作PropertyEditorforSomething类型的属性。

com
  钱克
    流行音乐
      某物
      SomethingEditor //Something 类的 PropertyEditor

请注意,您也可以在此处使用标准JavaBeans 机制(在此处BeanInfo进行了一定程度的描述 )。以下示例使用该机制显式注册一个或多个 具有关联类属性的实例:BeanInfoPropertyEditor

com
  钱克
    流行音乐
      某物
      SomethingBeanInfo //Something 类的 BeanInfo

SomethingBeanInfo被引用类的以下 Java 源代码将aCustomNumberEditor与类的age属性相关联Something

java
public class SomethingBeanInfo extends SimpleBeanInfo {

    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
                @Override
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                }
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}
科特林
class SomethingBeanInfo : SimpleBeanInfo() {

    override fun getPropertyDescriptors(): Array<PropertyDescriptor> {
        try {
            val numberPE = CustomNumberEditor(Int::class.java, true)
            val ageDescriptor = object : PropertyDescriptor("age", Something::class.java) {
                override fun createPropertyEditor(bean: Any): PropertyEditor {
                    return numberPE
                }
            }
            return arrayOf(ageDescriptor)
        } catch (ex: IntrospectionException) {
            throw Error(ex.toString())
        }

    }
}
注册其他自定义PropertyEditor实现

当将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 JavaBeansPropertyEditor实现将这些字符串转换为属性的复杂类型。Spring 预注册了许多自定义PropertyEditor实现(例如,将表示为字符串的类名转换为Class对象)。此外,Java 的标准 JavaBeansPropertyEditor查找机制允许PropertyEditor 对类进行适当命名,并将其放置在与其提供支持的类相同的包中,以便可以自动找到它。

如果需要注册其他 custom PropertyEditors,可以使用多种机制。最手动的方法,通常不方便或不推荐,是使用接口的registerCustomEditor()方法 ConfigurableBeanFactory,假设您有BeanFactory参考。另一种(稍微方便一点)机制是使用一个特殊的 bean factory 后处理器,称为CustomEditorConfigurer. 尽管您可以将 bean factory 后处理器与BeanFactory实现一起使用,但CustomEditorConfigurer具有嵌套属性设置,因此我们强烈建议您将其与 ApplicationContext.应用。

请注意,所有 bean 工厂和应用程序上下文都会自动使用许多内置属性编辑器,通过它们使用 aBeanWrapper来处理属性转换。上一节BeanWrapper 列出了寄存器的标准属性编辑器。此外,s 还覆盖或添加额外的编辑器,以便以适合特定应用程序上下文类型的方式处理资源查找。ApplicationContext

标准 JavaBeansPropertyEditor实例用于将表示为字符串的属性值转换为属性的实际复杂类型。您可以使用 CustomEditorConfigurerbean 工厂后处理器,方便地将对其他PropertyEditor实例的支持添加到ApplicationContext.

考虑以下示例,它定义了一个名为的用户类ExoticType和另一个名为的类DependsOnExoticType,需要将其ExoticType设置为属性:

java
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;
    }
}
科特林
package example

class ExoticType(val name: String)

class DependsOnExoticType {

    var type: ExoticType? = null
}

正确设置后,我们希望能够将 type 属性分配为字符串,然后将其PropertyEditor转换为实际 ExoticType实例。以下 bean 定义显示了如何设置这种关系:

<bean id="sample" class="example.DependsOnExoticType">
    <property name="type" value="aNameForExoticType"/>
</bean>

PropertyEditor实现可能类似于以下内容:

java
// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}
科特林
// converts string representation to ExoticType object
package example

import java.beans.PropertyEditorSupport

class ExoticTypeEditor : PropertyEditorSupport() {

    override fun setAsText(text: String) {
        value = ExoticType(text.toUpperCase())
    }
}

最后,以下示例显示了如何使用CustomEditorConfigurer向 注册新PropertyEditorApplicationContext,然后可以根据需要使用它:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
        </map>
    </property>
</bean>
使用PropertyEditorRegistrar

向 Spring 容器注册属性编辑器的另一种机制是创建和使用PropertyEditorRegistrar. 当您需要在几种不同的情况下使用同一组属性编辑器时,此接口特别有用。您可以编写相应的注册器并在每种情况下重复使用它。 PropertyEditorRegistrar实例与名为 PropertyEditorRegistry的接口一起工作,该接口由 Spring BeanWrapper (和DataBinder)实现。PropertyEditorRegistrar实例与CustomEditorConfigurer在此处描述)结合使用时特别方便,它公开了一个名为setPropertyEditorRegistrars(..). PropertyEditorRegistrar以这种方式添加到 a 的实例CustomEditorConfigurer可以很容易地与DataBinder和 Spring MVC 控制器。此外,它避免了在自定义编辑器上进行同步的需要:APropertyEditorRegistrar预计会PropertyEditor 为每次 bean 创建尝试创建新实例。

以下示例显示了如何创建自己的PropertyEditorRegistrar实现:

java
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...
    }
}
科特林
package com.foo.editors.spring

import org.springframework.beans.PropertyEditorRegistrar
import org.springframework.beans.PropertyEditorRegistry

class CustomPropertyEditorRegistrar : PropertyEditorRegistrar {

    override fun registerCustomEditors(registry: PropertyEditorRegistry) {

        // it is expected that new PropertyEditor instances are created
        registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor())

        // you could register as many custom property editors as are required here...
    }
}

另请参阅org.springframework.beans.support.ResourceEditorRegistrar示例 PropertyEditorRegistrar实现。请注意在 registerCustomEditors(..)方法的实现中,它如何创建每个属性编辑器的新实例。

下一个示例展示了如何配置 aCustomEditorConfigurer并将我们的实例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 框架的人来说,将 aPropertyEditorRegistrar与数据绑定 Web 控制器结合使用会非常方便。以下示例PropertyEditorRegistrar在方法的实现中使用 a @InitBinder

java
@Controller
public class RegisterUserController {

    private final PropertyEditorRegistrar customPropertyEditorRegistrar;

    RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
        this.customPropertyEditorRegistrar = propertyEditorRegistrar;
    }

    @InitBinder
    void initBinder(WebDataBinder binder) {
        this.customPropertyEditorRegistrar.registerCustomEditors(binder);
    }

    // other methods related to registering a User
}
科特林
@Controller
class RegisterUserController(
    private val customPropertyEditorRegistrar: PropertyEditorRegistrar) {

    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        this.customPropertyEditorRegistrar.registerCustomEditors(binder)
    }

    // other methods related to registering a User
}

这种PropertyEditor注册风格可以带来简洁的代码(@InitBinder方法的实现只有一行长),并且可以将通用PropertyEditor 注册代码封装在一个类中,然后根据需要在尽可能多的控制器之间共享。

3.4. 弹簧类型转换

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),源参数保证不为空。如果转换失败,您 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

当您需要集中整个类层次结构的转换逻辑时(例如,从转换StringEnum对象时),您可以实现 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>),其中 T 是 R 的子类。

考虑StringToEnumConverterFactory作为一个例子:

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接口。与 相比,a 具有更灵活但类型更弱的签名ConverterGenericConverter支持在多个源类型和目标类型之间进行转换。此外,a提供了GenericConverter可用的源和目标字段上下文,您可以在实现转换逻辑时使用它们。这样的上下文允许类型转换由字段注释或字段签名上声明的通用信息驱动。以下清单显示了 的接口定义GenericConverter

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

要实现 a GenericConvertergetConvertibleTypes()请返回支持的源→目标类型对。然后实施convert(Object, TypeDescriptor, TypeDescriptor)以包含您的转换逻辑。源TypeDescriptor提供对保存正在转换的值的源字段的访问。目标TypeDescriptor 提供对要设置转换值的目标字段的访问。

一个很好的例子GenericConverter是在 Java 数组和集合之间转换的转换器。这样的ArrayToCollectionConverter内省了声明目标集合类型的字段来解析集合的元素类型。这让源数组中的每个元素在集合被设置到目标字段之前被转换为集合元素类型。

因为GenericConverter是比较复杂的 SPI 接口,所以应该只在需要的时候使用。支持ConverterConverterFactory满足基本类型转换需求。
使用ConditionalGenericConverter

有时,您希望 aConverter只有在特定条件成立时才运行。例如,您可能希望Converter仅在目标字段上存在特定注释时才运行 a,或者您可能希望仅在目标类上定义 Converter了特定方法(例如方法)时才运行 a。是和 接口的联合,可让您定义此类自定义匹配条件:static valueOfConditionalGenericConverterGenericConverterConditionalConverter

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

一个很好的例子ConditionalGenericConverterIdToEntityConverter在持久实体标识符和实体引用之间进行转换。IdToEntityConverter 仅当目标实体类型声明了静态查找器方法(例如, )时,这样的才可能匹配findAccount(Long)。您可以在 matches(TypeDescriptor, TypeDescriptor).

3.4.4。ConversionServiceAPI _

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了 ,它提供了一个用于注册转换器的 SPI。在内部,ConversionService 实现委托其注册的转换器执行类型转换逻辑。

ConversionService包中提供了一个健壮的实现core.convert.supportGenericConversionService是适用于大多数环境的通用实现。为创建通用配置ConversionServiceFactory提供了一个方便的工厂。ConversionService

3.4.5。配置一个ConversionService

AConversionService是一个无状态对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,您通常ConversionService为每个 Spring 容器(或ApplicationContext)配置一个实例。ConversionService每当框架需要执行类型转换时,Spring 就会选择并使用它。您还可以将其 ConversionService注入任何 bean 并直接调用它。

如果没有ConversionService向 Spring 注册,则使用基于原始PropertyEditor的系统。

要使用 Spring 注册默认值,请使用ofConversionService添加以下 bean 定义:idconversionService

<bean id="conversionService"
    class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默认值ConversionService可以在字符串、数字、枚举、集合、映射和其他常见类型之间进行转换。要使用您自己的自定义转换器补充或覆盖默认转换器,请设置该converters属性。属性值可以实现任何ConverterConverterFactoryGenericConverter接口。

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>

ConversionService在 Spring MVC 应用程序中使用 a 也很常见。请参阅 Spring MVC 章节中的转换和格式化

在某些情况下,您可能希望在转换期间应用格式。有关 使用. _FormatterRegistry _FormattingConversionServiceFactoryBean

3.4.6。以ConversionService编程方式使用

要以ConversionService编程方式使用实例,您可以像对任何其他 bean 一样注入对它的引用。以下示例显示了如何执行此操作:

java
@Service
public class MyService {

    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void doIt() {
        this.conversionService.convert(...)
    }
}
科特林
@Service
class MyService(private val conversionService: ConversionService) {

    fun doIt() {
        conversionService.convert(...)
    }
}

对于大多数用例,您可以使用convert指定 的方法targetType,但它不适用于更复杂的类型,例如参数化元素的集合。例如,如果您想以编程方式将 a Listof转换Integer为 a Listof String,则需要提供源类型和目标类型的正式定义。

幸运的是,TypeDescriptor它提供了各种选项来使操作变得简单,如以下示例所示:

java
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)));
科特林
val cs = DefaultConversionService()

val input: List<Integer> = ...
cs.convert(input,
        TypeDescriptor.forObject(input), // List<Integer> type descriptor
        TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java)))

请注意,DefaultConversionService自动注册适用于大多数环境的转换器。这包括集合转换器、标量转换器和基本ObjectString转换器。您可以ConverterRegistry使用类上的静态addDefaultConverters 方法向any 注册相同的转换器DefaultConversionService

值类型的转换器可重用于数组和集合,因此无需创建特定的转换器来从 a Collectionof转换S为 a Collectionof T,假设标准集合处理是合适的。

3.5. 弹簧字段格式

正如上一节所讨论的,core.convert是一个通用的类型转换系统。它提供了一个统一的ConversionServiceAPI 以及一个强类型的ConverterSPI,用于实现从一种类型到另一种类型的转换逻辑。Spring 容器使用此系统绑定 bean 属性值。此外,Spring 表达式语言(SpEL)都DataBinder使用这个系统来绑定字段值。例如,当 SpEL 需要将 a 强制Short转换为 aLong以完成一次expression.setValue(Object bean, Object value)尝试时,core.convert 系统会执行强制。

现在考虑典型客户端环境的类型转换要求,例如 Web 或桌面应用程序。在这样的环境中,您通常转换 fromString 以支持客户端回发过程,以及转换回String以支持视图呈现过程。此外,您经常需要本地化String值。更通用的core.convert ConverterSPI 不直接解决此类格式要求。为了直接解决这些问题,Spring 3 引入了一个方便的SPI,它为客户端环境的实现Formatter提供了一个简单而健壮的替代方案。PropertyEditor

Converter通常,当您需要实现通用类型转换逻辑时,您可以使用SPI——例如,在 ajava.util.Date和 a之间进行转换LongFormatter当您在客户端环境(例如 Web 应用程序)中工作并且需要解析和打印本地化的字段值时,您可以使用SPI。ConversionService 为两个 SPI 提供了统一的类型转换 API 。

3.5.1。FormatterSPI _

Formatter实现字段格式化逻辑的SPI 简单且强类型。以下清单显示了Formatter接口定义:

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

FormatterPrinterParser构建块接口扩展。以下清单显示了这两个接口的定义:

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实现parse()操作以解析 T从客户端语言环境返回的格式化表示的实例。如果解析尝试失败,您Formatter 应该抛出一个ParseException或一个。IllegalArgumentException注意确保您的Formatter实现是线程安全的。

为了方便起见,format子包提供了几种Formatter实现。该number包提供NumberStyleFormatter,CurrencyStyleFormatterPercentStyleFormatter来格式化Number使用java.text.NumberFormat. 该datetime包提供了一个DateFormatter以格式化java.util.Date对象的java.text.DateFormat.

下面DateFormatter是一个示例Formatter实现:

java
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;
    }
}
科特林
class DateFormatter(private val pattern: String) : Formatter<Date> {

    override fun print(date: Date, locale: Locale)
            = getDateFormat(locale).format(date)

    @Throws(ParseException::class)
    override fun parse(formatted: String, locale: Locale)
            = getDateFormat(locale).parse(formatted)

    protected fun getDateFormat(locale: Locale): DateFormat {
        val dateFormat = SimpleDateFormat(this.pattern, locale)
        dateFormat.isLenient = false
        return dateFormat
    }
}

Spring 团队欢迎社区驱动的Formatter贡献。请参阅 GitHub 问题以做出贡献。

3.5.2. 注释驱动的格式

字段格式可以通过字段类型或注释进行配置。要将注解绑定到 a 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);
}

创建一个实现:

  1. 将 A 参数化为annotationType您希望与格式化逻辑相关联的字段 - 例如org.springframework.format.annotation.DateTimeFormat.

  2. getFieldTypes()返回可以使用注释的字段类型。

  3. 返回getPrinter()aPrinter以打印带注释的字段的值。

  4. getParser()返回 aParser以解析带clientValue注释的字段。

以下示例AnnotationFormatterFactory实现将注释绑定@NumberFormat 到格式化程序以指定数字样式或模式:

java
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();
            }
        }
    }
}
科特林
class NumberFormatAnnotationFormatterFactory : AnnotationFormatterFactory<NumberFormat> {

    override fun getFieldTypes(): Set<Class<*>> {
        return setOf(Short::class.java, Int::class.java, Long::class.java, Float::class.java, Double::class.java, BigDecimal::class.java, BigInteger::class.java)
    }

    override fun getPrinter(annotation: NumberFormat, fieldType: Class<*>): Printer<Number> {
        return configureFormatterFrom(annotation, fieldType)
    }

    override fun getParser(annotation: NumberFormat, fieldType: Class<*>): Parser<Number> {
        return configureFormatterFrom(annotation, fieldType)
    }

    private fun configureFormatterFrom(annotation: NumberFormat, fieldType: Class<*>): Formatter<Number> {
        return if (annotation.pattern.isNotEmpty()) {
            NumberStyleFormatter(annotation.pattern)
        } else {
            val style = annotation.style
            when {
                style === NumberFormat.Style.PERCENT -> PercentStyleFormatter()
                style === NumberFormat.Style.CURRENCY -> CurrencyStyleFormatter()
                else -> NumberStyleFormatter()
            }
        }
    }
}

要触发格式化,您可以使用@NumberFormat 注释字段,如以下示例所示:

java
public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}
科特林
class MyModel(
    @field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal
)
格式注解 API

包中存在可移植格式注释 API org.springframework.format.annotation 。您可以使用@NumberFormat格式化Number字段,例如DoubleLong@DateTimeFormat格式化java.util.Date,,java.util.CalendarLong 用于毫秒时间戳)以及 JSR-310 java.time

以下示例用于@DateTimeFormat将 a 格式化java.util.Date为 ISO 日期 (yyyy-MM-dd):

java
public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}
科特林
class MyModel(
    @DateTimeFormat(iso=ISO.DATE) private val date: Date
)

3.5.3. FormatterRegistrySPI _

FormatterRegistry是一个用于注册格式化程序和转换器的 SPI。 FormattingConversionServiceFormatterRegistry适用于大多数环境的实现。您可以以编程方式或声明方式将此变体配置为 Spring bean,例如使用FormattingConversionServiceFactoryBean. 由于此实现还实现ConversionService了 ,因此您可以直接将其配置为与 SpringDataBinder和 Spring 表达式语言 (SpEL) 一起使用。

以下清单显示了FormatterRegistrySPI:

package org.springframework.format;

public interface FormatterRegistry extends ConverterRegistry {

    void addPrinter(Printer<?> printer);

    void addParser(Parser<?> parser);

    void addFormatter(Formatter<?> formatter);

    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

    void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}

如前面的清单所示,您可以按字段类型或注解注册格式化程序。

FormatterRegistrySPI 允许您集中配置格式规则,而不是在控制器之间复制此类配置。例如,您可能希望强制所有日期字段都以某种方式格式化,或者具有特定注释的字段以某种方式格式化。使用 shared FormatterRegistry,您只需定义一次这些规则,并在需要格式化时应用它们。

3.5.4。FormatterRegistrarSPI _

FormatterRegistrar是一个 SPI,用于通过 FormatterRegistry 注册格式化程序和转换器。下面的清单显示了它的接口定义:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

FormatterRegistrar在为给定的格式类别(例如日期格式)注册多个相关转换器和格式器时,A很有用。在声明式注册不足的情况下,它也很有用——例如,当格式化程序需要在与其自己不同的特定字段类型下进行索引<T>或注册Printer/Parser对时。下一节提供有关转换器和格式化程序注册的更多信息。

3.5.5。在 Spring MVC 中配置格式化

请参阅Spring MVC 章节中的转换和格式化

3.6. 配置全局日期和时间格式

默认情况下,未使用注释的日期和时间字段是使用样式@DateTimeFormat从字符串转换而来的。DateFormat.SHORT如果您愿意,可以通过定义自己的全局格式来更改此设置。

为此,请确保 Spring 不注册默认格式化程序。相反,请在以下帮助下手动注册格式化程序:

  • org.springframework.format.datetime.standard.DateTimeFormatterRegistrar

  • org.springframework.format.datetime.DateFormatterRegistrar

例如,以下 Java 配置注册了一种全局yyyyMMdd格式:

java
@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 JSR-310 date conversion with a specific global format
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        // Register date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}
科特林
@Configuration
class AppConfig {

    @Bean
    fun conversionService(): FormattingConversionService {
        // Use the DefaultFormattingConversionService but do not register defaults
        return DefaultFormattingConversionService(false).apply {

            // Ensure @NumberFormat is still supported
            addFormatterForFieldAnnotation(NumberFormatAnnotationFormatterFactory())

            // Register JSR-310 date conversion with a specific global format
            val registrar = DateTimeFormatterRegistrar()
            registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"))
            registrar.registerFormatters(this)

            // Register date conversion with a specific global format
            val registrar = DateFormatterRegistrar()
            registrar.setFormatter(DateFormatter("yyyyMMdd"))
            registrar.registerFormatters(this)
        }
    }
}

如果您更喜欢基于 XML 的配置,可以使用 FormattingConversionServiceFactoryBean. 以下示例显示了如何执行此操作:

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

    <bean id="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.standard.DateTimeFormatterRegistrar">
                    <property name="dateFormatter">
                        <bean class="org.springframework.format.datetime.standard.DateTimeFormatterFactoryBean">
                            <property name="pattern" value="yyyyMMdd"/>
                        </bean>
                    </property>
                </bean>
            </set>
        </property>
    </bean>
</beans>

请注意,在 Web 应用程序中配置日期和时间格式时有额外的注意事项。请参阅 WebMVC 转换和格式化WebFlux 转换和格式化

3.7. Java Bean 验证

Spring Framework 提供对 Java Bean Validation API 的支持。

3.7.1. Bean 验证概述

Bean Validation 通过约束声明和 Java 应用程序的元数据提供了一种通用的验证方式。要使用它,您可以使用声明性验证约束来注释域模型属性,然后由运行时强制执行。有内置约束,您也可以定义自己的自定义约束。

考虑以下示例,该示例显示了一个PersonForm具有两个属性的简单模型:

java
public class PersonForm {
    private String name;
    private int age;
}
科特林
class PersonForm(
        private val name: String,
        private val age: Int
)

Bean Validation 允许您声明约束,如以下示例所示:

java
public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}
科特林
class PersonForm(
    @get:NotNull @get:Size(max=64)
    private val name: String,
    @get:Min(0)
    private val age: Int
)

一个 Bean Validation 验证器然后根据声明的约束来验证这个类的实例。有关 API 的一般信息,请参阅Bean 验证。有关特定约束,请参阅Hibernate Validator文档。要了解如何将 bean 验证提供程序设置为 Spring bean,请继续阅读。

3.7.2. 配置 Bean 验证提供程序

Spring 为 Bean Validation API 提供全面支持,包括将 Bean Validation 提供者引导为 Spring bean。这使您可以在应用程序中注入一个 javax.validation.ValidatorFactory或任何需要验证的位置。javax.validation.Validator

您可以使用 将LocalValidatorFactoryBean默认验证器配置为 Spring bean,如以下示例所示:

java
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}
XML
<bean id="validator"
    class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

前面示例中的基本配置触发 bean 验证以使用其默认引导机制进行初始化。Bean Validation 提供程序,例如 Hibernate Validator,预计会出现在类路径中并且会被自动检测到。

注入验证器

LocalValidatorFactoryBean实现javax.validation.ValidatorFactoryand javax.validation.Validator以及 Spring 的org.springframework.validation.Validator. 您可以将对这些接口中的任何一个的引用注入到需要调用验证逻辑的 bean 中。

javax.validation.Validator如果您更喜欢直接使用 Bean Validation API,则可以注入一个引用,如以下示例所示:

java
import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}
科特林
import javax.validation.Validator;

@Service
class MyService(@Autowired private val validator: Validator)

如果您的 bean 需要 Spring Validation API,您可以注入一个引用org.springframework.validation.Validator,如以下示例所示:

java
import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}
科特林
import org.springframework.validation.Validator

@Service
class MyService(@Autowired private val validator: Validator)
配置自定义约束

每个 bean 验证约束由两部分组成:

  • @Constraint声明约束及其可配置属性的注释。

  • javax.validation.ConstraintValidator实现约束行为的接口的实现。

要将声明与实现相关联,每个@Constraint注释都引用相应的ConstraintValidator实现类。在运行时, ConstraintValidatorFactory当在域模型中遇到约束注释时,实例化引用的实现。

默认情况下,LocalValidatorFactoryBean配置一个SpringConstraintValidatorFactory 使用 Spring 创建ConstraintValidator实例。这让您的自定义 ConstraintValidators从依赖注入中受益,就像任何其他 Spring bean 一样。

以下示例显示了一个自定义@Constraint声明,后跟一个 ConstraintValidator使用 Spring 进行依赖注入的关联实现:

java
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
科特林
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator::class)
annotation class MyConstraint
java
import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    // ...
}
科特林
import javax.validation.ConstraintValidator

class MyConstraintValidator(private val aDependency: Foo) : ConstraintValidator {

    // ...
}

如前面的示例所示,实现可以像任何其他 Spring bean 一样ConstraintValidator具有其依赖 项。@Autowired

弹簧驱动的方法验证

您可以通过 bean 定义将 Bean Validation 1.1(以及作为自定义扩展,也由 Hibernate Validator 4.3)支持的方法验证功能集成到 Spring 上下文中 MethodValidationPostProcessor

java
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration
public class AppConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}
XML
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

为了符合 Spring 驱动的方法验证的条件,所有目标类都需要使用 Spring 的@Validated注解进行注解,它还可以选择声明要使用的验证组。有关 MethodValidationPostProcessor Hibernate Validator 和 Bean Validation 1.1 提供程序的设置详细信息,请参阅。

方法验证依赖于目标类周围的AOP 代理,要么是接口上方法的 JDK 动态代理,要么是 CGLIB 代理。使用代理有某些限制,其中一些在 了解 AOP 代理中进行了描述。此外,请记住始终在代理类上使用方法和访问器;直接字段访问将不起作用。

其他配置选项

对于大多数情况,默认LocalValidatorFactoryBean配置就足够了。从消息插值到遍历解析,各种 Bean Validation 构造都有许多配置选项。有关这些选项的更多信息,请参阅 LocalValidatorFactoryBean javadoc。

3.7.3. 配置一个DataBinder

从 Spring 3 开始,您可以DataBinder使用Validator. 配置完成后,您可以通过调用Validator来调用binder.validate(). 任何验证都会 Errors自动添加到活页夹的BindingResult.

以下示例显示了如何DataBinder在绑定到目标对象后以编程方式调用验证逻辑:

java
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();
科特林
val target = Foo()
val binder = DataBinder(target)
binder.validator = FooValidator()

// bind to the target object
binder.bind(propertyValues)

// validate the target object
binder.validate()

// get BindingResult that includes any validation errors
val results = binder.bindingResult

您还可以通过和配置DataBinder具有多个Validator实例 的 a 。这在将全局配置的 bean 验证与在 DataBinder 实例上本地配置的 Spring 结合时很有用。请参阅 Spring MVC 验证配置dataBinder.addValidatorsdataBinder.replaceValidatorsValidator

3.7.4. Spring MVC 3 验证

请参阅Spring MVC 章节中的验证

4. Spring 表达式语言(SpEL)

Spring 表达式语言(简称“SpEL”)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于 Unified EL,但提供了额外的功能,最显着的是方法调用和基本的字符串模板功能。

虽然还有其他几种可用的 Java 表达式语言——OGNL、MVEL 和 JBoss EL 等等——但创建 Spring 表达式语言的目的是为 Spring 社区提供一种可在所有产品中使用的受良好支持的表达式语言。Spring投资组合。它的语言特性由 Spring 产品组合中的项目需求驱动,包括Spring Tools for Eclipse中代码完成支持的工具需求。也就是说,SpEL 基于与技术无关的 API,如果需要,它可以集成其他表达式语言实现。

虽然 SpEL 是 Spring 产品组合中表达式评估的基础,但它不直接与 Spring 绑定,可以独立使用。为了独立起见,本章中的许多示例都使用 SpEL,就好像它是一种独立的表达语言一样。这需要创建一些引导基础设施类,例如解析器。大多数 Spring 用户不需要处理这个基础设施,而是可以只编写表达式字符串进行评估。这种典型用途的一个示例是将 SpEL 集成到创建 XML 或基于注释的 bean 定义中,如 定义 bean 定义的表达式支持中所示。

本章介绍了表达式语言的特性、它的 API 和它的语言语法。在一些地方,InventorSociety被用作表达式评估的目标对象。这些类声明和用于填充它们的数据在本章末尾列出。

表达式语言支持以下功能:

  • 文字表达

  • 布尔和关系运算符

  • 常用表达

  • 类表达式

  • 访问属性、数组、列表和映射

  • 方法调用

  • 关系运算符

  • 任务

  • 调用构造函数

  • Bean 引用

  • 数组构造

  • 内联列表

  • 内联地图

  • 三元运算符

  • 变量

  • 用户自定义函数

  • 收藏投影

  • 收藏选择

  • 模板化表达式

4.1。评估

本节介绍SpEL接口及其表达语言的简单使用。完整的语言参考可以在 语言参考中找到。

以下代码介绍了 SpEL API 来评估文字字符串表达式 Hello World.

java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
1 消息变量的值为'Hello World'
科特林
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
1 消息变量的值为'Hello World'

您最可能使用的 SpEL 类和接口位于 org.springframework.expression包及其子包中,例如spel.support.

ExpressionParser接口负责解析表达式字符串。在前面的示例中,表达式字符串是用单引号括起来的字符串文字。该Expression接口负责评估先前定义的表达式字符串。分别调用 和 时ParseException可以 抛出的两个异常,和。EvaluationExceptionparser.parseExpressionexp.getValue

SpEL 支持广泛的功能,例如调用方法、访问属性和调用构造函数。

在下面的方法调用示例中,我们concat在字符串字面量上调用方法:

java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();
1 的值message现在是“Hello World!”。
科特林
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'.concat('!')") (1)
val message = exp.value as String
1 的值message现在是“Hello World!”。

以下调用 JavaBean 属性的示例调用了该String属性Bytes

java
ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();
1 此行将文字转换为字节数组。
科特林
val parser = SpelExpressionParser()

// invokes 'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") (1)
val bytes = exp.value as ByteArray
1 此行将文字转换为字节数组。

SpEL 还通过使用标准点表示法(例如 prop1.prop2.prop3)以及相应的属性值设置来支持嵌套属性。也可以访问公共字段。

以下示例显示了如何使用点表示法来获取文字的长度:

java
ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1)
int length = (Integer) exp.getValue();
1 'Hello World'.bytes.length给出文字的长度.
科特林
val parser = SpelExpressionParser()

// invokes 'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") (1)
val length = exp.value as Int
1 'Hello World'.bytes.length给出文字的长度.

可以调用 String 的构造函数,而不是使用字符串文字,如以下示例所示:

java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);
1 从文字构造一个新String的并使其大写。
科特林
val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()")  (1)
val message = exp.getValue(String::class.java)
1 从文字构造一个新String的并使其大写。

注意泛型方法的使用:public <T> T getValue(Class<T> desiredResultType). 使用此方法无需将表达式的值转换为所需的结果类型。EvaluationException如果无法将值强制T转换为类型或无法使用注册的类型转换器进行转换,则会引发An 。

SpEL 更常见的用法是提供针对特定对象实例(称为根对象)进行评估的表达式字符串。以下示例显示如何name从类的实例中检索属性Inventor或创建布尔条件:

java
// 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"); // Parse name as an expression
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"

exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
科特林
// Create and set a calendar
val c = GregorianCalendar()
c.set(1856, 7, 9)

// The constructor arguments are name, birthday, and nationality.
val tesla = Inventor("Nikola Tesla", c.time, "Serbian")

val parser = SpelExpressionParser()

var exp = parser.parseExpression("name") // Parse name as an expression
val name = exp.getValue(tesla) as String
// name == "Nikola Tesla"

exp = parser.parseExpression("name == 'Nikola Tesla'")
val result = exp.getValue(tesla, Boolean::class.java)
// result == true

4.1.1。理解EvaluationContext

EvaluationContext接口用于评估表达式以解析属性、方法或字段并帮助执行类型转换。Spring 提供了两种实现。

  • SimpleEvaluationContext:针对不需要完整范围的 SpEL 语言语法并应受到有意义限制的表达式类别,公开了基本 SpEL 语言功能和配置选项的子集。示例包括但不限于数据绑定表达式和基于属性的过滤器。

  • StandardEvaluationContext:公开全套 SpEL 语言功能和配置选项。您可以使用它来指定默认根对象并配置每个可用的评估相关策略。

SimpleEvaluationContext旨在仅支持 SpEL 语言语法的一个子集。它不包括 Java 类型引用、构造函数和 bean 引用。它还要求您明确选择对表达式中的属性和方法的支持级别。默认情况下,create()静态工厂方法只允许对属性进行读取访问。您还可以获得构建器来配置所需的确切支持级别,针对以下一项或某种组合:

  • 仅自定义PropertyAccessor(无反射)

  • 只读访问的数据绑定属性

  • 用于读取和写入的数据绑定属性

类型转换

默认情况下,SpEL 使用 Spring 核心 ( org.springframework.core.convert.ConversionService) 中可用的转换服务。此转换服务带有许多用于常见转换的内置转换器,但也是完全可扩展的,因此您可以在类型之间添加自定义转换。此外,它是泛型感知的。这意味着,当您在表达式中使用泛型类型时,SpEL 会尝试转换以维护它遇到的任何对象的类型正确性。

这在实践中意味着什么?假设使用 赋值setValue()来设置List属性。属性的类型实际上是List<Boolean>. BooleanSpEL 认识到列表中的元素在放入之前需要转换为。以下示例显示了如何执行此操作:

java
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
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");

// b is false
Boolean b = simple.booleanList.get(0);
科特林
class Simple {
    var booleanList: MutableList<Boolean> = ArrayList()
}

val simple = Simple()
simple.booleanList.add(true)

val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false")

// b is false
val b = simple.booleanList[0]

4.1.2. 解析器配置

可以使用解析器配置对象 ( org.springframework.expression.spel.SpelParserConfiguration) 来配置 SpEL 表达式解析器。配置对象控制一些表达式组件的行为。例如,如果您对数组或集合进行索引,并且指定索引处的元素是null,SpEL可以自动创建元素。这在使用由属性引用链组成的表达式时很有用。如果您对数组或列表进行索引并指定超出数组或列表当前大小末尾的索引,SpEL 可以自动增长数组或列表以适应该索引。为了在指定索引处添加元素,SpEL 将在设置指定值之前尝试使用元素类型的默认构造函数创建元素。如果元素类型没有默认构造函数,null将被添加到数组或列表中。如果没有知道如何设置值的内置或自定义转换器,null则将保留在数组或列表中指定索引处。以下示例演示了如何自动增长列表:

java
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
科特林
class Demo {
    var list: List<String>? = null
}

// Turn on:
// - auto null reference initialization
// - auto collection growing
val config = SpelParserConfiguration(true, true)

val parser = SpelExpressionParser(config)

val expression = parser.parseExpression("list[3]")

val demo = Demo()

val 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 包含一个基本的表达式编译器。通常会解释表达式,这在评估期间提供了很大的动态灵活性,但不能提供最佳性能。对于偶尔的表达式使用,这很好,但是,当被其他组件(例如 Spring Integration)使用时,性能可能非常重要,并且不需要动态。

SpEL 编译器旨在满足这一需求。在评估期间,编译器生成一个体现表达式运行时行为的 Java 类,并使用该类来实现更快的表达式评估。由于没有围绕表达式键入,编译器在执行编译时使用在表达式的解释评估期间收集的信息。例如,它不能仅从表达式中知道属性引用的类型,但在第一次解释评估期间,它会找出它是什么。当然,如果各种表达式元素的类型随着时间的推移而变化,那么基于这些派生信息进行编译可能会在以后造成麻烦。出于这个原因,编译最适合那些类型信息不会在重复计算中改变的表达式。

考虑以下基本表达式:

someArray[0].someProperty.someOtherProperty < 0.1

因为前面的表达式涉及数组访问、一些属性取消引用和数值运算,所以性能提升可能非常明显。在 50000 次迭代的示例微基准测试中,使用解释器评估需要 75 毫秒,而使用表达式的编译版本只需要 3 毫秒。

编译器配置

编译器默认不打开,但您可以通过两种不同的方式之一打开它。您可以通过使用解析器配置过程(前面讨论过)或在 SpEL 使用嵌入到另一个组件中时使用 Spring 属性来打开它。本节讨论这两个选项。

编译器可以在 org.springframework.expression.spel.SpelCompilerMode枚举中捕获的三种模式之一中运行。模式如下:

  • OFF(默认):编译器关闭。

  • IMMEDIATE: 在立即模式下,表达式会尽快编译。这通常是在第一次解释评估之后。如果编译表达式失败(通常是由于类型更改,如前所述),则表达式求值的调用者会收到异常。

  • MIXED:在混合模式下,表达式会随着时间在解释模式和编译模式之间静默切换。在经过一定次数的解释运行后,它们会切换到编译形式,如果编译形式出现问题(例如类型更改,如前所述),表达式会自动再次切换回解释形式。一段时间后,它可能会生成另一个已编译的表单并切换到它。基本上,用户进入IMMEDIATE模式的异常是在内部处理的。

IMMEDIATEmode 存在是因为MIXEDmode 可能会导致具有副作用的表达式出现问题。如果编译的表达式在部分成功后崩溃,它可能已经做了一些影响系统状态的事情。如果发生这种情况,调用者可能不希望它在解释模式下静默重新运行,因为部分表达式可能会运行两次。

选择模式后,使用SpelParserConfiguration配置解析器。以下示例显示了如何执行此操作:

java
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);
科特林
val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
        this.javaClass.classLoader)

val parser = SpelExpressionParser(config)

val expr = parser.parseExpression("payload")

val message = MyMessage()

val payload = expr.getValue(message)

当你指定编译模式时,你也可以指定一个类加载器(允许传递null)。已编译的表达式在任何提供的子类加载器下创建的子类加载器中定义。重要的是要确保,如果指定了类加载器,它可以看到表达式评估过程中涉及的所有类型。如果您不指定类加载器,则使用默认类加载器(通常是在表达式评估期间运行的线程的上下文类加载器)。

配置编译器的第二种方法是在 SpEL 嵌入到某个其他组件中时使用,并且可能无法通过配置对象对其进行配置。在这些情况下,可以spring.expression.compiler.mode 通过 JVM 系统属性(或通过 SpringProperties机制)将属性设置为 SpelCompilerMode枚举值之一(offimmediatemixed)。

编译器限制

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

应用程序上下文中的所有 bean 都可以作为具有公共 bean 名称的预定义变量使用。这包括用于访问运行时环境的标准上下文 bean,例如environment(of type org.springframework.core.env.Environment) 以及systemPropertiesand systemEnvironment(of type )。Map<String, Object>

以下示例显示了对systemProperties作为 SpEL 变量的 bean 的访问:

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

    <!-- other properties -->
</bean>

请注意,您不必在#此处使用符号作为预定义变量的前缀。

您还可以按名称引用其他 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注释放在字段、方法以及方法或构造函数参数上。

以下示例设置字段的默认值:

java
public class FieldValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}
科特林
class FieldValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    var defaultLocale: String? = null
}

以下示例显示了等效的但在属性设置器方法上:

java
public class PropertyValueTestBean {

    private String defaultLocale;

    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}
科特林
class PropertyValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    var defaultLocale: String? = null
}

自动装配的方法和构造函数也可以使用@Value注解,如以下示例所示:

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

    // ...
}
科特林
class SimpleMovieLister {

    private lateinit var movieFinder: MovieFinder
    private lateinit var defaultLocale: String

    @Autowired
    fun configure(movieFinder: MovieFinder,
                @Value("#{ systemProperties['user.region'] }") defaultLocale: String) {
        this.movieFinder = movieFinder
        this.defaultLocale = defaultLocale
    }

    // ...
}
java
public class MovieRecommender {

    private String defaultLocale;

    private CustomerPreferenceDao customerPreferenceDao;

    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }

    // ...
}
科特林
class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao,
            @Value("#{systemProperties['user.country']}") private val defaultLocale: String) {
    // ...
}

4.3. 语言参考

本节介绍 Spring 表达式语言的工作原理。它涵盖以下主题:

4.3.1。文字表达

支持的文字表达式类型是字符串、数值(int、real、hex)、boolean 和 null。字符串由单引号分隔。要将单引号本身放在字符串中,请使用两个单引号字符。

以下清单显示了文字的简单用法。通常,它们不会像这样孤立地使用,而是作为更复杂表达式的一部分使用——例如,在逻辑比较运算符的一侧使用文字。

java
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();
科特林
val parser = SpelExpressionParser()

// evals to "Hello World"
val helloWorld = parser.parseExpression("'Hello World'").value as String

val avogadrosNumber = parser.parseExpression("6.0221415E+23").value as Double

// evals to 2147483647
val maxValue = parser.parseExpression("0x7FFFFFFF").value as Int

val trueValue = parser.parseExpression("true").value as Boolean

val nullValue = parser.parseExpression("null").value

数字支持使用负号、指数表示法和小数点。默认情况下,实数使用Double.parseDouble().

4.3.2. 属性、数组、列表、映射和索引器

使用属性引用导航很容易。为此,请使用句点来指示嵌套属性值。Inventor类的实例pupintesla填充了示例部分中使用的类中列出的数据。为了“向下”导航对象图并获得 Tesla 的出生年份和 Pupin 的出生城市,我们使用以下表达式:

java
// evals to 1856
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context);
科特林
// evals to 1856
val year = parser.parseExpression("birthdate.year + 1900").getValue(context) as Int

val city = parser.parseExpression("placeOfBirth.city").getValue(context) as String

属性名称的第一个字母允许不区分大小写。因此,上述示例中的表达式可以分别写为Birthdate.Year + 1900PlaceOfBirth.City。此外,可以选择通过方法调用来访问属性——例如,getPlaceOfBirth().getCity()而不是 placeOfBirth.city.

数组和列表的内容使用方括号表示法获得,如以下示例所示:

java
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);
科特林
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

// Inventions Array

// evaluates to "Induction motor"
val invention = parser.parseExpression("inventions[3]").getValue(
        context, tesla, String::class.java)

// Members List

// evaluates to "Nikola Tesla"
val name = parser.parseExpression("members[0].name").getValue(
        context, ieee, String::class.java)

// List and Array navigation
// evaluates to "Wireless communication"
val invention = parser.parseExpression("members[0].inventions[6]").getValue(
        context, ieee, String::class.java)

通过在括号内指定文字键值来获取映射的内容。在下面的示例中,因为officers映射的键是字符串,我们可以指定字符串文字:

java
// 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");
科特林
// Officer's Dictionary

val pupin = parser.parseExpression("officers['president']").getValue(
        societyContext, Inventor::class.java)

// evaluates to "Idvor"
val city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue(
        societyContext, String::class.java)

// setting values
parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue(
        societyContext, "Croatia")

4.3.3. 内联列表

您可以使用符号直接在表达式中表示列表{}

java
// 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);
科特林
// evaluates to a Java list containing the four numbers
val numbers = parser.parseExpression("{1,2,3,4}").getValue(context) as List<*>

val listOfLists = parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context) as List<*>

{}本身意味着一个空列表。出于性能原因,如果列表本身完全由固定文字组成,则会创建一个常量列表来表示表达式(而不是在每次评估时构建一个新列表)。

4.3.4. 内联地图

您还可以使用符号直接在表达式中表示映射{key:value}。以下示例显示了如何执行此操作:

java
// 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);
科特林
// evaluates to a Java map containing the two entries
val inventorInfo = parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context) as Map<*, *>

val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *>

{:}其本身意味着一张空地图。出于性能原因,如果映射本身由固定文字或其他嵌套常量结构(列表或映射)组成,则会创建一个常量映射来表示表达式(而不是在每次评估时构建一个新映射)。引用映射键是可选的(除非键包含句点 ( .))。上面的示例不使用带引号的键。

4.3.5。数组构造

您可以使用熟悉的 Java 语法构建数组,可选择提供初始化程序以在构建时填充数组。以下示例显示了如何执行此操作:

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);
科特林
val numbers1 = parser.parseExpression("new int[4]").getValue(context) as IntArray

// Array with initializer
val numbers2 = parser.parseExpression("new int[]{1,2,3}").getValue(context) as IntArray

// Multi dimensional array
val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array<IntArray>

构造多维数组时,当前无法提供初始化程序。

4.3.6. 方法

您可以使用典型的 Java 编程语法来调用方法。您还可以在文字上调用方法。还支持可变参数。以下示例显示了如何调用方法:

java
// 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);
科特林
// string literal, evaluates to "bc"
val bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String::class.java)

// evaluates to true
val isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
        societyContext, Boolean::class.java)

4.3.7. 运营商

Spring 表达式语言支持以下类型的运算符:

关系运算符

使用标准运算符表示法支持关系运算符(等于、不等于、小于、小于或等于、大于和大于或等于)。以下清单显示了一些运算符示例:

java
// 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);
科特林
// evaluates to true
val trueValue = parser.parseExpression("2 == 2").getValue(Boolean::class.java)

// evaluates to false
val falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean::class.java)

// evaluates to true
val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java)

大于和小于比较null遵循一个简单的规则:null被视为无(即不为零)。因此,任何其他值总是大于nullX > null总是true),并且没有其他值永远小于零(X < null总是false)。

如果您更喜欢数字比较,请避免基于数字的比较,而null倾向于与零进行比较(例如,X > 0X < 0)。

除了标准的关系运算符外,SpEL 还支持instanceof基于正则表达式的matches运算符。以下清单显示了两者的示例:

java
// 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);
科特林
// evaluates to false
val falseValue = parser.parseExpression(
        "'xyz' instanceof T(Integer)").getValue(Boolean::class.java)

// evaluates to true
val trueValue = parser.parseExpression(
        "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java)

// evaluates to false
val falseValue = parser.parseExpression(
        "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java)
小心原始类型,因为它们会立即被装箱到它们的包装器类型中。例如,1 instanceof T(int)计算结果为false,而 1 instanceof T(Integer)计算结果为true,如预期的那样。

每个符号运算符也可以指定为纯字母等效项。这避免了使用的符号对于嵌入表达式的文档类型(例如在 XML 文档中)具有特殊含义的问题。文本等价物是:

  • lt( <)

  • gt( >)

  • le( <=)

  • ge( >=)

  • eq( ==)

  • ne( !=)

  • div( /)

  • mod( %)

  • not( !)。

所有文本运算符都不区分大小写。

逻辑运算符

SpEL 支持以下逻辑运算符:

  • and( &&)

  • or( ||)

  • not( !)

以下示例显示了如何使用逻辑运算符:

java
// -- 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);
科特林
// -- AND --

// evaluates to false
val falseValue = parser.parseExpression("true and false").getValue(Boolean::class.java)

// evaluates to true
val expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')"
val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)

// -- OR --

// evaluates to true
val trueValue = parser.parseExpression("true or false").getValue(Boolean::class.java)

// evaluates to true
val expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')"
val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)

// -- NOT --

// evaluates to false
val falseValue = parser.parseExpression("!true").getValue(Boolean::class.java)

// -- AND and NOT --
val expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"
val falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)
数学运算符

您可以+对数字和字符串使用加法运算符 ( )。您只能对数字使用减法 ( -)、乘法 ( *) 和除法 ( /) 运算符。您还可以对数字使用模数 ( %) 和指数幂 ( ^) 运算符。强制执行标准运算符优先级。以下示例显示了正在使用的数学运算符:

java
// 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
科特林
// Addition
val two = parser.parseExpression("1 + 1").getValue(Int::class.java)  // 2

val testString = parser.parseExpression(
        "'test' + ' ' + 'string'").getValue(String::class.java)  // 'test string'

// Subtraction
val four = parser.parseExpression("1 - -3").getValue(Int::class.java)  // 4

val d = parser.parseExpression("1000.00 - 1e4").getValue(Double::class.java)  // -9000

// Multiplication
val six = parser.parseExpression("-2 * -3").getValue(Int::class.java)  // 6

val twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double::class.java)  // 24.0

// Division
val minusTwo = parser.parseExpression("6 / -3").getValue(Int::class.java)  // -2

val one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double::class.java)  // 1.0

// Modulus
val three = parser.parseExpression("7 % 4").getValue(Int::class.java)  // 3

val one = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java)  // 1

// Operator precedence
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java)  // -21
赋值运算符

要设置属性,请使用赋值运算符 ( =)。这通常在对 的调用中完成,setValue但也可以在对 的调用中完成getValue。以下清单显示了使用赋值运算符的两种方式:

java
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);
科特林
val inventor = Inventor()
val context = SimpleEvaluationContext.forReadWriteDataBinding().build()

parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic")

// alternatively
val aleks = parser.parseExpression(
        "name = 'Aleksandar Seovic'").getValue(context, inventor, String::class.java)

4.3.8。类型

您可以使用特殊T运算符来指定java.lang.Class(类型)的实例。静态方法也可以使用此运算符调用。使用 StandardEvaluationContextaTypeLocator来查找类型,并且 StandardTypeLocator(可以替换) 是在对 java.lang包的理解的情况下构建的。这意味着T()java.lang 包中类型的引用不需要完全限定,但所有其他类型引用都必须是完全限定的。以下示例显示了如何使用T运算符:

java
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);
科特林
val dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class::class.java)

val stringClass = parser.parseExpression("T(String)").getValue(Class::class.java)

val trueValue = parser.parseExpression(
        "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
        .getValue(Boolean::class.java)

4.3.9。构造函数

您可以使用new运算符调用构造函数。除了位于java.lang包中的类型(IntegerFloatString等)之外,您应该对所有类型使用完全限定的类名。下面的例子展示了如何使用 new操作符来调用构造函数:

java
Inventor einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor.class);

// create new Inventor instance within the add() method of List
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))").getValue(societyContext);
科特林
val einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor::class.java)

// create new Inventor instance within the add() method of List
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
        .getValue(societyContext)

4.3.10。变量

您可以使用#variableName语法来引用表达式中的变量。变量是通过使用实现setVariable上的方法来设置的EvaluationContext

有效的变量名称必须由以下一个或多个受支持的字符组成。

  • 字母:AtoZatoz

  • 数字:09

  • 下划线:_

  • 美元符号:$

下面的例子展示了如何使用变量。

java
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"
科特林
val tesla = Inventor("Nikola Tesla", "Serbian")

val context = SimpleEvaluationContext.forReadWriteDataBinding().build()
context.setVariable("newName", "Mike Tesla")

parser.parseExpression("name = #newName").getValue(context, tesla)
println(tesla.name)  // "Mike Tesla"
#this#root变量_

#this变量始终被定义并引用当前的评估对象(针对哪些不合格的引用被解析)。该#root变量始终被定义并引用根上下文对象。尽管#this评估表达式的组件时可能会有所不同,但#root始终指的是根。以下示例显示了如何使用#this#root变量:

java
// 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);
科特林
// create an array of integers
val primes = ArrayList<Int>()
primes.addAll(listOf(2, 3, 5, 7, 11, 13, 17))

// create parser and set variable 'primes' as the array of integers
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataAccess()
context.setVariable("primes", primes)

// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
val primesGreaterThanTen = parser.parseExpression(
        "#primes.?[#this>10]").getValue(context) as List<Int>

4.3.11。功能

您可以通过注册可以在表达式字符串中调用的用户定义函数来扩展 SpEL。该功能通过EvaluationContext. 以下示例显示了如何注册用户定义的函数:

java
Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);
科特林
val method: Method = ...

val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
context.setVariable("myFunction", method)

例如,考虑以下反转字符串的实用程序方法:

java
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();
    }
}
科特林
fun reverseString(input: String): String {
    val backwards = StringBuilder(input.length)
    for (i in 0 until input.length) {
        backwards.append(input[input.length - 1 - i])
    }
    return backwards.toString()
}

然后您可以注册并使用上述方法,如以下示例所示:

java
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);
科特林
val parser = SpelExpressionParser()

val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
context.setVariable("reverseString", ::reverseString::javaMethod)

val helloWorldReversed = parser.parseExpression(
        "#reverseString('hello')").getValue(context, String::class.java)

4.3.12。Bean 引用

如果评估上下文已经配置了 bean 解析器,您可以使用@符号从表达式中查找 bean。以下示例显示了如何执行此操作:

java
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);
科特林
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
context.setBeanResolver(MyBeanResolver())

// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
val bean = parser.parseExpression("@something").getValue(context)

要访问工厂 bean 本身,您应该在 bean 名称前加上一个&符号。以下示例显示了如何执行此操作:

java
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);
科特林
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
context.setBeanResolver(MyBeanResolver())

// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
val bean = parser.parseExpression("&foo").getValue(context)

4.3.13。三元运算符(If-Then-Else)

您可以使用三元运算符在表达式内执行 if-then-else 条件逻辑。以下清单显示了一个最小示例:

java
String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);
科特林
val falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String::class.java)

在这种情况下,布尔值false会返回字符串 value 'falseExp'。一个更现实的例子如下:

java
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"
科特林
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'"

val queryResultString = parser.parseExpression(expression)
        .getValue(societyContext, String::class.java)
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

有关三元运算符的更短语法,请参阅下一节有关 Elvis 运算符的内容。

4.3.14。猫王接线员

Elvis 运算符是三元运算符语法的缩写,用于 Groovy语言。使用三元运算符语法,您通常必须重复一个变量两次,如以下示例所示:

String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

相反,您可以使用 Elvis 运算符(以与 Elvis 的发型相似而命名)。以下示例显示了如何使用 Elvis 运算符:

java
ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name);  // 'Unknown'
科特林
val parser = SpelExpressionParser()

val name = parser.parseExpression("name?:'Unknown'").getValue(Inventor(), String::class.java)
println(name)  // 'Unknown'

以下清单显示了一个更复杂的示例:

java
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
科特林
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

val tesla = Inventor("Nikola Tesla", "Serbian")
var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name)  // Nikola Tesla

tesla.setName(null)
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name)  // Elvis Presley

您可以使用 Elvis 运算符在表达式中应用默认值。以下示例显示了如何在@Value表达式中使用 Elvis 运算符:

@Value("#{systemProperties['pop3.port'] ?: 25}")

pop3.port如果已定义,则将注入系统属性,否则将注入 25。

4.3.15。安全导航操作员

安全导航运算符用于避免 aNullPointerException和来自Groovy 语言。通常,当您有一个对象的引用时,您可能需要在访问该对象的方法或属性之前验证它不为空。为避免这种情况,安全导航运算符返回 null 而不是引发异常。以下示例显示了如何使用安全导航运算符:

java
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!!!
科特林
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

val tesla = Inventor("Nikola Tesla", "Serbian")
tesla.setPlaceOfBirth(PlaceOfBirth("Smiljan"))

var city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java)
println(city)  // Smiljan

tesla.setPlaceOfBirth(null)
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java)
println(city)  // null - does not throw NullPointerException!!!

4.3.16。集合选择

选择是一种强大的表达式语言功能,可让您通过从其条目中进行选择将源集合转换为另一个集合。

选择使用.?[selectionExpression]. 它过滤集合并返回一个包含原始元素子集的新集合。例如,选择可以让我们轻松获得塞尔维亚发明人的列表,如下例所示:

java
List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "members.?[nationality == 'Serbian']").getValue(societyContext);
科特林
val list = parser.parseExpression(
        "members.?[nationality == 'Serbian']").getValue(societyContext) as List<Inventor>

数组和任何实现java.lang.Iterable或 的东西都支持选择java.util.Map。对于列表或数组,将针对每个单独的元素评估选择标准。针对映射,针对每个映射条目(Java 类型的对象)评估选择标准Map.Entry。每个地图条目都有其keyvalue 访问的属性,以便在选择中使用。

以下表达式返回一个新映射,该映射由原始映射中条目值小于 27 的元素组成:

java
Map newMap = parser.parseExpression("map.?[value<27]").getValue();
科特林
val newMap = parser.parseExpression("map.?[value<27]").getValue()

除了返回所有选定的元素之外,您还可以只检索第一个或最后一个元素。要获取与选择匹配的第一个元素,语法为 .^[selectionExpression]. 要获得最后一个匹配的选择,语法是 .$[selectionExpression].

4.3.17。集合投影

投影让集合驱动子表达式的评估,结果是一个新的集合。投影的语法是.![projectionExpression]. 例如,假设我们有一个发明者列表,但想要他们出生的城市列表。实际上,我们希望为发明者列表中的每个条目评估“placeOfBirth.city”。以下示例使用投影来执行此操作:

java
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]");
科特林
// returns ['Smiljan', 'Idvor' ]
val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*>

数组和任何实现java.lang.Iterable或 的东西都支持投影java.util.Map。当使用地图来驱动投影时,投影表达式会根据地图中的每个条目(表示为 Java Map.Entry)进行评估。跨地图投影的结果是一个列表,其中包含对每个地图条目的投影表达式的评估。

4.3.18。表达式模板

表达式模板允许将文字文本与一个或多个评估块混合。每个评估块都由您可以定义的前缀和后缀字符分隔。一个常见的选择是#{ }用作分隔符,如以下示例所示:

java
String randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"
科特林
val randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        TemplateParserContext()).getValue(String::class.java)

// evaluates to "random number is 0.7038186818312008"

'random number is '通过将文字文本与评估分隔符内的表达式的结果#{ }(在本例中,调用该random()方法的结果)连接来评估字符串。该parseExpression()方法的第二个参数是类型ParserContext。该ParserContext接口用于影响表达式的解析方式,以支持表达式模板功能。的定义TemplateParserContext如下:

java
public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return "#{";
    }

    public String getExpressionSuffix() {
        return "}";
    }

    public boolean isTemplate() {
        return true;
    }
}
科特林
class TemplateParserContext : ParserContext {

    override fun getExpressionPrefix(): String {
        return "#{"
    }

    override fun getExpressionSuffix(): String {
        return "}"
    }

    override fun isTemplate(): Boolean {
        return true
    }
}

4.4. 示例中使用的类

本节列出了本章示例中使用的类。

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;
    }
}
Inventor.kt
class Inventor(
    var name: String,
    var nationality: String,
    var inventions: Array<String>? = null,
    var birthdate: Date =  GregorianCalendar().time,
    var placeOfBirth: PlaceOfBirth? = null)
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;
    }
}
出生地.kt
class PlaceOfBirth(var city: String, var country: String? = null) {
社会.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;
    }
}
社会.kt
package org.spring.samples.spel.inventor

import java.util.*

class Society {

    val Advisors = "advisors"
    val President = "president"

    var name: String? = null

    val members = ArrayList<Inventor>()
    val officers = mapOf<Any, Any>()

    fun isMember(name: String): Boolean {
        for (inventor in members) {
            if (inventor.name == name) {
                return true
            }
        }
        return false
    }
}

5. Spring 面向切面编程

面向方面编程 (AOP) 通过提供另一种思考程序结构的方式来补充面向对象编程 (OOP)。OOP 中模块化的关键单元是类,而 AOP 中模块化的单元是方面。方面支持跨多种类型和对象的关注点(例如事务管理)的模块化。(这种关注点在 AOP 文献中通常被称为“横切”关注点。)

Spring 的关键组件之一是 AOP 框架。虽然 Spring IoC 容器不依赖 AOP(这意味着如果您不想使用 AOP,则无需使用 AOP),AOP 补充了 Spring IoC 以提供非常强大的中间件解决方案。

带有 AspectJ 切入点的 Spring AOP

Spring 通过使用 基于模式的方法@AspectJ 注释样式提供了编写自定义切面的简单而强大的方法。这两种风格都提供了完全类型化的建议和使用 AspectJ 切入点语言,同时仍然使用 Spring AOP 进行编织。

本章讨论基于模式和@AspectJ 的 AOP 支持。较低级别的 AOP 支持将在下一章中讨论。

AOP 在 Spring Framework 中用于:

  • 提供声明式企业服务。最重要的此类服务是 声明式事务管理

  • 让用户实现自定义方面,用 AOP 补充他们对 OOP 的使用。

如果您只对通用声明式服务或其他预打包的声明式中间件服务(例如池)感兴趣,则无需直接使用 Spring AOP,并且可以跳过本章的大部分内容。

5.1。AOP 概念

让我们从定义一些核心 AOP 概念和术语开始。这些术语不是 Spring 特定的。不幸的是,AOP 术语并不是特别直观。但是,如果 Spring 使用它自己的术语,那就更令人困惑了。

  • Aspect:跨多个类的关注点的模块化。事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。在 Spring AOP 中,方面是通过使用常规类(基于模式的方法)或使用 @Aspect注解注释的常规类(@AspectJ 样式)来实现的。

  • 连接点:程序执行过程中的一个点,例如方法的执行或异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。

  • 建议:方面在特定连接点采取的行动。不同类型的建议包括“周围”、“之前”和“之后”建议。(通知类型将在后面讨论。)包括 Spring 在内的许多 AOP 框架将通知建模为拦截器,并在连接点周围维护一个拦截器链。

  • 切入点:匹配连接点的谓词。Advice 与切入点表达式相关联,并在与切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。切入点表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言。

  • 简介:代表一个类型声明额外的方法或字段。Spring AOP 允许您向任何建议的对象引入新接口(和相应的实现)。例如,您可以使用介绍使 bean 实现 IsModified接口,以简化缓存。(介绍在 AspectJ 社区中称为类型间声明。)

  • 目标对象:一个或多个方面建议的对象。也称为“建议对象”。由于 Spring AOP 是使用运行时代理实现的,因此该对象始终是代理对象。

  • AOP 代理:由 AOP 框架创建的对象,用于实现切面契约(建议方法执行等)。在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。

  • 编织:将方面与其他应用程序类型或对象链接以创建建议对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。

Spring AOP 包括以下类型的通知:

  • 通知前:在连接点之前运行但不能阻止执行流继续到连接点的通知(除非它抛出异常)。

  • 返回通知后:在连接点正常完成后运行的通知(例如,如果方法返回而没有引发异常)。

  • 抛出建议后:如果方法因抛出异常而退出,则运行建议。

  • 之后(最终)通知:无论连接点以何种方式退出(正常或异常返回),都将运行建议。

  • 围绕建议:围绕连接点的建议,例如方法调用。这是最有力的建议。环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点还是通过返回自己的返回值或抛出异常来缩短建议的方法执行。

环绕建议是最一般的建议。由于 Spring AOP 与 AspectJ 一样,提供了全方位的通知类型,因此我们建议您使用可以实现所需行为的最不强大的通知类型。例如,如果您只需要使用方法的返回值更新缓存,则最好实现一个 after return 通知而不是一个 around 通知,尽管一个 around 通知可以完成同样的事情。使用最具体的通知类型提供了一个更简单的编程模型,并且出错的可能性更小。例如,您不需要调用used for around 通知proceed() 上的方法JoinPoint,因此,您不能不调用它。

所有的通知参数都是静态类型的,因此您可以使用适当类型的通知参数(例如,方法执行的返回值的类型)而不是Object数组。

切入点匹配的连接点的概念是 AOP 的关键,这将它与仅提供拦截的旧技术区分开来。切入点使建议的目标独立于面向对象的层次结构。例如,您可以将提供声明性事务管理的环绕建议应用于一组跨越多个对象(例如服务层中的所有业务操作)的方法。

5.2. Spring AOP 的能力和目标

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 框架的 AOP 功能通常与 Spring IoC 容器一起使用。方面是通过使用普通的 bean 定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现的关键区别。您无法使用 Spring AOP 轻松或高效地做一些事情,例如建议非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ 是最佳选择。然而,我们的经验是,Spring AOP 为企业 Java 应用程序中大多数适合 AOP 的问题提供了出色的解决方案。

Spring AOP 从不努力与 AspectJ 竞争以提供全面的 AOP 解决方案。我们相信 Spring AOP 等基于代理的框架和 AspectJ 等成熟框架都很有价值,它们是互补的,而不是竞争的。Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,以在一致的基于 Spring 的应用程序架构中实现 AOP 的所有使用。此集成不会影响 Spring AOP API 或 AOP Alliance API。Spring AOP 保持向后兼容。有关 Spring AOP API 的讨论,请参见下一章

Spring 框架的核心原则之一是非侵入性。这就是不应该强迫您将特定于框架的类和接口引入您的业务或领域模型的想法。但是,在某些地方,Spring Framework 确实为您提供了将 Spring Framework 特定的依赖项引入代码库的选项。为您提供此类选项的理由是,在某些情况下,以这种方式阅读或编写某些特定功能可能更容易。但是,Spring 框架(几乎)总是为您提供选择:您可以自由地就哪个选项最适合您的特定用例或场景做出明智的决定。

与本章相关的一个这样的选择是选择哪种 AOP 框架(以及哪种 AOP 风格)。您可以选择 AspectJ、Spring AOP 或两者兼而有之。您还可以选择 @AspectJ 注释样式方法或 Spring XML 配置样式方法。本章选择首先介绍@AspectJ 样式方法这一事实不应被视为Spring 团队更喜欢@AspectJ 注释样式方法而不是Spring XML 配置样式的指示。

有关每种样式的“原因和原因”的更完整讨论,请参阅选择使用哪种 AOP 声明样式。

5.3. AOP 代理

Spring AOP 默认为 AOP 代理使用标准的 JDK 动态代理。这使得任何接口(或一组接口)都可以被代理。

Spring AOP 也可以使用 CGLIB 代理。这是代理类而不是接口所必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于对接口而不是类进行编程是一种很好的做法,因此业务类通常实现一个或多个业务接口。在那些(希望很少见)需要建议未在接口上声明的方法或需要将代理对象作为具体类型传递给方法的情况下,可以 强制使用 CGLIB 。

重要的是要掌握 Spring AOP 是基于代理的这一事实。请参阅 了解 AOP 代理,以彻底检查此实现细节的实际含义。

5.4. @AspectJ 支持

@AspectJ 指的是一种将切面声明为带有注解的常规 Java 类的风格。@AspectJ 样式是由 AspectJ 项目作为 AspectJ 5 版本的一部分引入的。Spring 解释与 AspectJ 5 相同的注释,使用 AspectJ 提供的库进行切入点解析和匹配。但是,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

使用 AspectJ 编译器和编织器可以使用完整的 AspectJ 语言,并在将 AspectJ 与 Spring 应用程序一起使用中进行了讨论。

5.4.1。启用@AspectJ 支持

要在 Spring 配置中使用 @AspectJ 方面,您需要启用 Spring 支持以基于 @AspectJ 方面配置 Spring AOP,并根据这些方面是否建议它们来自动代理 bean。通过自动代理,我们的意思是,如果 Spring 确定一个 bean 由一个或多个方面通知,它会自动为该 bean 生成一个代理来拦截方法调用并确保根据需要运行通知。

可以使用 XML 或 Java 样式的配置启用 @AspectJ 支持。无论哪种情况,您还需要确保 AspectJ 的aspectjweaver.jar库位于应用程序的类路径中(1.8 版或更高版本)。该库在 libAspectJ 发行版的目录中或从 Maven 中央存储库中可用。

使用 Java 配置启用 @AspectJ 支持

要使用 Java 启用 @AspectJ 支持@Configuration,请添加@EnableAspectJAutoProxy 注释,如以下示例所示:

java
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
科特林
@Configuration
@EnableAspectJAutoProxy
class AppConfig
通过 XML 配置启用 @AspectJ 支持

要使用基于 XML 的配置启用 @AspectJ 支持,请使用该aop:aspectj-autoproxy 元素,如以下示例所示:

<aop:aspectj-autoproxy/>

这假定您使用 基于 XML 模式的配置中所述的模式支持。有关如何在命名空间中导入标签的信息,请参阅AOP 模式aop

5.4.2. 声明一个方面

启用 @AspectJ 支持后,在应用程序上下文中定义的任何带有 @AspectJ 方面(具有@Aspect注释)的类的 bean 都会被 Spring 自动检测并用于配置 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注解;

java
package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}
科特林
package org.xyz

import org.aspectj.lang.annotation.Aspect;

@Aspect
class NotVeryUsefulAspect

方面(用 注释的类@Aspect)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点、建议和介绍(类型间)声明。

通过组件扫描自动检测方面
您可以通过类中的@Bean方法将方面类注册为 Spring XML 配置中的常规 bean @Configuration,或者让 Spring 通过类路径扫描自动检测它们——与任何其他 Spring 管理的 bean 相同。但是,请注意, @Aspect注释不足以在类路径中进行自动检测。为此,您需要添加一个单独的@Component注解(或者,根据 Spring 组件扫描器的规则,一个符合条件的自定义构造型注解)。
建议方面与其他方面?
在 Spring AOP 中,切面本身不能成为其他切面通知的目标。类上的@Aspect注释将其标记为方面,因此将其排除在自动代理之外。

5.4.3. 声明切入点

切入点确定连接点,从而使我们能够控制建议何时运行。Spring AOP 仅支持 Spring bean 的方法执行连接点,因此您可以将切入点视为匹配 Spring bean 上的方法执行。切入点声明有两部分:一个包含名称和任何参数的签名和一个切入点表达式,它准确地确定我们感兴趣的方法执行。在 AOP 的 @AspectJ 注释样式中,切入点签名由常规方法定义提供, 并且切入点表达式使用@Pointcut注解来表示(作为切入点签名的方法必须有void返回类型)。

一个示例可能有助于明确切入点签名和切入点表达式之间的区别。以下示例定义了一个名为的切入点anyOldTransfer,它与任何名为的方法的执行相匹配transfer

java
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
科特林
@Pointcut("execution(* transfer(..))") // the pointcut expression
private fun anyOldTransfer() {} // the pointcut signature

形成@Pointcut注释值的切入点表达式是常规的 AspectJ 切入点表达式。有关 AspectJ 切入点语言的完整讨论,请参阅AspectJ Programming Guide(以及扩展的 AspectJ 5 Developer's Notebook)或有关 AspectJ 的书籍之一(例如Colyer 等人的Eclipse AspectJ或AspectJ in Action,拉姆尼瓦斯·拉达德)。

支持的切入点指示符

Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示符 (PCD):

  • execution:用于匹配方法执行连接点。这是使用 Spring AOP 时使用的主要切入点指示符。

  • within: 限制匹配到特定类型内的连接点(使用 Spring AOP 时执行匹配类型内声明的方法)。

  • this:限制匹配到连接点(使用 Spring AOP 时方法的执行),其中 bean 引用(Spring AOP 代理)是给定类型的实例。

  • target:将匹配限制在目标对象(被代理的应用程序对象)是给定类型的实例的连接点(使用 Spring AOP 时方法的执行)。

  • args: 限制匹配到参数是给定类型的实例的连接点(使用 Spring AOP 时方法的执行)。

  • @target:限制匹配到连接点(使用 Spring AOP 时方法的执行),其中执行对象的类具有给定类型的注释。

  • @args:将匹配限制为连接点(使用 Spring AOP 时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。

  • @within:将匹配限制为具有给定注释的类型内的连接点(使用 Spring AOP 时执行在具有给定注释的类型中声明的方法)。

  • @annotation:限制匹配到连接点的主题(在 Spring AOP 中运行的方法)具有给定注释的连接点。

其他切入点类型

完整的 AspectJ 切入点语言支持 Spring 中不支持的其他切入点指示符:callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@this@withincode。在 Spring AOP 解释的切入点表达式中使用这些切入点指示符会导致IllegalArgumentException抛出异常。

Spring AOP 支持的切入点指示符集可能会在未来的版本中扩展,以支持更多的 AspectJ 切入点指示符。

因为 Spring AOP 将匹配限制为仅方法执行连接点,所以前面对切入点指示符的讨论给出了比您在 AspectJ 编程指南中找到的更窄的定义。此外,AspectJ 本身具有基于类型的语义,并且在执行连接点,两者都this引用target同一个对象:执行方法的对象。Spring AOP 是一个基于代理的系统,它区分代理对象本身(绑定到this)和代理背后的目标对象(绑定到target)。

由于 Spring 的 AOP 框架基于代理的特性,根据定义,目标对象内的调用不会被拦截。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,代理上的公共和受保护的方法调用被拦截(如果需要,甚至包可见的方法)。但是,通过代理的常见交互应始终通过公共签名进行设计。

请注意,切入点定义通常与任何拦截的方法匹配。如果切入点严格来说是只公开的,即使在 CGLIB 代理场景中,通过代理进行潜在的非公开交互,也需要相应地定义它。

如果您的拦截需求包括目标类中的方法调用甚至构造函数,请考虑使用 Spring 驱动的本机 AspectJ 编织,而不是 Spring 的基于代理的 AOP 框架。这构成了具有不同特点的不同AOP使用模式,所以在做决定之前一定要让自己熟悉编织。

Spring AOP 还支持一个名为bean. 此 PCD 允许您将连接点的匹配限制为特定命名的 Spring bean 或一组命名的 Spring bean(使用通配符时)。beanPCD 具有以下形式:

java
bean(idOrNameOfBean)
科特林
bean(idOrNameOfBean)

idOrNameOfBean令牌可以是任何 Spring bean 的名称。提供了使用该*字符的有限通配符支持,因此,如果您为 Spring bean 建立了一些命名约定,您可以编写一个beanPCD 表达式来选择它们。与其他切入点指示符的情况一样,beanPCD 也可以与&&(and)、||(or) 和!(negation) 运算符一起使用。

beanPCD 仅在 Spring AOP 中受支持,而在本机 AspectJ 编织中不支持。它是 AspectJ 定义的标准 PCD 的 Spring 特定扩展,因此不适用于@Aspect模型中声明的方面。

PCD在bean实例级别(基于 Spring bean 名称概念构建)而不是仅在类型级别(基于编织的 AOP 仅限于此)运行。基于实例的切入点指示符是 Spring 基于代理的 AOP 框架的一种特殊功能,它与 Spring bean factory 紧密集成,通过名称识别特定 bean 是自然而直接的。

组合切入点表达式

您可以使用&&, ||and组合切入点表达式!。您还可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:

java
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} (1)

@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} (2)

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} (3)
1 anyPublicOperation如果方法执行连接点表示任何公共方法的执行,则匹配。
2 inTrading如果方法执行在交易模块中,则匹配。
3 tradingOperation如果方法执行代表交易模块中的任何公共方法,则匹配。
科特林
@Pointcut("execution(public * *(..))")
private fun anyPublicOperation() {} (1)

@Pointcut("within(com.xyz.myapp.trading..*)")
private fun inTrading() {} (2)

@Pointcut("anyPublicOperation() && inTrading()")
private fun tradingOperation() {} (3)
1 anyPublicOperation如果方法执行连接点表示任何公共方法的执行,则匹配。
2 inTrading如果方法执行在交易模块中,则匹配。
3 tradingOperation如果方法执行代表交易模块中的任何公共方法,则匹配。

如前所述,从较小的命名组件构建更复杂的切入点表达式是最佳实践。当按名称引用切入点时,应用正常的 Java 可见性规则(您可以看到相同类型的私有切入点、层次结构中的受保护切入点、任何地方的公共切入点,等等)。可见性不影响切入点匹配。

共享通用切入点定义

在使用企业应用程序时,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。我们建议CommonPointcuts为此目的定义一个捕获通用切入点表达式的方面。这样的方面通常类似于以下示例:

java
package com.xyz.myapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class CommonPointcuts {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.myapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.myapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.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.myapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.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.myapp.abc.service and com.xyz.myapp.def.service) then
     * the pointcut expression "execution(* com.xyz.myapp..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.myapp..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.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}
科特林
package com.xyz.myapp

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut

@Aspect
class CommonPointcuts {

    /**
    * A join point is in the web layer if the method is defined
    * in a type in the com.xyz.myapp.web package or any sub-package
    * under that.
    */
    @Pointcut("within(com.xyz.myapp.web..*)")
    fun inWebLayer() {
    }

    /**
    * A join point is in the service layer if the method is defined
    * in a type in the com.xyz.myapp.service package or any sub-package
    * under that.
    */
    @Pointcut("within(com.xyz.myapp.service..*)")
    fun inServiceLayer() {
    }

    /**
    * A join point is in the data access layer if the method is defined
    * in a type in the com.xyz.myapp.dao package or any sub-package
    * under that.
    */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    fun 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.myapp.abc.service and com.xyz.myapp.def.service) then
    * the pointcut expression "execution(* com.xyz.myapp..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.myapp..service.*.*(..))")
    fun 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.myapp.dao.*.*(..))")
    fun dataAccessOperation() {
    }

}

您可以在需要切入点表达式的任何地方引用在此类方面中定义的切入点。例如,要使服务层具有事务性,您可以编写以下代码:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config><aop:advisor>元素在基于模式的 AOP 支持中讨论。事务元素在事务管理中讨论。

例子

Spring AOP 用户可能execution最常使用切入点指示符。执行表达式的格式如下:

    执行(修饰符模式?ret 类型模式声明类型模式?名称模式(参数模式)
                投掷模式?)

除了返回类型模式(ret-type-pattern在前面的代码片段中)、名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么才能匹配连接点。 *最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,完全限定的类型名称才匹配。名称模式与方法名称匹配。您可以将*通配符用作名称模式的全部或一部分。如果您指定声明类型模式,请包含一个尾随.以将其连接到名称模式组件。参数模式稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量(零个或多个)的参数。该(*)模式匹配采用任何类型的一个参数的方法。 (*,String)匹配带有两个参数的方法。第一个可以是任何类型,而第二个必须是String. 有关更多信息,请参阅 AspectJ 编程指南的 语言语义部分。

以下示例显示了一些常见的切入点表达式:

  • 任何公共方法的执行:

        执行(公共 * *(..))
  • 名称以 开头的任何方法的执行set

        执行(*设置*(..))
  • AccountService接口定义的任何方法的执行:

        执行(* com.xyz.service.AccountService.*(..))
  • 包中定义的任何方法的执行service

        执行(* com.xyz.service.*.*(..))
  • 服务包或其子包之一中定义的任何方法的执行:

        执行(* com.xyz.service..*.*(..))
  • 服务包中的任何连接点(仅在 Spring AOP 中执行方法):

        内(com.xyz.service.*)
  • 服务包或其子包之一中的任何连接点(仅在 Spring AOP 中执行方法):

        内(com.xyz.service..*)
  • AccountService代理实现接口的任何连接点(仅在 Spring AOP 中执行方法) :

        这个(com.xyz.service.AccountService)
    this更常用于绑定形式。 有关如何使代理对象在建议正文中可用的信息, 请参阅声明建议部分。
  • AccountService目标对象实现接口的任何连接点(仅在 Spring AOP 中执行方法) :

        目标(com.xyz.service.AccountService)
    target更常用于绑定形式。有关如何使目标对象在建议正文中可用的信息, 请参阅声明建议部分。
  • 任何接受单个参数且在运行时传递的参数为的连接点(仅在 Spring AOP 中执行方法)Serializable

        参数(java.io.Serializable)
    args更常用于绑定形式。请参阅声明建议部分,了解如何使方法参数在建议正文中可用。

    请注意,此示例中给出的切入点与execution(* *(java.io.Serializable)). 如果在运行时传递的参数是 args 版本匹配 Serializable,如果方法签名声明了一个类型的参数,则执行版本匹配Serializable

  • 目标对象具有 @Transactional注释的任何连接点(仅在 Spring AOP 中执行方法):

        @target(org.springframework.transaction.annotation.Transactional)
    您也可以@target在绑定表单中使用。有关如何使注释对象在建议正文中可用的信息, 请参阅声明建议部分。
  • 目标对象的声明类型具有@Transactional注释的任何连接点(仅在 Spring AOP 中执行方法):

        @within(org.springframework.transaction.annotation.Transactional)
    您也可以@within在绑定表单中使用。有关如何使注释对象在建议正文中可用的信息, 请参阅声明建议部分。
  • 执行方法具有 @Transactional注释的任何连接点(仅在 Spring AOP 中执行方法):

        @annotation(org.springframework.transaction.annotation.Transactional)
    您也可以@annotation在绑定表单中使用。有关如何使注释对象在建议正文中可用的信息, 请参阅声明建议部分。
  • 任何接受单个参数的连接点(仅在 Spring AOP 中执行方法),并且传递的参数的运行时类型具有@Classified注释:

        @args(com.xyz.security.Classified)
    您也可以@args在绑定表单中使用。请参阅声明建议部分如何使注释对象在建议正文中可用。
  • Spring bean 上的任何连接点(方法仅在 Spring AOP 中执行)名为 tradeService

        豆(贸易服务)
  • 名称与通配符表达式匹配的 Spring bean 上的任何连接点(仅在 Spring AOP 中执行方法)*Service

        豆(*服务)
编写好的切入点

在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个代价高昂的过程。(动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中放置测试以确定代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点用 DNF(析取范式)重写,切入点的组件被排序,以便首先检查那些评估成本较低的组件。

然而,AspectJ 只能使用它被告知的内容。为了获得最佳匹配性能,您应该考虑他们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然属于以下三组之一:种类、范围和上下文:

  • 种类指示符选择一种特定类型的连接点: executiongetsetcallhandler

  • 范围指示符选择一组连接兴趣点(可能有多种):withinwithincode

  • 上下文指示符根据上下文匹配(并且可以选择绑定): this、、target@annotation

一个写得很好的切入点应该至少包括前两种类型(种类和范围)。您可以包含上下文指示符以根据连接点上下文进行匹配,或绑定该上下文以在建议中使用。由于额外的处理和分析,仅提供一个 kinded 指示符或仅提供一个上下文指示符有效,但可能会影响编织性能(使用的时间和内存)。范围指示符的匹配速度非常快,使用它们意味着 AspectJ 可以非常快速地消除不应进一步处理的连接点组。如果可能,一个好的切入点应始终包含一个切入点。

5.4.4。申报建议

Advice 与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。

咨询前

@Before您可以使用注释在方面声明之前的通知:

java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}
科特林
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before

@Aspect
class BeforeExample {

    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doAccessCheck() {
        // ...
    }
}

如果我们使用就地切入点表达式,我们可以将前面的示例重写为以下示例:

java
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() {
        // ...
    }
}
科特林
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before

@Aspect
class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    fun doAccessCheck() {
        // ...
    }
}
退货后的建议

返回通知后,当匹配的方法执行正常返回时运行。您可以使用@AfterReturning注解来声明它:

java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}
科特林
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning

@Aspect
class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doAccessCheck() {
        // ...
    }
}
你可以有多个通知声明(以及其他成员),都在同一个方面。我们在这些示例中只展示了一个通知声明,以集中每个通知的效果。

有时,您需要在通知正文中访问返回的实际值。您可以使用@AfterReturning绑定返回值的形式来获取该访问权限,如以下示例所示:

java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}
科特林
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning

@Aspect
class AfterReturningExample {

    @AfterReturning(
        pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning = "retVal")
    fun doAccessCheck(retVal: Any) {
        // ...
    }
}

属性中使用的名称returning必须与通知方法中的参数名称相对应。当方法执行返回时,返回值作为相应的参数值传递给通知方法。子句还将匹配限制为仅返回指定类型的值的returning那些方法执行(在这种情况下,Object匹配任何返回值)。

请注意,在返回通知后使用时,不可能返回完全不同的参考。

投掷后的建议

当匹配的方法执行通过抛出异常退出时,抛出通知运行后。您可以使用@AfterThrowing注解来声明它,如以下示例所示:

java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }
}
科特林
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing

@Aspect
class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doRecoveryActions() {
        // ...
    }
}

通常,您希望建议仅在引发给定类型的异常时运行,并且您还经常需要访问建议正文中引发的异常。您可以使用该throwing属性来限制匹配(如果需要 -Throwable 否则用作异常类型)并将抛出的异常绑定到通知参数。以下示例显示了如何执行此操作:

java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}
科特林
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing

@Aspect
class AfterThrowingExample {

    @AfterThrowing(
        pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing = "ex")
    fun doRecoveryActions(ex: DataAccessException) {
        // ...
    }
}

属性中使用的名称throwing必须与通知方法中的参数名称相对应。当通过抛出异常退出方法执行时,异常将作为相应的参数值传递给通知方法。子句还将匹配限制为仅抛出指定类型的异常(在本例中为 )的throwing那些方法执行。DataAccessException

请注意,@AfterThrowing这并不表示一般的异常处理回调。具体来说,@AfterThrowing建议方法只应该从连接点(用户声明的目标方法)本身接收异常,而不是从伴随的 @After/@AfterReturning方法接收异常。

(最终)建议之后

当匹配的方法执行退出时(最终)通知运行。它是通过使用@After注解来声明的。After通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似目的。下面的例子展示了如何使用 after finally 通知:

java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}
科特林
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.After

@Aspect
class AfterFinallyExample {

    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doReleaseLock() {
        // ...
    }
}

请注意,@AfterAspectJ 中的通知被定义为“在 finally 通知之后”,类似于 try-catch 语句中的 finally 块。它将在连接点(用户声明的目标方法)抛出的任何结果、正常返回或异常时调用,与之相反,@AfterReturning它仅适用于成功的正常返回。

周围的建议

最后一种建议是围绕建议。围绕通知“围绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定该方法何时、如何以及是否真正开始运行。如果您需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器),则通常使用环绕通知。

始终使用满足您要求的最不强大的建议形式。

例如,如果之前的建议足以满足您的需求,请不要使用环绕建议。

环绕通知是通过使用注解对方法进行@Around注解来声明的。该方法应声明Object为其返回类型,并且该方法的第一个参数必须是 type ProceedingJoinPoint。在通知方法的主体中,您必须调用proceed()ProceedingJoinPoint使底层方法运行。不带参数调用proceed()将导致调用者的原始参数在调用时提供给底层方法。对于高级用例,该proceed()方法有一个重载变体,它接受参数数组 ( Object[])。调用时,数组中的值将用作底层方法的参数。

proceed使用 an 调用时的行为与 AspectJ 编译器编译Object[]的 for around 建议的行为略有不同。proceed对于使用传统 AspectJ 语言编写的环绕通知,传递给的参数 proceed数量必须与传递给环绕通知的参数数量相匹配(而不是底层连接点采用的参数数量),并且传递给给定继续进行的值参数位置替换值绑定到的实体的连接点处的原始值(如果现在没有意义,请不要担心)。

Spring 采用的方法更简单,更符合其基于代理的、仅执行的语义。@AspectJ如果您编译为 Spring 编写的方面并proceed与 AspectJ 编译器和编织器一起使用参数,您只需要注意这种差异 。有一种方法可以编写跨 Spring AOP 和 AspectJ 100% 兼容的方面,这将在 下一节有关建议参数的部分中讨论。

around 通知返回的值是方法调用者看到的返回值。例如,一个简单的缓存方面可以从缓存中返回一个值,如果它有一个值,或者调用proceed()(并返回该值)如果它没有。请注意,proceed 在 around 建议的主体内可能会调用一次、多次或根本不调用。所有这些都是合法的。

如果您将周围建议方法的返回类型声明为void,null 将始终返回给调用者,有效地忽略任何调用的结果proceed()。因此,建议使用 around 通知方法声明返回类型为Object. 建议方法通常应该返回从调用返回的值proceed(),即使底层方法具有void返回类型。但是,根据用例,建议可以选择返回缓存值、包装值或其他值。

下面的例子展示了如何使用 around 通知:

java
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.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}
科特林
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.ProceedingJoinPoint

@Aspect
class AroundExample {

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
        // start stopwatch
        val retVal = pjp.proceed()
        // stop stopwatch
        return retVal
    }
}
建议参数

Spring 提供完全类型化的建议,这意味着您在建议签名中声明所需的参数(正如我们之前在返回和抛出示例中看到的那样),而不是Object[]一直使用数组。我们将在本节后面看到如何使参数和其他上下文值可用于建议主体。首先,我们看一下如何编写通用建议,以了解建议当前建议的方法。

访问当前JoinPoint

任何通知方法都可以声明类型为 的参数作为其第一个参数 org.aspectj.lang.JoinPoint。请注意,需要使用环绕通知来声明类型的第一个参数ProceedingJoinPoint,它是 的子类JoinPoint

JoinPoint接口提供了许多有用的方法:

  • getArgs():返回方法参数。

  • getThis():返回代理对象。

  • getTarget():返回目标对象。

  • getSignature():返回对所建议方法的描述。

  • toString():打印所建议方法的有用描述。

有关更多详细信息,请参阅javadoc

将参数传递给 Advice

我们已经看到了如何绑定返回值或异常值(在返回和抛出通知之后使用)。要使参数值可用于通知正文,您可以使用args. 如果在args表达式中使用参数名称代替类型名称,则在调用通知时相应参数的值将作为参数值传递。一个例子应该更清楚地说明这一点。假设您要建议执行以Account 对象为第一个参数的 DAO 操作,并且您需要访问建议正文中的帐户。您可以编写以下内容:

java
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}
科特林
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
fun validateAccount(account: Account) {
    // ...
}

args(account,..)切入点表达式的部分有两个目的。首先,它将匹配限制为仅那些方法至少接受一个参数的方法执行,并且传递给该参数的参数是Account. 其次,它通过参数使实际Account对象可用于通知account

另一种写法是声明一个切入点,Account 当它匹配一个连接点时“提供”对象值,然后从通知中引用命名的切入点。这将如下所示:

java
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}
科特林
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private fun accountDataAccessOperation(account: Account) {
}

@Before("accountDataAccessOperation(account)")
fun validateAccount(account: Account) {
    // ...
}

有关详细信息,请参阅 AspectJ 编程指南。

代理对象 ( this)、目标对象 ( target) 和注释 ( @within@target@annotation@args) 都可以以类似的方式绑定。接下来的两个示例显示了如何匹配带有@Auditable 注释的方法的执行并提取审计代码:

这两个示例中的第一个显示了@Auditable注解的定义:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}
科特林
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Auditable(val value: AuditCode)

这两个示例中的第二个显示了与方法执行相匹配的建议@Auditable

java
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}
科特林
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
fun audit(auditable: Auditable) {
    val code = auditable.value()
    // ...
}
建议参数和泛型

Spring AOP 可以处理类声明和方法参数中使用的泛型。假设你有一个像下面这样的泛型:

java
public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}
科特林
interface Sample<T> {
    fun sampleGenericMethod(param: T)
    fun sampleGenericCollectionMethod(param: Collection<T>)
}

您可以通过将通知参数绑定到要拦截方法的参数类型来将方法类型的拦截限制为某些参数类型:

java
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}
科特林
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
fun beforeSampleMethod(param: MyType) {
    // Advice implementation
}

这种方法不适用于泛型集合。所以你不能定义一个切入点如下:

java
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}
科特林
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
fun beforeSampleMethod(param: Collection<MyType>) {
    // Advice implementation
}

为了完成这项工作,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理null一般的值。要实现类似的效果,您必须键入参数Collection<?>并手动检查元素的类型。

确定参数名称

建议调用中的参数绑定依赖于切入点表达式中使用的名称与建议和切入点方法签名中声明的参数名称的匹配。参数名称无法通过 Java 反射获得,因此 Spring AOP 使用以下策略来确定参数名称:

  • 如果用户已明确指定参数名称,则使用指定的参数名称。建议和切入点注释都有一个可选argNames属性,您可以使用它来指定带注释的方法的参数名称。这些参数名称在运行时可用。以下示例显示了如何使用该argNames属性:

java
@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
}
科特林
@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(bean: Any, auditable: Auditable) {
    val code = auditable.value()
    // ... use code and bean
}

如果第一个参数是JoinPointProceedingJoinPoint或 类型,则可以在属性JoinPoint.StaticPart值中省略参数名称。argNames例如,如果您修改前面的通知以接收连接点对象,则argNames属性不需要包含它:

java
@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
}
科特林
@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) {
    val code = auditable.value()
    // ... use code, bean, and jp
}

JoinPoint对, ProceedingJoinPoint和类型的第一个参数的特殊处理对于JoinPoint.StaticPart不收集任何其他连接点上下文的建议实例特别方便。在这种情况下,您可以省略该argNames属性。例如,以下通知不需要声明argNames属性:

java
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}
科特林
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
fun audit(jp: JoinPoint) {
    // ... use jp
}
  • 使用argNames属性有点笨拙,所以如果argNames没有指定属性,Spring AOP 会查看类的调试信息并尝试从局部变量表中确定参数名称。只要使用调试信息(-g:vars至少)编译了类,就会出现此信息。使用此标志进行编译的后果是:(1)您的代码更容易理解(逆向工程),(2)类文件大小稍微大一点(通常无关紧要),(3)优化以删除未使用的本地您的编译器未应用变量。换句话说,打开此标志进行构建应该不会遇到任何困难。

    如果 AspectJ 编译器 ( ) 已经编译了 @AspectJ 方面,ajc即使没有调试信息,您也不需要添加argNames属性,因为编译器会保留所需的信息。
  • 如果在没有必要调试信息的情况下编译了代码,Spring AOP 会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,并且advice 方法只接受一个参数,则配对很明显)。如果给定可用信息,变量的绑定不明确,AmbiguousBindingException则抛出 an。

  • 如果上述所有策略均失败,IllegalArgumentException则抛出 an。

继续争论

我们之前提到过,我们将描述如何proceed使用在 Spring AOP 和 AspectJ 中一致工作的参数编写调用。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:

java
@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}
科特林
@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
                        accountHolderNamePattern: String): Any {
    val newPattern = preProcess(accountHolderNamePattern)
    return pjp.proceed(arrayOf<Any>(newPattern))
}

在许多情况下,无论如何都要执行此绑定(如前面的示例中所示)。

建议订购

当多条建议都想在同一个连接点运行时会发生什么?Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知执行的顺序。最高优先级的建议首先“在进入的路上”运行(因此,给定两条之前的建议,优先级最高的一条首先运行)。从连接点“退出”时,优先级最高的通知最后运行(因此,给定两条后通知,具有最高优先级的一条将运行第二个)。

当在不同方面定义的两条通知都需要在同一个连接点运行时,除非您另外指定,否则执行顺序是未定义的。您可以通过指定优先级来控制执行顺序。这是通过在org.springframework.core.Ordered方面类中实现接口或使用注释对其进行@Order注释以正常的 Spring 方式完成的。给定两个方面,从Ordered.getOrder()(或注释值)返回较低值的方面具有较高的优先级。

特定方面的每个不同的建议类型在概念上意味着直接应用于连接点。因此,建议方法不应该从伴随的/方法@AfterThrowing接收异常。@After@AfterReturning

从 Spring Framework 5.2.7 开始,在同一@Aspect类中定义的需要在同一连接点运行的通知方法根据其通知类型按以下顺序分配优先级,从最高到最低优先级:@Around, @Before, @After, @AfterReturning, @AfterThrowing。但是请注意,根据@AfterAspectJ对.@AfterReturning@AfterThrowing@After

当同一个类中定义的两条相同类型的advice(例如两个@Afteradvice方法)@Aspect都需要运行在同一个join point时,排序是未定义的(因为无法通过javac 编译类的反射)。考虑将此类建议方法折叠为每个@Aspect类中每个连接点的一个建议方法,或者将建议片段重构为单独的@Aspect类,您可以通过Ordered或在方面级别对这些类进行排序@Order

5.4.5。介绍

介绍(在 AspectJ 中称为类型间声明)使方面能够声明建议对象实现给定接口,并代表这些对象提供该接口的实现。

您可以使用@DeclareParents注释进行介绍。此注释用于声明匹配类型具有新的父级(因此得名)。例如,给定一个名为 的接口UsageTracked和一个名为 的接口的实现, DefaultUsageTracked以下方面声明服务接口的所有实现者也实现该UsageTracked接口(例如,通过 JMX 进行统计):

java
@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}
科特林
@Aspect
class UsageTracking {

    companion object {
        @DeclareParents(value = "com.xzy.myapp.service.*+", defaultImpl = DefaultUsageTracked::class)
        lateinit var mixin: UsageTracked
    }

    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    fun recordUsage(usageTracked: UsageTracked) {
        usageTracked.incrementUseCount()
    }
}

要实现的接口由注解字段的类型决定。注释的 value属性@DeclareParents是 AspectJ 类型模式。任何匹配类型的 bean 都会实现该UsageTracked接口。请注意,在前面示例的之前的建议中,服务 bean 可以直接用作UsageTracked接口的实现。如果以编程方式访问 bean,您将编写以下内容:

java
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
科特林
val usageTracked = context.getBean("myService") as UsageTracked

5.4.6。方面实例化模型

这是一个高级话题。如果您刚开始使用 AOP,您可以REST Assured地跳过它,直到以后。

默认情况下,应用程序上下文中的每个方面都有一个实例。AspectJ 将此称为单例实例化模型。可以定义具有备用生命周期的方面。Spring 支持 AspectJperthispertarget 实例化模型;percflow, percflowbelow, 和pertypewithin当前不受支持。

您可以通过在注解中perthis指定一个perthis子句来声明一个方面。@Aspect考虑以下示例:

java
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {

    private int someState;

    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}
科特林
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
class MyAspect {

    private val someState: Int = 0

    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    fun recordServiceUsage() {
        // ...
    }
}

在前面的示例中,子句的效果是为执行业务服务的每个唯一服务对象(每个唯一对象绑定到切入点表达式匹配的连接点)perthis创建一个方面实例。this方面实例是在第一次在服务对象上调用方法时创建的。当服务对象超出范围时,方面超出范围。在创建切面实例之前,其中的任何建议都不会运行。一旦创建了切面实例,其中声明的通知就会在匹配的连接点处运行,但仅当服务对象是与此切面关联的对象时。per有关子句的更多信息,请参阅 AspectJ 编程指南。

实例化模型的pertarget工作方式与 完全相同perthis,但它在匹配的连接点为每个唯一目标对象创建一个方面实例。

5.4.7。AOP 示例

现在您已经了解了所有组成部分的工作原理,我们可以将它们组合在一起做一些有用的事情。

由于并发问题(例如,死锁失败者),业务服务的执行有时会失败。如果该操作被重试,则很可能在下一次尝试时成功。对于在这种情况下适合重试的业务服务(不需要返回给用户解决冲突的幂等操作),我们希望透明地重试操作以避免客户端看到 PessimisticLockingFailureException. 这是一个明确跨越服务层中多个服务的要求,因此非常适合通过方面实现。

因为我们要重试操作,所以我们需要使用around通知,以便我们可以proceed多次调用。以下清单显示了基本方面的实现:

java
@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.CommonPointcuts.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;
    }
}
科特林
@Aspect
class ConcurrentOperationExecutor : Ordered {

    private val DEFAULT_MAX_RETRIES = 2
    private var maxRetries = DEFAULT_MAX_RETRIES
    private var order = 1

    fun setMaxRetries(maxRetries: Int) {
        this.maxRetries = maxRetries
    }

    override fun getOrder(): Int {
        return this.order
    }

    fun setOrder(order: Int) {
        this.order = order
    }

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
        var numAttempts = 0
        var lockFailureException: PessimisticLockingFailureException
        do {
            numAttempts++
            try {
                return pjp.proceed()
            } catch (ex: PessimisticLockingFailureException) {
                lockFailureException = ex
            }

        } while (numAttempts <= this.maxRetries)
        throw lockFailureException
    }
}

请注意,切面实现了Ordered接口,以便我们可以将切面的优先级设置为高于事务建议(我们希望每次重试时都有一个新事务)。maxRetriesorder属性都是由 Spring 配置的。主要动作发生在doConcurrentOperation环绕建议中。请注意,目前,我们将重试逻辑应用于每个businessService(). 我们尝试继续,如果我们失败了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注解:

java
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}
科特林
@Retention(AnnotationRetention.RUNTIME)
annotation class Idempotent// marker annotation

然后我们可以使用注解来注解服务操作的实现。将方面更改为仅重试幂等操作涉及改进切入点表达式,以便仅@Idempotent操作匹配,如下所示:

java
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}
科特林
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
    // ...
}

5.5. 基于模式的 AOP 支持

如果您更喜欢基于 XML 的格式,Spring 还支持使用aop命名空间标签定义方面。支持与使用 @AspectJ 样式时完全相同的切入点表达式和建议类型。因此,在本节中,我们将重点放在该语法上,并请读者参考上一节中的讨论(@AspectJ 支持),以了解编写切入点表达式和通知参数的绑定。

要使用本节中描述的 aop 命名空间标签,您需要导入 spring-aop架构,如基于 XML 架构的配置中所述。 有关如何在命名空间中导入标签的信息,请参阅AOP 模式aop

在您的 Spring 配置中,所有方面和顾问元素都必须放在一个元素中(您可以在应用程序上下文配置中<aop:config>拥有多个元素)。<aop:config>一个<aop:config>元素可以包含切入点、顾问和方面元素(请注意,这些元素必须按此顺序声明)。

配置<aop:config>风格大量使用了 Spring 的 自动代理机制。BeanNameAutoProxyCreator如果您已经通过使用或类似的方式使用显式自动代理,这可能会导致问题(例如未编织建议) 。推荐的使用模式是仅使用<aop:config>样式或仅使用AutoProxyCreator样式并且从不混合使用它们。

5.5.1。声明一个方面

当您使用模式支持时,方面是在 Spring 应用程序上下文中定义为 bean 的常规 Java 对象。在对象的字段和方法中捕获状态和行为,在 XML 中捕获切入点和建议信息。

您可以使用<aop:aspect>元素声明方面,并使用属性引用支持 bean ref,如以下示例所示:

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

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

支持方面的 bean(aBean在这种情况下)当然可以像任何其他 Spring bean 一样进行配置和依赖注入。

5.5.2. 声明切入点

您可以在元素内声明一个命名切入点<aop:config>,让切入点定义在多个方面和顾问之间共享。

表示服务层中任何业务服务执行的切入点可以定义如下:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

请注意,切入点表达式本身使用与@AspectJ 支持中描述的相同的 AspectJ 切入点表达式语言。如果您使用基于模式的声明样式,您可以在切入点表达式中引用类型 (@Aspects) 中定义的命名切入点。定义上述切入点的另一种方法如下:

<aop:config>

    <aop:pointcut id="businessService"
        expression="com.xyz.myapp.CommonPointcuts.businessService()"/>

</aop:config>

假设您有共享通用切入点定义CommonPointcuts中描述的方面。

然后在方面内声明切入点与声明顶级切入点非常相似,如以下示例所示:

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

必须通过包含匹配名称的参数来声明通知以接收收集的连接点上下文,如下所示:

java
public void monitor(Object service) {
    // ...
}
科特林
fun monitor(service: Any) {
    // ...
}

当组合切入点子表达式时,&amp;&amp;在 XML 文档中很尴尬,因此您可以分别使用andornot关键字来代替&amp;&amp;||!。例如,前面的切入点可以更好地写成如下:

<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 通知在匹配的方法执行之前运行。它 <aop:aspect>通过使用<aop:before>元素在 an 内声明,如以下示例所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

这里,dataAccessOperationid在顶层 ( <aop:config>) 级别定义的切入点的 。要改为内联定义切入点,请将pointcut-ref属性替换为pointcut属性,如下所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
        method="doAccessCheck"/>

    ...
</aop:aspect>

正如我们在讨论@AspectJ 样式时所指出的,使用命名切入点可以显着提高代码的可读性。

method属性标识doAccessCheck提供建议正文的方法 ( )。必须为包含通知的方面元素引用的 bean 定义此方法。在执行数据访问操作(切入点表达式匹配的方法执行连接点)之前,将doAccessCheck调用切面 bean 上的方法。

退货后的建议

当匹配的方法执行正常完成时,返回通知运行后。它在 an 内部声明<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。例如,您可以如下声明方法签名:

java
public void doAccessCheck(Object retVal) {...
科特林
fun doAccessCheck(retVal: Any) {...
投掷后的建议

当匹配的方法执行通过抛出异常退出时,抛出通知运行后。它<aop:aspect>通过使用after-throwing元素在 an 内声明,如以下示例所示:

<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。例如,方法签名可以声明如下:

java
public void doRecoveryActions(DataAccessException dataAccessEx) {...
科特林
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
(最终)建议之后

无论匹配的方法执行如何退出,(最终)通知都会运行。您可以使用after元素来声明它,如以下示例所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...
</aop:aspect>
周围的建议

最后一种建议是围绕建议。围绕通知“围绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定该方法何时、如何以及是否真正开始运行。如果您需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器),则通常使用环绕通知。

始终使用满足您要求的最不强大的建议形式。

例如,如果之前的建议足以满足您的需求,请不要使用环绕建议。

aop:around您可以使用该元素声明环绕通知。通知方法应该声明Object为它的返回类型,并且方法的第一个参数必须是 type ProceedingJoinPoint。在通知方法的主体中,您必须调用 proceed()ProceedingJoinPoint使底层方法运行。不带参数调用proceed()将导致调用者的原始参数在调用时提供给底层方法。对于高级用例,该proceed()方法有一个重载变体,它接受参数数组 ( Object[])。调用时,数组中的值将用作底层方法的参数。有关使用. _ proceed_Object[]

以下示例展示了如何在 XML 中声明环绕通知:

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...
</aop:aspect>

建议的实现doBasicProfiling可以与@AspectJ 示例中的完全相同(当然,要减去注释),如以下示例所示:

java
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}
科特林
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
    // start stopwatch
    val retVal = pjp.proceed()
    // stop stopwatch
    return pjp.proceed()
}
建议参数

基于模式的声明风格以与@AspectJ 支持相同的方式支持完全类型化的建议——通过按名称匹配切入点参数与建议方法参数。有关详细信息,请参阅建议参数。如果您希望为通知方法显式指定参数名称(不依赖于前面描述的检测策略),您可以使用arg-names 通知元素的属性来实现,该属性的处理方式与argNames 通知注释中的属性相同(如确定参数名称中所述)。以下示例显示如何在 XML 中指定参数名称:

<aop:before
    pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
    method="audit"
    arg-names="auditable"/>

arg-names属性接受以逗号分隔的参数名称列表。

以下基于 XSD 的方法稍微复杂一些的示例显示了一些与许多强类型参数结合使用的环绕建议:

java
package x.y.service;

public interface PersonService {

    Person getPerson(String personName, int age);
}

public class DefaultPersonService implements PersonService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}
科特林
package x.y.service

interface PersonService {

    fun getPerson(personName: String, age: Int): Person
}

class DefaultPersonService : PersonService {

    fun getPerson(name: String, age: Int): Person {
        return Person(name, age)
    }
}

接下来是方面。请注意,该profile(..)方法接受许多强类型参数,其中第一个参数恰好是用于继续进行方法调用的连接点。此参数的存在表明 profile(..)将用作around建议,如以下示例所示:

java
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());
        }
    }
}
科特林
import org.aspectj.lang.ProceedingJoinPoint
import org.springframework.util.StopWatch

class SimpleProfiler {

    fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any {
        val clock = StopWatch("Profiling for '$name' and '$age'")
        try {
            clock.start(call.toShortString())
            return call.proceed()
        } finally {
            clock.stop()
            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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://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>

考虑以下驱动程序脚本:

java
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);
    }
}
科特林
fun main() {
    val ctx = ClassPathXmlApplicationContext("x/y/plain.xml")
    val person = ctx.getBean("personService") as PersonService
    person.getPerson("Pengo", 12)
}

使用这样的 Boot 类,我们将在标准输出中获得类似于以下内容的输出:

秒表“Pengo”和“12”的分析:运行时间(毫秒)= 0
-----------------------------------------
ms % 任务名称
-----------------------------------------
00000 ? 执行(getFoo)
建议订购

当多条通知需要在同一个连接点(执行方法)运行时,排序规则如Advice Ordering中所述。方面之间的优先级通过元素order中的属性<aop:aspect>或通过将@Order注释添加到支持方面的 bean 或通过让 bean 实现Ordered接口来确定。

与定义在同一个@Aspect 类中的通知方法的优先级规则相反,当同一元素中定义的两条通知<aop:aspect>都需要在同一个连接点运行时,优先级由通知元素在其中声明的顺序决定封闭<aop:aspect>元素,从最高到最低优先级。

例如,给定一个around通知和一个before在同一 <aop:aspect>元素中定义的通知应用于同一连接点,为确保该around 通知的优先级高于该before通知,该<aop:around>元素必须在该<aop:before>元素之前声明。

作为一般经验法则,如果您发现在同一个<aop:aspect>元素中定义了多条适用于同一连接点的建议,请考虑将此类建议方法折叠为每个<aop:aspect>元素中每个连接点的一个建议方法,或重构建议分成单独<aop:aspect>的元素,您可以在方面级别订购。

5.5.4。介绍

介绍(在 AspectJ 中称为类型间声明)让方面声明建议对象实现给定接口并代表这些对象提供该接口的实现。

aop:declare-parents您可以使用aop:aspect. 您可以使用该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.CommonPointcuts.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

支持usageTrackingbean 的类将包含以下方法:

java
public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}
科特林
fun recordUsage(usageTracked: UsageTracked) {
    usageTracked.incrementUseCount()
}

要实现的接口由implement-interface属性决定。该types-matching属性的值是一个 AspectJ 类型模式。任何匹配类型的 bean 都会实现该UsageTracked接口。请注意,在前面示例的之前的建议中,服务 bean 可以直接用作UsageTracked接口的实现。要以编程方式访问 bean,您可以编写以下代码:

java
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
科特林
val usageTracked = context.getBean("myService") as UsageTracked

5.5.5。方面实例化模型

唯一受支持的模式定义方面的实例化模型是单例模型。未来版本可能支持其他实例化模型。

5.5.6。顾问

“顾问”的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接的等价物。顾问就像一个独立的小方面,只有一条建议。通知本身由 bean 表示,并且必须实现 Spring 中的 Advice Types 中描述的通知接口之一。顾问可以利用 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属性来定义Ordered顾问的值。

5.5.7。AOP 模式示例

本节展示了 一个 AOP 示例中的并发锁定失败重试示例在使用模式支持重写时的外观。

由于并发问题(例如,死锁失败者),业务服务的执行有时会失败。如果该操作被重试,则很可能在下一次尝试时成功。对于在这种情况下适合重试的业务服务(不需要返回给用户解决冲突的幂等操作),我们希望透明地重试操作以避免客户端看到 PessimisticLockingFailureException. 这是一个明确跨越服务层中多个服务的要求,因此非常适合通过方面实现。

因为我们要重试操作,所以我们需要使用around通知,以便我们可以proceed多次调用。下面的清单显示了基本的方面实现(这是一个使用模式支持的常规 Java 类):

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;
    }
}
科特林
class ConcurrentOperationExecutor : Ordered {

    private val DEFAULT_MAX_RETRIES = 2

    private var maxRetries = DEFAULT_MAX_RETRIES
    private var order = 1

    fun setMaxRetries(maxRetries: Int) {
        this.maxRetries = maxRetries
    }

    override fun getOrder(): Int {
        return this.order
    }

    fun setOrder(order: Int) {
        this.order = order
    }

    fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
        var numAttempts = 0
        var lockFailureException: PessimisticLockingFailureException
        do {
            numAttempts++
            try {
                return pjp.proceed()
            } catch (ex: PessimisticLockingFailureException) {
                lockFailureException = ex
            }

        } while (numAttempts <= this.maxRetries)
        throw lockFailureException
    }
}

请注意,切面实现了Ordered接口,以便我们可以将切面的优先级设置为高于事务建议(我们希望每次重试时都有一个新事务)。maxRetriesorder属性都是由 Spring 配置的。主要动作发生在doConcurrentOperationaround 通知方法中。我们尝试继续。如果我们以 a 失败PessimisticLockingFailureException,我们会再试一次,除非我们已经用尽了所有的重试尝试。

此类与@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,如以下示例所示:

java
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}
科特林
@Retention(AnnotationRetention.RUNTIME)
annotation class 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 容器管理的对象(例如域对象,通常是),则需要使用 AspectJ。如果您希望建议连接点而不是简单的方法执行(例如,字段获取或设置连接点等),您还需要使用 AspectJ。

当您使用 AspectJ 时,您可以选择 AspectJ 语言语法(也称为“代码风格”)或 @AspectJ 注释风格。显然,如果您不使用 Java 5+,那么已经为您做出了选择:使用代码风格。如果方面在您的设计中扮演重要角色,并且您能够使用 Eclipse 的AspectJ 开发工具 (AJDT)插件,那么 AspectJ 语言语法是首选选项。它更简洁,因为该语言是专门为编写方面而设计的。如果您不使用 Eclipse 或只有几个方面在您的应用程序中没有发挥主要作用,您可能需要考虑使用 @AspectJ 样式,在您的 IDE 中坚持常规 Java 编译,并添加一个方面编织阶段你的构建脚本。

5.6.2. 用于 Spring AOP 的 @AspectJ 或 XML?

如果您选择使用 Spring AOP,您可以选择 @AspectJ 或 XML 样式。有各种权衡需要考虑。

现有的 Spring 用户可能最熟悉 XML 样式,它由真正的 POJO 支持。当使用 AOP 作为配置企业服务的工具时,XML 可能是一个不错的选择(一个很好的测试是您是否将切入点表达式视为您可能想要独立更改的配置的一部分)。使用 XML 样式,可以说从您的配置中更清楚系统中存在哪些方面。

XML 样式有两个缺点。首先,它没有将它所解决的需求的实现完全封装在一个地方。DRY 原则说,系统内的任何知识都应该有一个单一的、明确的、权威的表示。使用 XML 样式时,如何实现需求的知识被拆分为支持 bean 类的声明和配置文件中的 XML。当您使用@AspectJ 样式时,此信息被封装在一个模块中:方面。其次,与@AspectJ 风格相比,XML 风格在表达方面稍有限制:仅支持“单例”方面实例化模型,并且无法组合 XML 中声明的命名切入点。例如,在 @AspectJ 样式中,您可以编写如下内容:

java
@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
科特林
@Pointcut("execution(* get*())")
fun propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
fun operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
fun 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>声明的顾问,甚至是相同配置中其他样式的代理和拦截器,完全可以混合@AspectJ 样式方面。所有这些都是通过使用相同的底层支持机制来实现的,并且可以毫无困难地共存。

5.8. 代理机制

Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的目标对象创建代理。JDK 动态代理内置在 JDK 中,而 CGLIB 是一个通用的开源类定义库(重新打包到spring-core.

如果要代理的目标对象实现了至少一个接口,则使用 JDK 动态代理。目标类型实现的所有接口都被代理。如果目标对象没有实现任何接口,则创建一个 CGLIB 代理。

如果您想强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法),您可以这样做。但是,您应该考虑以下问题:

  • 使用 CGLIB,final不能建议方法,因为它们不能在运行时生成的子类中被覆盖。

  • 从 Spring 4.0 开始,代理对象的构造函数不再被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。仅当您的 JVM 不允许绕过构造函数时,您可能会看到来自 Spring 的 AOP 支持的双重调用和相应的调试日志条目。

要强制使用 CGLIB 代理,请将元素的proxy-target-class属性值设置<aop:config>为 true,如下所示:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用 @AspectJ 自动代理支持时强制 CGLIB 代理,请将 元素的proxy-target-class属性设置为,如下所示:<aop:aspectj-autoproxy>true

<aop:aspectj-autoproxy proxy-target-class="true"/>

多个<aop:config/>部分在运行时被折叠成一个统一的自动代理创建器,它应用任何 部分(通常来自不同的 XML bean 定义文件)指定的最强代理设置。<aop:config/>这也适用于<tx:annotation-driven/>and<aop:aspectj-autoproxy/> 元素。

需要明确的是,使用proxy-target-class="true"on <tx:annotation-driven/><aop:aspectj-autoproxy/>或元素会强制对所有三个<aop:config/>使用 CGLIB 代理。

5.8.1. 了解 AOP 代理

Spring AOP 是基于代理的。在编写自己的方面或使用 Spring Framework 提供的任何基于 Spring AOP 的方面之前,掌握最后一条语句的实际含义是非常重要的。

首先考虑您有一个普通的、未代理的、没有什么特别的、直接的对象引用的场景,如以下代码片段所示:

java
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...
    }
}
科特林
class SimplePojo : Pojo {

    fun foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar()
    }

    fun bar() {
        // some logic...
    }
}

如果您在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图和清单所示:

aop 代理普通 pojo 调用
java
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();
    }
}
科特林
fun main() {
    val pojo = SimplePojo()
    // this is a direct method call on the 'pojo' reference
    pojo.foo()
}

当客户端代码的引用是代理时,情况会发生轻微变化。考虑下面的图表和代码片段:

aop 代理调用
java
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();
    }
}
科特林
fun main() {
    val factory = ProxyFactory(SimplePojo())
    factory.addInterface(Pojo::class.java)
    factory.addAdvice(RetryAdvice())

    val pojo = factory.proxy as Pojo
    // this is a method call on the proxy!
    pojo.foo()
}

这里要理解的关键是类的main(..)方法内部的客户端代码Main有对代理的引用。这意味着对该对象引用的方法调用是对代理的调用。因此,代理可以委托给与该特定方法调用相关的所有拦截器(建议)。但是,一旦调用最终到达目标对象(SimplePojo在这种情况下为引用),它可能对自身进行的任何方法调用,例如this.bar()or this.foo(),都将针对this引用而不是代理调用。这具有重要意义。这意味着自调用不会导致与方法调用相关的建议有机会运行。

好的,那该怎么办呢?最好的方法(术语“最好”在这里被松散地使用)是重构你的代码,这样自调用就不会发生。这确实需要您做一些工作,但它是最好的、侵入性最小的方法。下一种方法绝对可怕,我们不愿指出,正是因为它太可怕了。您可以(对我们来说很痛苦)将您的类中的逻辑完全绑定到 Spring AOP,如以下示例所示:

java
public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}
科特林
class SimplePojo : Pojo {

    fun foo() {
        // this works, but... gah!
        (AopContext.currentProxy() as Pojo).bar()
    }

    fun bar() {
        // some logic...
    }
}

这完全将您的代码与 Spring AOP 耦合在一起,并且它使类本身意识到它是在 AOP 上下文中使用的,而 AOP 上下文与 AOP 相悖。在创建代理时还需要一些额外的配置,如以下示例所示:

java
public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}
科特林
fun main() {
    val factory = ProxyFactory(SimplePojo())
    factory.addInterface(Pojo::class.java)
    factory.addAdvice(RetryAdvice())
    factory.isExposeProxy = true

    val pojo = factory.proxy as Pojo
    // this is a method call on the proxy!
    pojo.foo()
}

最后需要注意的是,AspectJ 不存在这个自调用问题,因为它不是基于代理的 AOP 框架。

5.9. @AspectJ 代理的程序化创建

除了使用<aop:config> 或声明配置中的方面之外<aop:aspectj-autoproxy>,还可以以编程方式创建建议目标对象的代理。有关 Spring AOP API 的完整详细信息,请参阅 下一章。在这里,我们希望专注于使用@AspectJ 方面自动创建代理的能力。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory该类为一个或多个@AspectJ 方面建议的目标对象创建代理。这个类的基本用法很简单,如下例所示:

java
// 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();
科特林
// create a factory that can generate a proxy for the given target object
val factory = 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.java)

// 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...
val proxy = factory.getProxy<Any>()

有关更多信息,请参阅javadoc

5.10。在 Spring 应用程序中使用 AspectJ

到目前为止,我们在本章中介绍的所有内容都是纯 Spring AOP。在本节中,如果您的需求超出了 Spring AOP 单独提供的功能,我们将了解如何使用 AspectJ 编译器或编织器来代替 Spring AOP 或作为 Spring AOP 的补充。

Spring 附带了一个小的 AspectJ 方面库,它在您的发行版中以spring-aspects.jar. 您需要将其添加到您的类路径中才能使用其中的方面。Using AspectJ to Dependency Inject Domain Objects with SpringAspectJ 的其他 Spring 方面讨论了这个库的内容以及如何使用它。使用 Spring IoC 配置 AspectJ 方面讨论了如何依赖注入使用 AspectJ 编译器编织的 AspectJ 方面。最后, 在 Spring Framework中使用 AspectJ 进行加载时编织介绍了使用 AspectJ 的 Spring 应用程序的加载时编织。

5.10.1。使用 AspectJ 通过 Spring 依赖注入域对象

Spring 容器实例化和配置应用程序上下文中定义的 bean。也可以要求 bean 工厂配置一个预先存在的对象,给定包含要应用的配置的 bean 定义的名称。 spring-aspects.jar包含一个注释驱动的方面,它利用此功能允许对任何对象进行依赖注入。该支持旨在用于在任何容器控制之外创建的对象。域对象通常属于这一类,因为它们通常是使用 new操作符以编程方式创建的,或者作为数据库查询的结果由 ORM 工具创建。

注释将@Configurable一个类标记为符合 Spring 驱动配置的条件。在最简单的情况下,您可以将其纯粹用作标记注释,如以下示例所示:

java
package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}
科特林
package com.xyz.myapp.domain

import org.springframework.beans.factory.annotation.Configurable

@Configurable
class Account {
    // ...
}

当以这种方式用作标记接口时,SpringAccount通过使用与完全限定类型名称com.xyz.myapp.domain.Account(由于 bean 的默认名称是其类型的完全限定名称,因此声明原型定义的一种方便方法是省略id属性,如以下示例所示:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要显式指定要使用的原型 bean 定义的名称,可以直接在注解中这样做,如以下示例所示:

java
package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}
科特林
package com.xyz.myapp.domain

import org.springframework.beans.factory.annotation.Configurable

@Configurable("account")
class Account {
    // ...
}

Spring 现在查找名为的 bean 定义并将account其用作配置新Account实例的定义。

您还可以使用自动装配来完全避免指定专用的 bean 定义。要让 Spring 应用自动装配,请使用 注解的autowire属性。@Configurable您可以分别按类型或名称指定自动装配@Configurable(autowire=Autowire.BY_TYPE)或 自动装配。@Configurable(autowire=Autowire.BY_NAME)作为替代方案,最好通过字段或方法级别或 在字段或方法级别为您的@Configurablebean指定显式的、注释驱动的依赖注入(有关更多详细信息,请参阅基于注释的容器配置)。@Autowired@Inject

dependencyCheck最后,您可以使用属性(例如, @Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))对新创建和配置的对象中的对象引用启用 Spring 依赖检查。如果此属性设置为true,Spring 会在配置后验证所有属性(不是原语或集合)都已设置。

请注意,单独使用注释没有任何作用。正是 AnnotationBeanConfigurerAspectinspring-aspects.jar作用于注解的存在。本质上,方面说,“在从一个注解类型的新对象的初始化返回后,@Configurable根据注解的属性使用 Spring 配置新创建的对象”。在这种情况下,“初始化”指的是新实例化的对象(例如,使用new运算符实例化的对象)以及Serializable正在进行反序列化的对象(例如,通过 readResolve())。

上一段中的关键词之一是“本质上”。在大多数情况下,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”意味着依赖项是在对象构建之后注入的。这意味着依赖项不能在类的构造函数体中使用。如果您希望在构造函数主体运行之前注入依赖项,从而可以在构造函数主体中使用,则需要在 @Configurable声明中定义 this,如下所示:

java
@Configurable(preConstruction = true)
科特林
@Configurable(preConstruction = true)

您可以在AspectJ Programming Guide的这个附录中找到有关 AspectJ 中各种切入点类型的语言语义的更多信息 。

为此,必须使用 AspectJ 编织器编织带注释的类型。您可以使用构建时 Ant 或 Maven 任务来执行此操作(例如,参见 AspectJ 开发环境指南)或加载时编织(参见Spring Framework 中使用 AspectJ 的加载时编织)。本身需要由 AnnotationBeanConfigurerAspectSpring 配置(为了获得对用于配置新对象的 bean 工厂的引用)。如果使用基于 Java 的配置,则可以添加@EnableSpringConfigured到任何 @Configuration类中,如下所示:

java
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
科特林
@Configuration
@EnableSpringConfigured
class AppConfig {
}

如果您更喜欢基于 XML 的配置,Spring context命名空间 定义了一个方便的context:spring-configured元素,您可以按如下方式使用它:

<context:spring-configured/>

在配置方面之前创建的对象实例会@Configurable导致向调试日志发出消息,并且不会进行对象配置。一个示例可能是 Spring 配置中的 bean,它在 Spring 初始化时创建域对象。在这种情况下,您可以使用 depends-onbean 属性手动指定 bean 依赖于配置方面。以下示例显示了如何使用该depends-on属性:

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>
不要@Configurable通过 bean 配置器方面激活处理,除非您真的想在运行时依赖它的语义。特别是,请确保不要@Configurable在容器中注册为常规 Spring bean 的 bean 类上使用。这样做会导致双重初始化,一次通过容器,一次通过方面。
单元测试@Configurable对象

支持的目标之一@Configurable是启用域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果@ConfigurableAspectJ 没有编织类型,则注释在单元测试期间没有影响。您可以在被测对象中设置模拟或存根属性引用并正常进行。如果@Configurable类型已由 AspectJ 编织,您仍然可以像往常一样在容器外部进行单元测试,但每次构造@Configurable对象时都会看到一条警告消息,指示它尚未由 Spring 配置。

使用多个应用程序上下文

AnnotationBeanConfigurerAspect用于实现支持的@Configurable是 AspectJ 单例方面。单例切面的范围与static成员的范围相同:每个类加载器都有一个切面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,您需要考虑在哪里定义@EnableSpringConfiguredbean 以及放置spring-aspects.jar在类路径上的什么位置。

考虑一个典型的 Spring Web 应用程序配置,它具有一个共享的父应用程序上下文,它定义了公共业务服务、支持这些服务所需的一切,以及每个 servlet 的一个子应用程序上下文(其中包含特定于该 servlet 的定义)。所有这些上下文共存于同一个类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能保存对其中一个的引用。在这种情况下,我们建议@EnableSpringConfigured在共享(父)应用程序上下文中定义 bean。这定义了您可能想要注入到域对象中的服务。结果是您无法使用@Configurable 机制(这可能不是您想要做的事情)来配置域对象,并引用在子(特定于servlet)上下文中定义的bean。

在同一个容器中部署多个 Web 应用程序时,确保每个 Web 应用程序都spring-aspects.jar使用自己的类加载器加载类型(例如,通过放置spring-aspects.jarin WEB-INF/lib)。如果spring-aspects.jar 仅添加到容器范围的类路径(因此由共享父类加载器加载),则所有 Web 应用程序共享相同的方面实例(这可能不是您想要的)。

5.10.2。AspectJ 的其他 Spring 方面

除了@Configurable切面之外,spring-aspects.jar还包含一个 AspectJ 切面,您可以使用它来驱动 Spring 的事务管理,用于使用注解进行@Transactional注解的类型和方法。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。

解释@Transactional注释的方面是 AnnotationTransactionAspect. 当您使用此方面时,您必须注释实现类(或该类中的方法或两者),而不是该类实现的接口(如果有)。AspectJ 遵循 Java 的规则,即不继承接口上的注释。

类上的@Transactional注释指定执行类中任何公共操作的默认事务语义。

类中方法的@Transactional注释会覆盖类注释(如果存在)给出的默认事务语义。可以注释任何可见性的方法,包括私有方法。直接注释非公共方法是获得执行此类方法的事务分界的唯一方法。

从 Spring Framework 4.2 开始,提供了一个类似的方面,为标准注释spring-aspects提供完全相同的功能。javax.transaction.Transactional检查 JtaAnnotationTransactionAspect更多细节。

对于想要使用 Spring 配置和事务管理支持但不想(或不能)使用注释的 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(..)) &&
        CommonPointcuts.inDomainModel() &&
        this(beanInstance);
}

5.10.3。使用 Spring IoC 配置 AspectJ 方面

当您在 Spring 应用程序中使用 AspectJ 方面时,很自然地希望并期望能够使用 Spring 配置这些方面。AspectJ 运行时本身负责切面创建,通过 Spring 配置 AspectJ 创建的切面的方式取决于切面使用的 AspectJ 实例化模型(per-xxx子句)。

大多数 AspectJ 方面都是单例方面。这些方面的配置很容易。您可以创建一个引用切面类型的 bean 定义,并包含factory-method="aspectOf"bean 属性。这确保 Spring 通过向 AspectJ 请求它而不是尝试自己创建实例来获取切面实例。以下示例显示了如何使用该factory-method="aspectOf"属性:

<bean id="profiler" class="com.xyz.profiler.Profiler"
        factory-method="aspectOf"> (1)

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
1 注意factory-method="aspectOf"属性

非单例方面更难配置。但是,可以通过创建原型 bean 定义并在 AspectJ 运行时创建了切面实例后使用@Configurable支持 来配置切面实例。spring-aspects.jar

如果你有一些@AspectJ 方面想用AspectJ 编织(例如,对域模型类型使用加载时编织)和其他@AspectJ 方面想和Spring AOP 一起使用,并且这些方面都在Spring 中配置,您需要告诉 Spring AOP @AspectJ 自动代理支持配置中定义的 @AspectJ 方面的确切子集应该用于自动代理。 您可以通过在声明中使用一个或多个<include/>元素来做到这一点。<aop:aspectj-autoproxy/>每个<include/>元素指定一个名称模式,只有名称与至少一个模式匹配的 bean 才会用于 Spring AOP 自动代理配置。以下示例显示了如何使用<include/>元素:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被<aop:aspectj-autoproxy/>元素的名称误导。使用它会导致创建 Spring AOP 代理。这里使用了@AspectJ 样式的方面声明,但不涉及 AspectJ 运行时。

5.10.4。在 Spring 框架中使用 AspectJ 进行加载时编织

加载时编织 (LTW) 是指将 AspectJ 方面编织到应用程序的类文件中的过程,因为它们正在加载到 Java 虚拟机 (JVM) 中。本节的重点是在 Spring Framework 的特定上下文中配置和使用 LTW。本节不是对 LTW 的一般介绍。有关 LTW 的详细信息以及仅使用 AspectJ 配置 LTW(根本不涉及 Spring)的详细信息,请参阅 AspectJ 开发环境指南的 LTW 部分

Spring 框架为 AspectJ LTW 带来的价值在于能够对编织过程进行更细粒度的控制。'Vanilla' AspectJ LTW 是通过使用 Java (5+) 代理来实现的,该代理在启动 JVM 时通过指定 VM 参数来打开。因此,它是一个 JVM 范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用 Spring 的 LTW 允许您逐个启用 LTW ClassLoader,这更细粒度,并且在“单 JVM 多应用程序”环境中更有意义(例如在典型的应用程序服务器环境中) )。

此外,在某些环境中,此支持支持加载时编织,而无需对应用程序服务器的启动脚本进行任何修改,这些脚本需要添加-javaagent:path/to/aspectjweaver.jar或(如我们在本节后面描述的那样)-javaagent:path/to/spring-instrument.jar。开发人员配置应用程序上下文以启用加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。

现在推销已经结束,让我们先看一个使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中介绍的元素。有关完整示例,请参阅 Petclinic 示例应用程序

第一个例子

假设您是一名应用程序开发人员,他的任务是诊断系统中某些性能问题的原因。与其打破分析工具,我们将打开一个简单的分析方面,让我们快速获得一些性能指标。然后,我们可以立即将更细粒度的分析工具应用于该特定区域。

此处提供的示例使用 XML 配置。您还可以通过Java 配置配置和使用 @AspectJ 。具体来说,您可以使用 @EnableLoadTimeWeaving注释作为替代<context:load-time-weaver/> (详见下文)。

以下示例显示了分析方面,这并不花哨。它是一个基于时间的分析器,使用@AspectJ 风格的方面声明:

java
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(){}
}
科特林
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
class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    fun profile(pjp: ProceedingJoinPoint): Any {
        val sw = StopWatch(javaClass.simpleName)
        try {
            sw.start(pjp.getSignature().getName())
            return pjp.proceed()
        } finally {
            sw.stop()
            println(sw.prettyPrint())
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    fun methodsToBeProfiled() {
    }
}

我们还需要创建一个META-INF/aop.xml文件,通知 AspectJ 编织器我们想要将我们的类编织ProfilingAspect到我们的类中。这种文件约定,即在 Java 类路径上存在一个(或多个文件)称为META-INF/aop.xml标准 AspectJ。以下示例显示了该aop.xml文件:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://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>

现在我们可以继续进行配置的特定于 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://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 的运行情况:

java
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();
    }
}
科特林
package foo

import org.springframework.context.support.ClassPathXmlApplicationContext

fun main() {
    val ctx = ClassPathXmlApplicationContext("beans.xml")

    val entitlementCalculationService = ctx.getBean("entitlementCalculationService") as EntitlementCalculationService

    // the profiling aspect is 'woven' around this method execution
    entitlementCalculationService.calculateEntitlement()
}

我们还有最后一件事要做。ClassLoader本节的介绍确实说过,可以根据 Spring有选择地打开 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类似于下一个示例。(我在实现中引入了一条Thread.sleep(..)语句,calculateEntitlement() 以便探查器实际上捕获 0 毫秒以外的时间(01234毫秒不是 AOP 引入的开销)。以下清单显示了我们在运行探查器时得到的输出:

计算权利

秒表'ProfilingAspect':运行时间(毫秒)= 1234
------ ----- ----------------
ms % 任务名称
------ ----- ----------------
01234 100% 计算权利

由于这个 LTW 是通过使用成熟的 AspectJ 来实现的,因此我们不仅限于建议 Spring bean。该Main程序的以下细微变化会产生相同的结果:

java
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();
    }
}
科特林
package foo

import org.springframework.context.support.ClassPathXmlApplicationContext

fun main(args: Array<String>) {
    ClassPathXmlApplicationContext("beans.xml")

    val entitlementCalculationService = StubEntitlementCalculationService()

    // the profiling aspect will be 'woven' around this method execution
    entitlementCalculationService.calculateEntitlement()
}

请注意,在前面的程序中,我们如何引导 Spring 容器,然后创建一个StubEntitlementCalculationService完全在 Spring 上下文之外的新实例。分析建议仍然被融入其中。

诚然,这个例子很简单。但是,Spring 中 LTW 支持的基础知识已经在前面的示例中介绍过,本节的其余部分将详细解释每一位配置和使用背后的“原因”。

这个ProfilingAspect例子中使用的可能是基本的,但它非常有用。这是开发时方面的一个很好的例子,开发人员可以在开发期间使用它,然后轻松地从部署到 UAT 或生产中的应用程序的构建中排除。
方面

您在 LTW 中使用的方面必须是 AspectJ 方面。您可以使用 AspectJ 语言本身编写它们,也可以使用 @AspectJ 样式编写方面。那么你的切面都是有效的 AspectJ 和 Spring AOP 切面。此外,编译的方面类需要在类路径上可用。

'META-INF/aop.xml'

AspectJ LTW 基础结构是通过使用META-INF/aop.xml Java 类路径上的一个或多个文件(直接或者更典型地在 jar 文件中)来配置的。

该文件的结构和内容在 AspectJ 参考文档的 LTW 部分中有详细说明。因为该aop.xml文件是 100% AspectJ,所以我们在此不再赘述。

所需的库 (JARS)

至少,您需要以下库来使用 Spring Framework 对 AspectJ LTW 的支持:

  • spring-aop.jar

  • aspectjweaver.jar

如果使用Spring 提供的代理来启用检测,还需要:

  • spring-instrument.jar

弹簧配置

Spring 的 LTW 支持中的关键组件是LoadTimeWeaver接口(在 org.springframework.instrument.classloading包中),以及 Spring 发行版附带的众多实现。ALoadTimeWeaver负责在运行时java.lang.instrument.ClassFileTransformers向 a添加一个或多个ClassLoader,这为各种有趣的应用程序打开了大门,其中之一恰好是方面的 LTW。

如果您不熟悉运行时类文件转换的概念,请java.lang.instrument在继续之前查看包的 javadoc API 文档。虽然该文档并不全面,但至少您可以看到关键接口和类(供您阅读本节时参考)。

LoadTimeWeaver为特定配置 aApplicationContext可以像添加一行一样简单。(请注意,您几乎肯定需要使用 a ApplicationContext作为 Spring 容器 - 通常, aBeanFactory是不够的,因为 LTW 支持使用BeanFactoryPostProcessors.)

要启用 Spring Framework 的 LTW 支持,您需要配置一个LoadTimeWeaver,这通常通过使用@EnableLoadTimeWeaving注解来完成,如下所示:

java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
科特林
@Configuration
@EnableLoadTimeWeaving
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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:load-time-weaver/>

</beans>

前面的配置会自动为您定义和注册许多 LTW 特定的基础设施 bean,例如 aLoadTimeWeaver和 an AspectJWeavingEnabler。默认LoadTimeWeaverDefaultContextLoadTimeWeaver类,它试图装饰一个自动检测到的LoadTimeWeaver. “自动检测”的确切类型LoadTimeWeaver 取决于您的运行时环境。下表总结了各种LoadTimeWeaver实现:

表 13. DefaultContextLoadTimeWeaver LoadTimeWeavers
运行环境 LoadTimeWeaver执行

Apache Tomcat中运行

TomcatLoadTimeWeaver

GlassFish中运行(仅限于 EAR 部署)

GlassFishLoadTimeWeaver

在 Red Hat 的JBoss ASWildFly中运行

JBossLoadTimeWeaver

在 IBM 的WebSphere中运行

WebSphereLoadTimeWeaver

在 Oracle 的 WebLogic中运行

WebLogicLoadTimeWeaver

JVM 始于 Spring InstrumentationSavingAgent ( java -javaagent:path/to/spring-instrument.jar)

InstrumentationLoadTimeWeaver

回退,期望底层的 ClassLoader 遵循通用约定(即addTransformer,可选的getThrowawayClassLoader方法)

ReflectiveLoadTimeWeaver

请注意,该表仅列出了LoadTimeWeavers在您使用DefaultContextLoadTimeWeaver. 您可以准确指定LoadTimeWeaver 要使用的实现。

要指定特定LoadTimeWeaver的 Java 配置,请实现 LoadTimeWeavingConfigurer接口并覆盖getLoadTimeWeaver()方法。以下示例指定了一个ReflectiveLoadTimeWeaver

java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}
科特林
@Configuration
@EnableLoadTimeWeaving
class AppConfig : LoadTimeWeavingConfigurer {

    override fun getLoadTimeWeaver(): LoadTimeWeaver {
        return ReflectiveLoadTimeWeaver()
    }
}

如果使用基于 XML 的配置,则可以将完全限定的类名指定为 元素的weaver-class属性值。<context:load-time-weaver/>同样,以下示例指定了一个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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:load-time-weaver
            weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>

LoadTimeWeaver稍后可以使用众所周知的名称从 Spring 容器中检索由配置定义和注册的loadTimeWeaver. 请记住,它LoadTimeWeaver仅作为 Spring 的 LTW 基础架构添加一个或多个ClassFileTransformers. 执行 LTW的实际 ClassFileTransformerClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime包的)类。有关更多详细信息,请参阅该类的类级别 javadoc ClassPreProcessorAgentAdapter,因为实际如何实现编织的细节超出了本文档的范围。

最后还有一个配置属性需要讨论:aspectjWeaving 属性(或者aspectj-weaving如果您使用 XML)。此属性控制是否启用 LTW。它接受三个可能值之一,默认值为 autodetect如果属性不存在。下表总结了三个可能的值:

表 14. AspectJ 编织属性值
注释值 XML 值 解释

ENABLED

on

AspectJ weaving 已打开,并且在加载时适当地编织方面。

DISABLED

off

LTW 已关闭。在加载时没有编织任何方面。

AUTODETECT

autodetect

如果 Spring LTW 基础架构可以找到至少一个META-INF/aop.xml文件,则 AspectJ weaving 处于打开状态。否则,它会关闭。这是默认值。

特定于环境的配置

最后一部分包含在应用程序服务器和 Web 容器等环境中使用 Spring 的 LTW 支持时所需的任何其他设置和配置。

Tomcat、JBoss、WebSphere、WebLogic

Tomcat、JBoss/WildFly、IBM WebSphere Application Server 和 Oracle WebLogic Server 都提供了一个ClassLoader能够进行本地检测的通用应用程序。Spring 的本机 LTW 可以利用这些 ClassLoader 实现来提供 AspectJ 编织。如前所述,您可以简单地启用加载时编织。具体来说,您无需修改​​ JVM 启动脚本即可添加 -javaagent:path/to/spring-instrument.jar.

请注意,在 JBoss 上,您可能需要禁用应用服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是向您的工件添加一个文件,该文件以WEB-INF/jboss-scanning.xml以下内容命名:

<scanning xmlns="urn:jboss:scanning:1.0"/>
通用 Java 应用程序

当特定实现不支持的环境中需要类检测时LoadTimeWeaver,JVM 代理是通用解决方案。对于这种情况,Spring 提供了InstrumentationLoadTimeWeaver需要 Spring 特定(但非常通用)的 JVM 代理,spring-instrument.jar由 common@EnableLoadTimeWeaving<context:load-time-weaver/>setups 自动检测。

要使用它,您必须通过提供以下 JVM 选项来使用 Spring 代理启动虚拟机:

-javaagent:/path/to/spring-instrument.jar

请注意,这需要修改 JVM 启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的服务器和您的操作策略)。也就是说,对于每个 JVM 一个应用程序的部署,例如独立的 Spring Boot 应用程序,您通常在任何情况下都可以控制整个 JVM 设置。

5.11。更多资源

有关 AspectJ 的更多信息,请访问 AspectJ 网站

Adrian Colyer 等人的Eclipse AspectJ。人。(Addison-Wesley, 2005) 为 AspectJ 语言提供了全面的介绍和参考。

强烈推荐 Ramnivas Laddad(Manning,2009 年)的AspectJ in Action ,第二版。本书的重点是 AspectJ,但也探讨了很多通用的 AOP 主题(在一定程度上)。

6. Spring AOP API

上一章描述了 Spring 使用 @AspectJ 和基于模式的方面定义对 AOP 的支持。在本章中,我们将讨论较低级别的 Spring AOP API。对于常见的应用程序,我们建议使用 Spring AOP 和 AspectJ 切入点,如前一章所述。

6.1。Spring中的切入点API

本节描述 Spring 如何处理关键的切入点概念。

6.1.1. 概念

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方法返回,并且 MethodMatcher 的方法返回,则在每次方法调用时都会调用三参数匹配方法。这让切入点在目标通知开始之前立即查看传递给方法调用的参数。trueisRuntime()true

大多数MethodMatcher实现都是静态的,这意味着它们的isRuntime()方法返回false. 在这种情况下,永远不会调用三参数matches方法。

如果可能,尽量使切入点静态化,允许 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 切入点原语的讨论,请参见前一章

6.1.4。便利切入点实现

Spring 提供了几个方便的切入点实现。您可以直接使用其中的一些;其他的旨在在特定于应用程序的切入点中进行子类化。

静态切入点

静态切入点基于方法和目标类,不能考虑方法的参数。对于大多数用途来说,静态切入点就足够了——而且是最好的。当第一次调用方法时,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,它让我们还可以引用 an Advice(请记住,anAdvice可以是拦截器,在通知之前,抛出通知等)。在幕后,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类型。

属性驱动切入点

一种重要的静态切入点是元数据驱动的切入点。这使用元数据属性的值(通常是源级元数据)。

动态切入点

动态切入点比静态切入点的评估成本更高。它们考虑了方法参数以及静态信息。这意味着必须在每次方法调用时对它们进行评估,并且结果不能被缓存,因为参数会有所不同。

主要的例子是control flow切入点。

控制流切入点

Spring 控制流切入点在概念上类似于 AspectJcflow切入点,但功能较弱。(目前无法指定一个切入点在与另一个切入点匹配的连接点下方运行。)控制流切入点匹配当前调用堆栈。例如,如果连接点被com.mycompany.web包中的方法或SomeCaller类调用,它可能会触发。控制流切入点是通过使用org.springframework.aop.support.ControlFlowPointcut类来指定的。

与其他动态切入点相比,控制流切入点在运行时的评估成本要高得多。在 Java 1.4 中,成本大约是其他动态切入点的五倍。

6.1.5。切入点超类

Spring 提供了有用的切入点超类来帮助您实现自己的切入点。

因为静态切入点是最有用的,所以你应该子类化 StaticMethodMatcherPointcut. 这只需要实现一个抽象方法(尽管您可以覆盖其他方法来自定义行为)。以下示例显示了如何子类化StaticMethodMatcherPointcut

java
class TestStaticPointcut extends StaticMethodMatcherPointcut {

    public boolean matches(Method m, Class targetClass) {
        // return true if custom criteria match
    }
}
科特林
class TestStaticPointcut : StaticMethodMatcherPointcut() {

    override fun matches(method: Method, targetClass: Class<*>): Boolean {
        // return true if custom criteria match
    }
}

还有用于动态切入点的超类。您可以将自定义切入点与任何建议类型一起使用。

6.1.6。自定义切入点

因为 Spring AOP 中的切入点是 Java 类而不是语言特性(如在 AspectJ 中),所以您可以声明自定义切入点,无论是静态的还是动态的。Spring 中的自定义切入点可以任意复杂。但是,如果可以,我们建议使用 AspectJ 切入点表达式语言。

Spring 的更高版本可能会支持 JAC 提供的“语义切入点”——例如,“所有更改目标对象中实例变量的方法”。

6.2. Spring 中的 Advice API

现在我们可以检查 Spring AOP 如何处理通知。

6.2.1. 建议生命周期

每个建议都是一个 Spring bean。建议实例可以在所有建议对象之间共享,或者对于每个建议对象都是唯一的。这对应于每个类或每个实例的建议。

每类建议最常使用。它适用于通用建议,例如事务顾问。这些不依赖于代理对象的状态或添加新状态。它们仅作用于方法和参数。

每个实例的建议适用于介绍,以支持 mixins。在这种情况下,建议将状态添加到代理对象。

您可以在同一个 AOP 代理中混合使用共享建议和实例建议。

6.2.2. Spring的建议类型

Spring 提供了几种建议类型,并且可以扩展以支持任意建议类型。本节介绍基本概念和标准通知类型。

拦截建议

Spring 中最基本的通知类型是围绕通知的拦截。

AllianceSpring 与使用方法拦截的环绕通知的 AOP 接口兼容。实现MethodInterceptor和围绕通知实现的类也应该实现以下接口:

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}

方法的MethodInvocation参数invoke()公开被调用的方法、目标连接点、AOP 代理和方法的参数。该 invoke()方法应该返回调用的结果:连接点的返回值。

以下示例显示了一个简单的MethodInterceptor实现:

java
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;
    }
}
科特林
class DebugInterceptor : MethodInterceptor {

    override fun invoke(invocation: MethodInvocation): Any {
        println("Before: invocation=[$invocation]")
        val rval = invocation.proceed()
        println("Invocation returned")
        return rval
    }
}

注意对 的proceed()方法的调用MethodInvocation。这沿着拦截器链向连接点前进。大多数拦截器调用此方法并返回其返回值。但是, aMethodInterceptor与任何周围的建议一样,可以返回不同的值或抛出异常,而不是调用proceed 方法。但是,您不想在没有充分理由的情况下执行此操作。

MethodInterceptor实现提供与其他符合 AOP 联盟的 AOP 实现的互操作性。本节其余部分讨论的其他通知类型实现了常见的 AOP 概念,但是以特定于 Spring 的方式。虽然使用最具体的通知类型有优势,但MethodInterceptor如果您可能希望在另一个 AOP 框架中运行方面,请坚持使用环绕通知。请注意,切入点目前在框架之间不能互操作,AOP 联盟目前没有定义切入点接口。
咨询前

更简单的通知类型是之前的通知。这不需要MethodInvocation 对象,因为它只在进入方法之前被调用。

before 通知的主要优点是不需要调用该proceed() 方法,因此不会因疏忽而未能沿拦截器链继续执行。

以下清单显示了该MethodBeforeAdvice界面:

public interface MethodBeforeAdvice extends BeforeAdvice {

    void before(Method m, Object[] args, Object target) throws Throwable;
}

(Spring 的 API 设计允许在通知之前使用字段,尽管通常的对象适用于字段拦截,而且 Spring 不太可能实现它。)

请注意,返回类型是void. Before 通知可以在连接点运行之前插入自定义行为,但不能更改返回值。如果之前的通知抛出异常,它会停止拦截器链的进一步执行。异常会向上传播到拦截器链。如果未选中或在调用方法的签名上,则直接将其传递给客户端。否则,它会被 AOP 代理包装在未经检查的异常中。

以下示例显示了 Spring 中的 before 通知,它计算所有方法调用:

java
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;
    }
}
科特林
class CountingBeforeAdvice : MethodBeforeAdvice {

    var count: Int = 0

    override fun before(m: Method, args: Array<Any>, target: Any?) {
        ++count
    }
}
之前的通知可以与任何切入点一起使用。
提出建议

如果连接点抛出异常,则在连接点返回后调用 Throws 通知。Spring 提供了类型化的 throws 建议。请注意,这意味着该 org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一个标签接口,标识给定对象实现了一个或多个类型化的 throws 建议方法。这些应采用以下形式:

afterThrowing([Method, args, target], subclassOfThrowable)

只有最后一个参数是必需的。方法签名可能有一个或四个参数,这取决于通知方法是否对方法和参数感兴趣。接下来的两个清单显示了作为 throws 建议示例的类。

RemoteException如果抛出a (包括来自子类),则会调用以下建议:

java
public class RemoteThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
}
科特林
class RemoteThrowsAdvice : ThrowsAdvice {

    fun afterThrowing(ex: RemoteException) {
        // Do something with remote exception
    }
}

与前面的通知不同,下一个示例声明了四个参数,以便它可以访问调用的方法、方法参数和目标对象。ServletException如果抛出a,则调用以下建议:

java
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}
科特林
class ServletThrowsAdviceWithArguments : ThrowsAdvice {

    fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
        // Do something with all arguments
    }
}

RemoteException最后一个示例说明了如何在处理和的单个类中使用这两种方法ServletException。任意数量的 throws 建议方法可以组合在一个类中。以下清单显示了最后一个示例:

java
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
    }
}
科特林
class CombinedThrowsAdvice : ThrowsAdvice {

    fun afterThrowing(ex: RemoteException) {
        // Do something with remote exception
    }

    fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
        // Do something with all arguments
    }
}
如果 throws-advice 方法本身抛出异常,它会覆盖原始异常(即,它会更改抛出给用户的异常)。覆盖异常通常是 RuntimeException,它与任何方法签名兼容。但是,如果 throws-advice 方法抛出检查异常,它必须匹配目标方法声明的异常,因此在某种程度上与特定目标方法签名耦合。不要抛出与目标方法的签名不兼容的未声明的检查异常!
抛出的建议可以与任何切入点一起使用。
退货后的建议

Spring 中的后返回通知必须实现该 org.springframework.aop.AfterReturningAdvice接口,如下清单所示:

public interface AfterReturningAdvice extends Advice {

    void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable;
}

返回后的通知可以访问返回值(它不能修改)、调用的方法、方法的参数和目标。

返回通知后的以下内容计算所有未引发异常的成功方法调用:

java
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;
    }
}
科特林
class CountingAfterReturningAdvice : AfterReturningAdvice {

    var count: Int = 0
        private set

    override fun afterReturning(returnValue: Any?, m: Method, args: Array<Any>, target: Any?) {
        ++count
    }
}

此建议不会更改执行路径。如果它抛出异常,它会被抛出拦截器链而不是返回值。

返回后的通知可以与任何切入点一起使用。
介绍建议

Spring 将引入通知视为一种特殊的拦截通知。

简介需要实现以下接口的 anIntroductionAdvisor和 an IntroductionInterceptor

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

invoke()从AOP联盟接口继承的方法MethodInterceptor必须实现引入。也就是说,如果被调用的方法在引入的接口上,则引入拦截器负责处理方法调用——它不能调用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 测试套件的示例,假设我们想为一个或多个对象引入以下接口:

java
public interface Lockable {
    void lock();
    void unlock();
    boolean locked();
}
科特林
interface Lockable {
    fun lock()
    fun unlock()
    fun locked(): Boolean
}

这说明了一个混合。我们希望能够将建议对象转换为Lockable,无论它们的类型如何,并调用锁定和解锁方法。如果我们调用该lock()方法,我们希望所有的 setter 方法都抛出一个LockedException. 因此,我们可以添加一个方面,它提供了使对象不可变的能力,而他们对此一无所知:AOP 的一个很好的例子。

首先,我们需要一个IntroductionInterceptor可以完成繁重工作的人。在这种情况下,我们扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor 便利类。我们可以IntroductionInterceptor直接实现,但 DelegatingIntroductionInterceptor在大多数情况下使用是最好的。

旨在将DelegatingIntroductionInterceptor介绍委托给所引入接口的实际实现,隐藏使用拦截来做到这一点。您可以使用构造函数参数将委托设置为任何对象。默认委托(使用无参数构造函数时)是this. 因此,在下一个示例中,委托是 的LockMixin子类DelegatingIntroductionInterceptor。给定一个委托(默认情况下,它本身),一个DelegatingIntroductionInterceptor实例会查找委托实现的所有接口(除了 IntroductionInterceptor),并支持对其中任何一个的介绍。诸如子类LockMixin可以调用该suppressInterface(Class intf) 方法来抑制不应该暴露的接口。然而,无论 anIntroductionInterceptor准备支持多少接口, IntroductionAdvisorused 控制实际暴露的接口。引入的接口隐藏了目标对同一接口的任何实现。

因此,LockMixin扩展DelegatingIntroductionInterceptor并实现Lockable 了自身。超类自动选择Lockable可以支持引入的,所以我们不需要指定。我们可以通过这种方式引入任意数量的接口。

注意locked实例变量的使用。这有效地为目标对象中保存的状态添加了额外的状态。

以下示例显示了示例LockMixin类:

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

}
科特林
class LockMixin : DelegatingIntroductionInterceptor(), Lockable {

    private var locked: Boolean = false

    fun lock() {
        this.locked = true
    }

    fun unlock() {
        this.locked = false
    }

    fun locked(): Boolean {
        return this.locked
    }

    override fun invoke(invocation: MethodInvocation): Any? {
        if (locked() && invocation.method.name.indexOf("set") == 0) {
            throw LockedException()
        }
        return super.invoke(invocation)
    }

}

通常,您不需要重写该invoke()方法。DelegatingIntroductionInterceptor实现(如果方法被引入,则调用该方法 delegate,否则向连接点前进)通常就足够了。在本例中,我们需要添加一个检查:如果处于锁定模式,则不能调用任何 setter 方法。

所需的引入只需要保存一个不同的 LockMixin实例并指定引入的接口(在这种情况下,只有 Lockable)。一个更复杂的示例可能会引用引入拦截器(将被定义为原型)。在这种情况下,没有与 a 相关的配置LockMixin,因此我们使用new. 下面的例子展示了我们的LockMixinAdvisor类:

java
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

    public LockMixinAdvisor() {
        super(new LockMixin(), Lockable.class);
    }
}
科特林
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)

我们可以非常简单地应用这个顾问,因为它不需要配置。(但是,没有 . 是不可能使用IntroductionInterceptorIntroductionAdvisor。)与介绍一样,顾问必须是每个实例的,因为它是有状态的。我们需要一个不同的实例LockMixinAdvisor,因此 LockMixin,对于每个被建议的对象。顾问包括被建议对象状态的一部分。

Advised.addAdvisor()我们可以通过使用XML 配置中的方法或(推荐的方式)以编程方式应用此顾问程序,就像任何其他顾问程序一样。下面讨论的所有代理创建选择,包括“自动代理创建者”,都能正确处理引入和有状态的 mixin。

6.3. Spring 中的 Advisor API

在 Spring 中,Advisor 是一个仅包含一个与切入点表达式关联的建议对象的方面。

除了介绍的特殊情况外,任何顾问都可以与任何建议一起使用。 org.springframework.aop.support.DefaultPointcutAdvisor是最常用的顾问类。它可以与MethodInterceptorBeforeAdvice或 一起使用ThrowsAdvice

可以在同一个 AOP 代理中混合 Spring 中的顾问和建议类型。例如,您可以在一个代理配置中对通知、抛出通知和通知之前使用拦截。Spring 自动创建必要的拦截器链。

6.4. 使用ProxyFactoryBean创建 AOP 代理

如果您将 Spring IoC 容器(an ApplicationContextor BeanFactory)用于您的业务对象(您应该这样做!),您希望使用 Spring 的 AOP FactoryBean实现之一。(请记住,工厂 bean 引入了一个间接层,让它创建不同类型的对象。)

Spring AOP 支持也在幕后使用了工厂 bean。

在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean. 这可以完全控制切入点、任何适用的建议及其顺序。但是,如果您不需要此类控制,则可以使用更简单的选项。

6.4.1. 基本

ProxyFactoryBean其他 SpringFactoryBean实现一样,引入了间接级别。如果你定义了一个ProxyFactoryBean命名foo的,引用的对象foo看不到ProxyFactoryBean实例本身,而是通过getObject()ProxyFactoryBean. 此方法创建一个包装目标对象的 AOP 代理。

使用一个ProxyFactoryBean或另一个 IoC 感知类来创建 AOP 代理的最重要的好处之一是通知和切入点也可以由 IoC 管理。这是一个强大的特性,可以实现其他 AOP 框架难以实现的某些方法。例如,一个通知本身可能引用应用程序对象(除了目标,它应该在任何 AOP 框架中都可用),受益于依赖注入提供的所有可插入性。

6.4.2. JavaBean 属性

与 Spring 提供的大多数FactoryBean实现一样, ProxyFactoryBean该类本身就是一个 JavaBean。它的属性用于:

一些关键属性继承自org.springframework.aop.framework.ProxyConfig (Spring 中所有 AOP 代理工厂的超类)。这些关键属性包括:

  • proxyTargetClasstrue如果要代理目标类,而不是目标类的接口。如果此属性值设置为true,则创建 CGLIB 代理(另请参阅基于 JDK 和 CGLIB 的代理)。

  • optimize:控制是否对通过 CGLIB 创建的代理应用积极优化。除非您完全了解相关的 AOP 代理如何处理优化,否则您不应轻率地使用此设置。这目前仅用于 CGLIB 代理。它对 JDK 动态代理没有影响。

  • frozen:如果代理配置是frozen,则不再允许更改配置。这对于轻微的优化和在创建代理后不希望调用者能够操作代理(通过Advised 接口)的情况都很有用。此属性的默认值为 false,因此允许更改(例如添加额外的建议)。

  • exposeProxy:确定当前代理是否应该在 a 中公开, ThreadLocal以便目标可以访问它。如果目标需要获取代理并且exposeProxy属性设置为true,目标可以使用该 AopContext.currentProxy()方法。

其他属性具体ProxyFactoryBean包括以下内容:

  • proxyInterfaces:String接口名称数组。如果未提供,则使用目标类的 CGLIB 代理(但另请参阅基于 JDK 和 CGLIB 的代理)。

  • interceptorNames:要应用的 、拦截器或其他建议名称的String数组。Advisor订购很重要,先到先得。也就是说列表中的第一个拦截器是第一个能够拦截调用的。

    这些名称是当前工厂中的 bean 名称,包括来自祖先工厂的 bean 名称。您不能在此处提及 bean 引用,因为这样做会导致 ProxyFactoryBean忽略通知的单例设置。

    您可以使用星号 ( ) 附加拦截器名称*。这样做会导致应用名称以要应用的星号之前的部分开头的所有顾问 bean。您可以在使用“全局”顾问中找到使用此功能的示例。

  • 单例:工厂是否应该返回单个对象,无论getObject()方法被调用的频率如何。几个FactoryBean实现提供了这样的方法。默认值为true。如果你想使用有状态的建议——例如,对于有状态的 mixins——使用原型建议和 false.

6.4.3. 基于 JDK 和 CGLIB 的代理

本节是关于如何ProxyFactoryBean 选择为特定目标对象(将被代理)创建基于 JDK 的代理或基于 CGLIB 的代理的权威文档。

ProxyFactoryBean关于创建基于 JDK 或 CGLIB 的代理 的行为在Spring 的 1.2.x 和 2.0 版本之间发生了变化。现在ProxyFactoryBean在自动检测接口方面表现出与类相似的语义 TransactionProxyFactoryBean

如果要代理的目标对象的类(以下简称目标类)没有实现任何接口,则创建基于CGLIB的代理。这是最简单的场景,因为 JDK 代理是基于接口的,没有接口意味着 JDK 代理甚至是不可能的。您可以插入目标 bean 并通过设置interceptorNames属性来指定拦截器列表。请注意,即使 的proxyTargetClass属性 ProxyFactoryBean已设置为,也会创建基于 CGLIB 的代理false。(这样做毫无意义,最好从 bean 定义中删除,因为它充其量是多余的,最坏的情况是令人困惑。)

如果目标类实现一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean.

如果 的proxyTargetClass属性ProxyFactoryBean已设置为true,则创建基于 CGLIB 的代理。这是有道理的,并且符合最小意外原则。即使 的proxyInterfaces属性 ProxyFactoryBean已设置为一个或多个完全限定的接口名称,该proxyTargetClass属性设置为这一事实也会true导致基于 CGLIB 的代理生效。

如果 的proxyInterfaces属性ProxyFactoryBean已设置为一个或多个完全限定的接口名称,则会创建一个基于 JDK 的代理。proxyInterfaces 创建的代理实现了属性中指定的所有接口。如果目标类碰巧实现了比proxyInterfaces属性中指定的接口多得多的接口,那很好,但是返回的代理不会实现这些额外的接口。

如果尚未设置 的proxyInterfaces属性,但目标类确实实现了一个(或多个)接口,则自动检测目标类确实实现了至少一个接口这一事实,并创建了一个基于 JDK 的代理。实际代理的接口是目标类实现的所有接口。实际上,这与为属性提供目标类实现的每个接口的列表相同。但是,它的工作量大大减少,而且更不容易出现印刷错误。ProxyFactoryBeanProxyFactoryBeanproxyInterfaces

6.4.4. 代理接口

考虑一个简单的例子ProxyFactoryBean。此示例涉及:

  • 被代理的目标 bean。这是personTarget示例中的 bean 定义。

  • AnAdvisor和 anInterceptor用于提供建议。

  • 一个 AOP 代理 bean 定义,用于指定目标对象(personTargetbean)、要代理的接口以及要应用的建议。

以下清单显示了该示例:

<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 名称。您可以在返回之前、之后使用顾问、拦截器和抛出建议对象。顾问的顺序很重要。

您可能想知道为什么该列表不包含 bean 引用。这样做的原因是,如果 的单例属性ProxyFactoryBean设置为false,它必须能够返回独立的代理实例。如果任何advisor 本身是原型,则需要返回一个独立的实例,因此必须能够从工厂获取原型的实例。持有参考资料是不够的。

前面显示的personbean 定义可以用来代替Person实现,如下所示:

java
Person person = (Person) factory.getBean("person");
科特林
val person = factory.getBean("person") as Person;

同一 IoC 上下文中的其他 bean 可以表达对它的强类型依赖,就像普通的 Java 对象一样。以下示例显示了如何执行此操作:

<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 代理而不是动态代理。为此,请将 前面显示的proxyTargetClass属性设置为。虽然最好对接口而不是类进行编程,但在处理遗留代码时,建议不实现接口的类的能力可能很有用。(通常,Spring 不是规定性的。虽然它使应用良好实践变得容易,但它避免了强制采用特定方法。)ProxyFactoryBeantrue

如果你愿意,你可以在任何情况下强制使用 CGLIB,即使你有接口。

CGLIB 代理通过在运行时生成目标类的子类来工作。Spring 将这个生成的子类配置为将方法调用委托给原始目标。子类用于实现装饰器模式,编织在通知中。

CGLIB 代理通常应该对用户透明。但是,有一些问题需要考虑:

  • Final不能建议方法,因为它们不能被覆盖。

  • 无需将 CGLIB 添加到您的类路径中。从 Spring 3.2 开始,CGLIB 被重新打包并包含在 spring-core JAR 中。换句话说,基于 CGLIB 的 AOP 像 JDK 动态代理一样“开箱即用”。

CGLIB 代理和动态代理之间几乎没有性能差异。在这种情况下,性能不应成为决定性的考虑因素。

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>

abstract请注意,在父 bean 示例中,我们通过将属性设置为 明确地将父 bean 定义标记为抽象true如前所述,因此它实际上可能不会被实例化。默认情况下,应用程序上下文(但不是简单的 bean 工厂)预先实例化所有单例。因此,重要的是(至少对于单例 bean),如果您有一个(父)bean 定义打算仅用作模板,并且此定义指定了一个类,则必须确保将abstract 属性设置为true. 否则,应用程序上下文实际上会尝试预先实例化它。

6.6. 以编程方式创建 AOP 代理ProxyFactory

使用 Spring 以编程方式创建 AOP 代理很容易。这使您可以在不依赖 Spring IoC 的情况下使用 Spring AOP。

目标对象实现的接口被自动代理。以下清单显示了为目标对象创建代理,其中包含一个拦截器和一个顾问:

java
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
科特林
val factory = ProxyFactory(myBusinessInterfaceImpl)
factory.addAdvice(myMethodInterceptor)
factory.addAdvisor(myAdvisor)
val tb = factory.proxy as MyBusinessInterface

第一步是构造一个类型为 的对象 org.springframework.aop.framework.ProxyFactory。您可以像前面的示例一样使用目标对象创建它,或者指定要在备用构造函数中代理的接口。

您可以添加建议(使用拦截器作为一种专门的建议)、顾问或两者兼而有之,并在ProxyFactory. 如果添加 IntroductionInterceptionAroundAdvisor,则可以使代理实现其他接口。

ProxyFactory(inherited from )还有一些方便的方法,AdvisedSupport可以让您添加其他建议类型,例如 before 和 throws 建议。 AdvisedSupport是 和 的超ProxyFactoryProxyFactoryBean

在大多数应用程序中,将 AOP 代理创建与 IoC 框架集成是最佳实践。我们建议您使用 AOP 从 Java 代码外部化配置,就像您通常应该做的那样。

6.7. 操作建议对象

无论您如何创建 AOP 代理,您都可以使用 org.springframework.aop.framework.Advised接口来操作它们。任何 AOP 代理都可以转换为该接口,无论它实现了哪些其他接口。该接口包括以下方法:

java
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();
科特林
fun getAdvisors(): Array<Advisor>

@Throws(AopConfigException::class)
fun addAdvice(advice: Advice)

@Throws(AopConfigException::class)
fun addAdvice(pos: Int, advice: Advice)

@Throws(AopConfigException::class)
fun addAdvisor(advisor: Advisor)

@Throws(AopConfigException::class)
fun addAdvisor(pos: Int, advisor: Advisor)

fun indexOf(advisor: Advisor): Int

@Throws(AopConfigException::class)
fun removeAdvisor(advisor: Advisor): Boolean

@Throws(AopConfigException::class)
fun removeAdvisor(index: Int)

@Throws(AopConfigException::class)
fun replaceAdvisor(a: Advisor, b: Advisor): Boolean

fun isFrozen(): Boolean

getAdvisors()方法Advisor为添加到工厂的每个顾问、拦截器或其他建议类型返回一个。如果您添加了Advisor,则在此索引处返回的顾问就是您添加的对象。如果您添加了拦截器或其他通知类型,Spring 会将其包装在带有始终返回的切入点的顾问中true。因此,如果您添加了 a MethodInterceptor,则为此索引返回的顾问是 a DefaultPointcutAdvisor,它返回您的 MethodInterceptor和匹配所有类和方法的切入点。

这些addAdvisor()方法可用于添加任何Advisor. 通常,持有切入点和建议的顾问是通用DefaultPointcutAdvisor的,您可以将其与任何建议或切入点一起使用(但不能用于介绍)。

默认情况下,即使已创建代理,也可以添加或删除顾问或拦截器。唯一的限制是不可能添加或删除介绍顾问,因为工厂的现有代理不显示界面更改。(您可以从工厂获得一个新的代理来避免这个问题。)

以下示例显示了将 AOP 代理强制转换为Advised接口并检查和操作其建议:

java
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);
科特林
val advised = myObject as Advised
val advisors = advised.advisors
val oldAdvisorCount = advisors.size
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(DebugInterceptor())

// Add selective advice using a pointcut
advised.addAdvisor(DefaultPointcutAdvisor(mySpecialPointcut, myAdvice))

assertEquals("Added two advisors", oldAdvisorCount + 2, advised.advisors.size)
在生产中修改关于业务对象的建议是否可取(不是双关语)是值得怀疑的,尽管毫无疑问,有合法的使用案例。但是,它在开发中非常有用(例如,在测试中)。我们有时发现能够以拦截器或其他建议的形式添加测试代码非常有用,进入我们想要测试的方法调用。(例如,通知可以进入为该方法创建的事务中,可能在将事务标记为回滚之前运行 SQL 以检查数据库是否正确更新。)

根据您创建代理的方式,您通常可以设置一个frozen标志。在这种情况下,该Advised isFrozen()方法返回true,并且任何通过添加或删除来修改建议的尝试都会导致AopConfigException. 冻结建议对象状态的能力在某些情况下很有用(例如,防止调用代码删除安全拦截器)。

6.8. 使用“自动代理”工具

到目前为止,我们已经考虑了通过使用ProxyFactoryBean或类似的工厂 bean 显式创建 AOP 代理。

Spring 还允许我们使用“自动代理”bean 定义,它可以自动代理选定的 bean 定义。这是建立在 Spring 的“bean 后处理器”基础设施之上的,它可以在容器加载时修改任何 bean 定义。

在这个模型中,您在 XML bean 定义文件中设置了一些特殊的 bean 定义来配置自动代理基础设施。这使您可以声明符合自动代理条件的目标。你不需要使用ProxyFactoryBean.

有两种方法可以做到这一点:

  • 通过使用在当前上下文中引用特定 bean 的自动代理创建者。

  • 一个值得单独考虑的自动代理创建的特殊情况:由源级元数据属性驱动的自动代理创建。

6.8.1. 自动代理 Bean 定义

本节介绍 org.springframework.aop.framework.autoproxy包提供的自动代理创建者。

BeanNameAutoProxyCreator

该类BeanNameAutoProxyCreatorBeanPostProcessor自动为名称与文字值或通配符匹配的 bean 创建 AOP 代理。以下示例显示了如何创建BeanNameAutoProxyCreatorbean:

<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 定义(例如前面示例中的jdkMyBean和)是具有目标类的普通旧 bean 定义。onlyJdkAOP 代理由BeanNameAutoProxyCreator. 相同的建议适用于所有匹配的 bean。请注意,如果使用顾问(而不是前面示例中的拦截器),则切入点可能会以不同的方式应用于不同的 bean。

DefaultAdvisorAutoProxyCreator

一个更通用且极其强大的自动代理创建者是 DefaultAdvisorAutoProxyCreator. 这会自动在当前上下文中应用符合条件的顾问,而无需在自动代理顾问的 bean 定义中包含特定的 bean 名称。它提供了与BeanNameAutoProxyCreator.

使用此机制涉及:

  • 指定DefaultAdvisorAutoProxyCreatorbean 定义。

  • 在相同或相关的上下文中指定任意数量的顾问。请注意,这些必须是顾问,而不是拦截器或其他建议。这是必要的,因为必须有一个切入点来评估,以检查每个建议对候选 bean 定义的资格。

自动评估每个顾问中包含的DefaultAdvisorAutoProxyCreator切入点,以查看应该将什么(如果有)建议应用于每个业务对象(例如businessObject1businessObject2示例中的)。

这意味着可以将任意数量的顾问自动应用于每个业务对象。如果任何顾问中没有切入点与业务对象中的任何方法匹配,则不会代理该对象。当为新业务对象添加 bean 定义时,它们会在必要时自动代理。

自动代理通常具有使调用者或依赖项无法获得不建议的对象的优点。调用getBean("businessObject1")ApplicationContext会返回一个 AOP 代理,而不是目标业务对象。(前面显示的“inner bean”习语也提供了这个好处。)

以下示例创建一个DefaultAdvisorAutoProxyCreatorbean 和本节中讨论的其他元素:

<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示例中使用的 具有可配置的 order 值。默认设置是无序的。

6.9. 使用TargetSource实现

Spring 提供了 a 的概念TargetSource,在 org.springframework.aop.TargetSource接口中表达。该接口负责返回实现连接点的“目标对象”。TargetSource 每次 AOP 代理处理方法调用时,都会要求实现提供目标实例。

使用 Spring AOP 的开发人员通常不需要直接使用TargetSource实现,但这提供了支持池、热插拔和其他复杂目标的强大方法。例如TargetSource,通过使用池来管理实例,池可以为每次调用返回不同的目标实例。

如果不指定 a TargetSource,则使用默认实现来包装本地对象。每次调用都返回相同的目标(如您所料)。

本节的其余部分描述了 Spring 提供的标准目标源以及如何使用它们。

使用自定义目标源时,您的目标通常需要是原型而不是单例 bean 定义。这允许 Spring 在需要时创建新的目标实例。

6.9.1. 热插拔目标源

org.springframework.aop.target.HotSwappableTargetSource存在让 AOP 代理的目标被切换,同时让调用者保留对它的引用。

更改目标源的目标会立即生效。HotSwappableTargetSource是线程安全的。

swap()您可以使用HotSwappableTargetSource 上的方法更改目标,如以下示例所示:

java
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
科特林
val swapper = beanFactory.getBean("swapper") as HotSwappableTargetSource
val 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 的引用的客户端不知道更改,但会立即开始命中新目标。

尽管此示例没有添加任何建议(使用 a 不需要添加建议TargetSource),但 anyTargetSource可以与任意建议一起使用。

6.9.2. 合并目标源

使用池化目标源提供了与无状态会话 EJB 类似的编程模型,其中维护了相同实例的池,方法调用将释放池中的对象。

Spring pooling 和 SLSB pooling 的一个关键区别是 Spring pooling 可以应用于任何 POJO。与一般的 Spring 一样,此服务可以以非侵入方式应用。

Spring 提供对 Commons Pool 2.2 的支持,它提供了相当高效的池化实现。您需要commons-pool应用程序类路径中的 Jar 才能使用此功能。您还可以子类化 org.springframework.aop.target.AbstractPoolingTargetSource以支持任何其他池 API。

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实现可以创建目标的新实例以根据需要增加池。有关其属性的信息,请参阅您希望使用的javadoc 和具体子类。AbstractPoolingTargetSourcemaxSize是最基本的,并且始终保证存在。

在这种情况下,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公开池对象的拦截器名称列表中。

演员表定义如下:

java
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
科特林
val conf = beanFactory.getBean("businessObject") as PoolingConfig
println("Max pool size is " + conf.maxSize)
通常不需要池化无状态服务对象。我们不认为它应该是默认选择,因为大多数无状态对象自然是线程安全的,如果资源被缓存,实例池是有问题的。

使用自动代理可以实现更简单的池化。您可以设置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如果您需要为每个传入请求(即每个线程)创建一个对象,则目标源很有用。a 的概念ThreadLocal提供了一个 JDK 范围的工具来透明地在线程旁边存储资源。设置 a ThreadLocalTargetSource与为其他类型的目标源解释的几乎相同,如以下示例所示:

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
</bean>
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.空安全

尽管 Java 不允许您使用其类型系统来表达 null 安全性,但 Spring 框架现在在org.springframework.lang包中提供了以下注解,以便您声明 API 和字段的可空性:

  • @Nullable: 表示特定参数、返回值或字段可以是 的注解null

  • @NonNull: 表示特定参数、返回值或字段不能存在的注释(在参数/返回值和字段 where和applynull上不需要)。@NonNullApi@NonNullFields

  • @NonNullApi:包级别的注释,将非空声明为参数和返回值的默认语义。

  • @NonNullFields:包级别的注释,将非空声明为字段的默认语义。

Spring 框架本身利用了这些注解,但它们也可以在任何基于 Spring 的 Java 项目中用于声明空安全 API 和可选的空安全字段。尚不支持泛型类型参数、可变参数和数组元素可空性,但应在即将发布的版本中提供, 有关最新信息,请参阅SPR-15942 。可空性声明预计将在 Spring Framework 版本之间进行微调,包括次要版本。方法体内使用的类型的可空性超出了此功能的范围。

其他常见的库,如 Reactor 和 Spring Data 提供了使用类似可空性安排的空安全 API,为 Spring 应用程序开发人员提供一致的整体体验。

7.1。用例

除了为 Spring Framework API 可空性提供显式声明之外,IDE(例如 IDEA 或 Eclipse)可以使用这些注释来提供与空安全性相关的有用警告,以避免NullPointerException在运行时。

它们还用于在 Kotlin 项目中使 Spring API 为空安全,因为 Kotlin 原生支持空安全Kotlin 支持文档中提供了更多详细信息。

7.2. JSR-305 元注释

Spring 注释使用JSR 305 注释(一种休眠但广泛传播的 JSR)进行元注释。JSR-305 元注释让 IDEA 或 Kotlin 等工具供应商以通用方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。

没有必要也不建议将 JSR-305 依赖项添加到项目类路径以利用 Spring 空安全 API。只有在代码库中使用空安全注解的项目(例如基于 Spring 的库)才应添加com.google.code.findbugs:jsr305:3.0.2 GradlecompileOnly配置或 Mavenprovided范围以避免编译警告。

8. 数据缓冲器和编解码器

Java NIO 提供ByteBuffer但许多库在其上构建自己的字节缓冲区 API,特别是对于重用缓冲区和/或使用直接缓冲区有利于性能的网络操作。例如,Netty 有ByteBuf层次结构,Undertow 使用 XNIO,Jetty 使用池化字节缓冲区和要释放的回调,等等。该spring-core模块提供了一组抽象来处理各种字节缓冲区 API,如下所示:

8.1。DataBufferFactory

DataBufferFactory用于通过以下两种方式之一创建数据缓冲区:

  1. 分配一个新的数据缓冲区,如果知道的话,可以选择预先指定容量,即使实现DataBuffer可以按需增长和缩小,这也会更有效。

  2. 包装现有的byte[]or java.nio.ByteBuffer,它使用实现装饰给定数据DataBuffer并且不涉及分配。

请注意,WebFlux 应用程序不会DataBufferFactory直接创建 a ,而是通过客户端上的ServerHttpResponse或访问它。ClientHttpRequest工厂的类型取决于底层的客户端或服务器,例如 NettyDataBufferFactory对于 Reactor Netty,DefaultDataBufferFactory对于其他的。

8.2.DataBuffer

DataBuffer界面提供了与 Netty 类似的操作,java.nio.ByteBuffer但也带来了一些额外的好处,其中一些是受 Netty 启发的ByteBuf。以下是部分福利列表:

  • 以独立位置读取和写入,即不需要调用来flip()在读取和写入之间交替。

  • 容量随需扩展java.lang.StringBuilder

  • 池化缓冲区和引用计数通过PooledDataBuffer.

  • 将缓冲区视为java.nio.ByteBufferInputStreamOutputStream

  • 确定给定字节的索引或最后一个索引。

8.3.PooledDataBuffer

正如 ByteBuffer的 Javadoc 中所解释的,字节缓冲区可以是直接的或非直接的。直接缓冲区可以驻留在 Java 堆之外,这消除了本地 I/O 操作复制的需要。这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但它们的创建和释放成本也更高,这导致了池化缓冲区的想法。

PooledDataBuffer是它的扩展,DataBuffer它有助于引用计数,这对于字节缓冲池至关重要。它是如何工作的?当 aPooledDataBuffer被分配时,引用计数为 1。调用retain()递增计数,调用release()递减计数。只要计数大于0,就保证缓冲区不会被释放。当计数减少到 0 时,可以释放池化缓冲区,这实际上可能意味着为缓冲区保留的内存返回到内存池。

请注意,PooledDataBuffer与其直接操作,在大多数情况下,最好使用DataBufferUtils应用版本中的便捷方法或 DataBuffer仅当它是PooledDataBuffer.

8.4.DataBufferUtils

DataBufferUtils提供了许多实用方法来操作数据缓冲区:

  • 如果底层字节缓冲区 API 支持,则将数据缓冲区流加入可能具有零副本的单个缓冲区,例如通过复合缓冲区。

  • InputStream或 NIOChannel变为Flux<DataBuffer>,反之亦然 a Publisher<DataBuffer>变为OutputStream或 NIO Channel

  • DataBuffer如果缓冲区是 的实例,则 释放或保留 a 的方法PooledDataBuffer

  • 跳过或从字节流中获取,直到特定的字节数。

8.5。编解码器

org.springframework.core.codec包提供以下策略接口:

  • Encoder编码Publisher<T>成数据缓冲区流。

  • Decoder解码Publisher<DataBuffer>成更高级别的对象流。

spring-core模块提供byte[]ByteBufferDataBufferResourceString编码器和解码器实现。该spring-web模块添加了 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 和其他编码器和解码器。请参阅 WebFlux 部分中的编解码器。

8.6. 使用DataBuffer

使用数据缓冲区时,必须特别注意确保缓冲区被释放,因为它们可能被池化。我们将使用编解码器来说明它是如何工作的,但这些概念更普遍适用。让我们看看编解码器必须在内部做什么来管理数据缓冲区。

ADecoder是在创建更高级别对象之前最后读取输入数据缓冲区的,因此它必须按如下方式释放它们:

  1. 如果 aDecoder简单地读取每个输入缓冲区并准备立即释放它,它可以通过DataBufferUtils.release(dataBuffer).

  2. 如果 aDecoder正在使用,等运算符Flux或其他在内部预取和缓存数据项的运算符,或者正在使用 , 等运算符 以及其他省略项的运算符,则 必须将其添加到组合链中以确保先释放此类缓冲区被丢弃,也可能是由于错误或取消信号。MonoflatMapreducefilterskipdoOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)

  3. 如果Decoder以任何其他方式保持一个或多个数据缓冲区,则必须确保在完全读取时释放它们,或者在缓存数据缓冲区被读取和释放之前发生错误或取消信号的情况下。

请注意,这DataBufferUtils#join提供了一种将数据缓冲区流聚合到单个数据缓冲区中的安全有效的方法。同样skipUntilByteCounttakeUntilByteCount是解码器使用的其他安全方法。

Encoder分配其他人必须读取(和释放)的数据缓冲区。所以 anEncoder 没有什么可做的。但是,Encoder如果在用数据填充缓冲区时发生序列化错误,则必须注意释放数据缓冲区。例如:

java
DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
    // serialize and populate buffer..
    release = false;
}
finally {
    if (release) {
        DataBufferUtils.release(buffer);
    }
}
return buffer;
科特林
val buffer = factory.allocateBuffer()
var release = true
try {
    // serialize and populate buffer..
    release = false
} finally {
    if (release) {
        DataBufferUtils.release(buffer)
    }
}
return buffer

an 的消费者Encoder负责释放它接收到的数据缓冲区。在 WebFlux 应用程序中,输出Encoder用于写入 HTTP 服务器响应或客户端 HTTP 请求,在这种情况下,释放数据缓冲区是写入服务器响应或客户端请求的代码的责任.

请注意,在 Netty 上运行时,有用于 解决缓冲区泄漏问题的调试选项。

9. 记录

spring-jcl从 Spring Framework 5.0 开始,Spring 在模块中实现了自己的 Commons Logging 桥接器。该实现检查类路径中是否存在 Log4j 2.x API 和 SLF4J 1.7 API,并使用找到的第一个作为日志实现,回退到 Java 平台的核心日志工具(也称为JULjava.util.logging)如果 Log4j 2.x 和 SLF4J 都不可用。

将 Log4j 2.x 或 Logback(或其他 SLF4J 提供程序)放入您的类路径中,无需任何额外的桥梁,并让框架自动适应您的选择。有关详细信息,请参阅 Spring Boot 日志记录参考文档

Spring 的 Commons Logging 变体仅用于核心框架和扩展中的基础设施日志记录目的。

对于应用程序代码中的日志记录需求,更喜欢直接使用 Log4j 2.x、SLF4J 或 JUL。

可以Log通过org.apache.commons.logging.LogFactory以下示例中的方式检索实现。

java
public class MyBean {
    private final Log log = LogFactory.getLog(getClass());
    // ...
}
科特林
class MyBean {
  private val log = LogFactory.getLog(javaClass)
  // ...
}

10. 附录

10.1。XML 模式

附录的这一部分列出了与核心容器相关的 XML 模式。

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

        <!-- bean definitions here -->

</beans>
使用<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>

前面的配置使用 SpringFactoryBean实现 (the FieldRetrievingFactoryBean) 将isolationbean 上的属性值设置为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 属性或构造函数参数

FieldRetrievingFactoryBean 是一个FactoryBean检索一个static或非静态字段值的。它通常用于检索public static final常量,然后可用于为另一个 bean 设置属性值或构造函数参数。

以下示例显示了如何static使用 staticField 属性公开字段:

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

这确实意味着对于 beanid是什么不再有任何选择(因此任何其他引用它的 bean 也必须使用这个更长的名称),但是这种形式定义起来非常简洁,用作内部 bean 也非常方便因为id不必为 bean 引用指定 ,如以下示例所示:

<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. 以下示例枚举显示了注入枚举值是多么容易:

java
package javax.persistence;

public enum PersistenceContextType {

    TRANSACTION,
    EXTENDED
}
科特林
package javax.persistence

enum class PersistenceContextType {

    TRANSACTION,
    EXTENDED
}

现在考虑以下类型的 setterPersistenceContextType和相应的 bean 定义:

java
package example;

public class Client {

    private PersistenceContextType persistenceContextType;

    public void setPersistenceContextType(PersistenceContextType type) {
        this.persistenceContextType = type;
    }
}
科特林
package example

class Client {

    lateinit var persistenceContextType: PersistenceContextType
}
<bean class="example.Client">
    <property name="persistenceContextType" value="TRANSACTION"/>
</bean>
使用<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"/>

前面的配置使用 SpringFactoryBean实现 (the PropertyPathFactoryBean) 创建一个int名为 的 bean(类型为 ) ,该 beantestBean.age的值等于 bean 的age属性testBean

现在考虑以下示例,它添加了一个<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"/>

path元素的属性值<property-path/>遵循 的形式 beanName.beanProperty。在这种情况下,它获取age名为 的 bean 的属性 testBean。该age属性的值为10

<util:property-path/>用于设置 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。

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

前面的配置使用 SpringFactoryBean实现 ( PropertiesFactoryBean) 来实例化具有java.util.Properties从提供的Resource位置加载的值的实例)。

下面的示例使用一个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"/>
使用<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>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </list>
    </property>
</bean>

前面的配置使用 SpringFactoryBean实现 (the ListFactoryBean) 创建一个java.util.List实例并使用从提供的 中获取的值对其进行初始化sourceList

下面的示例使用一个<util:list/>元素进行更简洁的表示:

<!-- creates a java.util.List instance with the supplied values -->
<util:list id="emails">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:list>

您还可以使用元素上的属性显式控制List实例化和填充的确切类型。例如,如果我们真的需要实例化 a,我们可以使用以下配置:list-class<util:list/>java.util.LinkedList

<util:list id="emails" list-class="java.util.LinkedList">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>d'[email protected]</value>
</util:list>

如果未list-class提供任何属性,则容器选择一个List实现。

使用<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="[email protected]"/>
            <entry key="raskolnikov" value="[email protected]"/>
            <entry key="stavrogin" value="[email protected]"/>
            <entry key="porfiry" value="[email protected]"/>
        </map>
    </property>
</bean>

前面的配置使用 SpringFactoryBean实现 (the 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="[email protected]"/>
    <entry key="raskolnikov" value="[email protected]"/>
    <entry key="stavrogin" value="[email protected]"/>
    <entry key="porfiry" value="[email protected]"/>
</util:map>

您还可以使用元素上的属性显式控制Map实例化和填充的确切类型。例如,如果我们真的需要实例化 a,我们可以使用以下配置:'map-class'<util:map/>java.util.TreeMap

<util:map id="emails" map-class="java.util.TreeMap">
    <entry key="pechorin" value="[email protected]"/>
    <entry key="raskolnikov" value="[email protected]"/>
    <entry key="stavrogin" value="[email protected]"/>
    <entry key="porfiry" value="[email protected]"/>
</util:map>

如果未'map-class'提供任何属性,则容器选择一个Map实现。

使用<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>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </set>
    </property>
</bean>

前面的配置使用 SpringFactoryBean实现 (the SetFactoryBean) 来创建一个java.util.Set实例,该实例使用从提供的 中获取的值进行初始化sourceSet

下面的示例使用一个<util:set/>元素进行更简洁的表示:

<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:set>

您还可以使用元素上的属性显式控制Set实例化和填充的确切类型。例如,如果我们真的需要实例化 a,我们可以使用以下配置:set-class<util:set/>java.util.TreeSet

<util:set id="emails" set-class="java.util.TreeSet">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:set>

如果未set-class提供任何属性,则容器选择一个Set实现。

10.1.2. aop架构_

这些aop标签处理在 Spring 中配置所有 AOP,包括 Spring 自己的基于代理的 AOP 框架和 Spring 与 AspectJ AOP 框架的集成。这些标签在标题为Aspect Oriented Programming with 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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->

</beans>

10.1.3. context架构_

context标签处理与管道相关的ApplicationContext配置——也就是说,通常不是对最终用户很重要的 bean,而是在 Spring 中完成大量“grunt”工作的 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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- bean definitions here -->

</beans>
使用<property-placeholder/>

此元素激活占位符的替换,${…​}这些占位符根据指定的属性文件(作为Spring 资源位置)进行解析。此元素是一种便利机制,可为您设置一个PropertySourcesPlaceholderConfigurer。如果您需要对特定设置进行更多控制 PropertySourcesPlaceholderConfigurer,您可以自己将其显式定义为 bean。

使用<annotation-config/>

此元素激活 Spring 基础结构以检测 bean 类中的注释:

  • 弹簧@Configuration型号

  • @Autowired/@Inject , @Value, 和@Lookup

  • JSR-250 的@Resource@PostConstruct@PreDestroy(如果有)

  • JAX-WS@WebServiceRef和 EJB 3 @EJB(如果可用)

  • JPA@PersistenceContext@PersistenceUnit(如果有)

  • Spring的@EventListener

或者,您可以选择BeanPostProcessors 为这些注释显式激活个人。

该元素不会激活 Spring 的 @Transactional注解处理;您可以<tx:annotation-driven/> 为此目的使用该元素。同样,Spring 的 缓存注释也需要显式 启用
使用<component-scan/>

这个元素在基于注解的容器配置一节中有详细说明。

使用<load-time-weaver/>

此元素在 Spring Framework 中使用 AspectJ 进行加载时编织的部分中有详细说明。

使用<spring-configured/>

这个元素在使用 AspectJ 通过 Spring 依赖注入域对象的部分中有详细说明。

使用<mbean-export/>

此元素在有关配置基于注释的 MBean 导出的部分中有详细说明。

10.1.4. Bean 模式

最后但同样重要的是,我们在beans模式中有元素。这些元素自框架诞生之初就存在于 Spring 中。模式中各种元素的示例在beans这里没有显示,因为它们在依赖项和详细配置中得到了相当全面的介绍 (实际上,在整个章节中)。

<bean/>请注意,您可以向XML 定义添加零个或多个键值对。如果有的话,如何使用这些额外的元数据完全取决于您自己的自定义逻辑(因此通常只有在您编写自己的自定义元素时才使用,如题为XML Schema Authoring的附录中所述)。

以下示例显示<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"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://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>
1 这是示例meta元素

在前面的示例中,您可以假设有一些逻辑使用 bean 定义并设置一些使用提供的元数据的缓存基础设施。

10.2. XML 模式创作

从 2.0 版开始,Spring 提供了一种机制,可以将基于模式的扩展添加到基本的 Spring XML 格式以定义和配置 bean。本节介绍如何编写您自己的自定义 XML bean 定义解析器并将此类解析器集成到 Spring IoC 容器中。

为了便于编写使用模式感知 XML 编辑器的配置文件,Spring 的可扩展 XML 配置机制基于 XML Schema。如果您不熟悉标准 Spring 发行版附带的 Spring 当前 XML 配置扩展,您应该首先阅读XML Schemas的上一节。

要创建新的 XML 配置扩展:

  1. 创作一个 XML 模式来描述您的自定义元素。

  2. 代码编写自定义NamespaceHandler实现

  3. 代码编写一个或多个BeanDefinitionParser(这是完成实际工作的地方)。

  4. 使用 Spring注册您的新工件。

对于一个统一的示例,我们创建一个 XML 扩展(自定义 XML 元素),它允许我们配置该类型的对象 SimpleDateFormat(来自java.text包)。完成后,我们将能够定义类型的 bean 定义SimpleDateFormat如下:

<myns:dateformat id="dateFormat"
    pattern="yyyy-MM-dd HH:mm"
    lenient="true"/>

(我们将在本附录后面包含更详细的示例。第一个简单示例的目的是引导您完成制作自定义扩展的基本步骤。)

10.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.example/schema/myns"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:beans="http://www.springframework.org/schema/beans"
        targetNamespace="http://www.mycompany.example/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>
1 指示的行包含所有可识别标签的扩展库(意味着它们具有id我们可以用作容器中的 bean 标识符的属性)。我们可以使用这个属性,因为我们导入了 Spring 提供的 beans命名空间。

上述模式允许我们SimpleDateFormat使用元素直接在 XML 应用程序上下文文件中配置对象<myns:dateformat/>,如以下示例所示:

<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-MM-dd HH:mm"/>
    <property name="lenient" value="true"/>
</bean>

前面两个片段中的第二个在容器中创建了一个带有几个属性集的 bean(由dateFormattype 的名称标识)。SimpleDateFormat

创建配置格式的基于模式的方法允许与具有模式感知 XML 编辑器的 IDE 紧密集成。通过使用正确编写的模式,您可以使用自动完成功能让用户在枚举中定义的多个配置选项之间进行选择。

10.2.2. 编码一个NamespaceHandler

除了模式之外,我们还需要NamespaceHandler解析 Spring 在解析配置文件时遇到的这个特定命名空间的所有元素。对于这个例子, NamespaceHandler应该负责myns:dateformat 元素的解析。

NamespaceHandler接口具有三种方法:

  • init(): 允许NamespaceHandler在使用处理程序之前由 Spring 调用 and 的初始化。

  • BeanDefinition parse(Element, ParserContext):当 Spring 遇到顶级元素(未嵌套在 bean 定义或不同的命名空间内)时调用。此方法本身可以注册 bean 定义、返回 bean 定义或两者兼而有之。

  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext):当 Spring 遇到不同命名空间的属性或嵌套元素时调用。一个或多个 bean 定义的装饰(例如)与 Spring 支持的范围一起使用。我们首先突出一个简单的例子,不使用装饰,然后我们在一个更高级的例子中展示装饰。

尽管您可以为整个命名空间编写自己的代码NamespaceHandler(并因此提供解析命名空间中每个元素的代码),但通常情况下,Spring XML 配置文件中的每个顶级 XML 元素都会导致单个 bean 定义(在我们的例子中,单个<myns:dateformat/> 元素导致单个SimpleDateFormatbean 定义)。Spring 提供了许多支持这种场景的便利类。在以下示例中,我们使用NamespaceHandlerSupport该类:

java
package org.springframework.samples.xml;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class MyNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
    }
}
科特林
package org.springframework.samples.xml

import org.springframework.beans.factory.xml.NamespaceHandlerSupport

class MyNamespaceHandler : NamespaceHandlerSupport {

    override fun init() {
        registerBeanDefinitionParser("dateformat", SimpleDateFormatBeanDefinitionParser())
    }
}

你可能会注意到这个类中实际上并没有很多解析逻辑。事实上,这个NamespaceHandlerSupport类有一个内置的委托概念。它支持注册任意数量的BeanDefinitionParser 实例,当它需要解析其命名空间中的元素时,它会委托给这些实例。这种清晰的关注点分离允许NamespaceHandler处理其名称空间中所有自定义元素的解析编排,同时委派BeanDefinitionParsers执行 XML 解析的繁琐工作。这意味着每个BeanDefinitionParser都只包含解析单个自定义元素的逻辑,正如我们在下一步中看到的那样。

10.2.3. 使用BeanDefinitionParser

BeanDefinitionParser如果NamespaceHandler遇到已映射到特定 bean 定义解析器(dateformat在本例中)的类型的 XML 元素,则使用A。换句话说,BeanDefinitionParser它负责解析模式中定义的一个不同的顶级 XML 元素。在解析器中,我们可以访问 XML 元素(因此也可以访问它的子元素),以便我们可以解析自定义 XML 内容,如下例所示:

java
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.addConstructorArgValue(pattern);

        // this however is an optional property
        String lenient = element.getAttribute("lenient");
        if (StringUtils.hasText(lenient)) {
            bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
        }
    }

}
1 我们使用 Spring-providedAbstractSingleBeanDefinitionParser来处理创建单个BeanDefinition.
2 我们为AbstractSingleBeanDefinitionParser超类提供我们的单人所BeanDefinition代表的类型。
科特林
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

class SimpleDateFormatBeanDefinitionParser : AbstractSingleBeanDefinitionParser() { (1)

    override fun getBeanClass(element: Element): Class<*>? { (2)
        return SimpleDateFormat::class.java
    }

    override fun doParse(element: Element, bean: BeanDefinitionBuilder) {
        // this will never be null since the schema explicitly requires that a value be supplied
        val pattern = element.getAttribute("pattern")
        bean.addConstructorArgValue(pattern)

        // this however is an optional property
        val lenient = element.getAttribute("lenient")
        if (StringUtils.hasText(lenient)) {
            bean.addPropertyValue("lenient", java.lang.Boolean.valueOf(lenient))
        }
    }
}
1 我们使用 Spring-providedAbstractSingleBeanDefinitionParser来处理创建单个BeanDefinition.
2 我们为AbstractSingleBeanDefinitionParser超类提供我们的单人所BeanDefinition代表的类型。

在这个简单的例子中,这就是我们需要做的所有事情。我们的单曲的创建 BeanDefinitionAbstractSingleBeanDefinitionParser超类处理,bean 定义的唯一标识符的提取和设置也是如此。

10.2.4. 注册处理程序和模式

编码完成。剩下要做的就是让 Spring XML 解析基础架构知道我们的自定义元素。我们通过 namespaceHandler在两个特殊用途的属性文件中注册我们的自定义和自定义 XSD 文件来做到这一点。这些属性文件都放置在META-INF应用程序的目录中,例如,可以与 JAR 文件中的二进制类一起分发。Spring XML 解析基础结构通过使用这些特殊的属性文件自动选择您的新扩展,其格式将在接下来的两节中详细介绍。

写作META-INF/spring.handlers

调用的属性文件spring.handlers包含 XML Schema URI 到命名空间处理程序类的映射。对于我们的示例,我们需要编写以下内容:

http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler

(该:字符是 Java 属性格式中的有效分隔符,因此 :URI 中的字符需要使用反斜杠进行转义。)

targetNamespace 键值对的第一部分(键)是与您的自定义命名空间扩展关联的 URI,并且需要与自定义 XSD 架构中指定的属性值完全匹配。

编写'META-INF/spring.schemas'

调用的属性文件spring.schemas包含 XML 模式位置(在使用模式作为xsi:schemaLocation属性的一部分的 XML 文件中与模式声明一起引用)到类路径资源的映射。需要此文件以防止 Spring 绝对必须使用EntityResolver需要 Internet 访问来检索模式文件的默认值。如果您在此属性文件中指定映射,Spring 会在类路径上搜索模式(在本例中, myns.xsdorg.springframework.samples.xml包中)。以下代码段显示了我们需要为自定义模式添加的行:

http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd

(记住该:字符必须被转义。)

鼓励您将 XSD 文件(或多个文件)与类路径中的NamespaceHandler和类一起部署BeanDefinitionParser

10.2.5。在 Spring XML 配置中使用自定义扩展

使用您自己实现的自定义扩展与使用 Spring 提供的“自定义”扩展之一没有什么不同。以下示例<dateformat/>在 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"
    xmlns:myns="http://www.mycompany.example/schema/myns"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.mycompany.example/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>
1 我们的自定义 bean。

10.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.example/schema/component"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.foo.example/schema/component http://www.foo.example/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 方法。这使得Component使用 setter 注入来为类配置 bean 定义变得困难(或者说不可能)。以下清单显示了Component该类:

java
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;
    }
}
科特林
package com.foo

import java.util.ArrayList

class Component {

    var name: String? = null
    private val components = ArrayList<Component>()

    // mmm, there is no setter method for the 'components'
    fun addComponent(component: Component) {
        this.components.add(component)
    }

    fun getComponents(): List<Component> {
        return components
    }
}

此问题的典型解决方案是创建一个自定义FactoryBean来公开该属性的设置器components属性。以下清单显示了这样的自定义 FactoryBean

java
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;
    }
}
科特林
package com.foo

import org.springframework.beans.factory.FactoryBean
import org.springframework.stereotype.Component

class ComponentFactoryBean : FactoryBean<Component> {

    private var parent: Component? = null
    private var children: List<Component>? = null

    fun setParent(parent: Component) {
        this.parent = parent
    }

    fun setChildren(children: List<Component>) {
        this.children = children
    }

    override fun getObject(): Component? {
        if (this.children != null && this.children!!.isNotEmpty()) {
            for (child in children!!) {
                this.parent!!.addComponent(child)
            }
        }
        return this.parent
    }

    override fun getObjectType(): Class<Component>? {
        return Component::class.java
    }

    override fun isSingleton(): Boolean {
        return true
    }
}

这很好用,但它向最终用户暴露了很多 Spring 管道。我们要做的是编写一个自定义扩展来隐藏所有这些 Spring 管道。如果我们坚持前面描述的步骤,我们首先创建 XSD 模式来定义我们的自定义标签的结构,如以下清单所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.example/schema/component"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.foo.example/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

java
package com.foo;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
    }
}
科特林
package com.foo

import org.springframework.beans.factory.xml.NamespaceHandlerSupport

class ComponentNamespaceHandler : NamespaceHandlerSupport() {

    override fun init() {
        registerBeanDefinitionParser("component", ComponentBeanDefinitionParser())
    }
}

接下来是自定义BeanDefinitionParser。请记住,我们正在创建BeanDefinition描述 a 的 a ComponentFactoryBean。以下清单显示了我们的自定义BeanDefinitionParser实现:

java
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);
    }
}
科特林
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

class ComponentBeanDefinitionParser : AbstractBeanDefinitionParser() {

    override fun parseInternal(element: Element, parserContext: ParserContext): AbstractBeanDefinition? {
        return parseComponentElement(element)
    }

    private fun parseComponentElement(element: Element): AbstractBeanDefinition {
        val factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean::class.java)
        factory.addPropertyValue("parent", parseComponent(element))

        val childElements = DomUtils.getChildElementsByTagName(element, "component")
        if (childElements != null && childElements.size > 0) {
            parseChildComponents(childElements, factory)
        }

        return factory.getBeanDefinition()
    }

    private fun parseComponent(element: Element): BeanDefinition {
        val component = BeanDefinitionBuilder.rootBeanDefinition(Component::class.java)
        component.addPropertyValue("name", element.getAttribute("name"))
        return component.beanDefinition
    }

    private fun parseChildComponents(childElements: List<Element>, factory: BeanDefinitionBuilder) {
        val children = ManagedList<BeanDefinition>(childElements.size)
        for (element in childElements) {
            children.add(parseComponentElement(element))
        }
        factory.addPropertyValue("children", children)
    }
}

最后,需要通过修改META-INF/spring.handlersMETA-INF/spring.schemas文件将各种工件注册到 Spring XML 基础架构,如下所示:

# 在 'META-INF/spring.handlers'
http\://www.foo.example/schema/component=com.foo.ComponentNamespaceHandler
# 在 'META-INF/spring.schemas'
http\://www.foo.example/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>

然后我们可以在解析属性BeanDefinition时 创建另一个。'jcache:cache-name'然后这BeanDefinition会为我们初始化命名的 JCache。我们还可以修改现有BeanDefinition的 , 'checkingAccountService'以便它依赖于这个新的 JCache-initializing BeanDefinition。以下清单显示了我们的JCacheInitializer

java
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...
    }
}
科特林
package com.foo

class JCacheInitializer(private val name: String) {

    fun 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.example/schema/jcache"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.foo.example/schema/jcache"
        elementFormDefault="qualified">

    <xsd:attribute name="cache-name" type="xsd:string"/>

</xsd:schema>

接下来,我们需要创建关联的NamespaceHandler,如下:

java
package com.foo;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class JCacheNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        super.registerBeanDefinitionDecoratorForAttribute("cache-name",
            new JCacheInitializingBeanDefinitionDecorator());
    }

}
科特林
package com.foo

import org.springframework.beans.factory.xml.NamespaceHandlerSupport

class JCacheNamespaceHandler : NamespaceHandlerSupport() {

    override fun init() {
        super.registerBeanDefinitionDecoratorForAttribute("cache-name",
                JCacheInitializingBeanDefinitionDecorator())
    }

}

接下来,我们需要创建解析器。请注意,在这种情况下,因为我们要解析 XML 属性,所以我们写的是 aBeanDefinitionDecorator而不是 a BeanDefinitionParser。以下清单显示了我们的BeanDefinitionDecorator实现:

java
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;
    }
}
科特林
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

class JCacheInitializingBeanDefinitionDecorator : BeanDefinitionDecorator {

    override fun decorate(source: Node, holder: BeanDefinitionHolder,
                        ctx: ParserContext): BeanDefinitionHolder {
        val initializerBeanName = registerJCacheInitializer(source, ctx)
        createDependencyOnJCacheInitializer(holder, initializerBeanName)
        return holder
    }

    private fun createDependencyOnJCacheInitializer(holder: BeanDefinitionHolder,
                                                    initializerBeanName: String) {
        val definition = holder.beanDefinition as AbstractBeanDefinition
        var dependsOn = definition.dependsOn
        dependsOn = if (dependsOn == null) {
            arrayOf(initializerBeanName)
        } else {
            val dependencies = ArrayList(listOf(*dependsOn))
            dependencies.add(initializerBeanName)
            dependencies.toTypedArray()
        }
        definition.setDependsOn(*dependsOn)
    }

    private fun registerJCacheInitializer(source: Node, ctx: ParserContext): String {
        val cacheName = (source as Attr).value
        val beanName = "$cacheName-initializer"
        if (!ctx.registry.containsBeanDefinition(beanName)) {
            val initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer::class.java)
            initializer.addConstructorArg(cacheName)
            ctx.registry.registerBeanDefinition(beanName, initializer.getBeanDefinition())
        }
        return beanName
    }
}

META-INF/spring.handlers最后,我们需要通过修改和文件将各种工件注册到 Spring XML 基础架构中META-INF/spring.schemas,如下所示:

# 在 'META-INF/spring.handlers'
http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler
# 在 'META-INF/spring.schemas'
http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd

10.3. 应用程序启动步骤

附录的这一部分列出了StartupSteps核心容器被检测的现有内容。

每个启动步骤的名称和详细信息不是公共合同的一部分,可能会发生变化;这被认为是核心容器的实现细节,并将跟随其行为变化。
表 15. 核心容器中定义的应用程序启动步骤
姓名 描述 标签

spring.beans.instantiate

bean 及其依赖项的实例化。

beanNamebean 的名称,beanType注入点所需的类型。

spring.beans.smart-initialize

SmartInitializingSingletonbean的初始化。

beanNamebean 的名称。

spring.context.annotated-bean-reader.create

的创建AnnotatedBeanDefinitionReader

spring.context.base-packages.scan

扫描基础包。

packages用于扫描的基本包数组。

spring.context.beans.post-process

bean 后处理阶段。

spring.context.bean-factory.post-process

调用BeanFactoryPostProcessorbean。

postProcessor当前的后处理器。

spring.context.beandef-registry.post-process

调用BeanDefinitionRegistryPostProcessorbean。

postProcessor当前的后处理器。

spring.context.component-classes.register

通过 注册组件类AnnotationConfigApplicationContext#register

classes用于注册的给定类的数组。

spring.context.config-classes.enhance

使用 CGLIB 代理增强配置类。

classCount增强类的计数。

spring.context.config-classes.parse

配置类解析阶段使用ConfigurationClassPostProcessor.

classCount已处理类的计数。

spring.context.refresh

应用程序上下文刷新阶段。


1. see XML Configuration