本部分文档涵盖了 Spring Framework 框架中必不可少的所有技术

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

还提供了 Spring 与 AspectJ 的集成(目前,在功能上最丰富) 以及 Java 企业领域中最成熟的 AOP 实现) .

1. IOC 容器

本章介绍了 Spring 的 IoC 容器.

1.1. Spring IoC 容器和 bean 的介绍

本章介绍 Spring 框架中控制反转 IOC 的实现. IOC 与大家熟知的依赖注入同理,这是一个通过依赖注入对象的过程 也就是说,依赖的对象通过构造参数、工厂方法参数或者属性注入, 创建 bean 后容器注入这些依赖对象. 也就是 domain 对象.创建过程相对于普通创建对象的过程是反向的(因此称之为 IoC) ,它通过直接使用构造类来控制实例化, 或者定义它们之间的依赖,或者类似于服务定位模式的一种机制.

org.springframework.beansorg.springframework.context 包是实现 Spring IOC 容器的基础 . BeanFactory 接口提供了一种更先进的配置机制来管理任意类型的对象. ApplicationContextBeanFactory 的子接口. 他提供了:

  • 更容易与 Spring 的 AOP 特性集成

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

  • 事件发布

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

简而言之, BeanFactory 提供了配置框架的基本功能,ApplicationContext 添加了更多特定于企业的功能. ApplicationContext 完全扩展了 BeanFactory 的功能,这些内容将在介绍 Spring IoC 容器的专门章节讲解. 有关使用 BeanFactory 代替 ApplicationContext, 的更多信息,请参考 BeanFactory API.

在 Spring 中,由 Spring IOC 容器管理的,构成程序的骨架的对象称为 Bean. bean 对象是指经过 IoC 容器实例化,组装和管理的对象. 此外,bean 就是应用程序中众多对象之一 . bean 和 bean 的依赖是由容器所使用的配置元数据反射而来.

1.2. 容器概述

org.springframework.context.ApplicationContext 是 Spring IoC 容器实现的代表,它负责实例化,配置和组装 Bean. 容器通过读取配置元数据获取有关实例化、配置和组装哪些对象的说明 . 配置元数据可以使用 XML、Java 注解或 Java 代码来呈现. 它允许你处理应用程序的对象与其他对象之间的互相依赖.

Spring 提供了 ApplicationContext 接口的几个实现. 在独立应用程序中,通常创建 ClassPathXmlApplicationContextFileSystemXmlApplicationContext 的实例. 虽然 XML 一直是定义配置元数据的传统格式, 但是您可以指定容器使用 Java 注解或编程的方式编写元数据格式,并通过提供少量的 XML 配置以声明对某些额外元数据的支持.

在大多数应用场景中,不需要用户显式的编写代码来实例化 IOC 容器的一个或者多个实例. 例如,在 Web 应用场景中,只需要在 web.xml 中添加大概 8 行简单的 web 描述样板就行了(see 快速对 Web 应用的 ApplicationContext 实例化). 如果你使用的是基于 Eclipse 的 Spring Tools for Eclipse 开发环境,该样板配置只需点击几下鼠标或按几下键盘就能创建了.

下图展示了 Spring 工作方式的高级视图,应用程序的类与元数据配置相互配合,这样,在 ApplicationContext 创建和初始化后,你立即拥有一个可配置的,可执行的系统或应用程序.

  1. IOC容器

container magic

1.2.1. 配置元数据

如上图所示,Spring IOC 容器使用元数据配置这种形式,这个配置元数据表示了应用开发人员告诉 Spring 容器以何种方式实例化、配置和组装应用程序中的对象.

配置元数据通常以简单、直观的 XML 格式提供,本章的大部分内容都使用这种格式来说明 Spring IoC 容器的关键概念和特性.

XML 并不是配置元数据的唯一方式,Spring IoC 容器本身是完全与元数据配置的实际分离的. 现在,许多开发人员选择 基于 Java 配置 来开发应用程序.

更多其他格式的元数据见:

Spring 的 Bean (至少一个) 由容器来管理,基于 XML 的元数据配置将这些 bean 配置为 <beans/> 元素.并放置于 <beans/> 元素内部. 基于 Java 的配置通常是使用 @Configuration 注解过的类中,在它的方法上使用 @Bean 注解.

这些 bean 定义会对应到构成应用程序的实际对象. 通常你会定义服务层对象,数据访问对象(DAOs) ,表示对象(如Struts Action 的实例),基础对象(如 Hibernate 的 SessionFactories, JMS Queues,) . 通常,不会在容器中配置细粒度的 domain 对象,因为创建和加载 domain 对象通常是 DAO 和业务逻辑的职责. 但是,你可以使用 Spring 与 AspectJ 集成独立于 IoC 容器来创建的对象,请参阅 Spring 使用 AspectJ 进行依赖注入 domain 对象.

下面的示例显示了基于 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");
Kotlin
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")

当你了解 Spring IoC 容器之后,你可能想知道更多关于 Spring 的 Resource (如 资源(Resources) 中描述的). 它提供了一种简便的方法来通过 InputStream 读取 URI 定义的位置 ,资源路径被用于构建应用程序上下文的应用环境和资源路径, 如 应用上下文和资源路径 描述

下面的例子显示了服务层对象 (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定义的名称. idref 元素之间的这种联系表达了组合对象之间的相互依赖. 有关对象间的依赖,请参阅 依赖.

组合基于 XML 的元数据配置

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

上面的例子中,使用了3个文件: services.xml, messageSource.xml, 和 themeSource.xml 来加载外部Bean的定义. 导入文件采用的都是相对路径,因此 services.xml 必须和导入文件位于同一目录或类路径中, 而 messageSource.xmlthemeSource.xml 必须在导入文件的资源位置中. 正如你所看到的,前面的斜线将会被忽略,但考虑到这些路径是相对的,最佳的使用是不用斜线的. 这个XML文件的内容都会被导入,包括顶级的 <beans/> 元素, 但必须遵循 Spring Schema 定义 XML bean 定义的规则.

这种相对路径的配置是可行的,但不推荐这样做. 在使用 "../" 引用目录时,这样做会对当前应用程序之外的文件产生依赖. 特别是对于 classpath: URLs (例如, classpath:../services.xml), ,不建议使用此引用方式,因为在该引用方式中,运行时解析过程选择 “最近的” classpath 根目录,然后查看其父目录. 类路径的变化或者选择了不正确的目录都会导致此配置不可用.

您可以使用完全限定的资源位置而不是相对路径:例如, file:C:/config/services.xmlclasspath:/config/services.xml. 但是,请注意,您正在将应用程序的配置与特定的绝对位置耦合. 通常会选取间接的方式应对这种绝对路径,例如使用占位符 "${…}" 来解决对JVM系统属性的引用.

import 是由 bean 命名空间本身提供的功能. 在 Spring 提供的 XML 命名空间中,如 contextutil 命名空间,可以用于对普通 bean 定义进行更高级的功能配置.

DSL 定义Groovy Bean

作为从外部配置元数据的另一个示例, bean 定义也可以使用 Spring 的 Groovy DSL 来定义. Grails 框架有此配置实例,通常, 可以在具有以下结构的 ".groovy" 文件中配置 bean 定义. 例如:

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

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

1.2.3. 使用容器

ApplicationContext 是能够创建 bean 定义以及处理相互依赖的高级工厂接口,使用方法 T getBean(String name, Class<T> requiredType), 获取容器实例.

ApplicationContext 可以读取 bean 定义并访问它们 如下:

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();
Kotlin
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 定义) 如下:

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

最灵活的实现是 GenericApplicationContext , 例如读取 XML 文件的 XmlBeanDefinitionReader 如下面的示例所示:

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

您还可以为 Groovy 文件使用 GroovyBeanDefinitionReader 如下面的示例所示:

Java
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();
Kotlin
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. Bean 的概述

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

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

  • 限定包类名称: 通常,定义的 bean 的实际实现类.

  • bean 行为配置元素, 定义 Bean 的行为约束(例如作用域,生命周期回调等等)

  • bean 需要引用其他 bean 来完成工作. 这些引用也称为协作或依赖.

  • 其他配置用于新对象的创建,例如使用 bean 的数量来管理连接池,或者限制池的大小.

以下是每个 bean 定义的属性:

Table 1. Bean的定义
属性 对应章节介绍…​

Class

实例化 Bean

Name

Bean 的命名

Scope

Bean 的作用域

Constructor arguments

依赖注入

Properties

依赖注入

Autowiring mode

自动装配

Lazy initialization mode

懒加载 Bean

Initialization method

初始化方法回调

Destruction method

销毁方法的回调

除了 bean 定义包含如何创建特定的 bean 的信息外, ApplicationContext 实现还允许用户在容器中注册现有的、已创建的对象. 这是通过 getBeanFactory() 方法访问 ApplicationContextBeanFactory 来完成的, 该方法返回 BeanFactory DefaultListableBeanFactory 实现. DefaultListableBeanFactory 支持通过 registerSingleton(..)registerBeanDefinition(..) 方法来注册对象. 然而,典型的应用程序只能通过元数据配置来定义 bean.

为了让容器正确推断它们在自动装配和其它内置步骤,需要尽早注册 Bean 的元数据和手动使用单例的实例. 虽然覆盖现有的元数据和现有的单例实例在某种程度上是支持的, 但是新 bean 在运行时(同时访问动态工厂) 注册官方并不支持,可能会导致并发访问异常、bean 容器中的不一致状态,或者两者兼有.

1.3.1. Bean 的命名

每个 bean 都有一个或多个标识符,这些标识符在容器托管时必须是唯一的. bean 通常只有一个标识符,但如果需要多个标识符时,可以考虑使用别名.

在基于 XML 的配置中,开发者可以使用 id 属性, name 属性, 或两者都指定 bean 的标识符 id 属性 允许您指定一个 id. 通常这些名字使用字母和数字的组合('myBean', 'someService', 等.), 但也可以包含特殊字符. 如果你想使用 bean 别名,您可以在 name 属性上定义,使用逗号 (,), 分好 (;), 或空白符. 由于历史因素, 请注意,在 Spring 3.1 之前的版本中, id 属性被定义为 xsd:ID 类型, 它会限制某些字符. 从 3.1 开始,它被定义为 xsd:string 类型. 请注意,由于 bean id 的唯一性,他仍然由容器执行,不再由 XML 解析器执行.

您也无需提供 bean 的 nameid 如果没有显式地提供 nameid 容器会给 bean 生成唯一的名称. 然而,如果你想引用 bean 的名字,可以使用 ref 元素或使用 Service Locator 来进行查找(此时必须提供名称) . 不使用名称的情况有: 内部 bean自动装配.

Bean 的命名约定

bean 的命名是按照标准的 Java 字段名称命名来进行的. 也就是说,bean 名称开始需要以小写字母开头,后面采用 "驼峰式" 的方法. 例如 accountManager, accountService, userDao, loginController.

一致的 beans 命名能够让配置更方便阅读和理解,如果你正在使用 Spring AOP,当你通过 bean 名称应用到通知时,这种命名方式会有很大的帮助.

在类路径中进行组件扫描时, Spring 会根据上面的规则为未命名的组件生成 bean 名称,规则是: 采用简单的类名,并将其初始字符转化为小写字母. 然而,在特殊情况下,当有一个以上的字符,同时第一个和第二个字符都是大写时,原来的规则仍然应该保留. 这些规则与 Java 中定义实例的相同. 例如 Spring 使用的 java.beans.Introspector.decapitalize 类.
定义外部 Bean 的别名

在对 bean 定义时,除了使用 id 属性指定唯一的名称外,还可以提供多个别名,这需要通过 name 属性指定. 所有这个名称都会指向同一个 bean,在某些情况下提供别名非常有用,例如为了让应用每一个组件都能更容易的对公共组件进行引用.

然而,在定义 bean 时就指定所有的别名并不是很恰当的. 有时期望能够在当前位置为那些在别处定义的 bean 引入别名. 在 XML 配置文件中, 可以通过 <alias/> 元素来定义 bean 别名,例如:

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

上面示例中,在同一个容器中名为 fromName 的 bean 定义,在增加别名定义后,也可以使用 toName 来引用. .

例如,在子系统 A 中通过名字 subsystemA-dataSource 配置的数据源. 在子系统 B 中可能通过名字 subsystemB-dataSource 来引用. .当两个子系统构成主应用的时候,主应用可能通过名字 myApp-dataSource 引用数据源,将全部三个名字引用同一个对象,你可以将下面的别名定义添加到应用配置中:

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

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

Java 配置

如果你使用 Javaconfiguration, @Bean 可以用来提供别名,详情见 使用 @Bean 注解

1.3.2. 实例化 Bean

bean 定义基本上就是用来创建一个或多个对象的配置,当需要 bean 的时候,容器会查找配置并且根据 bean 定义封装的元数据来创建(或获取) 实际对象.

如果你使用基于 XML 的配置,那么可以在 <bean/> 元素中通过 class 属性来指定对象类型. class 属性实际上就是 BeanDefinition 实例中的 class 属性. 他通常是必需的(一些例外情况,通过实例工厂方法实例化Bean 继承的定义). 有两种方式使用 Class 属性

  • 通常情况下,会直接通过反射调用构造方法来创建 bean,这种方式与 Java 代码的 new 创建相似.

  • 通过静态工厂方法创建,类中包含静态方法. 通过调用静态方法返回对象的类型可能和 Class 一样,也可能完全不一样.

内部类的名

如果你想配置静态内部类,那么必须使用内部类的二进制名称.

例如,在 com.example 包下 有一个名为 SomeThing 的类, 这个类里面有个静态内部类 OtherThing,他们可以以 ($) 或 (.) 作为分隔符. 这种情况下 bean 定义的 class 属性应该写作 com.example.SomeThing$OtherThingcom.example.SomeThing.OtherThing.

使用 $ 字符来分隔外部类和内部类的名称

通过构造器实例化

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

Spring IoC 容器几乎可以管理您希望它管理的任何类. 它不仅限于管理真正的 JavaBeans. 大多数 Spring 用户更喜欢管理那些只有一个默认构造函数(无参数) 和有合适的 setter 和 getter 方法的真实的 JavaBeans,还可以在容器中放置更多的外部非 bean 形式(non-bean-style)类,例如: 如果需要使用一个绝对违反 JavaBean 规范的遗留连接池时 Spring 也是可以管理它的.

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

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

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

给构造方法指定参数以及为 bean 实例化设置属性将在后面的 依赖注入 中说明.

通过静态工厂方法实例化

当采用静态工厂方法创建 bean 时,除了需要指定 class 属性外,还需要通过 factory-method 属性来指定创建 bean 实例的工厂方法. Spring 将会调用此方法(其可选参数接下来会介绍) 返回实例对象. 从这样看来,它与通过普通构造器创建类实例没什么两样.

下面的 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;
    }
}
Kotlin
class ClientService private constructor() {
    companion object {
        private val clientService = ClientService()
        @JvmStatic
        fun createInstance() = clientService
    }
}

给工厂方法指定参数以及为 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 类:

Java
public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}
Kotlin
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 类:

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;
    }
}
Kotlin
class DefaultServiceLocator {
    companion object {
        private val clientService = ClientServiceImpl()
        private val accountService = AccountServiceImpl()
    }

    fun createClientServiceInstance(): ClientService {
        return clientService
    }

    fun createAccountServiceInstance(): AccountService {
        return accountService
    }
}

这种方法表明可以通过依赖注入(DI) 来管理和配置工厂 bean 本身. 请参阅详细信息中的 依赖和配置细节.

在 Spring 文档中, "factory 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 到让多个 Bean 协同工作.

1.4.1. 依赖注入

依赖注入 (DI) 是让对象只通过构造参数、工厂方法的参数或者配置的属性来定义他们的依赖的过程. 这些依赖也是其他对象所需要协同工作的对象, 容器会在创建 Bean 的时候注入这些依赖. 整个过程完全反转了由 Bean 自己控制实例化或者依赖引用,所以这个过程也称之为 "控制反转"

当使用了依赖注入的特性以后,会让开发者更容易管理和解耦对象之间的依赖,使代码变得更加简单. 对象之间不再关注依赖,也不需要知道依赖类的位置. 如此一来,开发的类更易于测试 尤其是当开发者的依赖是接口或者抽象类的情况时,开发者可以轻易地在单元测试中 mock 对象.

依赖注入主要使用两种方式: 基于构造函数的注入 and 基于 Setter 方法的依赖注入.

基于构造函数的注入

基于构造函数的依赖注入是由 IoC 容器来调用类的构造函数,构造函数的参数代表这个 Bean 所依赖的对象. 构造函数的依赖注入与调用带参数的静态工厂方法基本一样. 调用具有特定参数的静态工厂方法来构造 bean 几乎是等效的,本讨论同样处理构造函数和静态工厂方法的参数. 下面的例子展示了一个通过构造函数来实现依赖注入的类. :

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...
}
Kotlin
// 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 的构造函数参数不存在歧义,那么构造器参数的顺序也就是就是这些参数实例化以及装载的顺序. 参考如下代码:

Java
public class ThingOne {

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

假设 ThingTwoThingThree 不存在继承关系 也没有什么歧义. 下面的配置完全可以工作正常. 开发者无需再到 <constructor-arg/> 元素中指定构造函数参数的 indextype

<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 IoC 容器无法判断值的类型,所以也是无法匹配的,考虑代码:

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

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

index 从 0 开始.

构造函数参数名称

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

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

需要注意的是,解析这个配置的代码必须启用了 debug 来编译,这样 Spring 才可以从构造函数查找参数名称. 开发者也可以使用 @ConstructorProperties 注解来显式声明构造函数的名称. 例如下面代码:

Java
public class ExampleBean {

    // Fields omitted

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}
Kotlin
class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)
基于 Setter 方法的依赖注入

基于 setter 函数的依赖注入是让容器调用 Bean 的无参构造函数,或者无参静态工厂方法,然后再来调用 setter 方法来实现依赖注入.

下面的例子展示了使用 setter 方法进行的依赖注入的过程. 其中类对象只是简单的 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...
}
Kotlin
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...
}

ApplicationContext 所管理 Bean 同时支持基于构造函数和基于 setter 方法的依赖注入,同时也支持使用 setter 方法在通过构造函数注入依赖之后再次注入依赖. 开发者在 BeanDefinition 中可以使用 PropertyEditor 实例来自由选择注入方式. 然而,大多数的开发者并不直接使用这些类,而是更喜欢使用 XML 配置来进行 bean 定义, 或者基于注解的组件 (例如使用 @Component, @Controller), 或者在配置了 @Configuration 类中使用 @Bean 的方法. 然后,这些会在 Spring 内部转换为 BeanDefinition 实例,并用于加载整个 Spring IoC 容器实例.

如何选择基于构造器和基于 setter 方法?

因为开发者可以混用两种依赖注入方式,两种方式用于处理不同的情况: 必要的依赖通常通过构造函数注入,而可选的依赖则通过 setter 方法注入. 其中,在 setter 方法上添加 @Required 注解可用于构造必要的依赖. 但是,最好使用带有参数验证的构造函数注入.

Spring 团队推荐使用基于构造函数的注入,因为这种方式会促使开发者将组件开发成不可变对象并且确保注入的依赖不为 null. 另外,基于构造函数的注入的组件被客户端调用的时候也已经是完全构造好的 . 当然,从另一方面来说,过多的构造函数参数也是非常糟糕的代码方式,这种方式说明类附带了太多的功能,最好重构将不同职能分离.

基于 setter 的注入只用于可选的依赖,但是也最好配置一些合理的默认值. 否则,只能对代码的依赖进行非 null 值检查了. 基于 setter 方法的注入有一个便利之处是: 对象可以重新配置和重新注入. 因此,使用 setter 注入管理 JMX MBeans 是很方便的

依赖注入的两种风格适合大多数的情况,但是在使用第三方库的时候,开发者可能并没有源码,那么就只能使用基于构造函数的依赖注入了.

决定依赖的过程

容器解析 Bean 的过程如下:

  • 创建并根据描述的元数据来实例化 ApplicationContext 元数据配置可以是 XML 文件、Java 代码或者注解.

  • 每一个 Bean 的依赖都通过构造函数参数或属性,或者静态工厂方法的参数等等来表示. 这些依赖会在 Bean 创建的时候装载和注入

  • 每一个属性或者构造函数的参数都是真实定义的值或者引用容器其他的 Bean.

  • 每一个属性或者构造参数可以根据指定的类型转换为所需的类型. Spring 也可以将 String 转成默认的 Java 内置类型. 例如 int,long, String, boolean,等.

Spring 容器会在容器创建的时候针对每一个 Bean 进行校验. 但是 Bean 的属性在 Bean 没有真正创建之前是不会进行配置的,单例类型的 Bean 是容器创建的时候配置成预实例状态的. Bean 的作用域 后面再说, 其他的 Bean 都只有在请求的时候,才会创建,显然创建 Bean 对象会有一个依赖顺序图,这个图表示 Bean 之间的依赖. 容器根据此来决定创建和配置 Bean 的顺序.

循环依赖

如果开发者主要使用基于构造函数的依赖注入,那么很有可能出现循环依赖的情况.

例如: 类 A 在构造函数中依赖于类 B 的实例,而类 B 的构造函数又依赖类 A 的实例. 如果这样配置类 A 和类 B 相互注入的话,Spring IoC 容器会发现这个运行时的循环依赖, 并且抛出 BeanCurrentlyInCreationException 异常.

开发者可以选择 setter 方法来配置依赖注入,这样就不会出现循环依赖的情况. 或者根本就不使用基于构造函数的依赖注入,而仅仅使用基于 setter 方法的依赖注入. 换言之,但是开发者可以将循环依赖配置为基于 Setter 方法的依赖注入(尽管不推荐这样做)

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

你可以信任 Spring 做正确的事. 它在容器加载时检测配置问题,例如对不存在的 bean 和循环依赖的引用. 当实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖. 这也意味着 Spring 容器加载正确后会在 bean 注入依赖出错的时候抛出异常. 例如,bean 抛出缺少属性或者属性不合法的异常 ,这种延迟的解析也是 ApplicationContext 的实现会令单例 Bean 处于预实例化状态的原因. 这样,通过创建 bean,可以在真正使用 bean 之前消耗一些内存代价而发现配置的问题 . 开发者也可以覆盖默认的行为让单例 bean 延迟加载,而不总是处于预实例化状态.

如果不存在循环依赖的话,bean 所引用的依赖会预先全部构造. 举例来说,如果 bean A 依赖于 bean B,那么 Spring IoC 容器会先配置 bean B,然后调用 bean A 的 setter 方法来构造 bean A. 换言之,bean 先会实例化,然后再注入依赖,最后才是相关生命周期方法的调用(就像 配置文件的 init 方法 或者InitializingBean 的回调函数) .

依赖注入的例子

下面的例子使用基于 XML 的元数据配置,然后使用 setter 方式进行依赖注入. 下面是 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;
    }
}
Kotlin
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;
    }
}
Kotlin
class ExampleBean(
        private val beanOne: AnotherBean,
        private val beanTwo: YetAnotherBean,
        private val i: Int)

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

现在考虑这个示例的情况,其中,不使用构造函数,而是告诉 Spring 调用静态工厂方法来返回对象的实例:

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

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

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

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

静态工厂方法的参数由 <constructor-arg/> 元素提供,与实际使用的构造函数完全相同. 工厂方法返回类的类型不必与包含静态工厂方法 的类完全相同, 尽管在本例中是这样. 实例(非静态) 工厂方法的使用方式也是相似的(除了使用 factory-bean 属性而不是 class 属性. 因此此处不在展开讨论.

1.4.2. 依赖和配置细节

如上一节所述, 您可以将 bean 的属性和构造函数参数定义为对其他 bean 的引用,或者作为其内联定义的值. Spring 可以允许您在基于 XML 的配置元数据(定义 Bean) 中使用子元素 <property/><constructor-arg/> 来达到这种目的.

直接值(基本类型,String 等等)

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

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

以下示例使用 p-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(例如 IntelliJIDEA 或者 Spring 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 的容器会将 <value/> 里面的文本通过 JavaBean 的 PropertyEditor 机制转换成 java.util.Properties 实例, 这种嵌套 <value/> 元素的快捷方式也是 Spring 团队推荐使用的.

idref 元素

idref 元素只是一种防错方法,可以将容器中另一个 bean 的 id (字符串值 - 而不是引用) 传递给 <constructor-arg/><property/> 元素.

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

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

前面的 bean 定义代码段运行时与以下代码段完全等效:

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

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

Spring 团队更推荐第一种方式,因为使用了 idref 标签,它会让容器在部署阶段就对 bean 进行校验,以确保 bean 一定存在. 而使用第二种方式的话,是没有任何校验的. 只有实际上引用了 client bean 的 targetName 属性 不对其值进行校验. 在实例化 client 的时候才会被发现. 如果 clientprototype 类型的 Bean 的话,那么类似拼写之类的错误会在容器部署以后很久才能发现.

idref 元素的 local 属性 属性在 Spring 4.0 以后的 xsd 中已经不再支持了,而是使用了 bean 引用. 如果更新了版本的话,只要将 idref local 引用都转换成 idref bean 即可.

在 Spring 2.0 之前的版本中,<idref/>ProxyFactoryBean bean定义中的 AOP interceptors 的配置中 常见,指定拦截器名称时使用 <idref/> 元素可防止您拼写错误的拦截器 ID.

引用其他的 Bean(装配)

ref 元素是 <constructor-arg/> or <property/> 定义元素中的最后一个元素. 你可以通过这个标签配置一个 bean 来引用另一个 bean. 当需要引用一个 bean 的时候,被引用的 bean 会先实例化, 然后配置属性,也就是引用的依赖. 如果该 bean 是单例 bean 的话 ,那么该 bean 会早由容器初始化. 最终会引用另一个对象的所有引用,bean 的作用域以及校验取决于你是否有通过 bean, 或 parent 这些属性来指定对象的 id 或者 name 属性. .

通过指定 bean 属性中的 <ref/> 元素来指定依赖是最常见的一种方式,可以引用容器或者父容器中的 bean,不在同一个 XML 文件定义也可以引用. 其中 bean 属性中的值可以和其他引用 bean 中的 id 属性一致,或者和其中的某个 name 属性一致,以下示例显示如何使用 ref 元素:

<ref bean="someBean"/>

通过指定 bean 的 parent 属性可以创建一个引用到当前容器的父容器之中. parent 属性的值可以与目标 bean 的 id 属性一致,或者和目标 bean 的 name 属性中的某个一致,目标 bean 必须是当前引用目标 bean 容器的父容器 . 开发者一般只有在具有层次化容器,并且希望通过代理来包裹父容器中一个存在的 bean 的时候才会用到这个属性. 以下一对列表显示了如何使用 parent 属性:

<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
</bean>
ref 元素中的 local 标签在 xsd 4.0,以后已经不再支持了,开发者可以通过将已存在的 ref local 改为 ref bean 来完成 Spring 版本升级.
内部 bean

定义在 <bean/> 元素的 <property/> 或者 <constructor-arg/> 元素之内的 bean 叫做内部 bean,如下例所示:

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

内部 bean 定义不需要定义的 ID 或名称. 如果指定,则容器不使用此类值作为标识符. 容器还会在创建时忽略 scope 标签,因为内部 bean 始终是匿名的,并且始终使用外部 bean 创建. 开发者是无法将内部 bean 注入到外部 bean 以外的其他 bean 中的.

作为一个极端情况,可以从自定义作用域接收销毁回调,例如: 请求作用域的内部 bean 包含了单例 bean,那么内部 bean 实例会绑定到包含的 bean,而包含的 bean 允许访问 request 的 scope 生命周期. 这种场景并不常见,内部 bean 通常只是供给它的外部 bean 使用.

集合

<list/>, <set/>, <map/>, 和 <props/> 元素中,您可以分别配置 Java Collection 类型 List, Set, Map, 和 Properties 的属性和参数. 以下示例显示了如何使用它们:

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

当然,map 的 key 或者 value,或者集合的 value 都可以配置为下列元素之一:

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

Spring 的容器也支持集合合并,开发者可以定义父样式的 <list/>, <map/>, <set/><props/> 元素, 同时有子样式的 <list/>, <map/>, <set/><props/> 元素. 也就是说,子集合的值是父元素和子元素集合的合并值.

有关合并的这一节讨论父子 bean 机制,不熟悉父和子 bean 定义的读者可能希望在继续之前阅读相关部分

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

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

请注意,在 child bean 定义的 adminEmails 中的 <props/> 使用 merge=true 属性. 当容器解析并实例化 child bean时, 生成的实例有一个 adminEmails 属性集合, 其实例中包含的 adminEmails 集合就是child的 adminEmails 以及 parent 的 adminEmails 集合. 以下清单显示了结果:

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

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

这个合并的行为和 <list/>, <map/>, 和 <set/> 之类的集合类型的行为是类似的. <list/> 在特定例子中,与 List 集合类型类似, 有着隐含的 ordered 概念. 所有的父元素里面的值,是在所有子元素的值之前配置的. 但是像 Map, Set, 和 Properties 的集合类型,是不存在顺序的.

集合合并的限制

您不能合并不同类型的集合(例如要将 MapList 合并是不可能的) . 如果开发者硬要这样做就会抛出异常, merge 的属性是必须特指到更低级或者继承的子节点定义上, 特指 merge 属性到父集合的定义上是冗余的,而且在合并上也没有任何效果.

强类型的集合

感谢 Java 对泛型类型的支持. 也就是,开发者可以声明 Collection 类型,然后这个集合只包含 String 元素(举例来说) . 如果开发者通过 Spring 来注入强类型的 Collection 到 bean 中,开发者就可以利用 Spring 的类型转换支持来做到 以下 Java 类和 bean 定义显示了如何执行此操作:

Java
public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
Kotlin
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>

something 的属性 accounts 准备注入的时候,accounts 的泛型信息 MapMap<String, Float> 就会通过反射拿到. 这样 Spring 的类型转换系统能够识别不同的类型,如上面的例子 Float 然后会将字符串的值 9.99, 2.75, 和 3.99 转换成对应的 Float 类型.

Null 和 空字符串

Strings 将属性的空参数视为空字符串. 下面基于XML的元数据配置就会将 email 属性配置 String 值("").

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

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

Java
exampleBean.setEmail("");
Kotlin
exampleBean.email = ""

<null/> 将被处理为 null 值. 以下清单显示了一个示例:

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

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

Java
exampleBean.setEmail(null);
Kotlin
exampleBean.email = null
使用 p 命名空间简化 XML 配置

p 命名空间让开发者可以使用 bean 的属性,而不必使用嵌套的 <property/> 元素.

Spring 是支持基于 XML 的格式化 命名空间扩展的. 本节讨论的 beans 配置都是基于 XML 的,p 命名空间是定义在 Spring Core 中的(不是在 XSD 文件) .

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

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

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

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

上面的例子在 bean 中定义了 email 的属性. 这种定义告知 Spring 这是一个属性声明. 如前面所描述的,p 命名空间并没有标准的定义模式,所以开发者可以将属性的名称配置为依赖名称.

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

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

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

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

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

此示例不仅包含使用 p 命名空间的属性值,还使用特殊格式来声明属性引用. 第一个 bean 定义使用 <property name="spouse" ref="jane"/> 来创建从 bean john 到 bean jane 的引用, 而第二个 bean 定义使用 p:spouse-ref="jane" 来作为指向 bean 的引用. 在这个例子中 spouse 是属性的名字,而 -ref 部分表名这个依赖不是直接的类型,而是引用另一个 bean.

p 命名空间并不如标准XML格式灵活. 例如,声明属性的引用可能和一些以 Ref 结尾的属性相冲突,而标准的 XML 格式就不会. Spring 团队推荐开发者能够和团队商量一下,协商使用哪一种方式,而不要同时使用三种方法.
使用 c 命名空间简化 XML

使用 p 命名空间简化 XML 配置 p 命名空间类似,c 命名空间是在 Spring 3.1 首次引入的,c 命名空间允许使用内联的属性来配置构造参数而不必使用 constructor-arg .

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

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

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

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

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

</beans>

c: 命名空间使用了和 p: 命名空间相类似的方式(使用了 -ref 来配置引用).而且,同样的,c 命名空间也是定义在 Spring Core 中的(不是 XSD 模式).

在少数的例子之中,构造函数的参数名字并不可用(通常,如果字节码没有 debug 信息的编译),你可以使用回调参数的索引,如下面的例子:

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

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

组合属性名

开发者可以配置混合的属性,只需所有的组件路径(除了最后一个属性名字) 不能为 null 即可. 参考如下定义:

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

somethingfred 属性, 而其中 fred 属性有 bob 属性,而 bob 属性之中有 sammy 属性,那么最后这个 sammy 属性会配置为 123 . 想要上述的配置能够生效,fred 属性需要有 bob 属性而且在 fred 构造之后不为 null 即可.

1.4.3. 使用 depends-on

如果一个 bean 是另一个 bean 的依赖,通常这个 bean 也就是另一个 bean 的属性之一. 多数情况下,开发者可以在配置 XML 元数据的时候使用 <ref/> 元素 然而,有时 bean 之间的依赖不是直接关联的. 例如: 需要调用类的静态实例化器来触发依赖,类似数据库驱动注册. depends-on 属性可以显式强制初始化一个或多个 bean. 以下示例使用 depends-on 属性表示对单个bean的依赖:

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

如果想要依赖多个bean,可以提供多个名字作为 depends-on 的值. 以逗号、空格或者分号分割:

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

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
depends-on 属性既可以指定初始化时间依赖性,也可以仅在 singleton bean 的情况下指定相应的销毁时间依赖性. 独立定义了 depends-on 属性的 bean 会优先销毁 (相对于 depends-on 的 bean 销毁,这样 depends-on 可以控制销毁的顺序.

1.4.4. 懒加载 Bean

默认情况下, ApplicationContext 会在实例化的过程中创建和配置所有的单例singleton bean. 总的来说, 这个预初始化是很不错的. 因为这样能及时发现环境上的一些配置错误,而不是系统运行了很久之后才发现. 如果这个行为不是迫切需要的,开发者可以通过将 Bean 标记为延迟加载就能阻止这个预初始化 懒加载 bean 会通知 IoC 不要让 bean 预初始化而是在被引用的时候才会实例化.

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

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

当将 bean 配置为上述 XML 的时候, ApplicationContext 之中的 lazy bean 是不会随着 ApplicationContext 的启动而进入到预初始化状态的. 只有那些 not.lazy 加载的 bean 是处于预初始化的状态的.

然而,如果延迟加载的类是作为单例非延迟加载的 bean 的依赖而存在的话,ApplicationContext 仍然会在 ApplicationContext 启动的时候加载. 因为作为单例 bean 的依赖,会随着单例 bean 的实例化而实例化.

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

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

1.4.5. 自动装配

Spring 容器可以根据 bean 之间的依赖自动装配,开发者可以让 Spring 通过 ApplicationContext 来自动解析这些关联,自动装载有很多优点:

  • 自动装载能够明显的减少指定的属性或者是构造参数. (在 本章其他地方讨论 的其他机制,如 bean 模板,在这方面也很有价值. )

  • 自动装载可以扩展开发者的对象,比如说,如果开发者需要加一个依赖,只需关心如何更改配置即可自动满足依赖关联. 这样,自动装载在开发过程中是极其高效的,无需明确选择装载的依赖会使系统更加稳定

使用基于 XML 的配置元数据(see 依赖注入), 可以使用 <bean/> 元素的 autowire 属性 为 bean 定义指定 autowire 模式. 自动装配功能有四种方式. 开发者可以指定每个 bean 的装配方式,这样 bean 就知道如何加载自己的依赖. 下表描述了四种自动装配模式:

Table 2. Autowiring modes
Mode Explanation

no

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

byName

按属性名称自动装配. Spring 查找与需要自动装配的属性同名的 bean. 例如,如果 bean 配置为根据名字装配,他包含 的属性名字为 master(即,它具有 setMaster(..) 方法) ,则 Spring 会查找名为 master 的 bean 定义并使用它来设置属性.

byType

如果需要自动装配的属性的类型在容器中只存在一个的话,他允许自动装配. 如果存在多个,则抛出致命异常,这表示您不能对该 bean 使用 byType 自动装配. 如果没有匹配的 bean,则不会发生任何事情(未设置该属性) .

constructor

类似于 byType ,但应用于构造函数参数. 如果容器中没有一个 Bean 的类型和构造函数参数类型一致的话,则会引发致命错误.

通过 byType 或者 constructor 的自动装配方式,开发者可以装载数组和强类型集合. 在这样的例子中,所有容器中的匹配了指定类型的 bean 都会自动装配到 bean 上来完成依赖注入. 开发者可以自动装配 key 为 String 强类型的 Map . 自动装配的 Map 值会包含所有的 bean 实例值来匹配指定的类型,Mapkey 会包含关联的 bean 的名字.

自动装配的局限和缺点

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

  • propertyconstructor-arg 设置中的显式依赖始终覆盖自动装配. 开发者不能自动装配一些简单属性,您不能自动装配简单属性,例如基本类型 ,Strings, 和 Classes(以及此类简单属性的数组) . 这种限制是按设计的.

  • 自动装配比显式的配置更容易歧义,尽管上表表明了不同自动配置的特点,Spring 也会尽可能避免不必要的装配错误. 但是 Spring 管理的对象关系仍然不如显式配置那样明确.

  • 从 Spring 容器生成文档的工具可能无法有效的提供装配信息.

  • 容器中的多个 bean 定义可能与 setter 方法或构造函数参数所指定的类型相匹配, 这有利于自动装配. 对于 arrays, collections, 或者 Map 实例来说这不是问题. 但是如果是对只有一个依赖的值是有歧义的话,那么这个项是无法解析的. 如果没有唯一的 bean,则会抛出异常.

在后面的场景,你可有如下的选择:

  • 放弃自动装配,改用显式的配置.

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

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

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

将 bean 从自动装配中排除

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

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

开发者可以通过模式匹配而不是 Bean 的名字来限制自动装配的候选者. 最上层的 <beans/> 元素会在 default-autowire-candidates 属性中来配置多种模式. 例如,限制自动装配候选者的名字以 Repository 结尾,可以配置成 *Repository. 如果需要配置多种模式,只需要用逗号分隔开即可. bean定义的 autowire-candidate 属性的显式值 truefalse 始终优先. 对于此类 bean,模式匹配规则不适用.

上面的这些技术在配置那些无需自动装配的 bean 是相当有效的,当然这并不是说这类 bean 本身无法自动装配其他的 bean. 而是说这些 bean 不再作为自动装配的依赖候选者.

1.4.6. 查找方法注入

在大多数的应用场景下,多数的 bean 都是singletons的. 当这个单例的 bean 需要和另一个单例的或者非单例的 bean 协作使用的时候,开发者只需要配置依赖 bean 为这个 bean 的属性即可. 但是有时会因为 bean 具有不同的生命周期而产生问题. 假设单例的 bean A 在每个方法调用中使用了非单例的 bean B. 容器只会创建 bean A 一次,而只有一个机会来配置属性. 那么容器就无法为每一次创建 bean A 时都提供新的 bean B 实例.

一种解决方案就是放弃 IoC,开发者可以通过实现 ApplicationContextAware 接口 让bean A对 ApplicationContextAware 可见 . 从容器中调用 getBean("B") 调用来使 bean A 知道该容器,以便每次 bean A 需要它时都请求一个(通常是新的) bean B 实例. 参考下面例子.

Java
// 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;
    }
}
Kotlin
// 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 容器的一个高级功能,可以让您处理这种问题. Spring 提供了一个稍微高级的注入方式来处理这种问题

您可以在此 this blog entry中阅读有关方法注入的更多信息.

查找方法注入

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

  • 如果想让这个动态子类正常工作,那么 Spring 容器所继承的 Bean 不能是 final 的,而覆盖的方法也不能是 final 的.

  • 对具有抽象方法的类进行单元测试时,需要开发者对类进行子类化,并提供抽象方法的具体实现.

  • 组件扫描也需要具体的方法,因为它需要获取具体的类.

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

对于前面代码片段中的 CommandManager 类,Spring 容器动态地覆盖 createCommand() 方法的实现. CommandManager 类不再拥有任何的 Spring 依赖,如下:

Java
// 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();
}
Kotlin
// 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>

当需要新的 myCommand bean实例时,标识为 commandManager 的bean会调用自身的 createCommand() 方法.开发者必须小心部署 myCommand bean为singletonbean. 如果所需的 bean 是单例的,那么每次都会返回相同的 myCommand bean 实例.

另外,如果是基于注解的配置模式,你可以在查找方法上定义 @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();
}
Kotlin
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();
}
Kotlin
abstract class CommandManager {

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

    @Lookup
    protected abstract fun createCommand(): Command
}

注意开发者可以通过创建子类实现 lookup 方法,以便使它们与 Spring 的组件扫描规则兼容,同时抽象类会在默认情况下被忽略. 这种限制不适用于显式注册 bean 或明确导入 bean 的情况.

另一种可以访问不同生命周期的方法是 ObjectFactory/Provider 注入,具体参看 bean 的作用域的注入

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

替换任意方法

从前面的描述中,我们知道查找方法是有能力来覆盖任何由容器管理的 bean 方法的. 开发者最好跳过这一部分,除非一定需要用到这个功能.

通过基于 XML 的元数据配置,开发者可以使用 replaced-method 元素来替换已存在方法的实现. 考虑以下类,它有一个我们想要覆盖的名为 computeValue 的方法:

Java
public class MyValueCalculator {

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

    // some other methods...
}
Kotlin
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 ...;
    }
}
Kotlin
/**
 * 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 方法的 XML 配置如下类似于以下示例:

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

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

您可以在 <replaced-method/> 元素中使用一个或多个 <arg-type/> 元素来指示被覆盖的方法的方法. 当需要覆盖的方法存在重载方法时,必须指定所需参数. 为了方便起见,字符串的类型会匹配以下类型,它完全等同于 java.lang.String:

java.lang.String
String
Str

因为,通常来说参数的个数已经足够区别不同的方法,这种快捷的写法可以省去很多的代码.

1.5. Bean 的作用域

创建 bean 定义时,同时也会定义该如何创建 Bean 实例. 这些具体创建的过程是很重要的,因为它意味着像创建类一样,您可以通过简单的定义来创建许多 bean 的实例.

您不仅可以将不同的依赖注入到 bean 中,还可以配置 bean 的作用域. 这种方法是非常强大而且也非常灵活,开发者可以通过配置来指定对象的作用域,无需在 Java 类的层次上配置. bean 可以配置多种作用域,Spring 框架支持六种作用域,有四种作用域是当开发者使用基于 Web 的 ApplicationContext 的时候才有效的. 您还可以创建自定义作用域.

下表描述了支持的作用域:

Table 3. Bean scopes
Scope Description

singleton

(默认) 每一 Spring IOC 容器都拥有唯一的实例对象.

prototype

一个 Bean 定义可以创建任意多个实例对象.

request

将单个 bean 作用域限定为单个 HTTP 请求的生命周期. 也就是说,每个 HTTP 请求都有自己的 bean 实例,它是在单个 bean 定义的后面创建的. 只有基于 Web 的 Spring ApplicationContext 的才可用.

session

将单个 bean 作用域限定为HTTP Session 的生命周期. 只有基于 Web 的Spring ApplicationContext 的才可用.

application

将单个 bean 作用域限定为 ServletContext 的生命周期. 只有基于 Web 的 Spring ApplicationContext 的才可用.

websocket

将单个 bean 作用域限定为 WebSocket 的生命周期. 只有基于 Web 的 Spring ApplicationContext 的才可用.

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

1.5.1. Singleton 作用域

单例 bean 在全局只有一个共享的实例,所有依赖单例 bean 的场景中,容器返回的都是同一个实例.

换句话说,当您定义一个 bean 并且它的作用域是一个单例时,Spring IoC 容器只会根据 bean 的定义来创建该 bean 的唯一实例. 这些唯一的实例会缓存到容器中,后续针对单例 bean 的请求和引用,都会从这个缓存中拿到这个唯一实例. 下图显示了单例作用域的工作原理:

singleton

Spring 的单例 bean 概念不同于设计模式(GoF) 之中所定义的单例模式. 设计模式中的单例模式是将一个对象的作用域硬编码的,一个 ClassLoader 只能有唯一的一个实例. 而 Spring 的单例作用域是以容器为前提的,每个容器每个 bean 只能有一个实例. 这意味着,如果在单个 Spring 容器中为特定类定义一个 bean,则 Spring 容器会根据 bean 定义创建唯一的 bean 实例. 单例作用域是 Spring 的默认作用域. 下面的例子是在 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. Prototype 作用域

非单例的、原型 bean 指的是每次请求 bean 实例时,返回的都是新的对象实例. 也就是说,每次注入到另外的 bean 或者通过调用 getBean() 方法来获得的 bean 都是全新的实例. 基于线程安全性的考虑,当 bean 对象有状态时使用原型作用域,而无状态时则使用单例作用域.

下图显示了原型作用域的工作原理:

prototype

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

用下面的例子来说明 Spring 的原型作用域:

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

与其他作用域相比,Spring 不会完整地管理原型 bean 的生命周期. Spring 容器只会初始化、配置和装载这些 bean,然后传递给 Client. 但是之后就不会再有该原型实例的进一步记录. 也就是说,初始化生命周期回调方法在所有作用域的 bean 是都会调用的,但是销毁生命周期回调方法在原型 bean 是不会调用的. 所以,客户端代码必须注意清理原型 bean 以及释放原型 bean 所持有的资源. 可以通过使用自定义的bean post-processor(Bean的后置处理器) 来让 Spring 释放掉原型 bean 所持有的资源.

在某些方面,Spring 容器关于原型作用域的 bean 就是取代了 Java 的 new 操作符. 所有的生命周期的控制都由客户端来处理(有关 Spring 容器中 bean 的生命周期的详细信息,请参阅生命周期回调) .

1.5.3. 单例 bean 依赖原型 bean

当单例 bean 依赖原型 bean 时,请注意在实例化时解析依赖. 因此,如果将原型 bean 注入到单例的 bean 中,则会实例化一个新的原型 bean,然后将依赖注入到单例 bean 中. 这个依赖的原型 bean 仍然是同一个实例.

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

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

request, session, application, 和 websocket 作用域只有在 Web 中使用 Spring 的 ApplicationContext 的实现,(例如 ClassPathXmlApplicationContext) 的情况下才用得上. 如果在普通的 Spring IoC 容器,例如 ClassPathXmlApplicationContext 中使用这些作用域,将会抛出 IllegalStateException 异常来说明使用了未知的作用域.

初始化 Web Configuration

为了能够使用 request, session, application, 和 websocket 作用域 (Web 作用域的 bean) , 需要在配置 bean 之前作一些基础配置. 而对于标准的作用域,例如单例和原型作用域,这种基础配置是不需要的.

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

例如,如果开发者使用了 Spring Web MVC 框架,那么每一个请求都会通过 Spring 的 DispatcherServlet 来处理,那么也无需特殊的设置了. DispatcherServletDispatcherPortlet 已经包含了相应状态.

如果您使用 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>

或者,如果对 Listener 不是很熟悉,请考虑使用 Spring 的 RequestContextFilter. Filter 映射取决于 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 所做的工作实际上是一样的,都是将 request 对象请求绑定到服务的 Thread 上. 这才使 bean 在之后的调用链上对请求和会话作用域可见.

Request 作用域

参考下面这个 XML 配置的 bean 定义:

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

Spring 容器会在每次使用 LoginAction 来处理每个 HTTP 请求时都会创建新的 loginAction 实例. 也就是说,loginAction bean 的作用域是 HTTP Request 级别的. 开发者可以随意改变实例的状态,因为其他通过 loginAction 请求来创建的实例根本看不到开发者改变的实例状态,所有创建的 Bean 实例都是根据独立的请求创建的. 当请求处理完毕,这个 bean 也将会销毁.

当使用注解配置或 Java 配置时,使用 @RequestScope 注解修饰的 bean 会被设置成 request 作用域. 以下示例显示了如何执行此操作:

Java
@RequestScope
@Component
public class LoginAction {
    // ...
}
Kotlin
@RequestScope
@Component
class LoginAction {
    // ...
}
Session 作用域

参考下面 XML 配置的 bean 的定义:

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

Spring 容器通过在单个 HTTP 会话的生命周期中使用 UserPreferences bean 定义来创建 UserPreferences bean 的新实例. 换言之,UserPreferences Bean 的作用域是 HTTP Session 级别的,在 request-scoped 作用域的 bean 上, 开发者可以随意的更改实例的状态,同样,其他 HTTP Session 的基本实例在每个 Session 中都会请求 userPreferences 来创建新的实例,所以,开发者更改 bean 的状态, 对于其他的 Bean 仍然是不可见的. 当 HTTP Session 被销毁时,根据这个 Session 来创建的 bean 也将会被销毁.

使用注解配置和 Java 配置时,使用 @SessionScope 注解修饰的 bean 会被设置成 session 作用域.

Java
@SessionScope
@Component
public class UserPreferences {
    // ...
}
Kotlin
@SessionScope
@Component
class UserPreferences {
    // ...
}
Application 作用域

参考下面用 XML 配置的 bean 的定义:

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

Spring 容器会在整个 Web 应用内使用到 appPreferences 的时候创建一个新的 AppPreferences 的实例. 也就是说,appPreferences bean是在 ServletContext 级别的, 就像普通的 ServletContext 属性一样. 这种作用域和 Spring 的单例作用域有一些相似的地方,但是也有两个重要的不同之处,它对于每一个 ServletContext 来说是单例的,但是对 Spring ApplicationContext 来说不是的(某些 web 应用可能包含多个 ApplicationContext) . 实际上它是被暴露在外的,并且作为 ServletContext 属性对外可见.

当使用注解配置或 Java 配置时,使用 @ApplicationScope 注解修饰的 bean 会被设置成 application 作用域 . 以下示例显示了如何执行此操作:

Java
@ApplicationScope
@Component
public class AppPreferences {
    // ...
}
Kotlin
@ApplicationScope
@Component
class AppPreferences {
    // ...
}
WebSocket 作用域

WebSocket 作用域与 WebSocket 会话的生命周期相关联,适用于 STOMP over WebSocket 应用程序,请参阅 WebSocket 作用域了解更多详情。

依赖有 Scope 的 Bean

Spring IoC 容器不仅仅管理对象(bean) 的实例化,同时也负责装配依赖. 如果开发者要将一个 bean 装配到比它作用域更广的 bean 时(例如 HTTP 请求返回的 bean) ,那么开发者应当选择注入 AOP 代理而不是使用带作用域的 bean. 也就是说,开发者需要注入代理对象,而这个代理对象既可以找到实际的 bean,还能够创建全新的 bean.

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

当针对原型作用域的 bean 声明 <aop:scoped-proxy/> 时,每个通过代理的调用都会产生新的目标实例.

此外,作用域代理并不是取得作用域 bean 的唯一安全方式. 开发者也可以通过简单的声明注入(即构造函数或 setter 参数或自动装配字段) ObjectFactory<MyTargetBean>, 然后允许通过类似 getObject() 的方法调用来获取一些指定的依赖,而不是直接储存依赖的实例.

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

JSR-330 将这样的方法称为 Provider,它使用 Provider<MyTargetBean> 声明以及相关的 get() 方法来尝试获取每一个配置. 有关 JSR-330 整体的更多详细信息,请参看此处 .

以下示例中的配置只有一行,但了解 “why” 以及它背后的 “how” 非常重要:

<?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 定义代理的行.

要创建这样的一个代理,只需要在带作用域的 bean 定义中添加子节点 <aop:scoped-proxy/> 即可(具体查看选择要创建的代理类型基于 XML Schema 的配置) . 为什么在 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) 注入了 HTTP Session 级别的 userPreferences 依赖. 显然, 问题就是 userPreferences 在 Spring 容器中只会实例化一次. 它的依赖(在这种情况下只有一个,userPreferences ) 也只注入一次. 这意味着 userManager 每次使用的是完全相同的 userPreferences 对象(即最初注入它的对象) 进行操作.

这不是将短周期作用域 bean 注入到长周期作用域bean时所需的行为,例如将 HTTP Session 级别的作用域 bean 作为依赖注入到单例 bean 中. 相反,开发者需要一个 userManager 对象, 而在 HTTP Session 的生命周期中, 开发者需要一个特定于 HTTP Session 的 userPreferences 对象. 因此,容器创建一个对象,该对象暴露与 UserPreferences 类(理想情况下为 UserPreferences 实例的对象) 完全相同的公共接口, 该对象可以从作用域机制( HTTP Request、Session 等) 中获取真实的 UserPreferences 对象. 容器将这个代理对象注入到 userManager 中, 而不知道这个 UserPreferences 引用是一个代理. 在这个例子中,当一个 UserManager 实例在依赖注入的 UserPreferences 对象上调用一个方法时, 它实际上是在调用代理的方法,再由代理从 HTTP Session (本例) 获取真实的 UserPreferences 对象,并将方法调用委托给检索到的实际 UserPreferences 对象.

因此,在将 request- and session-scoped 的bean来作为依赖时,您需要以下(正确和完整) 配置,如以下示例所示: 所以当开发者希望能够正确的使用配置请求、会话或者全局会话级别的bean来作为依赖时,需要进行如下类似的配置.

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

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

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

CGLIB 代理只拦截 public 方法调用! 不要在这样的代理上调用非 public 方法. 它们不会委托给实际的作用域目标对象.

或者,您可以通过为 <aop:scoped-proxy/> 元素的 proxy-target-class 属性的值指定 false 来配置 Spring 容器, 以便为此类作用域 bean 创建基于 JDK 接口的标准代理. 使用基于接口的 JDK 代理意味着开发者无需引入第三方库即可完成代理. 但是,这也意味着带作用域的 bean 需要额外实现一个接口,而依赖是从这些接口来获取的. 以下示例显示基于接口的代理:

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

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

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

1.5.5. 自定义 Scopes

bean 的作用域机制是可扩展的,开发者可以自定义作用域,甚至重新定义已经存在的作用域,但是 Spring 团队不推荐这样做,而且开发者也不能重写 singletonprototype 作用域.

创建自定义作用域

为了能够使 Spring 可以管理开发者定义的作用域,开发者需要实现 org.springframework.beans.factory.config.Scope·. 如何实现自定义的作用域, 可以参考 Spring 框架的一些实现或者有关 Scope 的javadoc

Scope 接口有四个方法用于操作对象,例如获取、移除或销毁等操作.

例如,传入 Session 作用域该方法将会返回一个 session-scoped 的 bean (如果它不存在,那么将会返回绑定 session 作用域的新实例) . 下面的方法返回相应作用域的对象:

Java
Object get(String name, ObjectFactory<?> objectFactory)
Kotlin
fun get(name: String, objectFactory: ObjectFactory<*>): Any

下面的方法将从相应的作用域中移除对象. 同样,以会话为例,该函数会删除会话作用域的 Bean. 删除的对象会作为返回值返回,当无法找到对象时将返回 null. 以下方法从相应作用域中删除对象:

Java
Object remove(String name)
Kotlin
fun remove(name: String): Any

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

Java
void registerDestructionCallback(String name, Runnable destructionCallback)
Kotlin
fun registerDestructionCallback(name: String, destructionCallback: Runnable)

有关销毁回调的更多信息,请参看 javadoc 或 Spring 的 Scope 实现部分.

下面的方法获取相应作用域的区分标识符:

Java
String getConversationId()
Kotlin
fun getConversationId(): String

这个标识符在不同的作用域中是不同的. 例如对于会话作用域,这个标识符就是会话的标识符.

使用自定义作用域

在实现了自定义作用域后,开发者还需要让 Spring 容器能够识别发现所创建的新作用域. 下面的方法就是在 Spring 容器中用来注册新 Scope 的:

Java
void registerScope(String scopeName, Scope scope);
Kotlin
fun registerScope(scopeName: String, scope: Scope)

这个方法是在 ConfigurableBeanFactory 的接口中声明的,可以用在多数的 ApplicationContext 实现,也可以通过 BeanFactory 属性来调用.

registerScope(..) 方法的第一个参数是相关作用域的唯一名称. 举例来说,Spring 容器中的单例和原型就以它本身来命名. 第二个参数就是开发者希望注册和使用的自定义 Scope 实现的实例对象

假定开发者实现了自定义 Scope ,然后可以按如下步骤来注册.

下一个示例使用 SimpleThreadScope ,这个例子在 Spring 中是有实现的,但没有默认注册. 您自定义的作用域也可以通过如下的方式来注册.
Java
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
Kotlin
val threadScope = SimpleThreadScope()
beanFactory.registerScope("thread", threadScope)

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

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

在自定义作用域中,开发者也不限于仅仅通过编程的方式来注册作用域,还可以通过配置 CustomScopeConfigurer 类来实现. 如以下示例所示:

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

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

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

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

</beans>
FactoryBean 实现中添加了 <aop:scoped-proxy/> <bean> 元素时,它是工厂 bean 本身的作用域,而不是从 getObject() 方法返回的对象.

1.6. 自定义 Bean 的特性

Spring Framework 提供了许多可用于自定义 bean 特性的接口. 本节将它们分组如下:

1.6.1. 生命周期回调

你可以实现 InitializingBeanDisposableBean 接口,让容器里管理 Bean 的生命周期. 容器会在调用 afterPropertiesSet() 之后和 destroy() 之前会允许 bean 在初始化和销毁 bean 时执行某些操作.

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

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

在内部,Spring 框架使用 BeanPostProcessor 实现来处理任何回调接口并调用适当的方法. 如果您需要 Spring 默认提供的自定义功能或其他生命周期行为,您可以自己实现 BeanPostProcessor. 有关更多信息,请参阅 容器扩展点.

除了初始化和销毁方法的回调,Spring 管理的对象也实现了 Lifecycle 接口来让管理的对象在容器的生命周期内启动和关闭.

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

初始化方法回调

org.springframework.beans.factory.InitializingBean 接口允许 bean 在所有的必要的依赖配置完成后执行 bean 的初始化, InitializingBean 接口中指定使用如下方法:

void afterPropertiesSet() throws Exception;

Spring 团队是不建议开发者使用 InitializingBean 接口,因为这样会将代码耦合到 Spring 的特殊接口上. 他们建议使用 @PostConstruct 注解或者指定一个 POJO 的实现方法, 这会比实现接口更好. 在基于 XML 的元数据配置上,开发者可以使用 init-method 属性来指定一个没有参数的方法,使用 Java 配置的开发者可以在 @Bean 上添加 initMethod 属性. 请参阅接收生命周期回调:

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

    public void init() {
        // do some initialization work
    }
}
Kotlin
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
    }
}
Kotlin
class AnotherExampleBean : InitializingBean {

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

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

销毁方法的回调

实现 org.springframework.beans.factory.DisposableBean 接口的 Bean 就能让容器通过回调来销毁 bean 所引用的资源. DisposableBean 接口指定一个方法:

void destroy() throws Exception;

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

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

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}
Kotlin
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)
    }
}
Kotlin
class AnotherExampleBean : DisposableBean {

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

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

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

当您不使用 Spring 特有的 InitializingBeanDisposableBean 回调接口来实现初始化和销毁方法时,您定义方法的名称最好类似于 init(), initialize(), dispose(). 这样可以在项目中标准化类方法,并让所有开发者都使用一样的名字来确保一致性.

您可以配置 Spring 容器来针对每一个 Bean 都查找这种名字的初始化和销毁回调方法. 也就是说, 任意的开发者都会在应用的类中使用一个叫 init() 的初始化回调. 而不需要在每个 bean 中都定义 init-method="init" 这种属性, Spring IoC 容器会在 bean 创建的时候调用那个回调方法(如前面描述的标准生命周期一样) . 这个特性也将强制开发者为其他的初始化以及销毁回调方法使用同样的名字.

假设您的初始化回调方法名为 init(),而您的 destroy 回调方法名为 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.");
        }
    }
}
Kotlin
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>

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

您可以使用 <beans/> 元素上的 default-destroy-method 属性,以类似方式(在 XML 中) 配置destroy方法回调.

当某些 bean 已有的回调方法与配置的默认回调方法不相同时,开发者可以通过特指的方式来覆盖掉默认的回调方法. 以 XML 为例,可以通过使用元素的 init-methoddestroy-method 属性来覆盖掉 <bean/> 中的配置.

Spring 容器会做出如下保证,bean 会在装载了所有的依赖以后,立刻就开始执行初始化回调. 这样的话,初始化回调只会在直接的 bean 引用装载好后调用, 而此时 AOP 拦截器还没有应用到 bean 上. 首先目标的 bean 会先完全初始化, 然后 AOP 代理和拦截链才能应用. 如果目标 bean 和代理是分开定义的,那么开发者的代码甚至可以跳过 AOP 而直接和引用的 bean 交互. 因此,在初始化方法中应用拦截器会前后矛盾,因为这样做耦合了目标 bean 的生命周期和代理/拦截器,还会因为与 bean 产生了直接交互进而引发不可思议的现象.

组合生命周期策略

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

如果 bean 配置了多个生命周期机制,而且每个机制都配置了不同的方法名字时,每个配置的方法会按照以下描述的顺序来执行. 但是,如果配置了相同的名字, 例如初始化回调为 init(),在不止一个生命周期机制配置为这个方法的情况下,这个方法只会执行一次. 如 上一节中所述.

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

  1. 包含 @PostConstruct 注解的方法

  2. InitializingBean 接口中的 afterPropertiesSet() 方法

  3. 自定义的 init() 方法

Destroy 方法以相同的顺序调用:

  1. 包含 @PreDestroy 注解的方法

  2. DisposableBean 接口中的 destroy() 方法

  3. 自定义的 destroy() 方法

开始和关闭回调

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

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

任何 Spring 管理的对象都可以实现 Lifecycle 接口. 然后,当 ApplicationContext 接收到启动和停止信号时(例如,对于运行时的停止/重启场景) , ApplicationContext 会通知到所有上下文中包含的生命周期对象. 它通过委托 LifecycleProcessor 完成此操作,如下面的清单所示:

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

请注意,LifecycleProcessorLifecycle 接口的扩展. 它还添加了另外两种方法来响应刷新和关闭的上下文.

注意,常规的 org.springframework.context.Lifecycle 接口只是为明确的开始/停止通知提供一个约束,而并不表示在上下文刷新就会自动开始. 要对特定 bean 的自动启动(包括启动阶段) 进行细粒度控制,请考虑实现 org.springframework.context.SmartLifecycle 接口.

同时,停止通知并不能保证在销毁之前出现. 在正常的关闭情况下,所有的 Lifecycle 都会在销毁回调准备好之前收到停止通知,然而, 在上下文生命周期中的热刷新或者停止尝试刷新时,则只会调用销毁方法.

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

public interface Phased {

    int getPhase();
}

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

public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);
}

当启动时,拥有最低 phased 的对象会优先启动,而当关闭时,会相反的顺序执行. 因此,如果一个对象实现了 SmartLifecycle,然后令其 getPhase() 方法返回 Integer.MIN_VALUE 值的话, 就会让该对象最早启动,而最晚销毁. 显然,如果 getPhase() 方法返回了 Integer.MAX_VALUE 值则表明该对象会最晚启动,而最早销毁. 当考虑到使用 phased 值时,也同时需要了解正常没有实现 SmartLifecycleLifecycle 对象的默认值,这个值是 0 . 因此,配置任意的负值都将表明将对象会在标准组件启动之前启动 , 而在标准组件销毁以后再进行销毁.

SmartLifecycle 接口也定义了一个名为 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() 方法已经被调用,则就会执行关闭流程. 但是如果上下文正在关闭中则不会在进行此流程, 而刷新的回调会使用到 SmartLifecycle 的另一个特性. 当上下文刷新完毕(所有的对象已经实例化并初始化) 后, 就会调用刷新回调,默认的生命周期处理器会检查每一个 SmartLifecycle 对象的 isAutoStartup() 方法返回的 Boolean 值.如果为真, 对象将会自动启动而不是等待明确的上下文调用, 或者调用自己的 start() 方法(不同于上下文刷新,标准的上下文实现是不会自动启动的) . phase 的值以及 "depends-on" 关系会决定对象启动和销毁的顺序.

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

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

如果开发者在非 Web 应用环境使用 Spring IoC 容器的话(例如,在桌面客户端的环境下) 开发者需要在 JVM 上注册一个关闭的钩子,来确保在关闭 Spring IoC 容器的时候能够调用相关的销毁方法来释放掉引用的资源. 当然,开发者也必须正确配置和实现那些销毁回调.

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

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...
    }
}
Kotlin
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

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

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

这样 bean 就能够通过编程的方式创建和操作 ApplicationContext 了. 通过 ApplicationContext 接口,或者通过将引用转换成已知的接口的子类, 例如 ConfigurableApplicationContext 就能够提供一些额外的功能. 其中的一个用法就是可以通过编程的方式来获取其他的 bean.有时候这个能力非常有用. 当然,Spring 团队并不推荐这样做, 因为这样会使代码与 Spring 框架耦合,同时也没有遵循 IoC 的风格. ApplicationContext 中其它的方法可以提供一些诸如资源的访问、发布应用事件或者添加 MessageSource 之类的功能. ApplicationContextApplicationContext 的附加功能中描述了这些附加功能.

自动装配是另一种获取 ApplicationContext 引用的替代方法. 传统的的构造函数 和 byType 的装载方式自动装配模式(如 自动装配中所述) 可以通过构造函数或 setter 方法的方式注入,开发者也可以通过注解注入的方式. 为了更为方便,包括可以注入的字段和多个参数方法,请使用新的基于注解的自动装配功能. 这样,ApplicationContext 将自动装配字段、构造函数参数或方法参数,如果相关的字段,构造函数或方法带有 @Autowired 注解,则该参数需要 ApplicationContext 类型. 有关更多信息,请参阅使用 @Autowired.

ApplicationContext 创建实现了 org.springframework.beans.factory.BeanNameAware 接口的类,那么这个类就可以针对其名字进行配置. 以下清单显示了 BeanNameAware 接口的定义:

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

这个回调的调用在属性配置完成之后,但是在初始化回调之前. 例如 InitializingBean.afterPropertiesSet() 方法以及自定义的初始化方法等.

1.6.3. 其他 Aware 接口

除了 ApplicationContextAwareBeanNameAware(前面已讨论过) 之外,Spring还提供了一系列 Aware 回调接口,让 bean 告诉容器,它们需要一些具体的基础配置信息. 一些重要的 Aware 接口如下表:

Table 4. Aware interfaces
Name Injected Dependency Explained in…​

ApplicationContextAware

声明 ApplicationContext.

ApplicationContextAwareBeanNameAware

ApplicationEventPublisherAware

ApplicationContext 的事件发布者.

ApplicationContext 的附加功能

BeanClassLoaderAware

用于加载bean类的类加载器

实例化 Bean

BeanFactoryAware

声明 BeanFactory.

BeanFactory API

BeanNameAware

声明bean的名称.

ApplicationContextAwareBeanNameAware

LoadTimeWeaverAware

定义的 weaver 用于在加载时处理类定义.

在 Spring 框架中使用 AspectJ 的加载时织入

MessageSourceAware

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

ApplicationContext 的附加功能

NotificationPublisherAware

Spring JMX 通知发布者

Notifications

ResourceLoaderAware

配置的资源加载器

资源(Resources)

ServletConfigAware

当前 ServletConfig 容器运行. 仅在 Web 下的 Spring ApplicationContext 中有效 ApplicationContext.

Spring MVC

ServletContextAware

容器运行的当前 ServletContext. 仅在 Web 下的 Spring ApplicationContext 中有效.

Spring MVC

请再次注意,使用这些接口会将您的代码绑定到 Spring API,而不会遵循 IoC 原则. 因此,我们建议将它们用于需要以编程方式访问容器的基础架构 bean.

1.7. Bean 继承的定义

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

如果开发者编程式地使用 ApplicationContext 接口,子 bean 定义可以通过 ChildBeanDefinition 类来表示. 很多开发者不会使用这个级别的方法, 而是会在类似于 ClassPathXmlApplicationContext 中声明式地配置 bean 定义. 当你使用基于 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 如果没有指定 class,它将使用父 bean 定义的 class. 但也可以进行重写. 在后一种情况中,子 bean 必须与父bean兼容,也就是说,它必须接受父bean的属性值.

子 bean 定义从父类继承作用域、构造器参数、属性值和可重写的方法,除此之外,还可以增加新值. 开发者指定任何作用域、初始化方法、销毁方法和/或者静态工厂方法设置都会覆盖相应的父bean设置.

剩下的设置会取子 bean 定义: 依赖、自动注入模式、依赖检查、单例、延迟加载.

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

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

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

父 bean 不能单独实例化,因为它不完整,并且也明确标记为 abstract. 当定义是 abstract 的时,它只能用作纯模板 bean 定义,用作子定义的父定义. 如果试图单独地使用声明了 abstract 的父 bean, 通过引用它作为另一个bean的 ref 属性,或者使用父 bean id 进行显式的 getBean() 调用,都将返回一个错误. 同样,容器内部的 preInstantiateSingletons() 方法也会忽略定义为 abstract 的bean.

ApplicationContext 默认会预先实例化所有的单例 bean. 因此,如果开发者打算把(父) bean 定义仅仅作为模板来使用,同时为它指定了 class 属性, 那么必须确保设置 abstract 的属性值为 true. 否则,应用程序上下文会(尝试) 预实例化这个 abstract bean.

1.8. 容器扩展点

通常,应用程序开发者无需继承 ApplicationContext 的实现类. 相反,Spring IoC 容器可以通过插入特殊的集成接口实现进行扩展. 接下来的几节将介绍这些集成接口.

1.8.1. 使用 BeanPostProcessor 自定义 Bean

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

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

BeanPostProcessor 实例在 bean (或对象) 实例上运行. 也就是说,Spring IoC 容器实例化一个 bean 实例,然后才能用 BeanPostProcessor 对这个实例进行处理.

BeanPostProcessor 会在整个容器内起作用,所有它仅仅与正在使用的容器相关. 如果在一个容器中定义了 BeanPostProcessor,那么它只会处理那个容器中的 bean. 换句话说,在一个容器中定义的 bean 不会被另一个容器定义的 BeanPostProcessor 处理,即使这两个容器都是同一层次结构的一部分.

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

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

ApplicationContex 会自动地检测所有定义在配置元文件中,并实现了 BeanPostProcessor 接口的 bean. ApplicationContext 会注册这些 beans 为后置处理器, 使他们可以在 bean 创建完成之后被调用. bean 后置处理器可以像其他 bean 一样部署到容器中.

当在配置类上使用 @Bean 工厂方法声明 BeanPostProcessor 时,工厂方法返回的类型应该是实现类自身. ,或至少也是 org.springframework.beans.factory.config.BeanPostProcessor 接口, 要清楚地表明这个 bean 的后置处理器的本质特点. 否则,在它完全创建之前,ApplicationContext 将不能通过类型自动探测它. 由于 BeanPostProcessor 在早期就需要被实例化, 以适应上下文中其他 bean 的实例化,因此这个早期的类型检查是至关重要的.

BeanPostProcessor 实例的编程注册
以编程方式注册 BeanPostProcessor 实例,虽然 BeanPostProcessor 注册的推荐方法是通过 ApplicationContext 自动检测(如前所述) ,但您可以以编程的方式使用 ConfigurableBeanFactoryaddBeanPostProcessor 方法进行注册. 这对于在注册之前需要对条件逻辑进行评估,或者是在继承层次的上下文之间复制 bean 的后置处理器中是有很有用的. 但请注意,以编程方式添加的 BeanPostProcessor 实例不遵循 Ordered 接口. 这里,注册顺序决定了执行的顺序. 另请注意,以编程方式注册的 BeanPostProcessor 实例始终在通过自动检测注册的实例之前处理,而不管任何显式排序.
BeanPostProcessor 实例 和 AOP 自动代理

实现 BeanPostProcessor 接口的类是特殊的,容器会对它们进行不同的处理. 所有 BeanPostProcessor 和他们直接引用的 beans 都会在容器启动的时候被实例化, 并作为 ApplicationContext 特殊启动阶段的一部分. 接着,所有的 BeanPostProcessor 都会以一个有序的方式进行注册,并应用于容器中的所有bean. 因为 AOP 自动代理本身被实现为 BeanPostProcessor,这个 BeanPostProcessor 和它直接应用的beans都不适合进行自动代理,因此也就无法在它们中织入切面.

对于所有这样的 bean,您应该看到一条信息性日志消息: Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying).

如果你使用自动装配或 @Resource(可能会回退到自动装配) 将 Bean 连接到 BeanPostProcessor 中,Spring 可能会在搜索类型匹配的依赖候选时访问到意外类型的 bean; 因此,对它们不适合进行自动代理,或者对其他类型的 bean 进行后置处理. 例如,如果有一个使用 @Resource 注解的依赖,其中字段或 setter 名称不直接对应于bean的声明名称而且没有使用name属性, 则 Spring 会访问其他 bean 以按类型匹配它们.

以下示例显示如何在 ApplicationContext 中编写,注册和使用 BeanPostProcessor 实例.

Example: Hello World, BeanPostProcessor-style

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

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

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

}
Kotlin
import org.springframework.beans.factory.getBean

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

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

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

自定义 BeanPostProcessor 实现与回调接口或注解配合使用,是一种常见的扩展 Spring IoC 容器手段,一个例子就是 AutowiredAnnotationBeanPostProcessor,这是 BeanPostProcessor 实现. 自动注解字段, setter 方法, 和任意配置方法.

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

下一个我们要关注的扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor. 这个接口的语义与 BeanPostProcessor 类似, 但有一处不同,BeanFactoryPostProcessor 操作 bean 的元数据配置. 也就是说,Spring IoC 容器允许 BeanFactoryPostProcessor 读取配置元数据, 并可能在容器实例化除 BeanFactoryPostProcessor 实例之外的任何 bean 之前 更改它.

您可以配置多个 BeanFactoryPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanFactoryPostProcessor 实例的运行顺序( BeanFactoryPostProcessor 必须实现了 Ordered 接口才能设置这个属性) . 如果编写自己的 BeanFactoryPostProcessor ,则应考虑实现 Ordered 接口. 有关更多详细信息, 请参阅 BeanFactoryPostProcessorOrdered 接口的 javadoc.

如果想修改实际的 bean 实例(也就是说,从元数据配置中创建的对象) 那么需要使用 BeanPostProcessor(前面在使用 BeanPostProcessor 自定义 Bean 中进行了描述使用 BeanPostProcessor 自定义 Bean) 来替代. 在 BeanFactoryPostProcessor (例如使用 BeanFactory.getBean()) 中使用这些 bean 的实例虽然在技术上是可行的,但这么来做会将bean过早实例化, 这违反了标准的容器生命周期. 同时也会引发一些副作用,例如绕过 bean 的后置处理.

BeanFactoryPostProcessor 会在整个容器内起作用,所有它仅仅与正在使用的容器相关. 如果在一个容器中定义了 BeanFactoryPostProcessor, 那么它只会处理那个容器中的 bean. 换句话说,在一个容器中定义的 bean 不会被另一个容器定义的 BeanFactoryPostProcessor 处理,即使这两个容器都是同一层次结构的一部分.

bean 工厂后置处理器在 ApplicationContext 中声明时自动执行,这样就可以对定义在容器中的元数据配置进行修改. Spring 包含许多预定义的 bean 工厂后处理器, 例如 PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer. 您还可以使用自定义 BeanFactoryPostProcessor. 例如,注册自定义属性编辑器 .

ApplicationContext 自动检测部署到其中的任何实现 BeanFactoryPostProcessor 接口的 bean. 它在适当的时候使用这些 bean 作为 bean 工厂后置处理器. 你可以部署这些后置处理器为你想用的任意其它 bean.

注意,和 BeanPostProcessor 一样,通常不应该配置 BeanFactoryPostProcessor 来进行延迟初始化. 如果没有其它 bean 引用 Bean(Factory)PostProcessor, 那么后置处理器就不会被初始化. 因此,标记它为延迟初始化就会被忽略,,即便你在 <beans /> 元素声明中设置 default-lazy-init=true 属性,Bean(Factory)PostProcessor 也会提前初始化 bean.
Example: 类名替换 PropertySourcesPlaceholderConfigurer

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

考虑以下这个基于 XML 的元数据配置代码片段,这里的 DataSource 使用了占位符来定义:

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

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

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

而真正的值是来自于标准的 Java Properties 格式的文件:

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

在上面的例子中,${jdbc.username} 字符串在运行时将替换为值 'sa',并且同样适用于与属性文件中的键匹配的其他占位符值. PropertySourcesPlaceholderConfigurer 检查 bean 定义的大多数属性和属性中的占位符. 此外,您可以自定义占位符前缀和后缀.

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

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

PropertySourcesPlaceholderConfigurer 不仅在您指定的属性文件中查找属性. 默认情况下,如果它在指定的属性文件中找不到属性,则会检查 Spring Environment 属性和常规 Java System 属性.

你可以使用 PropertySourcesPlaceholderConfigurer 来替换类名,当开发者在运行时需要选择某个特定的实现类时,这是很有用的. 例如

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

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

如果在运行时无法将类解析为有效类,则在即将创建 bean 时,bean 的解析将失败,这是 ApplicationContext 在对非延迟初始化 bean 的 preInstantiateSingletons() 阶段发生的事.

Example: The PropertyOverrideConfigurer

PropertyOverrideConfigurer, 另外一种 bean 工厂后置处理器,类似于 PropertyPlaceholderConfigurer,但与后者不同的是: 对于所有的 bean 属性,原始定义可以有默认值或也可能没有值. 如果一个 Properties 覆盖文件没有配置特定的 bean 属性,则就会使用默认的上下文定义

注意,bean 定义是不知道是否被覆盖的,所以从 XML 定义文件中不能马上看到那个配置正在被使用. 在拥有多个 PropertyOverrideConfigurer 实例的情况下,为相同 bean 的属性定义不同的值时,基于覆盖机制只会有最后一个生效.

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

beanName.property=value

例如:

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

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

复合属性名称也是被支持的,只要被重写的最后一个属性以外的路径中每个组件都已经是非空时(假设由构造方法初始化) . 在下面的示例中,tom bean 的 fred 属性的 bob 属性的 sammy 属性设置值为 123:

tom.fred.bob.sammy=123
指定的覆盖值通常是文字值,它们不会被转换成 bean 的引用. 这个约定也适用于当 XML 中的 bean 定义的原始值指定了 bean 引用时. 使用 Spring 2.5 中引入的 context 命名空间,可以使用专用配置元素配置属性覆盖,如以下示例所示:
<context:property-override location="classpath:override.properties"/>

1.8.3. 使用 FactoryBean 自定义初始化逻辑

为自己工厂的对象实现 org.springframework.beans.factory.FactoryBean 接口.

FactoryBean 接口就是 Spring IoC 容器实例化逻辑的可插拔点,如果你的初始化代码非常复杂,那么相对于(潜在地) 大量详细的 XML 而言,最好是使用 Java 语言来表达. 你可以创建自定义的 FactoryBean ,在该类中编写复杂的初始化代码. 然后将自定义的 FactoryBean 插入到容器中.

FactoryBean<T> 接口提供下面三个方法

  • T getObject(): 返回这个工厂创建的对象实例. 这个实例可能是共享的,这取决于这个工厂返回的是单例还是原型实例.

  • boolean isSingleton(): 如果 FactoryBean 返回单例,那么这个方法就返回 true,否则返回 false. 此方法的默认实现返回 true

  • `Class<?> getObjectType(): 返回由 getObject() 方法返回的对象类型,如果事先不知道的类型则会返回 null.

Spring 框架大量地使用了 FactoryBean 的概念和接口,FactoryBean 接口的 50 多个实现都随着 Spring 一同提供.

当开发者需要向容器请求一个真实的 FactoryBean 实例(而不是它生产的bean) 时,调用 ApplicationContextgetBean() 方法时在 bean 的 id 之前需要添加连字符(&) 所以对于一个给定 idmyBeanFactoryBean, 调用容器的 getBean("myBean") 方法返回的是 FactoryBean 的代理,而调用 getBean("&myBean") 方法则返回 FactoryBean 实例本身

1.9. 基于注解的容器配置

注解是否比 XML 配置更好?

在引入基于注解的配置之后,便出现了注解是否比 XML 配置更好的问题. 答案是,得看情况,每种方法都有其优缺点,通常由开发人员决定使用那种方式. 首先看看两种定义方式,注解在它们的声明中提供了很多上下文信息,使得配置变得更短、更简洁; 但是,XML 擅长于在不接触源码或者无需反编译的情况下装配组件,一些开发人员更喜欢在源码上使用注解配置. 而另一些人认为注解类不再是 POJO,同时认为注解配置会很分散,最终难以控制.

无论选择如何,Spring 都可以兼顾两种风格,甚至可以将它们混合在一起. Spring 通过其 JavaConfig 选项,允许注解以无侵入的方式使用,即无需接触目标组件源代码. 而且在工具应用方面, Spring Tools for Eclipse 支持所有配置形式.

XML 配置的替代方法是基于注解的配置,它依赖于字节码元数据来连接组件进而替代 XML 声明. 开发人员通过使用相关类、方法或字段上声明的注解来将配置移动到组件类本身. 而不是使用 XML bean 来配置. 如 Example: AutowiredAnnotationBeanPostProcessor 所述,AutowiredAnnotationBeanPostProcessor,将 BeanPostProcessor 与注解混合使用是扩展 Spring IoC 容器的常用方法. 例如,Spring 2.0 引入了使用 @Required 注解强制属性必须在配置的时候被填充, Spring 2.5 使用同样的方式来驱动 Spring 的依赖注入. 本质上,@Autowired 注解提供的功能与 自动装配 中描述的相同,但具有更细粒度的控制和更广泛的适用性. Spring 2.5 还增加了对 JSR-250 注解的支持,例如 @PostConstruct@PreDestroy. Spring 3.0 增加了对 JSR-330 (Dependency Injection for 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 的相同应用程序上下文中查找 bean 上的注解. 这意味着,如果将 <context:annotation-config/> 放在 DispatcherServletWebApplicationContext 中, 它只检查控制器中的 @Autowired bean,而不检查您的服务. 有关更多信息,请参阅 The DispatcherServlet .

1.9.1. @Required

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

Java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}
Kotlin
class SimpleMovieLister {

    @set:Required
    lateinit var movieFinder: MovieFinder

    // ...
}

被注解的 bean 属性必须在配置时通过 bean 定义中的显式赋值或自动注入值. 如果受影响的 bean 属性尚未指定值,容器将抛出异常; 这导致及时的、明确的失败,避免在运行后再抛出 NullPointerException 或类似的异常. 在这里,建议开发者将断言放入 bean 类本身,例如放入 init 方法. 这样做强制执行那些必需的引用和值,即使是在容器外使用这个类.

如果要启用对 @Required 注解的支持https://docs.spring.io/spring-framework/docs/5.3.22/javadoc-api/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.html[RequiredAnnotationBeanPostProcessor]必须注册为 bean.

@RequiredRequiredAnnotationBeanPostProcessor 从 Spring Framework 5.1 开始, @Required 注解已正式弃用,转而使用构造函数注入进行必需的属性设置(或用自定义 InitializingBean.afterPropertiesSet() 的实现或 自定义 @PostConstruct 方法的 bean 属性 setter 方法) .

1.9.2. @Autowired

JSR 330 的 @Inject 注解代替本节中包含的示例中的 Spring 的 @Autowired 注解. 有关详细信息,请参见此处

开发者可以在构造器上使用 @Autowired 注解:

Java
public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

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

    // ...
}
Kotlin
class MovieRecommender @Autowired constructor(
    private val customerPreferenceDao: CustomerPreferenceDao)

从 Spring Framework 4.3 开始,如果目标 bean 仅定义一个构造函数,则不再需要 @Autowired 构造函数. 如果有多个构造函数可用并且没有 primary/default 构造器,则至少有一个必须注解 @Autowired 以让容器知道它使用的是哪个. 可以查阅 构造函数解析 的讨论获取更多细节.

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

Java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}
Kotlin
class SimpleMovieLister {

    @set: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;
    }

    // ...
}
Kotlin
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;
    }

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

    @Autowired
    private lateinit var movieCatalog: MovieCatalog

    // ...
}

确保您的组件(例如,MovieCatalogCustomerPreferenceDao ) 始终按照用于 @Autowired 注入的类型声明. 否则,由于在运行时未找到类型匹配,注入可能会失败.

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

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

Java
public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}
Kotlin
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;
    }

    // ...
}
Kotlin
class MovieRecommender {

    @Autowired
    lateinit var movieCatalogs: Set<MovieCatalog>

    // ...
}

如果想让数组元素或集合元素按特定顺序排列, 可以实现 org.springframework.core.Ordered , 或者使用 @Order 或标准的 @Priority 注解,否则,它们的顺序遵循容器中相应目标 bean 定义的注册顺序.

您可以在类级别和 @Bean 方法上声明 @Order 注解,可能是通过单个 bean 定义(在多个定义使用相同 bean 类的情况下) . @Order 值可能会影响注入点的优先级,但要注意它们不会影响单例启动顺序,这是由依赖和 @DependsOn 声明确定的.

请注意,标准的 javax.annotation.Priority 注解在 @Bean 级别不可用,因为它无法在方法上声明. 它的语义可以通过 @Order 值与 @Primary 定义每个类型的单个 bean 上.

只要键类型是 String,Map 类型就可以自动注入. Map 值将包含所有类型的 bean,并且键将包含相应的 bean 名称. 如以下示例所示:

Java
public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

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

    // ...
}
Kotlin
class MovieRecommender {

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

    // ...
}

默认情况下,当没有候选的 bean 可用时,自动注入将会失败; 对于声明的数组,集合或映射,至少应有一个匹配元素.

默认的处理方式是将带有注解的方法、构造函数和字段标明为必须依赖,也可以使用 @Autowired 中的 required=false 属性. 来标明这种依赖不是必须的,如下:

Java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}
Kotlin
class SimpleMovieLister {

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

    // ...
}

如果不需要的方法(或在多个参数的情况下,其中一个依赖) 不可用,则根本不会调用该方法. 在这种情况下,完全不需要填充非必需字段,而将其默认值保留在适当的位置.

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

每个类仅可以有一个带 @Autowired 注解,并将 required 属性设置为 true 的构造函数,但是可以注解多个构造函数. 在这种情况下,它们都必须声明 required = false 才能被 被视为自动装配的候选对象(类似于XML中的 autowire = constructor).而 Spring 使用的是最贪婪的构造函数.这个构造函数的依赖可以得到满足,那就是具有最多参数的构造函数.同样,如果一个类 声明了多个构造函数,但是没有一个用 @Autowired 注解,然后将使用 primary/default 构造函数(如果存在).如果一个类仅声明一个构造函数,即使没有注解,也将始终使用它.请注意带注解的构造函数不必是 public 的.

推荐使用 @Required 注解来代替 @Autowiredrequired 属性,required 属性表示该属性不是自动装配必需的,如果该属性不能被自动装配. 则该属性会被忽略. 另一方面, @Required 会强制执行通过容器支持的任何方式来设置属性. 如果没有值被注入的话,会引发相应的异常.

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

public class SimpleMovieLister {

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

从 Spring Framework 5.0 开始,您还可以使用 @Nullable 注解(任何包中的任何类型,例如,来自 JSR-305 的 javax.annotation.Nullable)

Java
public class SimpleMovieLister {

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

    @Autowired
    var movieFinder: MovieFinder? = null

    // ...
}

您也可以使用 @Autowired 作为常见的可解析依赖的接口,BeanFactory, ApplicationContext, Environment, ResourceLoader, ApplicationEventPublisher, 和 MessageSource 这些接口及其扩展接口 (例如 ConfigurableApplicationContextResourcePatternResolver) 会自动解析,无需特殊设置. 以下示例自动装配 ApplicationContext 对象:

Java
public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...
}
Kotlin
class MovieRecommender {

    @Autowired
    lateinit var context: ApplicationContext

    // ...
}

@Autowired, @Inject, @Value, 和 @Resource 注解 由 Spring BeanPostProcessor 实现. 也就是说开发者不能使用自定义的 BeanPostProcessor 或者自定义 BeanFactoryPostProcessor 来使用这些注解 必须使用 XML 或 Spring @Bean 方法显式地"连接"这些类型.

1.9.3. @Primary

由于按类型的自动注入可能匹配到多个候选者,所以通常需要对选择过程添加更多的约束. 使用 Spring 的 @Primary 注解是实现这个约束的一种方法. 它表示如果存在多个候选者且另一个 bean 只需要一个特定类型的 bean 依赖时,就明确使用标记有 @Primary 注解的那个依赖. 如果候选中只有一个"Primary" bean,那么它就是自动注入的值

请考虑以下配置,将 firstMovieCatalog 定义为主要 MovieCatalog :

Java
@Configuration
public class MovieConfiguration {

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

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

    // ...
}
Kotlin
@Configuration
class MovieConfiguration {

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

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

    // ...
}

使用上述配置,以下 MovieRecommender 将与 firstMovieCatalog 一起自动装配:

Java
public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...
}
Kotlin
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. @Qualifier

@Primary 是一种用于解决自动装配多个值的注入的有效的方法,当需要对选择过程做更多的约束时,可以使用 Spring 的 @Qualifier 注解,可以为指定的参数绑定限定的值. 缩小类型匹配集,以便为每个参数选择特定的 bean. 在最简单的情况下,这可以是一个简单的描述性值,如以下示例所示:

Java
public class MovieRecommender {

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

    // ...
}
Kotlin
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;
    }

    // ...
}
Kotlin
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 带有限定符 main 的 bean 会被装配到拥有相同值的构造方法参数上.
2 带有限定符 action 的 bean 会被装配到拥有相同值的构造方法参数上.

bean 的 name 会作为备用的 qualifier 值,因此可以定义 bean 的 idmain 替代内嵌的 qualifier 元素.这种匹配方式同样有效. 但是,虽然可以使用这个约定来按名称引用特定的 bean, 但是 @Autowired 默认是由带限定符的类型驱动注入的. 这就意味着 qualifier 值,甚至是 bean 的 name 作为备选项,只是为了缩小类型匹配的范围. 它们在语义上不表示对唯一 bean id 的引用. 良好的限定符值是像 mainEMEApersistent 这样的,能表示与 bean id 无关的特定组件的特征,在匿名 bean 定义的情况下可以自动生成.

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

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

如果打算 by name 来驱动注解注入,那么就不要使用 @Autowired(多数情况) ,即使在技术上能够通过 @Qualifier 值引用 bean 名字. 相反,应该使用 JSR-250 @Resource 注解,该注解在语义上定义为通过其唯一名称标识特定目标组件, 其中声明的类型与匹配进程无关. @Autowired 具有多种不同的语义,在 by type 选择候选 bean 之后,指定的 String 限定的值只会考虑这些被选择的候选者. 例如将 account 限定符与标有相同限定符标签的 bean 相匹配.

对于自身定义为 collection, Map, 或者 array 类型的 bean, @Resource 是一个很好的解决方案,通过唯一名称引用特定的集合或数组 bean. 也就是说,从 Spring4.3 开始, 只要元素类型信息保存在 @Bean 返回类型签名或集合(或其子类) 中,您就可以通过 Spring 的 @Autowired 类型匹配算法匹配 Map 和数组类型. 在这种情况下,可以使用限定的值来选择相同类型的集合,如上一段所述.

从 Spring4.3 开始,@Autowired 也考虑了注入的自引用,即引用当前注入的 bean. 自引用只是一种后备选项,还是优先使用正常的依赖注入操作其它 bean. 在这个意义上,自引用不参与到正常的候选者选择中,并且总是次要的, 相反,它们总是拥有最低的优先级. 在实践中,自引用通常被用作最后的手段. 例如,通过 bean 的事务代理在同一实例上调用其他方法 在这种情况下,考虑将受影响的方法分解为单独委托的 bean,或者使用 @Resource,它可以通过其唯一名称获取代理返回到当前的 bean 上.

尝试将 @Bean 方法的结果注入相同的配置类也实际上是一种自引用方案. 要么在实际需要的方法签名中惰性解析此类引用(与配置类中的自动装配字段相对) , 要么将受影响的 @Bean 方法声明为静态,将其与包含的配置类实例及其生命周期脱钩. 否则,仅在回退阶段考虑此类 Bean,而将其他配置类上的匹配 Bean 选作主要候选对象(如果可用) .

@Autowired 可以应用在字段、构造函数和多参数方法上,允许在参数上使用 qualifier 限定符注解缩小取值范围. 相比之下,@Resource 仅支持具有单个参数的字段和 bean 属性 setter 方法. 因此,如果注入目标是构造函数或多参数方法,请使用 qualifiers 限定符.

开发者也可以创建自定义的限定符注解,只需定义一个注解,在其上提供了 @Qualifier 注解即可. 如以下示例所示:

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

    String value();
}
Kotlin
@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;
    }

    // ...
}
Kotlin
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/> 标签的子元素,然后指定 type 类型和 value 值来匹配自定义的 qualifier 注解. type 是自定义注解的权限定类名(包路径+类名) . 如果没有重名的注解,那么可以使用类名(不含包路径) . 以下示例演示了两种方法:

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

类路径扫描和组件管理,将展示一个基于注解的替代方法,可以在 XML 中提供 qualifier 元数据, 请参阅为注解提供Qualifier元数据.

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

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

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

然后将注解添加到需要自动注入的字段或属性中:

Java
public class MovieRecommender {

    @Autowired
    @Offline (1)
    private MovieCatalog offlineCatalog;

    // ...
}
1 This line adds the @Offline annotation.
Kotlin
class MovieRecommender {

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

    // ...
}
1 此行添加 @Offline 注解.

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

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

开发者还可以为自定义限定名 qualifier 注解增加属性,用于替代简单的 value 属性. 如果在要自动注入的字段或参数上指定了多个属性值,则 bean 的定义必须全部匹配这些属性值才能被视为自动注入候选者. 例如,请考虑以下注解定义:

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

    String genre();

    Format format();
}
Kotlin
@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
}
Kotlin
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;

    // ...
}
Kotlin
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 meta 属性而不是使用 <qualifier/> 子元素. 如果可行, <qualifier/> 元素及其属性优先, 但如果不存在此类限定符,那么自动注入机制会使用 <meta/> 标签中提供的值,如以下示例中的最后两个 bean 定义:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        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();
    }
}
Kotlin
@Configuration
class MyConfiguration {

    @Bean
    fun stringStore() = StringStore()

    @Bean
    fun integerStore() = IntegerStore()
}

假设上面的 bean 都实现了泛型接口,即 Store<String>Store<Integer> ,那么可以用 @Autowire 来注解 Store 接口, 并将泛型用作限定符,如下例所示:

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

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean
Kotlin
@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;
Kotlin
// 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,它允许开发者注册自定义的 qualifier 注解类型,而无需指定 @Qualifier 注解,以下示例显示如何使用 CustomAutowireConfigurer:

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

AutowireCandidateResolver 通过以下方式确定自动注入的候选者:

  • 每个bean定义的 autowire-candidate

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

  • 存在 @Qualifier 注解以及使用 CustomAutowireConfigurer 注册的任何自定义注解

当多个 bean 有资格作为自动注入的候选项时,primary 的确定如下: 如果候选者中只有一个 bean 定义的 primary 属性设置为 true,则选择它.

1.9.7. @Resource

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

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

Java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

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

如果未明确指定名称,则默认名称是从字段名称或 setter 方法生成的. 如果是字段,则采用字段名称. 在 setter 方法的情况下,它采用 bean 属性名称. 下面的例子将把名为 movieFinde r的 bean 注入其 setter 方法:

Java
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    @set:Resource
    private lateinit var movieFinder: MovieFinder

}
ApplicationContext 若使用了 CommonAnnotationBeanPostProcessor ,注解提供的name名字将被解析为 bean 的 name 名字. 如果配置了 Spring 的 SimpleJndiBeanFactory, 这些 name 名称就可以通过 JNDI 解析. 但是,推荐使用默认的配置,简单地使用 Spring 的 JNDI,这样可以保持逻辑引用. 而不是直接引用.

@Resource 在没有明确指定 name 时,其行为类似于 @Autowired,对于特定bean(Spring API内的 bean) , @Resource 找到主要类型匹配而不是特定的命名 bean, 并解析众所周知的可解析依赖: ApplicationContext, ResourceLoader, ApplicationEventPublisher, 和 MessageSource 接口.

因此,在以下示例中,customerPreferenceDao 字段首先查找名为 customerPreferenceDao 的 bean,如果未找到,则会使用类型匹配 CustomerPreferenceDao 类的实例:

Java
public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; (1)

    public MovieRecommender() {
    }

    // ...
}
1 context 将会注入 ApplicationContext
Kotlin
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;
    }
}
Kotlin
@Component
class MovieRecommender(@Value("\${catalog.name}") private val catalog: String)

使用以下配置:

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

以及以下 application.properties 文件:

catalog.name=MovieCatalog

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

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

Java
@Configuration
public class AppConfig {

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

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

如果无法解析任何 ${} 占位符,则使用上述配置 Spring 初始化会失败. 也可以使用 setPlaceholderPrefix,setPlaceholderSuffixsetValueSeparator 之类的方法来自定义占位符.

Spring Boot 默认配置一个 PropertySourcesPlaceholderConfigurer bean,它将从 application.propertiesapplication.yml 文件获取属性.

Spring 提供的内置转换器支持允许自动处理简单的类型转换(例如,转换为 Integerint) . 多个逗号分隔的值可以自动转换为 String 数组,而无需付出额外的努力.

可以提供如下默认值:

Java
@Component
public class MovieRecommender {

    private final String catalog;

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

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

Java
@Configuration
public class AppConfig {

    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        conversionService.addConverter(new MyCustomConverter());
        return conversionService;
    }
}
Kotlin
@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;
    }
}
Kotlin
@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;
    }
}
Kotlin
@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 bean 生命周期中调用相应的方法,就像是 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...
    }
}
Kotlin
class CachingMovieLister {

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

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

有关组合各种生命周期机制的影响的详细信息,请参阅组合生命周期策略.

@Resource 一样,@PostConstruct@PreDestroy 注解也是 JDK6-8 的标准 java 库中的一部分,但是,在 JDK 9 中,整个 javax.annotation 和 java 核心模块分离,最终在 java 11 中移除. 如果你需要引用,则通过 maven 获取 javax.annotation-api artifact. 就像其他任何库一样,只需添加到应用程序的类路径中即可.

1.10. 类路径扫描和管理组件类路径扫描和管理组件

本章中的大多数示例会使用 XML 配置指定在 Spring 容器中生成每个 BeanDefinition 的元数据,上一节(基于注解的容器配置) 演示了如何通过源代码注解提供大量的元数据配置. 然而,即使在这些示例中,注解也仅仅用于驱动依赖注入. "base" bean依然会显式地在XML文件中定义. 本节介绍通过扫描类路径隐式检测候选组件的选项. 候选者组件是 class 类, 这些类经过过滤匹配,由 Spring 容器注册的 bean 定义会成为 Spring bean. 这消除了使用 XML 执行 bean 注册的需要(也就是没有 XML 什么事儿了),可以使用注解(例如 @Component), AspectJ 类型表达式或开发者自定义过滤条件来选择哪些类将在容器中注册 bean 定义.

从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多功能都是核心 Spring 框架的一部分. 这允许开发者使用 Java 而不是使用传统的 XML 文件来定义 bean. 有关如何使用这些新功能的示例,请查看 @Configuration, @Bean,@Import, 和 @DependsOn 注解.

1.10.1. @Component 注解和更多模板注解

@Repository 注解用于满足存储库(也称为数据访问对象或DAO)的情况,这个注解的用途是自动转换异常. 如异常转换中所述.

Spring 提供了更多的构造型注解: @Component,@Service, 和 @Controller. @Component 可用于管理任何 Spring 的组件. @Repository, @Service, 或 @Controller@Component 的特殊化. 用于更具体的用例(分别在持久性,服务和表示层中) . 因此,您可以使用 @Component 注解组件类,但是,通过使用 @Repository, @Service, 和 @Controller 注解它们,能够让你的类更易于被合适的工具处理或与相应的切面关联. 例如,这些注解可以使目标组件变成切入点. 在 Spring 框架的未来版本中,@Repository, @Service, 和 @Controller 也可能带有附加的语义. 因此,如果在使用 @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 相同的方式处理.
Kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component (1)
annotation class Service {

    // ...
}
1 @Component 使 @Service 以与 @Component 相同的方式处理.

元注解也可以进行组合,进而创建组合注解. 例如,来自 Spring MVC 的 @RestController 注解是由 @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;

}
Kotlin
@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 {
    // ...
}
Kotlin
@Service
@SessionScope
class SessionScopedService {
    // ...
}

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

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

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

1.10.3. 自动探测类并注册 bean 定义

Spring 可以自动检测各代码层中被注解的类,并使用 ApplicationContext 内注册相应的 BeanDefinition. 例如,以下两个类就可以被自动探测:

Java
@Service
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

想要自动检测这些类并注册相应的 bean,需要在 @Configuration 配置中添加 @ComponentScan 注解,其中 basePackages 属性是两个类的父包路径. (或者,您可以指定以逗号或分号或空格分隔的列表,其中包含每个类的父包) .

Java
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}
Kotlin
@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:component-scan> 时,通常不需要包含 <context:annotation-config> 元素.

类路径扫描的包必须保证这些包出现在类路径中. 当使用 Ant 构建 JAR 时,请确保你没有激活 JAR 任务的纯文件开关. 此外在某些环境装由于安全策略,类路径目录可能不能访问. JDK 1.7.0_45 及更高版本上的独立应用程序(需要在清单中设置 'Trusted-Library') - 请参阅 stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources) .

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

在使用 component-scan 元素时, AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 都会隐式包含. 意味着这两个组件也是自动探测和注入的. 所有这些都无需 XML 配置.

您可以通过 annotation-config=false 属性来禁用 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 的注册.

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

默认情况下,使用 @Component, @Repository, @Service,@Controller @Configuration 注解的类或者注解为 @Component 的自定义注解类才能被检测为候选组件. 但是,开发者可以通过应用自定义过滤器来修改和扩展此行为. 将它们添加为 @ComponentScan 注解的 includeFiltersexcludeFilters 参数(或作为 component-scan 元素. 元素的 include-filterexclude-filter 子元素. 每个 filter 元素都需要包含 typeexpression 属性. 下表介绍了筛选选项:

Table 5. 过滤类型
过滤类型 表达式例子 描述

annotation (default)

org.example.SomeAnnotation

要在目标组件中的类级别出现的注解.

assignable

org.example.SomeClass

目标组件可分配给(继承或实现) 的类(或接口) .

aspectj

org.example..*Service+

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

regex

org\.example\.Default.*

要由目标组件类名匹配的正则表达式.

custom

org.example.MyTypeFilter

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

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

Java
@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    // ...
}
Kotlin
@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, 或 @Configuration.

1.10.5. 在组件中定义bean的元数据

Spring 组件也可以向容器提供 bean 定义元数据. 在 @Configuration 注解的类中使用 @Bean 注解定义 bean 元数据(也就是 Spring bean),以下示例显示了如何执行此操作:

Java
@Component
public class FactoryMethodComponent {

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

    public void doWork() {
        // Component method implementation omitted
    }
}
Kotlin
@Component
class FactoryMethodComponent {

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

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

这个类是一个 Spring 组件,它有个 doWork() 方法. 然而,它还有一个工厂方法 publicInstance() 用于产生 bean 定义. @Bean 注解了工厂方法, 还设置了其他 bean 定义的属性,例如通过 @Qualifier 注解的 qualifier 值. 可以指定的其他方法级别的注解是 @Scope, @Lazy 以及自定义的 qualifier 注解.

除了用于组件初始化的角色之外,@Lazy 注解也可以在 @Autowired 或者 @Inject 注解上,在这种情况下,该注入将会变成延迟注入代理 lazy-resolution proxy(也就是懒加载) 对于复杂的惰性交互,尤其是组合对于可选依赖项,我们推荐使用 ObjectProvider<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);
    }
}
Kotlin
@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,名称为 country 的 bean 自动装配为另一个名为 privateInstance 的 bean 的 age 属性值. Spring 表达式语言元素通过记号 #{ <expression> } 来定义属性的值. 对于 @Value 注解,表达式解析器在解析表达式后,会查找 bean 的名字并设置 value 值.

从 Spring4.3 开始,您还可以声明一个类型为 InjectionPoint 的工厂方法参数(或其更具体的子类: DependencyDescriptor) 以访问触发创建当前 bean 的请求注入点. 请注意,这仅适用于真实创建的 bean 实例,而不适用于注入现有实例. 因此,这个特性对 prototype scope 的 bean 最有意义. 对于其他作用域,工厂方法将只能看到触发在给定 scope 中创建新 bean 实例的注入点. 例如,触发创建一个延迟单例 bean 的依赖. 在这种情况下,使用提供的注入点元数据拥有优雅的语义. 以下示例显示了如何使用 InjectionPoint:

Java
@Component
public class FactoryMethodComponent {

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

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

在 Spring 组件中处理 @Bean 和在 @Configuration 中处理是不一样的,区别在于,在 @Component 中,不会使用 CGLIB 增强去拦截方法和属性的调用. 在 @Configuration 注解的类中, @Bean 注解创建的 bean 对象会使用 CGLIB 代理对方法和属性进行调用. 方法的调用不是常规的 Java 语法,而是通过容器来提供通用的生命周期管理和代理 Spring bean, 甚至在通过编程的方式调用 @Bean 方法时也会产生对其它 bean 的引用. 相比之下,在一个简单的 @Component 类中调用 @Bean 方法中的方法或字段具有标准 Java 语义,这里没有用到特殊的 CGLIB 处理或其他约束.

开发者可以将 @Bean 方法声明为 static 的,并允许在不将其包含的配置类作为实例的情况下调用它们. 这在定义后置处理器 bean 时是特别有意义的. 例如 BeanFactoryPostProcessorBeanPostProcessor),,因为这类 bean 会在容器的生命周期前期被初始化,而不会触发其它部分的配置.

对静态 @Bean 方法的调用永远不会被容器拦截,即使在 @Configuration 类内部. 这是用为 CGLIB 的子类代理限制了只会重写非静态方法. 因此, 对另一个 @Bean 方法的直接调用只能使用标准的 Java 语法. 也只能从工厂方法本身直接返回一个独立的实例.

由于 Java 语言的可见性,@Bean 方法并不一定会对容器中的 bean 有效. 开发者可能很随意的在非 @Configuration 类中定义为静态方法. 然而, 在 @Configuration 类中的正常 @Bean 方法都会被重写,因此,它们不应该定义为 privatefinal.

@Bean 方法也可以用在父类中,同样适用于 Java 8 接口中的默认方法. 这使得组建复杂的配置时能具有更好的灵活性,甚至可能通过 Java 8 的默认方法实现多重继承. 这种特性在 Spring 4.2 开始支持.

最后,请注意,单个类可以为同一个 bean 保存多个 @Bean 方法,例如根据运行时可用的依赖选择合适的工厂方法. 使用算法会选择 "最贪婪" 的构造方法, 一些场景可能会按如下方法选择相应的工厂方法: 满足最多依赖的会被选择,这与使用 @Autowired 时选择多个构造方法时类似.

1.10.6. 命名自动注册组件

扫描处理过程,其中一步就是自动探测组件,扫描器使用 BeanNameGenerator 对探测到的组件命名. 默认情况下,各代码层注解( @Component, @Repository, @Service, 和 @Controller)所包含的 name 值,将会作为相应的 bean 定义的名字.

如果这些注解没有 name 值,或者是其他一些被探测到的组件(比如使用自定义过滤器探测到的),默认会又 bean name 生成器生成,使用小写类名作为 bean 名字. 例如,如果检测到以下组件类,则名称为 myMovieListermovieFinderImpl:

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

如果您不想依赖默认的 bean 命名策略,则可以提供自定义 bean 命名策略. 首先,实现 BeanNameGenerator 接口,并确保包括一个默认的无参构造函数. 然后,在配置扫描程序时提供完全限定的类名,如以下示例注解和 bean 定义所示:

如果由于多个自动检测到的组件具有相同的非限定类名称(即,具有相同名称但位于以下位置的类不同的软件包) ,则可能需要配置一个默认为 BeanNameGenerator 的 生成的bean名称的完全限定的类名称. 从 Spring Framework 5.2.3 开始, 位于 org.springframework.context.annotation 包中的 FullyQualifiedAnnotationBeanNameGenerator 可以用于这种目的.
Java
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    // ...
}
Kotlin
@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 {
    // ...
}
Kotlin
@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
    // ...
}
@Scope 注解仅在具体 bean 类(用于带注解的组件) 或工厂方法(用于 @Bean 方法) 上进行关联. 与 XML bean 定义相比,没有 bean 继承的概念,并且 类级别的继承结构与元数据无关.

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

想要提供自定义作用域的解析策略,而不是依赖于基于注解的方法,那么需要实现 ScopeMetadataResolver 接口,并确保包含一个默认的无参数构造函数. 然后,在配置扫描程序时提供完全限定类名. 以下注解和 bean 定义示例显示:
Java
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    // ...
}
Kotlin
@Configuration
@ComponentScan(basePackages = ["org.example"], scopeResolver = MyScopeResolver::class)
class AppConfig {
    // ...
}
<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

当使用某个非单例作用域时,为作用 domain 对象生成代理可能非常必要,原因参看 依赖有 Scope 的 Bean 作为依赖的作用域 bean. 为此,组件扫描元素上提供了 scoped-proxy 属性. 三个可能的值是: no, interfaces, 和 targetClass. 例如,以下配置导致标准 JDK 动态代理:

Java
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
    // ...
}
Kotlin
@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元数据

在前面使用 @Qualifier 讨论过 @Qualifier 注解. 该部分中的示例演示了在解析自动注入候选者时使用 @Qualifier 注解和自定义限定符注解以提供细粒度控制. 因为这些示例基于 XML bean 定义,所以使用 XML 中的 bean 元素的 qualifiermeta 子元素在候选 bean 定义上提供了限定符元数据. 当依靠类路径扫描并自动检测组件时, 可以在候选类上提供具有类型级别注解的限定符元数据. 以下三个示例演示了此技术:

Java
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
Kotlin
@Component
@Qualifier("Action")
class ActionMovieCatalog : MovieCatalog
Java
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
Kotlin
@Component
@Genre("Action")
class ActionMovieCatalog : MovieCatalog {
    // ...
}
Java
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}
Kotlin
@Component
@Offline
class CachingMovieCatalog : MovieCatalog {
    // ...
}
与大多数基于注解的替代方法一样,注解元数据绑定到类定义本身,而使用在 XML 配置时,允许同一类型的 beans 在 qualifier 元数据中提供变量, 因为元数据是依据实例而不是类来提供的.

1.10.9. 生成候选组件的索引

虽然类路径扫描非常快,但通过在编译时创建候选的静态列表. 可以提高大型应用程序的启动性能. 在此模式下,应用程序的所有模块都必须使用此机制.

@ComponentScan<context: component-scan/> 指令保留为是请求上下文扫描某些程序包中的候选项. 当 ApplicationContext 检测到此类索引时,它将自动使用它,而不是扫描类路径.

若要生成索引, 只需向包含组件扫描指令目标组件的每个模块添加一个附加依赖. 以下示例显示了如何使用 Maven 执行此操作:

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

在 Gradle 4.5 及更早版本中,依赖应在 compileOnly 中声明配置,如以下示例所示:

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

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

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

spring-context-indexer artifact 将产生一个名为 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.Inject 可以在 Maven 中央仓库中找到 repository (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 注解实现依赖注入

@javax.inject.Inject 可以使用以下的方式来替代 @Autowired 注解:

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(...);
        // ...
    }
}
Kotlin
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(...);
        // ...
    }
}
Kotlin
import javax.inject.Inject

class SimpleMovieLister {

    @Inject
    lateinit var movieFinder: Provider<MovieFinder>


    fun listMovies() {
        movieFinder.get().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;
    }

    // ...
}
Kotlin
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.Optional@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) {
        // ...
    }
}
Kotlin
class SimpleMovieLister {

    @Inject
    var movieFinder: MovieFinder? = null
}

1.11.2. @Named@ManagedBean 注解: 标准与 @Component 注解相同

可以使用 @javax.inject.Namedjavax.annotation.ManagedBean 来替代 @Component 注解:

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

    // ...
}
Kotlin
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;
    }

    // ...
}
Kotlin
import javax.inject.Inject
import javax.inject.Named

@Named
class SimpleMovieLister {

    @Inject
    lateinit var movieFinder: MovieFinder

    // ...
}

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

Java
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}
Kotlin
@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig  {
    // ...
}
@Component 相反,JSR-330 @Named 和 JSR-250 @ManagedBean 注解不可组合. 请使用 Spring 的原型模型(stereotype mode)来构建自定义组件注解.

1.11.3. 使用 JSR-330标准注解的限制

使用标准注解时,需要知道哪些重要功能是不可用的. 如下表所示:

Table 6. Spring 的组件模型元素 vs JSR-330 变量
Spring javax.inject.* javax.inject restrictions / comments

@Autowired

@Inject

@Inject 没有 'required' 属性. 可以与 Java 8 的 Optional 一起使用.

@Component

@Named / @ManagedBean

JSR-330 不提供可组合模型,只是一种识别命名组件的方法.

@Scope("singleton")

@Singleton

JSR-330 的默认作用域就像 Spring 的 prototype. 但是,为了使其与 Spring 的一般默认值保持一致,默认情况下,Spring 容器中声明的 JSR-330 bean 是一个 singleton. 为了使用除 singleton 之外的作用域,您应该使用 Spring 的 @Scope 注解. javax.inject 还提供了 @Scope 注解. 然而,这个仅用于创建自己的注解.

@Qualifier

@Qualifier / @Named

javax.inject.Qualifier 只是用于构建自定义限定符的元注解. 可以通过 javax.inject.Named 创建与 Spring 中 @Qualifier 一样的限定符.

@Value

-

@Required

-

@Lazy

-

ObjectFactory

Provider

javax.inject.Provider javax.inject.Provider 是 Spring 的 ObjectFactory 的直接替代品, 仅仅使用简短的 get() 方法即可. 它也可以与 Spring 的 @Autowired 结合使用,也可以与非注解的构造函数和 setter 方法结合使用.

1.12. 基于 Java 的容器配置

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

1.12.1. 基本概念: @Bean@Configuration

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

@Bean 注解用于表明方法的实例化,、配置和初始化都是由 Spring IoC 容器管理的新对象,对于那些熟悉 Spring 的 <beans /> XML 配置的人来说, @Bean 注解扮演的角色与 <bean /> 元素相同. 开发者可以在任意的 Spring @Component 中使用 @Bean 注解方法 ,但大多数情况下,@Bean 是配合 @Configuration 使用的.

使用 @Configuration 注解类时,这个类的目的就是作为 bean 定义的地方. 此外,@Configuration 类允许通过调用同一个类中的其他 @Bean 方法来定义 bean 间依赖. 最简单的 @Configuration 类如下所示:

Java
@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}
Kotlin
@Configuration
class AppConfig {

    @Bean
    fun myService(): MyService {
        return MyServiceImpl()
    }
}

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

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
完整的 @Configuration 模式对比 "lite" 模式的 @Bean?

@Bean 方法在没有用 @Configuration 注解的类中声明时,它们将会被称为 “lite” 的模式处理. 例如,@Component 中声明的 bean 方法或者一个普通的旧类中的 bean 方法将被视为 “lite” 的. 包含类的主要目的不同,而 @Bean 方法在这里是一种额外的好处. 例如,服务组件可以通过在每个适用的组件类上使用额外的 @Bean 方法将管理视图暴露给容器. 在这种情况下,@Bean 方法是一种通用的工厂方法机制.

与完整的 @Configuration 不同, “lite” 的 @Bean 方法不能声明 bean 之间的依赖. 相反,它们对其包含组件的内部状态进行操作,并且可以有选择的对它们可能声明的参数进行操作. 因此,这样的 @Bean 注解的方法不应该调用其他 @Bean 注解的方法. 每个这样的方法实际上只是特定 bean 引用的工厂方法,没有任何特殊的运行时语义. 不经过 CGLIB 处理,所以在类设计方面没有限制(即,包含类可能是 final 的) .

在常见的场景中,@Bean 方法将在 @Configuration 类中声明,确保始终使用 “full” 模式,这将防止相同的 @Bean 方法被意外地多次调用,这有助于减少在 “lite” 模式下操作时难以跟踪的细微错误.

@Bean@Configuration 注解将在下面的章节深入讨论,首先,我们将介绍使用基于 Java 代码的配置来创建 Spring 容器的各种方法.

1.12.2. 使用 AnnotationConfigApplicationContext 初始化 Spring 容器

以下部分介绍了 Spring 的 AnnotationConfigApplicationContext,它是在 Spring 3.0 中引入的. 这是一个强大的(versatile) ApplicationContext 实现,它不仅能解析 @Configuration 注解类 ,也能解析 @Component 注解的类和使用 JSR-330 注解的类.

当使用 @Configuration 类作为输入时,@Configuration 类本身被注册为一个 bean 定义,类中所有声明的 @Bean 方法也被注册为 bean 定义.

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

简单结构

与实例化 ClassPathXmlApplicationContext 时 Spring XML 文件用作输入的方式大致相同, 在实例化 AnnotationConfigApplicationContext 时可以使用 @Configuration 类作为输入. 这允许完全无 XML 使用 Spring 容器,如以下示例所示:

Java
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
Kotlin
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();
}
Kotlin
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()
}

上面假设 MyServiceImpl, Dependency1, 和 Dependency2 使用 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();
}
Kotlin
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 此注解可启用组件扫描.
Kotlin
@Configuration
@ComponentScan(basePackages = ["com.acme"]) (1)
class AppConfig  {
    // ...
}
1 此注解可启用组件扫描.

有经验的用户可能更熟悉使用 XML 的 context: 命名空间配置形式,如下例所示:

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

上面的例子中,com.acme 包会被扫描,只要是使用了 @Component 注解的类,都会被注册进容器中. 同样地,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);
}
Kotlin
fun main() {
    val ctx = AnnotationConfigApplicationContext()
    ctx.scan("com.acme")
    ctx.refresh()
    val myService = ctx.getBean<MyService>()
}
请记住 @Configuration 类是使用 @Component 进行 元注解的,因此它们是组件扫描的候选者. 在前面的示例中, 假设 AppConfigcom.acme 包(或下面的任何包) 中声明,它在 scan() 调用期间被拾取. 在 refresh() 之后,它的所有 @Bean 方法都被处理并在容器中注册为 bean 定义.
使用 AnnotationConfigWebApplicationContext 支持Web应用程序

WebApplicationContextAnnotationConfigApplicationContext 的结合是 AnnotationConfigWebApplicationContext 配置. 这个实现可以用于配置 Spring ContextLoaderListener servlet 监听器 ,Spring MVC 的 DispatcherServlet 等等. 以下 web.xml 代码段配置典型的 Spring MVC Web 应用程序(请注意 contextClass context-param 和 init-param 的使用) :

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

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

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

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

    <!-- map all requests for /app/* to the dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>
对于编程用例,GenericWebApplicationContext 可以替代 AnnotationConfigWebApplicationContext。 见 GenericWebApplicationContext javadoc 了解详细信息。

1.12.3. 使用 @Bean 注解

@Bean 是一个方法级别的注解,它与 XML 中的 <bean/> 元素类似. 注解支持 <bean/> 提供的一些属性,例如

开发者可以在 @Configuration 类或 @Component 类中使用 @Bean 注解.

声明一个 Bean

要声明一个 bean,只需使用 @Bean 注解方法即可. 使用此方法,将会在 ApplicationContext 内注册一个 bean,bean 的类型是方法的返回值类型. 默认情况下, bean 名称将与方法名称相同. 以下示例显示了 @Bean 方法声明:

Java
@Configuration
public class AppConfig {

    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}
Kotlin
@Configuration
class AppConfig {

    @Bean
    fun transferService() = TransferServiceImpl()
}

前面的配置完全等同于以下 Spring XML:

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

这两个声明都在 ApplicationContext 中创建一个名为 transferService 的 bean,并且绑定了 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();
    }
}
Kotlin
@Configuration
class AppConfig {

    @Bean
    fun transferService(): TransferService {
        return TransferServiceImpl()
    }
}

但是,这会将预先类型预测的可见性限制为指定的接口类型(TransferService),然后在实例化受影响的单一 bean 时,只知道容器的完整类型(TransferServiceImpl) . 非延迟的单例 bean 根据它们的声明顺序进行实例化,因此开发者可能会看到不同类型的匹配结果,这具体取决于另一个组件尝试按未类型匹配的时间(如 @Autowired TransferServiceImpl, 一旦 transferService bean 已被实例化,这个问题就被解决了).

如果通过声明的服务接口都是引用类型,那么 @Bean 返回类型可以安全地加入该设计决策.但是,对于实现多个接口的组件或可能由其实现类型引用的组件, 更安全的方法是声明可能的最具体的返回类型(至少按照注入点所要求的特定你的bean) .
Bean 的依赖

一个使用 @Bean 注解的方法可以具有任意数量的参数描述构建该 bean 所需的依赖,例如,如果我们的 TransferService 需要 AccountRepository, 我们可以使用方法参数来实现该依赖,如以下示例所示:

Java
@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}
Kotlin
@Configuration
class AppConfig {

    @Bean
    fun transferService(accountRepository: AccountRepository): TransferService {
        return TransferServiceImpl(accountRepository)
    }
}

这个解析机制与基于构造函数的依赖注入非常相似. 有关详细信息,请参阅相关部分

接收生命周期回调

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

完全支持常规的 Spring 生命周期回调. 如果 bean 实现 InitializingBean, DisposableBean, 或 Lifecycle,则它们各自的方法由容器调用.

同样地,还完全支持标准的 *Aware,如 BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware.

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

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();
    }
}
Kotlin
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 Config 定义的 bean 中 close 方法或者 shutdown 方法,会作为销毁回调而自动调用. 若 bean 中有 closeshutdown 方法,并且您不希望在容器关闭时调用它,则可以将 @Bean(destroyMethod="") 添加到 bean 定义中以禁用默认 (inferred) 模式.

开发者可能希望对通过 JNDI 获取的资源执行此操作,因为它的生命周期是在应用程序外部管理的. 更进一步,使用 DataSource 时一定要关闭它,不关闭将会出问题.

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

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

同样地,使用 @Bean 方法,通常会选择使用程序化的 JNDI 查找: 使用 Spring 的 JndiTemplate/JndiLocatorDelegate 帮助类或直接使用 JNDI 的 InitialContext , 但是不要使用 JndiObjectFactoryBean 的变体,因为它会强制开发者声明一个返回类型作为 FactoryBean 的类型用于代替实际的目标类型,这会使得交叉引用变得很困难.

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

Java
@Configuration
public class AppConfig {

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

    // ...
}
Kotlin
@Configuration
class AppConfig {

    @Bean
    fun beanOne() = BeanOne().apply {
        init()
    }

    // ...
}
当您直接使用 Java( new 对象那种) 工作时,您可以使用对象执行任何您喜欢的操作,并且不必总是依赖于容器生命周期.
指定 Bean 的作用域

Spring包含 @Scope 注解,以便您可以指定 bean 的作用域.

使用 @Scope 注解

可以使用任意标准的方式为 @Bean 注解的 bean 指定一个作用域,你可以使用Bean Scopes中的任意标准作用域

默认作用域是 singleton 的,但是可以使用 @Scope 注解来覆盖. 如下例所示:

Java
@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
}
Kotlin
@Configuration
class MyConfiguration {

    @Bean
    @Scope("prototype")
    fun encryptor(): Encryptor {
        // ...
    }
}
@Scopescoped-proxy

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

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

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;
}
Kotlin
// 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();
    }
}
Kotlin
@Configuration
class AppConfig {

    @Bean("myThing")
    fun thing() = Thing()
}
Bean 的别名

正如 Bean 的命名 中所讨论的,有时需要为单个 bean 提供多个名称,也称为 bean 别名. @Bean 注解的 name 属性为此接受 String 数组. 以下示例显示如何为 bean 设置多个别名:

Java
@Configuration
public class AppConfig {

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

    @Bean("dataSource", "subsystemA-dataSource", "subsystemB-dataSource")
    fun dataSource(): DataSource {
        // instantiate, configure and return DataSource bean...
    }
}
Bean 的描述

有时,提供更详细的 bean 文本描述会很有帮助. 当 bean 被暴露(可能通过 JMX) 用于监视目的时,这可能特别有用.

要向 @Bean 添加描述,可以使用 @Description 注解,如以下示例所示:

Java
@Configuration
public class AppConfig {

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

    @Bean
    @Description("Provides a basic example of a bean")
    fun thing() = Thing()
}

1.12.4. 使用 @Configuration 注解

@Configuration 是一个类级别的注解,表明该类将作为 bean 定义的元数据配置. @Configuration 类会将有 @Bean 注解的暴露方法声明为 bean .在 @Configuration 类上调用 @Bean 方法也可以用于定义 bean 间依赖, 有关一般介绍,请参阅 基本概念:基本概念: @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();
    }
}
Kotlin
@Configuration
class AppConfig {

    @Bean
    fun beanOne() = BeanOne(beanTwo())

    @Bean
    fun beanTwo() = BeanTwo()
}

在前面的示例中,beanOne 通过构造函数注入接收对 beanTwo 的引用.

这种声明 bean 间依赖的方法只有在 @Configuration 类中声明 @Bean 方法时才有效. 您不能使用普通的 @Component 类声明 bean 间依赖.
查找方法注入

如前所述,查找方法注入 是一项很少使用的高级功能. 在单例作用域的 bean 依赖于原型作用域的 bean 的情况下,它很有用. Java 提供了很友好的 API 来实现这种模式. 以下示例显示了如何使用查找方法注入:

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();
}
Kotlin
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() 方法被覆盖,以便查找新的(prototype)对象. 以下示例显示了如何执行此操作:

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();
        }
    }
}
Kotlin
@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();
    }
}
Kotlin
@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 中,实例化的 bean 默认具有 singleton 作用域. 这就是它的神奇之处:所有 @Configuration 类在启动时都使用 CGLIB 进行子类化. 在子类中,子方法在调用父方法并创建新实例之前,首先检查容器是否有任何缓存(作用域) bean.

这种行为可以根据 bean 的作用域而变化,我们这里只是讨论单例.

从 Spring 3.2 开始,不再需要将 CGLIB 添加到类路径中,因为 CGLIB 类已经在 org.springframework.cglib 下重新打包并直接包含在 spring-core JAR 中.

由于 CGLIB 在启动时动态添加功能,因此存在一些限制. 特别是,配置类不能是 final 的. 但是,从 4.3 开始,配置类允许使用任何构造函数,包括使用 @Autowired 或单个非默认构造函数声明进行默认注入.

如果想避免因 CGLIB 带来的限制,请考虑声明非 @Configuration 类的 @Bean 方法,例如在 @Component 类 .这样在 @Bean 方法之间的交叉方法调用将不会被拦截,此时必须在构造函数或方法级别上进行依赖注入.

1.12.5. 编写基于 Java 的配置

Spring 的基于 Java 的配置功能允许您撰写注解,这可以降低配置的复杂性.

使用 @Import 注解

就像在 Spring XML 文件中使用 <import/> 元素来帮助模块化配置一样,@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();
    }
}
Kotlin
@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);
}
Kotlin
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编译器会有一些限制 ,即需符合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");
}
Kotlin
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 : 这意味着它们可以利用 @Autowired@Value 注入以及与任何其他 bean 相同的其他功能.

确保以这种方式注入的依赖只是最简单的. @Configuration 类在上下文初始化期间很早就被处理,并且强制以这种方式注入依赖可能会导致意外的早期初始化. 尽可能采用基于参数的注入,如前面的示例所示.

另外,要特别注意通过 @BeanBeanPostProcessorBeanFactoryPostProcessor 定义. 这些通常应该声明为 static @Bean 方法,而不是触发其包含配置类的实例化. 否则,@Autowired@Value 不能在配置类本身上工作,因为它早于 AutowiredAnnotationBeanPostProcessor 被创建为 bean 实例.

以下示例显示了如何将一个 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");
}
Kotlin
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")
}
仅在 Spring Framework 4.3 中支持 @Configuration 类中的构造函数注入. 另请注意,如果目标 bean 仅定义了一个构造函数,则无需指定 @Autowired. 在前面的示例中,RepositoryConfig 构造函数中不需要 @Autowired.

完全导入 bean 便于查找

在上面的场景中,@Autowired 可以很好的工作,使设计更具模块化,但是自动注入哪个 bean 依然有些模糊不清.例如, 作为一个开发者查看 ServiceConfig 类时,你怎么知道 @Autowired AccountRepository 在哪定义的呢?代码中并未明确指出, 还好, Spring Tools for Eclipse 提供的工具可以呈现图表,显示所有内容的连线方式,这可能就是您所需要的. 此外,您的Java IDE可以轻松找到 AccountRepository 类型的所有声明和用法,并快速显示返回该类型的 @Bean 方法的位置.

万一需求不允许这种模糊的装配,并且您希望从 IDE 中从一个 @Configuration 类直接导航到另一个 @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());
    }
}
Kotlin
@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())
    }
}

在前面的情况中,定义 AccountRepository 是完全明确的. 但是,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");
}
Kotlin
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")
}

现在,ServiceConfig 与具体的 DefaultRepositoryConfig 松散耦合,内置的IDE工具仍然很有用: 您可以很容易获取 RepositoryConfig 实现类的继承体系. 以这种方式,操作 @Configuration 类及其依赖与操作基于接口的代码的过程没有什么区别

如果要影响某些 bean 的启动创建顺序,可以考虑将其中一些声明为 @Lazy (用于在首次访问时创建而不是在启动时) 或 @DependsOn 某些其他 bean (确保在创建之前创建特定的其他 bean (当前的 bean ,超出后者的直接依赖性所暗示的) ) .
有条件地包含 @Configuration 类或 @Bean 方法

基于某些任意系统状态,有条件地启用或禁用完整的 @Configuration 类甚至单独的 @Bean 方法通常很有用. 一个常见的例子是, 只有在 Spring 环境中启用了特定的配置文件时才使用 @Profile 注解来激活 bean (有关详细信息,请参阅 BeanBean 定义 Profiles) .

@Profile 注解实际上是通过使用更灵活的注解 @Conditional 实现的. @Conditional 注解表示特定的 org.springframework.context.annotation.Condition 实现. 它表明 @Bean 被注册之前会先"询问 @Conditional 注解.

Condition 接口的实现提供了一个返回 truefalsematches(…) 方法. 例如,以下清单显示了用于 @Profile 的实际 Condition 实现:

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;
}
Kotlin
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 类支持但不一定成为 Spring XML 的 100% 完全替代品. 某些工具(如 Spring XML 命名空间) 仍然是配置容器的理想方法. 在 XML 方便或必要的情况下,您可以选择: 通过使用例如 ClassPathXmlApplicationContext 以 “XML-centric” 的方式实例化容器, 或者通过使用 AnnotationConfigApplicationContext 以 “Java-centric” 的方式实例化它. @ImportResource 注解,根据需要导入XML.

以XML为中心使用 @Configuration

更受人喜爱的方法是从包含 @Configuration 类的 XML 启动容器.例如,在使用 Spring 的现有系统中,大量使用的是 Spring XML 配置,所以很容易根据需要创建 @Configuration 类 ,并将他们到包含 XML 文件中. 我们将介绍在这种 “XML-centric” 的情况下使用 @Configuration 类的选项.

@Configuration 类声明为普通的Spring <bean/> 元素

请记住,@Configuration 类最终也只是容器中的 bean 定义. 在本系列示例中,我们创建一个名为 AppConfig@Configuration 类,并将其作为 <bean/> 定义包含在 system-test-config.xml 中. 由于 <context:annotation-config/> 已打开,容器会识别 @Configuration 注解并正确处理 AppConfig 中声明的 @Bean 方法.

以下示例显示了 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());
    }
}
Kotlin
@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.password=
Java
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
Kotlin
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 引用它,并且不太可能通过名称从容器中明确地获取它. 类似地,DataSource bean 只是按类型自动装配,因此不严格要求显式的 bean id.

使用 <context:component-scan/> 来获取 @Configuration

因为 @Configuration@Component 注解的元注解,所以 @Configuration 注解的类也可以被自动扫描. 使用与上面相同的场景,可以重新定义 system-test-config.xml 以使用组件扫描. 请注意,在这种情况下,我们不需要显式声明 <context:annotation-config/>,,因为 <context:component-scan/> 启用相同的功能.

以下示例显示了已修改的 system-test-config.xml 文件:

<beans>
    <!-- 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-centric” 的方法来配置容器并尽可能少的使用 XML. 以下示例(包括配置类,定义 bean 的 XML 文件,属性文件和主类) 显示了如何使用 @ImportResource 注解来实现根据需要使用 XML 的 “Java-centric” 的配置:

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);
    }
}
Kotlin
@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.password=
Java
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
Kotlin
import org.springframework.beans.factory.getBean

fun main() {
    val ctx = AnnotationConfigApplicationContext(AppConfig::class.java)
    val transferService = ctx.getBean<TransferService>()
    // ...
}

Environment 接口是集成在容器中的抽象,它模拟了应用程序环境的两个关键方面: profilesproperties

profile 配置是一个被命名的, bean 定义的逻辑组,这些 bean 只有在给定的 profile 配置激活时才会注册到容器.无论是以 XML 还是通过注解定义, bean 都可以分配给配置文件 . Environment 对象在 profile 中的角色是判断哪一个 profile 应该在当前激活和哪一个 profile 应该在默认情况下激活.

属性在几乎所有应用程序中都发挥着重要作用,可能源自各种源: 属性文件,JVM 系统属性,系统环境变量,JNDI,servlet 上下文参数,ad-hoc 属性对象,Map 对象等. 与属性相关的 Environment 对象的作用是为用户提供方便的服务接口,用于配置属性源和从中解析属性.

1.12.6. Bean 定义 Profiles

bean 定义 profiles 是核心容器内的一种机制,该机制能在不同环境中注册不同的 bean.“environment” 这个词对不同的用户来说意味着不同的东西,这个功能可以帮助解决许多用例,包括:

  • 在 QA 或生产环境中,针对开发中的内存数据源而不是从JNDI查找相同的数据源.

  • 开发期使用监控组件,当部署以后则关闭监控组件,使应用更高效

  • 为用户各自注册自定义 bean 实现.

考虑 DataSource 的实际应用程序中的第一个用例. 在测试环境中,配置可能类似于以下内容:

Java
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}
Kotlin
@Bean
fun dataSource(): DataSource {
    return EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("my-schema.sql")
            .addScript("my-test-data.sql")
            .build()
}

现在考虑如何将此应用程序部署到 QA 或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的 JNDI 目录. 我们的 dataSource bean 现在看起来如下:

Java
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
Kotlin
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
    val ctx = InitialContext()
    return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}

问题是如何根据当前环境在使用这两种变体之间切换. 随着时间的推移,Spring 用户已经设计了许多方法来完成这项工作,通常依赖于系统环境变量和包含 ${placeholder} 标记的 XML <import/> 语句的组合, 这些标记根据值解析为正确的配置文件路径一个环境变量. bean 定义 profiles 是核心容器功能,可为此问题提供解决方案.

概括一下上面的场景,环境决定 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();
    }
}
Kotlin
@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");
    }
}
Kotlin
@Configuration
@Profile("production")
class JndiDataConfig {

    @Bean(destroyMethod = "")
    fun dataSource(): DataSource {
        val ctx = InitialContext()
        return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
    }
}
如前所述,使用 @Bean 方法,您通常选择使用 Spring 的 JndiTemplate/JndiLocatorDelegate 帮助程序或前面显示的 直接 JNDI InitialContext 用法但不使用 JndiObjectFactoryBean 变量来使用编程 JNDI 查找,这会强制您将返回类型声明为 FactoryBean 类型.

profile 字符串可以包含简单的 profile 名称(例如,production) 或 profile 表达式. profile 表达式允许表达更复杂的概要逻辑(例如,production & us-east) . profile 表达式支持以下运算符:

  • !: 逻辑 非

  • &: 逻辑 与

  • |: 逻辑 或

你必须使用括号混合 &| . 例如,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 {
}
Kotlin
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
如果 @Configuration 类标有 @Profile,类中所有 @Bean@Import 注解相关的类都将被忽略,除非该 profile 被激活. 如果一个 @Component@Configuration 类被标记为 @Profile({"p1", "p2"}). 那么除非 profile 'p1' or 'p2' 已被激活. 否则该类将不会注册/处理. 如果给定的配置文件以 NOT 运算符( ! )为前缀,如果配置文件为没有激活,则注册的元素将被注册. 例如,给定 @Profile({"p1", "!p2"}),如果配置文件 p1 处于 active 状态或配置文件 p2 未激活,则会进行注册.

@Profile 也能注解方法,用于配置一个配置类中的指定 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 环境可用.
Kotlin
@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 环境可用.

@Bean 方法上还添加有 @Profile 注解,可能会应用在特殊情况. 在相同 Java 方法名称的重载 @Bean 方法(类似于构造函数重载) 的情况下, 需要在所有重载方法上一致声明 @Profile 条件,如果条件不一致,则只有重载方法中第一个声明的条件才重要. 因此,@Profile 不能用于选择具有特定参数签名的重载方法, 所有工厂方法对相同的 bean 在 Spring 构造器中的解析算法在创建时是相同的.

如果想定义具有不同配置文件条件的备用 bean ,请使用不同的 Java 方法名称,通 XML bean 定义 profiles 过 @Bean 名称属性指向相同的 bean 名称. 如上例所示. 如果参数签名都是相同的(例如,所有的变体都是无参的工厂方法) ,这是安排有效 Java 类放在首要位置的唯一方法(因为只有一个 特定名称和参数签名的方法) .

XML bean 定义profiles

XML 中的 <beans> 元素有一个 profile 属性,我们之前的示例配置可以在两个XML文件中重写,如下所示:

<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
<beans profile="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

也可以不用分开 2 个文件,在同一个 XML 中配置 2 个 <beans/>,<beans/> 元素也有 profile 属性. 如以下示例所示:

<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 强制允许将 profile 元素定义在文件的最后面,这有助于在 XML 文件中提供灵活的方式而又不引起混乱.

对应 XML 不支持前面描述的 profile 表达式. 但是,有可能通过使用 ! 来否定一个 profile 表达式. 也可以通过嵌套 profiles 来应用 "and" ,如以下示例所示:

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

    <!-- other bean definitions -->

    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>

在前面的示例中,如果 productionus-east profiles 都处于激活状态,则会暴露 dataSource bean.

激活 Profile

现在已经更新了配置,但仍然需要指定要激活哪个配置文件, 如果我们现在开始我们的示例应用程序, 我们会看到抛出 NoSuchBeanDefinitionException,因为容器找不到名为 dataSource 的Spring bean.

激活配置文件可以通过多种方式完成,但最直接的方法是以编程方式对可通过 ApplicationContext 提供的 Environment API 进行操作. 以下示例显示了如何执行此操作:

Java
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
Kotlin
val ctx = AnnotationConfigApplicationContext().apply {
    environment.setActiveProfiles("development")
    register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
    refresh()
}

此外,配置文件也可以通过 spring.profiles.active 属性声明式性地激活,可以通过系统环境变量,JVM系统属性,web.xml 中的 Servlet上下文参数指定, 甚至作为JNDI中的一个条目设置(PropertySource 抽象) . 在集成测试中,可以通过 spring-test 模块中的 @ActiveProfiles 注解来声明 active 配置文件(参见使用环境配置文件的上下文配置)

配置文件不是 "二选一" 的. 开发者可以一次激活多个配置文件. 使用编程方式,您可以为 setActiveProfiles() 方法提供多个配置文件名称,该方法接受 String…​ 变量参数. 以下示例激活多个配置文件:

Java
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
Kotlin
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")

声明性地,spring.profiles.active 可以接受以逗号分隔的 profile 名列表,如以下示例所示:

    -Dspring.profiles.active="profile1,profile2"
默认 Profile

default 配置文件表示默认开启的 profile 配置. 考虑以下配置:

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();
    }
}
Kotlin
@Configuration
@Profile("default")
class DefaultDataConfig {

    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .build()
    }
}

如果没有配置文件激活,上面的 dataSource 就会被创建. 这提供了一种默认的方式,如果有任何一个配置文件启用,default 配置就不会生效.

默认配置文件的名字(default) 可以通过 EnvironmentsetDefaultProfiles() 方法或者 spring.profiles.default 属性修改.

1.12.7. 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);
Kotlin
val ctx = GenericApplicationContext()
val env = ctx.environment
val containsMyProperty = env.containsProperty("my-property")
println("Does my environment contain the 'my-property' property? $containsMyProperty")

在上面的代码段中,一个高级别的方法用于访问 Spring 是否为当前环境定义了 my-property 属性. 为了回答这个问题,Environment 对象对一组 PropertySource 对象进行搜索. PropertySource 是对任何键值对的简单抽象,Spring 的 StandardEnvironment 配置有两个 PropertySource 对象 ,一个表示JVM系统属性(System.getProperties()),一个表示系统环境变量(System.getenv()).

这些默认 property 源位于 StandardEnvironment 中,用于独立应用程序. StandardServletEnvironment用默认的 property 配置源填充. 默认配置源包括 Servlet 配置和 Servlet 上下文参数,它可以选择启用 JndiPropertySource. 有关详细信息,请参阅它的 javadocs

具体地说,当您使用 StandardEnvironment 时,如果在运行时存在 my-property 系统属性或 my-property 环境变量,则对 env.containsProperty("my-property") 的调用将返回 true.

执行的搜索是分层的. 默认情况下,系统属性优先于环境变量,因此如果在调用 env.getProperty("my-property") 期间碰巧在两个位置都设置了 my-property 属性, 系统属性值返回优先于环境变量. 请注意,属性值未合并,而是由前面的条目完全覆盖.

对于常见的 StandardServletEnvironment,完整层次结构如下,最高优先级条目位于顶部:

  1. ServletConfig参数(如果适用 - 例如,在 DispatcherServlet 上下文的情况下)

  2. ServletContext参数(web.xml context-param 条目)

  3. JNDI 环境变量(java:comp/env/ entries)

  4. JVM 系统属性(-D 命令行参数)

  5. JVM 系统环境(操作系统环境变量)

最重要的是,整个机制都是可配置的. 也许开发者需要一个自定义的 properties 源,并将该源整合到这个检索层级中. 为此,请实现并实例化您自己的 PropertySource,并将其添加到当前 EnvironmentPropertySource 集合中. 以下示例显示了如何执行此操作:

Java
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
Kotlin
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())

在上面的代码中, MyPropertySource 在搜索中添加了最高优先级. 如果它包含 my-property 属性,则会检测并返回该属性, 优先于其他 PropertySource 中的任何 my-property 属性. MutablePropertySources API 暴露了许多方法,允许你显式操作 property 属性源.

1.12.8. 使用 @PropertySource

@PropertySource 注解提供了便捷的方式,用于增加 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;
    }
}
Kotlin
@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;
    }
}
Kotlin
@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.

根据Java 8惯例,@PropertySource 注解是可重复的. 但是,所有这些 @PropertySource 注解都需要在同一级别声明,可以直接在配置类上声明, 也可以在同一自定义注解中作为元注解声明. 不建议混合直接注解和元注解,因为直接注解有效地覆盖了元注解.

1.12.9. 在声明中的占位符

之前,元素中占位符的值只能针对 JVM 系统属性或环境变量进行解析. 现在已经打破了这种情况. 因为环境抽象集成在整个容器中,所以很容易通过它来对占位符进行解析. 这意味着开发者可以以任何喜欢的方式来配置这个解析过程,可以改变是优先查找系统 properties 或者是有限查找环境变量,或者删除它们; 增加自定义 property 源,使之成为更合适的配置

具体而言,只要在 Environment 中可用,无论 customer 属性在何处定义,以下语句都可以工作:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>

1.13. 注册 LoadTimeWeaver

LoadTimeWeaver 被 Spring 用来在将类加载到 Java 虚拟机(JVM) 中时动态地转换成 字节码 文件

若要开启加载时织入,要在 @Configuration 类中增加 @EnableLoadTimeWeaving 注解,如以下示例所示:

Java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
Kotlin
@Configuration
@EnableLoadTimeWeaving
class AppConfig

或者,对于 XML 配置,您可以使用 context:load-time-weaver 元素:

<beans>
    <context:load-time-weaver/>
</beans>

一旦配置为 ApplicationContext,该 ApplicationContext 中的任何 bean 都可以实现 LoadTimeWeaverAware,从而接收对 load-time weaver 实例的引用. 这特别适用于 Spring 对 JPA 支持. 其中 JPA 类转换可能需要加载时织入. 有关更多详细信息,请参阅 LocalContainerEntityManagerFactoryBean. 有关 AspectJ 加载时编织的更多信息,请参阅在 Spring 框架中使用 AspectJ 的加载时织入.

1.14. ApplicationContext 的附加功能

正如 前面章节中讨论的,org.springframework.beans.factory 包提供了管理和操作 bean 的基本功能,包括以编程方式. org.springframework.context 包添加了 ApplicationContext 接口,该接口扩展了 BeanFactory 接口,此外还扩展了其他接口, 以更面向应用程序框架的方式提供其他功能. 许多人以完全声明的方式使用 ApplicationContext, 甚至不以编程方式创建它,而是依赖于诸如 ContextLoader 之类的支持类来自动实例化 ApplicationContext,作为 Java EE Web 应用程序的正常启动过程的一部分.

为了增强 BeanFactory 的功能,上下文包还提供了以下功能.

  • 通过 MessageSource 接口访问 i18n 风格的消息.

  • 通过 ResourceLoader 接口访问 URL 和文件等资源.

  • 事件发布,即通过使用 ApplicationEventPublishe r接口实现 ApplicationListener 接口的 bean .

  • 通过 HierarchicalBeanFactory 接口,加载多级 contexts,允许关注某一层级 context,比如应用的 Web 层.

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

  • String getMessage(MessageSourceResolvable resolvable, Locale locale): 前面方法中使用的所有属性也包装在名为 MessageSourceResolvable 的类中,您可以将此方法用于此类.

当一个 ApplicationContext 被加载时,它会自动搜索在上下文中定义的一个 MessageSource, bean 必须包含名称 messageSource,如果找到这样的 bean 则将对前面方法的所有调用委派给消息源. 如果没有找到消息源,ApplicationContext 会尝试找到一个包含同名 bean 的父对象. 如果有,它使用那个 bean 作为 MessageSource. 如果 ApplicationContext 找不到消息的任何源,则会实例化空的 DelegatingMessageSource,以便能够接受对上面定义的方法的调用.

Spring 提供了三个 MessageSource 实现, ResourceBundleMessageSource, ReloadableResourceBundleMessageSourceStaticMessageSource,为了做嵌套消息三者都实现了 HierarchicalMessageSource. StaticMessageSource 很少使用,但提供了以编程方式向源添加消息. 以下示例显示了 ResourceBundleMessageSource:

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

该示例假定您在类路径中定义了三个资源包,format, exceptionswindows. 任何解析消息的请求都将以标准 JDK 方式处理, 通过 ResourceBundle 解析消息. 出于示例的目的,假设上述两个资源包文件的内容如下:

    # in format.properties
    message=Alligators rock!
    # in exceptions.properties
    argument.required=The {0} argument is required.

下一个示例显示了执行 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);
}
Kotlin
fun main() {
    val resources = ClassPathXmlApplicationContext("beans.xml")
    val message = resources.getMessage("message", null, "Default", Locale.ENGLISH)
    println(message)
}

上述程序产生的结果如下:

Alligators rock!

总而言之,MessageSource 在名为 beans.xml 的文件中定义,该文件存在于类路径的根目录中. messageSourcebean 定义通过其 basenames 属性引用许多资源包. 在列表中传递给 basenames 属性的三个文件作为类路径根目录下的文件存在,分别称为 format.properties, exceptions.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);
    }
}
Kotlin
    class Example {

    lateinit var messages: MessageSource

    fun execute() {
        val message = messages.getMessage("argument.required",
                arrayOf("userDao"), "Required", Locale.ENGLISH)
        println(message)
    }
}

调用 execute() 方法得到的结果如下:

The userDao argument is required.

关于国际化 (“i18n”),Spring 的各种 MessageSource 实现遵循与标准 JDK ResourceBundle 相同的区域设置解析和回退规则. 简而言之,继续前面定义的示例 messageSource,如果要根据 British(en-GB)语言环境解析消息,则应分别创建名为 format_en_GB.properties,exceptions_en_GB.propertieswindows_en_GB.properties 的文件.

通常,区域设置解析由应用程序的环境配置管理. 在以下示例中,手动指定解析(英国) 消息的区域设置:

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.
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);
}
Kotlin
fun main() {
    val resources = ClassPathXmlApplicationContext("beans.xml")
    val message = resources.getMessage("argument.required",
            arrayOf("userDao"), "Required", Locale.UK)
    println(message)
}

运行上述程序产生的结果如下:

Ebagum lad, the 'userDao' argument is required, I say, required.

您还可以使用 MessageSourceAware 接口获取对已定义的任何 MessageSource 的引用. 在创建和配置 bean 时,应用程序上下文的 MessageSource 会注入实现 MessageSourceAware 接口的 ApplicationContext 中定义的任何 bean .

因为 Spring 的 MessageSource 是基于 Java 的 ResourceBundle 的,所以没有合并具有相同名称的捆绑包,但只会使用找到的第一个捆绑包。 后面具有相同名称的包将被忽略。
NOTE

作为 ResourceBundleMessageSource 的替代,Spring 提供了一个 ReloadableResourceBundleMessageSource 类. 此变体支持相同的 bundle 文件格式,但比基于标准 JDK 的 ResourceBundleMessageSource 实现更灵活. 特别是,它允许从任何 Spring 资源位置(不仅从类路径) 读取文件,并支持 bundle 属性文件的热重新加载(同时在其间有效地缓存它们) . 有关详细信息,请参阅 ReloadableResourceBundleMessageSource javadoc.

1.14.2. 标准和自定义事件

ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的. 如果将实现 ApplicationListener 接口的 bean 部署到上下文中,则每次将 ApplicationEvent 发布到 ApplicationContext 时,都会通知该 bean . 从本质上讲,这是标准的 Observer 设计模式.

从 Spring 4.2 开始,事件架构已经得到显着改进,并提供了一个基于注解的模型 使其有发布任意事件的能力(即,不一定从 ApplicationEvent 扩展的对象) . 当发布这样的对象时,我们将它包装在一个事件中.

下表描述了 Spring 提供的标准事件:

Table 7. Built-in Events
事件 说明

ContextRefreshedEvent

初始化或刷新 ApplicationContext 时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法) . 这里, “initialized” 意味着加载所有 bean ,检测并激活 bean 的后置处理器,预先实例化单例,并且可以使用 ApplicationContext 对象. 只要上下文尚未关闭,只要所选的 ApplicationContext 实际支持这种 "热" 刷新,就可以多次触发刷新. 例如,XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持.

ContextStartedEvent

通过使用 ConfigurableApplicationContext 接口上的 start() 方法启动 ApplicationContext 时发布. 通常,此信号用于在显式停止后重新启动 bean ,但它也可用于启动尚未为自动启动配置的组件(例如,在初始化时尚未启动的组件) .

ContextStoppedEvent

通过使用 ConfigurableApplicationContext 接口上的 close() 方法停止 ApplicationContext 时发布. 这里, “stopped” 表示所有生命周期 bean 都会收到明确的停止信号. 可以通过 start() 调用重新启动已停止的上下文.

ContextClosedEvent

通过使用 ConfigurableApplicationContext 接口上的 close() 方法关闭 ApplicationContext 时发布. 这里, "closed" 意味着所有单例 bean 都被销毁. 封闭的环境达到其寿命终结. 它无法刷新或重新启动.

RequestHandledEvent

一个特定于Web的事件,告诉所有 bean 已经为 HTTP 请求提供服务. 请求完成后发布此事件. 此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序.

ServletRequestHandledEvent

RequestHandledEvent 的子类,添加了特定于 Servlet 的上下文信息.

您还可以创建和发布自己的自定义事件. 以下示例显示了一个扩展 Spring 的 ApplicationEvent 基类的简单类:

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...
}
Kotlin
class BlockedListEvent(source: Any,
                    val address: String,
                    val content: String) : ApplicationEvent(source)

要发布自定义 ApplicationEvent,请在 ApplicationEventPublisher 上调用 publishEvent() 方法. 通常,这是通过创建一个实现 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...
    }
}
Kotlin
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 接口与应用程序上下文进行交互.

要接收自定义 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...
    }
}
Kotlin
class BlockedListNotifier : ApplicationListener<BlockedListEvent> {

    lateinit var notificationAddres: String

    override fun onApplicationEvent(event: BlockedListEvent) {
        // notify appropriate parties via notificationAddress...
    }
}

请注意,ApplicationListener 通常使用自定义事件的类型进行参数化(前面示例中为 BlockedListEvent) . 这意味着 onApplicationEvent() 方法可以保持类型安全,从而避免任何向下转换的需要. 您可以根据需要注册任意数量的事件监听器,但请注意,默认情况下,事件监听器会同步接收事件. 这意味着 publishEvent() 方法将阻塞,直到所有监听器都已完成对事件的处理. 这种同步和单线程方法的一个优点是,当监听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文内运行. 如果需要另一个事件发布策略, 请参阅 Spring 的 ApplicationEventMulticasterSimpleApplicationEventMulticaster javadoc 的实现

以下示例显示了用于注册和配置上述每个类的 bean 定义:

<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="blockedlist@example.org"/>
</bean>

总而言之,当调用 emailService bean 的 sendEmail() 方法时,如果有任何应列入 blocked 的电子邮件消息,则会发布 BlockedListEvent 类型的自定义事件. blockedListNotifier bean 注册为 ApplicationListener 并接收 BlockedListEvent ,此时它可以通知相关方.

Spring 的事件机制是为在同一应用程序上下文中的 Spring bean 之间的简单通信而设计的. 但是,对于更复杂的企业集成需求,单独维护的 Spring Integration 项目提供了完整的支持并可用于构建轻量级, pattern-oriented(面向模式) ,依赖 Spring 编程模型的事件驱动架构.
基于注解的事件监听器

从 Spring 4.2 开始,您可以使用 @EventListener 注解在托管 bean 的任何 public 方法上注册事件监听器. 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...
    }
}
Kotlin
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() {
    // ...
}
Kotlin
@EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class)
fun handleContextStart() {
    // ...
}

还可以通过使用定义 SpEL 表达式的注解的 condition 属性来添加额外的运行时过滤,该表达式应匹配以实际调用特定事件的方法.

以下示例显示了仅当事件的 content 属性等于 my-event 时才能重写我们的通知程序以进行调用:

Java
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blockedListEvent) {
    // notify appropriate parties via notificationAddress...
}
Kotlin
@EventListener(condition = "#blEvent.content == 'my-event'")
fun processBlockedListEvent(blockedListEvent: BlockedListEvent) {
    // notify appropriate parties via notificationAddress...
}

每个 SpEL 表达式都针对专用上下文进行评估. 下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:

  1. 事件 SpEL 可用的元数据

名字 位置 描述 例子

Event

root object

真实的 ApplicationEvent.

#root.event or event

Arguments array

root object

用于调用目标的参数(作为数组)

#root.args or args; args[0] to access the first argument, etc.

Argument name

evaluation context

任何方法参数的名称. 如果由于某种原因,名称不可用(例如,因为没有调试信息) ,参数名称也可以在 #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...
}
Kotlin
@EventListener
fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}
异步监听器不支持此功能.

这将通过 handleBlockedListEvent 方法处理每个 BlockedListEvent 并发布一个新的 ListUpdateEvent,如果需要发布多个 Collection ,则可以返回事件 集合.

异步的监听器

如果希望特定监听器异步处理事件,则可以使用常规 @Async 支持.. 以下示例显示了如何执行此操作:

Java
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}
Kotlin
@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...
}
Kotlin
@EventListener
@Order(42)
fun processBlockedListEvent(event: BlockedListEvent) {
    // notify appropriate parties via notificationAddress...
}
泛型事件

您还可以使用泛型来进一步定义事件的结构. 考虑使用 EntityCreatedEvent<T>,其中 T 是创建的实际实体的类型. 例如,您可以创建以下监听器定义以仅接收 PersonEntityCreatedEvent:

Java
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}
Kotlin
@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()));
    }
}
Kotlin
class EntityCreatedEvent<T>(entity: T) : ApplicationEvent(entity), ResolvableTypeProvider {

    override fun getResolvableType(): ResolvableType? {
        return ResolvableType.forClassWithGenerics(javaClass, ResolvableType.forInstance(getSource()))
    }
}
这不仅适用于 ApplicationEvent ,也适用于作为事件发送的任意对象.

1.14.3. 通过便捷的方式访问底层资源

为了最佳地使用和理解应用程序上下文,您应该熟悉 Spring 的抽象 Resource ,如参考资料中所述资源(Resources).

应用程序上下文是 ResourceLoader,可用于加载 Resource 对象. Resource 本质上是 JDK java.net.URL 类的功能更丰富的版本. 实际上 Resource 的实现类中大多含有 java.net.URL 的实例. Resource 几乎能从任何地方透明的获取底层资源,可以是 classpath 类路径、文件系统、标准的 URL 资源及变种 URL 资源. 如果资源定位字串是简单的路径, 没有任何特殊前缀,就适合于实际应用上下文类型.

可以配置一个 bean 部署到应用上下文中,用以实现特殊的回调接口,ResourceLoaderAware 它会在初始化期间自动回调. 应用程序上下文本身作为 ResourceLoader 传入. 可以暴露 Resourcetype 属性,这样就可以访问静态资源 静态资源可以像其他 properties 那样被注入 Resource. 可以使用简单的字串路径指定资源,这需要依赖于特殊的 JavaBean PropertyEditor(由上下文自动注册) ,当 bean 部署时候它将转换资源中的字串为实际的资源对象.

提供给 ApplicationContext 构造函数的一个或多个位置路径实际上是资源字符串,并且以简单形式对特定上下文实现进行适当处理. ClassPathXmlApplicationContext 将一个简单的定位路径视为类路径位置. 开发者还可以使用带有特殊前缀的定位路径,这样就可以强制从 classpath 或者 URL 定义加载路径, 而不用考虑实际的上下文类型.

1.14.4. 应用程序启动跟踪

ApplicationContext 管理 Spring 应用程序的生命周期, 并提供丰富的编程模型.因此, 复杂的应用程序可以同时拥有 复杂的组件依赖和启动阶段.

使用特定指标跟踪应用程序的启动步骤可以帮助您了解在启动阶段的那块花了一些时间, 它也是了解整体上下文生命周期更好的方法.

AbstractApplicationContext (及其子类)配有一个 ApplicationStartup, 它收集有关各个启动阶段的 StartupStep 数据:

  • 应用程序上下文生命周期(基本软件包扫描, 配置类管理)

  • bean生命周期 (instantiation, smart initialization, post processing)

  • 应用程序事件处理

这是 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();
Kotlin
// 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 profilers 的替代品, 也不是 metrics libraries, 例如 Micrometer.

要开始收集自定义的 StartupStep, 组件可以获取 ApplicationStartup 直接从应用程序上下文实例, 使其组件实现 ApplicationStartupAware, 或在任何注入点上请求 ApplicationStartup 类型.

创建自定义启动步骤时, 开发人员不应使用 "spring.*" 命名空间,该名称空间保留给 Spring 内部使用, 并且可能会发生变化.

1.14.5. 快速对 Web 应用的 ApplicationContext 实例化

开发者可以通过使用 ContextLoader 来声明性地创建 ApplicationContext 实例,当然也可以通过使用 ApplicationContext 的实现来编程实现 ApplicationContext.

您可以使用 ContextLoaderListener 注册 ApplicationContext ,如以下示例所示:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听器检查 contextConfigLocation 参数. 如果参数不存在,则监听器将 /WEB-INF/applicationContext.xml 用作默认值. 当参数确实存在时,监听器使用预定义的分隔符(逗号,分号和空格) 分隔 String,并将值用作搜索应用程序上下文的位置. 还支持 Ant 样式的路径模式. 示例是 /WEB-INF/*Context.xml(对于所有名称以 Context.xml 结尾且位于 WEB-INF 目录中的文件) 和 /WEB-INF/**/*Context.xml (对于所有这样的文件) WEB-INF 的任何子目录中的文件.

1.14.6. 使用Java EE RAR文件部署Spring的 ApplicationContext

可以将 Spring ApplicationContext 部署为 RAR 文件,将上下文及其所有必需的 bean 类和库 JAR 封装在 Java EE RAR 部署单元中,这相当于引导一个独立的 ApplicationContext. 只是托管在 Java EE 环境中,能够访问 Java EE 服务器设施. 在部署无头WAR文件(实际上,没有任何 HTTP 入口点,仅用于在 Java EE 环境中引导 Spring ApplicationContext 的 WAR 文件) 的情况下RAR部署是更自然的替代方案

RAR 部署非常适合不需要 HTTP 入口点但仅由消息端点和调度作业组成的应用程序上下文. 在这种情况下,Bean 可以使用应用程序服务器资源, 例如 JTA 事务管理器和 JNDI 绑定的 JDBC DataSource 和 JMS ConnectionFactory 实例, 并且还可以通过 Spring 的标准事务管理和 JNDI 和 JMX 支持设施向平台的JMX服务器注册. 应用程序组件还可以通过 Spring 的 TaskExecutor 抽象实现与应用程序服务器的 JCA WorkManager 交互

有关 RAR 部署中涉及的配置详细信息,请参阅 SpringContextResourceAdapter 类的javadoc

对于将 Spring ApplicationContext 简单部署为 Java EE RAR 文件:

  1. 将所有应用程序类打包到一个 RAR 文件(这是一个具有不同文件扩展名的标准JAR文件) . .

  2. 将所有必需的库 JAR 添加到 RAR 存档的根目录中.

  3. 添加 META-INF/ra.xml 部署描述符 (如 SpringContextResourceAdapter 的 javadoc 所示) 和相应的 Spring XML bean 定义文件(通常是 META-INF/applicationContext.xml) .

  4. 将生成的RAR文件放入应用程序服务器的部署目录中.

这种 RAR 部署单元通常是独立的. 它们不会将组件暴露给外部世界,甚至不会暴露给同一应用程序的其他模块. 与基于 RAR 的 ApplicationContext 的交互通常通过与其他模块共享的 JMS 目标进行. 例如,基于 RAR 的 ApplicationContext 还可以调度一些作业或对文件系统(等等) 中的新文件作出响应. 如果它需要允许来自外部的同步访问,它可以(例如) 导出RMI端点,这可以由同一台机器上的其他应用程序模块使用.

1.15. BeanFactory API

BeanFactory API为 Spring 的 IoC 功能提供了基础. 它的特定契约主要用于与 Spring 的其他部分和相关的第三方框架集成其 DefaultListableBeanFactory 实现是更高级别 GenericApplicationContext 容器中的密钥委托.

BeanFactory 和相关接口(例如 BeanFactoryAware, InitializingBean,DisposableBean) 是其他框架组件的重要集成点. 通过不需要任何注解或甚至反射,它们允许容器与其组件之间的非常有效的交互. 应用程序级 bean 可以使用相同的回调接口,但通常更喜欢通过注解或通过编程配置进行声明性依赖注入.

请注意,核心 BeanFactory API 级别及其 DefaultListableBeanFactory 实现不会对配置格式或要使用的任何组件注解做出假设. 所有这些风格都通过扩展(例如 XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor) 进行,并作为核心元数据表示在共享 BeanDefinition 对象上运行. 这是使 Spring 的容器如此灵活和可扩展的本质.

1.15.1. 选择 BeanFactory 还是 ApplicationContext?

本节介绍 BeanFactoryApplicationContext 容器级别之间的差异以及影响.

您应该使用 ApplicationContext,除非您有充分的理由不这样做,使用 GenericApplicationContext 及其子类 AnnotationConfigApplicationContext 作为自定义引导的常见实现. 这些是 Spring 用于所有常见目的的核心容器的主要入口点: 加载配置文件,触发类路径扫描,以编程方式注册 bean 定义和带注解的类,以及(从 5.0 开始) 注册功能 bean 定义.

因为 ApplicationContext 包括 BeanFactory 的所有功能,和 BeanFactory 相比更值得推荐,除了一些特定的场景,例如在资源受限的设备上运行的内嵌的应用. 在 ApplicationContext(例如 GenericApplicationContext 实现) 中,按照约定(即通过 bean 名称或 bean 类型 - 特别是后处理器) 检测到几种 bean , 而普通的 DefaultListableBeanFactory 对任何特殊 bean 都是不可知的.

对于许多扩展容器功能,例如注解处理和 AOP 代理, BeanPostProcessor 的扩展点是必不可少的. 如果仅使用普通的 DefaultListableBeanFactory,则默认情况下不会检测到并激活此类后置处理器. 这种情况可能令人困惑,因为您的 bean 配置实际上没有任何问题. 相反,在这种情况下,容器需要至少得多一些额外的处理.

下表列出了 BeanFactoryApplicationContext 接口和实现提供的功能.

Table 8. Feature Matrix
特性 BeanFactory ApplicationContext

Bean实例化/装配

Yes

Yes

集成生命周期管理

No

Yes

自动注册 BeanPostProcessor

No

Yes

自动注册 BeanFactoryPostProcessor

No

Yes

便利的 MessageSource 访问 (国际化)

No

Yes

内置 ApplicationEvent 发布机制

No

Yes

要使用 DefaultListableBeanFactory 显式注册 bean 的后置处理器,您需要以编程方式调用 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
Kotlin
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

要将 BeanFactoryPostProcessor 应用于普通的 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);
Kotlin
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)

在这两种情况下,显示注册步骤都不方便,这就是为什么各种 ApplicationContext 变体优先于 Spring 支持的应用程序中的普通 DefaultListableBeanFactory, 尤其是在典型企业设置中依赖 BeanFactoryPostProcessorBeanPostProcessor 实例来扩展容器功能时.

AnnotationConfigApplicationContext 具有注册的所有常见注解后置处理器,并且可以通过配置注解(例如 @EnableTransactionManagement) 在封面下引入其他处理器. 在 Spring 的基于注解的配置模型的抽象级别, bean 的后置处理器的概念变成仅仅是内部容器细节.

2. 资源(Resources)

本章介绍 Spring 如何处理资源以及如何在 Spring 中使用资源. 它包括以下主题:

2.1. 介绍

遗憾的是,Java 的标准 java.net.URL 类和各种 URL 前缀的标准处理程序不足以完全访问底层资源. 例如,没有标准化的 URL 实现可用于访问需要从类路径或相对于 ServletContext 获取的资源. 虽然可以为专用 URL 前缀注册新的处理程序(类似于 http:)这样的前缀的现有处理程序,但这通常非常复杂,并且 URL 接口仍然缺少一些理想的功能,例如检查当前资源是否存在的方法.

2.2. Resource 接口

位于 org.springframework.core.io. 包中的 Spring Resource 接口的目标是成为一个更强大的接口,用于抽象对底层资源的访问. 以下清单显示了 Resource 接口定义,见 Resource Javadoc 了解更多详细信息:

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 不能被多次读取 ,只能在一次读取后即关闭以防止内存泄漏. 除了 InputStreamResource 外,其他常用 Resource 实现都会返回 false.

  • getDescription(): 返回当前资源的描述,当处理资源出错时,资源的描述会用于输出错误的信息. 一般来说,资源的描述是一个完全限定的文件名称,或者是当前资源的真实 URL.

其他方法允许您获取表示资源的实际 URLFile 对象(如果底层实现兼容并支持该功能) .

Resource 接口的某些实现也实现了扩展 WritableResource 接口,用于支持对资源的写入.

在 Spring 里, Resource 抽象有着相当广泛的使用,例如,当需要某个资源时, Resource 可以当作方法签名里的参数类型被使用. 在 Spring API 中,有些方法(例如各种 ApplicationContext 实现的构造函数) 会直接采用普通格式的 String 路径来创建合适的 Resource,调用者也可以通过在路径里带上指定的前缀来创建特定的 Resource 实现.

不但 Spring 内部和使用 Spring 的应用都大量地使用了 Resource 接口,而且开发者在应用代码中将它作为一个通用的工具类也是非常通用的. 当你仅需要使用到 Resource 接口实现时, 可以直接忽略 Spring 的其余部分.虽然这样会与 Spring 耦合,但是也只是耦合一部分而已. 使用这些 Resource 实现代替底层的访问是极其美好的. 这与开发者引入其他库的目的也是一样的

Resource 抽象不会取代功能. 它尽可能地包裹它. 例如,UrlResource 包装URL并使用包装的 URL 来完成其工作.

2.3. 内置 Resource 实现

有关 Spring中 可用的 Resource 实现的完整列表, 请参阅的 "All Known Implementing Classes" 部分 Resource Javadoc.

2.3.1. UrlResource

UrlResource 封装了 java.net.URL 用来访问正常 URL 的任意对象. 例如 file: ,HTTPS 目标,FTP 目标等. 所有的 URL 都可以用标准化的字符串来表示,例如通过正确的标准化前缀. 可以用来表示当前 URL 的类型. 这包括 file:,用于访问文件系统路径,https: 用于通过 HTTPS 协议访问资源,ftp: 用于通过 FTP 访问资源,以及其他.

通过 java 代码可以显式地使用 UrlResource 构造函数来创建 UrlResource,但也可以调用 API 方法来使用代表路径的 String 参数来隐式创建 UrlResource. 对于后一种情况,JavaBeans PropertyEditor 最终决定要创建哪种类型的 Resource. 如果路径字符串包含众所周知的(对于它,那么) 前缀(例如 classpath:),它会为该前缀创建适当的专用 Resource.但是,如果它无法识别前缀,则假定该字符串是标准URL字符串并创建 UrlResource.

2.3.2. ClassPathResource

ClassPathResource 代表从类路径中获取资源,它使用线程上下文加载器,指定类加载器或给定 class 类来加载资源.

当类路径上资源存于文件系统中时,ClassPathResource 支持使用 java.io.File 来访问. 但是当类路径上的资源位于未解压(没有被 Servlet 引擎或其他可解压的环境解压) 的 jar 包中时, ClassPathResource 就不再支持以 java.io.File 的形式访问. 鉴于此,Spring中各种 Resource 的实现都支持以 java.net.URL 的形式访问资源.

可以显式使用 ClassPathResource 构造函数来创建 ClassPathResource,但是更多情况下,是调用 API 方法使用的. 即使用一个代表路径的 String 参数来隐式创建 ClassPathResource. 对于后一种情况,将会由JavaBeans的 PropertyEditor 来识别路径中 classpath: 前缀,并创建 ClassPathResource.

2.3.3. FileSystemResource

FileSystemResource 是用于处理 java.io.Filejava.nio.file.Path 的实现.Spring 的 String 的标准路径 字符串转换, 但通过 java.nio.file.Files API 执行所有操作. 对于纯基于 java.nio.path.Path 的支持改为使用 PathResource. 显然,它同时能解析作为 File 和作为 URL 的资源.

2.3.4. PathResource

这是 Resource 用于处理 java.nio.file.Path 的实现, 执行所有通过 Path API 进行操作和转换. 它支持解析为 File, 并且 作为 URL, 并且实现了扩展的 WritableResource 接口. PathResource 实际上是 FileSystemResource 的纯基于 java.nio.path.Path 的替代品, 它具有不同的 createRelative 行为.

2.3.5. ServletContextResource

这是 ServletContext 资源的 Resource 实现,用于解释相关 Web 应用程序根目录中的相对路径.

ServletContextResource 完全支持以流和 URL 的方式访问资源,但只有当 Web 项目是解压的(不是以 war 等压缩包形式存在) ,而且该 ServletContext 资源必须位于文件系统中, 它支持以 java.io.File 的方式访问资源. 无论它是在文件系统上扩展还是直接从 JAR 或其他地方(如数据库) (可以想象) 访问,实际上都依赖于 Servlet 容器.

2.3.6. InputStreamResource

InputStreamResource 是针对 InputStream 提供的 Resource 实现. 在一般情况下,如果确实无法找到合适的 Resource 实现时,才去使用它. 同时请优先选择 ByteArrayResource 或其他基于文件的 Resource 实现,迫不得已的才使用它.

与其他 Resource 实现相比,这是已打开资源的描述符. 因此,它从 isOpen() 返回 true.

2.3.7. ByteArrayResource

这是给定字节数组的 Resource 实现. 它为给定的字节数组创建一个 ByteArrayInputStream.

当需要从字节数组加载内容时,ByteArrayResource 会是个不错的选择,无需求助于单独使用的 InputStreamResource.

2.4. ResourceLoader

ResourceLoader 接口用于加载 Resource 对象,换句话说,就是当一个对象需要获取 Resource 实例时,可以选择实现 ResourceLoader 接口,以下清单显示了 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");
Kotlin
val template = ctx.getResource("some/resource/path/myTemplate.txt")

针对 ClassPathXmlApplicationContext,该代码返回 ClassPathResource. 如果对 FileSystemXmlApplicationContext 实例执行相同的方法,它将返回 FileSystemResource. 对于 WebApplicationContext,它将返回 ServletContextResource. 它同样会为每个上下文返回适当的对象.

因此,您可以以适合特定应用程序上下文的方式加载资源.

另一方面,您可以通过指定特殊的 classpath: 前缀来强制使用 ClassPathResource,而不管应用程序上下文类型如何,如下例所示:

Java
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
Kotlin
val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt")

同样,您可以通过指定任何标准 java.net.URL 前缀来强制使用 UrlResource. 以下对示例使用 filehttps 前缀:

Java
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
Kotlin
val template = ctx.getResource("file:///some/resource/path/myTemplate.txt")
Java
Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");
Kotlin
val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt")

下表总结了将: String 对象转换为 Resource 对象的策略:

Table 9. Resource strings
前缀 示例 解释

classpath:

classpath:com/myapp/config.xml

从类路径加载

file:

file:///data/config.xml

从文件系统加载为 URL. 另请参见 FileSystemResource 的警告.

https:

https://myserver/logo.png

作为 URL 加载.

(none)

/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 是一个独立的实现, 可以使用 在 ApplicationContext 之外, 并且 ResourceArrayPropertyEditor 还用于 填充 Resource[] bean 属性. PathMatchingResourcePatternResolver 能够 将指定的资源位置路径解析为一个或多个匹配的 Resource 对象. 源路径可以是简单路径, 具有与目标一一对应的映射`Resource`, 或者可能包含特殊的 classpath*: 前缀 和/或 内部 Ant 风格的正则表达式(使用 Spring 的 org.springframework.util.AntPathMatcher 匹配). 后者都是有效的通配符.

实际上, 任何标准 ApplicationContext 中的默认 ResourceLoader 都是一个实例 PathMatchingResourcePatternResolver 的实现, 它实现了 ResourcePatternResolver 接口. ApplicationContext 实例本身也是如此, 实现 ResourcePatternResolver 接口并将其委托给默认值 PathMatchingResourcePatternResolver.

2.6. ResourceLoaderAware 接口

ResourceLoaderAware 是一个特殊的标识接口,用来提供 ResourceLoader 引用的对象. 以下清单显示了 ResourceLoaderAware 接口的定义:

public interface ResourceLoaderAware {

    void setResourceLoader(ResourceLoader resourceLoader);
}

当类实现 ResourceLoaderAware 并部署到应用程序上下文(作为 Spring 管理的 bean) 时,它被应用程序上下文识别为 ResourceLoaderAware. 然后,应用程序上下文调用 setResourceLoader(ResourceLoader),将其自身作为参数提供(请记住,Spring 中的所有应用程序上下文都实现了 ResourceLoader 接口) .

由于 ApplicationContext 实现了 ResourceLoader,因此 bean 还可以实现 ApplicationContextAware 接口并直接使用提供的应用程序上下文来加载资源. 但是,通常情况下,如果您需要,最好使用专用的 ResourceLoader 接口. 代码只能耦合到资源加载接口(可以被认为是实用程序接口) ,而不能耦合到整个Spring ApplicationContext 接口.

除了实现 ResourceLoaderAware 接口,还可以采取另外一种替代方案-依赖 ResourceLoader 的自动装配. "传统" 构造函数和 byType 自动装配模式都支持对ResourceLoader 的装配. 前者是以构造参数的形式装配, 后者作为 setter 方法的参数参与装配. 如果为了获得更大的灵活性(包括属性注入的能力和多参方法) ,可以考虑使用基于注解的新型注入方式. 使用注解@Autowired标识 ResourceLoader 变量,便可将其注入到成员属性、构造参数或方法参数中. 这些参数需要 ResourceLoader 类型. 有关更多信息,请参阅使用@Autowired.

为包含通配符的资源路径加载一个或多个 Resource 对象或使用特殊的 classpath*: 资源前缀, 请考虑使用以下实例:ResourcePatternResolver 自动连接到您的应用程序组件而不是 ResourceLoader.

2.7. 资源依赖

如果 bean 本身要通过某种动态过程来确定和提供资源路径,那么 bean 使用 ResourceLoaderResourcePatternResolver 接口来加载资源就变得更有意义了. 假如需要加载某种类型的模板,其中所需的特定资源取决于用户的角色 . 如果资源是静态的,那么完全可以不使用 ResourceLoader (or ResourcePatternResolver interface) 接口,只需让 bean 暴露它需要的 Resource 属性,并按照预期注入属性即可.

是什么使得注入这些属性变得如此简单? 是因为所有应用程序上下文注册和使用一个特殊的 PropertyEditor JavaBean,它可以将 String paths 转换为 Resource 对象. 因此,如果 myBean 有一个类型为 Resourcetemplate 属性. 如下所示:

Java
public class MyBean {

    private Resource template;

    public setTemplate(Resource template) {
        this.template = template;
    }

    // ...
}
Kotlin
class MyBean(var template: Resource)

在 XML 配置文件中, 它可以用一个简单的字符串配置该资源, 如以下示例所示:

<bean id="myBean" class="example.MyBean">
    <property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

请注意,资源路径没有前缀. 因此,因为应用程序上下文本身将用作 ResourceLoader, 所以资源本身通过 ClassPathResource,FileSystemResourceServletContextResource 加载,具体取决于上下文的确切类型.

如果需要强制使用特定的 Resource 类型,则可以使用前缀. 以下两个示例显示如何强制 ClassPathResourceUrlResource (后者用于访问文件系统文件) :

<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 的 key 下-例如, 在可用于 Spring Environment 的属性文件中 (请参见[beans-environment]) . 然后可以通过 @Value 引用模板路径. 使用属性占位符的注解 (请参见 @Value) . Spring 会以字符串形式获取模板路径的值, 特殊的 PropertyEditor 将字符串转换为 Resource 对象, 以注入到 MyBean 构造函数中. 下面的示例演示如何实现此目的.

Java
@Component
public class MyBean {

    private final Resource template;

    public MyBean(@Value("${template.path}") Resource template) {
        this.template = template;
    }

    // ...
}
Kotlin
@Component
class MyBean(@Value("\${template.path}") private val template: Resource)

如果我们要支持在多个路径下的同一路径下发现的多个模板类路径中的位置-例如, 类路径中的多个 jar 中-我们可以使用特殊的 classpath*: 前缀和通配符将 templates.path key 定义为 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;
    }

    // ...
}
Kotlin
@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");
Kotlin
val ctx = ClassPathXmlApplicationContext("conf/appContext.xml")

bean 定义是从类路径加载的,因为使用了 ClassPathResource. 但是,请考虑以下示例,该示例创建 FileSystemXmlApplicationContext:

Java
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/appContext.xml");
Kotlin
val ctx = FileSystemXmlApplicationContext("conf/appContext.xml")

现在,bean 定义是从文件系统位置加载的(在这种情况下,相对于当前工作目录) .

若位置路径带有 classpath 前缀或 URL 前缀,会覆盖默认创建的用于加载 bean 定义的 Resource 类型. 请考虑以下示例:

Java
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
Kotlin
val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml")

使用 FileSystemXmlApplicationContext 从类路径加载 bean 定义. 但是,它仍然是 FileSystemXmlApplicationContext. 如果它随后用作 ResourceLoader,则任何未加前缀的路径仍被视为文件系统路径.

构造 ClassPathXmlApplicationContext 实例的快捷方式

ClassPathXmlApplicationContext 提供了多个构造函数,以利于快捷创建 ClassPathXmlApplicationContext 的实例. 基础的想法是, 使用只包含多个 XML 文件名(不带路径信息) 的字符串数组和一个 Class 参数的构造器,所省略路径信息 ClassPathXmlApplicationContext 会从 Class 参数中获取.

请考虑以下目录布局:

com/
  example/
    services.xml
    repositories.xml
    MessengerService.class

以下示例显示如何实例化由名为 services.xmlrepositories.xml (位于类路径中) 的文件中定义的 bean 组成的 ClassPathXmlApplicationContext 实例:

Java
ApplicationContext ctx = new ClassPathXmlApplicationContext(
    new String[] {"services.xml", "repositories.xml"}, MessengerService.class);
Kotlin
val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java)

有关各种构造函数的详细信息,请参阅 ClassPathXmlApplicationContext javadoc.

2.8.2. 使用通配符构造应用上下文

从前文可知,应用上下文构造器的资源路径可以是单一的路径(即一对一地映射到目标资源) . 也可以使用高效的通配符. 可以包含特殊的 "classpath*:" 前缀或 ant 风格的正则表达式(使用Spring的 PathMatcher 来匹配) .

通配符机制可用于组装应用程序的组件,应用程序里所有组件都可以在一个公用的位置路径发布自定义的上下文片段,那么最终的应用上下文可使用 classpath*: . 在同一路径前缀(前面的公用路径) 下创建,这时所有组件上下文的片段都会被自动装配.

请注意,此通配符特定于在应用程序上下文构造函数中使用资源路径(或直接使用 PathMatcher 实用程序类层次结构时) ,并在构造时解析. 它与 Resource 类型本身无关. 您不能使用 classpath*: 前缀来构造实际的 Resource,,因为资源一次只指向一个资源.

Ant 风格模式

路径位置可以包含 Ant 样式模式,如以下示例所示:

/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml

当路径位置包含 Ant 样式模式时,解析程序遵循更复杂的过程来尝试解析通配符. 解释器会先从位置路径里获取最靠前的不带通配符的路径片段, 并使用这个路径片段来创建一个 Resource,并从中获取一个 URL. 如果此 URL 不是 jar: URL 或特定于容器的变体(例如,在 WebLogic 中为 zip:,在WebSphere中为 wsjar,等等) 则从 Resource 里获取 java.io.File 对象,并通过其遍历文件系统. 进而解决位置路径里通配符. 对于 jar URL,解析器要么从中获取 java.net.JarURLConnection, 要么手动解析 jar URL,然后遍历 jar 文件的内容以解析通配符.

可移植性所带来的影响

如果指定的路径定为 file URL(不管是显式还是隐式的) ,首先默认的 ResourceLoader 就是文件系统,其次通配符使用程序可以完美移植.

如果指定的路径是 classpath 位置,则解析器必须通过 Classloader.getResource() 方法调用获取最后一个非通配符路径段URL. 因为这只是路径的一个节点(而不是末尾的文件) ,实际上它是未定义的(在 ClassLoader javadoc 中) ,在这种情况下并不能确定返回什么样的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");
Kotlin
val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml")

classpath*: 的使用表示该类路径下所有匹配文件名称的资源都会被获取(本质上就是调用了 ClassLoader.getResources(…) 方法,接着将获取到的资源装配成最终的应用上下文.

通配符类路径依赖于底层类加载器的 getResources() 方法. 由于现在大多数应用程序服务器都提供自己的类加载器实现,因此行为可能会有所不同,尤其是在处理 jar 文件时. 要在指定服务器测试 classpath* 是否有效,简单点可以使用 getClass().getClassLoader().getResources("<someFileInsideTheJar>") 来加载类路径 jar 包里的文件. 尝试在两个不同的路径加载相同名称的文件,如果返回的结果不一致,就需要查看一下此服务器中与 ClassLoader 设置相关的文档.

您还可以将 classpath*: 前缀与位置路径的其余部分中的 PathMatcher 模式组合在一起(例如,classpath*:META-INF/*-beans.xml) . 这种情况的解析策略非常简单,取位置路径最靠前的无通配符片段,然后调用 ClassLoader.getResources() 获取所有匹配到的类层次加载器加载资源,随后将 PathMatcher 的策略应用于每一个得到的资源.

通配符的补充说明

请注意,除非所有目标资源都存在文件系统中,否则 classpath*: 与Ant样式模式结合,都只能在至少有一个确定了根路径的情况下,才能达到预期的效果. 这意味着 classpath*:*.xml 等模式可能无法从 jar 文件的根目录中检索文件,而只能从根目录中的扩展目录中检索文件.

问题的根源是 JDK 的 ClassLoader.getResources() 方法的局限性. 当向 ClassLoader.getResources() 传入空串时(表示搜索潜在的根目录) , 只能获取的文件系统的位置路径,即获取不了 jar 中文件的位置路径. Spring 也会评估 URLClassLoader 运行时配置和 jar 文件中的 java.class.path 清单,但这不能保证导致可移植行为.

扫描类路径包需要在类路径中存在相应的目录条目. 使用 Ant 构建 JAR 时,请不要激活 JAR 任务的 files-only. 此外,在某些环境中,类路径目录可能不会基于安全策略暴露 - 例如,JDK 1.7.0_45 及更高版本上的独立应用程序(需要在清单中设置’Trusted-Library' . 请参阅 stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources.

在 JDK 9 的模块路径(Jigsaw) 上,Spring 的类路径扫描通常按预期工作. 此处强烈建议将资源放入专用目录,避免上述搜索 jar 文件根级别的可移植性问题.

如果有多个类路径上都用搜索到的根包,那么使用 classpath: 和ant风格模式一起指定资源并不保证会找到匹配的资源. 请考虑以下资源位置示例:

com/mycompany/package1/service-context.xml

现在考虑一个人可能用来尝试查找该文件的 Ant 风格路径:

classpath:com/mycompany/**/service-context.xml

这样的资源可能只在一个位置,但是当使用前面例子之类的路径来尝试解析它时,解析器会处理 getResource("com/mycompany") ;返回的(第一个) URL. 当在多个类路径存在基础包节点 "com/mycompany" 时(如在多个 jar 存在这个基础节点) ,解析器就不一定会找到指定资源. 因此,这种情况下建议结合使用 classpath*: 和 ant 风格模式,classpath*: 会让解析器去搜索所有包含以下基础包节点所有的类路径: classpath*:com/mycompany/**/service-context.xml.

2.8.3. FileSystemResource 的警告

FileSystemResourceFileSystemApplicationContext 之间没有联系(即,当 FileSystemApplicationContext 不是实际的 ResourceLoader 时) 时会按预期处理绝对路径和相对路径. 相对路径是相对与当前工作目录而言的,而绝对路径则是相对文件系统的根目录而言的.

但是,出于向后兼容性(历史) 的原因,当 FileSystemApplicationContextResourceLoader 时,这会发生变化. FileSystemApplicationContext 强制所有有联系的 FileSystemResource 实例将所有位置路径视为相对路径, 无论它们是否以 '/' 开头. 实际上,这意味着以下示例是等效的:

Java
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/context.xml");
Kotlin
val ctx = FileSystemXmlApplicationContext("conf/context.xml")
Java
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("/conf/context.xml");
Kotlin
val ctx = FileSystemXmlApplicationContext("/conf/context.xml")

以下示例也是等效的(即使它们有所不同,因为一个案例是相对的而另一个案例是绝对的) :

Java
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
Kotlin
val ctx: FileSystemXmlApplicationContext = ...
ctx.getResource("some/resource/path/myTemplate.txt")
Java
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");
Kotlin
val ctx: FileSystemXmlApplicationContext = ...
ctx.getResource("/some/resource/path/myTemplate.txt")

实际上,如果确实需要使用绝对路径,建议放弃使用 FileSystemResourceFileSystemXmlApplicationContext,而强制使用 file:UrlResource.

Java
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
Kotlin
// 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");
Kotlin
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml")

3. 验证, 数据绑定和类型转换

在业务逻辑中添加数据验证利弊参半,Spring 提供的验证(和数据绑定) 方案也未必能解决这个问题. 具体来说,验证不应该与 Web 层绑定,并且应该易于本地化,并且应该可以插入任何可用的验证器. 考虑到这些问题,Spring 提出了一个基本的,非常有用的 Validator 联系,它在应用程序的每一层都可用.

Spring 为开发者提供了一个称作 DataBinder 的对象来处理数据绑定,所谓的数据绑定就是将用户输入自动地绑定到相应的领域模型(或者说用来处理用户所输入的任何对象) . Spring 的 ValidatorDataBinder 构成了验证包,这个包主要用在 web 层使用,但绝不限于此.

BeanWrapper 是 Spring 框架中的一个基本概念,并且在很多地方使用. 但是,在使用过程中可能从未碰过它. 鉴于这是一份参考文档,那么对 BeanWrapper 的解释很有必要. 我们将在本章中解释 BeanWrapper,在开发者尝试将数据绑定到对象的时候一定会使用到它.

Spring的 DataBinder 和底层的 BeanWrapper 都使用 PropertyEditorSupport 实现来解析和格式化属性值. PropertyEditor 和PropertyEditorSupport 接口是 JavaBeans 规范的一部分,本章也对其进行了解释. Spring 3 引入了一个 core.convert 包,它提供了一个 通用类型转换工具,以及更高级别的 “format” 包格式化 UI 字段值。 您可以将这些包用作更简单的替代方案 PropertyEditorSupport 实现。 本章还将讨论它们。

Spring 可以通过一些基础设置来支持 Java Bean 的验证. 也可以统一个适配器来兼容 Spring 的 Validator 验证接口 应用程序既可以一次性开启控制全局的 Bean Validation,如 Spring Validation中所述,并专门用于所有验证需求. 应用程序还可以为每个 DataBinder 实例注册其他 Spring Validator 实例,如配置配置 DataBinder中所述.

3.1. 使用 Spring 的 Validator 接口来进行数据验证

Spring 提供了 Validator 接口用来进行对象的数据验证. Validator 接口在进行数据验证的时候会要求传入一个 Errors 对象,当有错误产生时会将错误信息放入该对象.

考虑以下对象的示例:

Java
public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}
Kotlin
class Person(val name: String, val age: Int)

下一个示例通过实现 org.springframework.validation.Validator 接口的以下两个方法为 Person 类提供验证行为:

  • supports(Class): 判断该 Validator 是否能验证提供的 Class 的实例?

  • validate(Object, org.springframework.validation.Errors): 验证给定的对象,如果有验证失败信息,则将其放入 Errors 对象.

实现一个 Validator 相当简单,尤其是使用 Spring 提供的 ValidationUtils 工具类时. 以下示例为 Person 实例实现 Validator:

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

ValidationUtils 中的静态方法 rejectIfEmpty(..) 方法用于拒绝 name 属性(如果它为 null 或空字符串) . 更多信息可以参考 ValidationUtils 的javadocs.

虽然可以实现一个 Validator 类来验证富对象中的每个嵌套对象,但最好将每个嵌套对象类的验证逻辑封装在自己的 Validator 实现中. 例如,有一个名为 Customer 的复杂对象,它有两个 String 类型的属性(first name和second name) ,另外还有一个 Address 对象. 它与 Customer 毫无关系, 它还实现了名为 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();
        }
    }
}
Kotlin
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 javadoc 中找到.

3.2. 通过错误编码得到错误信息

上一节介绍了数据绑定和数据验证,如何拿到验证错误信息是最后需要讨论的问题. 在上一个的例子中,验证器拒绝了 nameage 属性. 如果我们想通过使用 MessageSource 输出错误消息, 可以在验证失败时设置错误编码(本例中就是 nameage ) . 当调用(直接或间接地,通过使用 ValidationUtils 类) Errors 接口中的 rejectValue 方法或者它的任意一个方法时,它的实现不仅仅注册传入的错误编码参数, 还会注册一些遵循一定规则的错误编码. 注册哪些规则的错误编码取决于开发者使用的 MessageCodesResolver. 当使用默认的 DefaultMessageCodesResolver 时, 除了会将错误信息注册到指定的错误编码上,这些错误信息还会注册到包含属性名的错误编码上. 假如调用 rejectValue("age", "too.darn.old") 方法, Spring 除了会注册 too.darn.old 错误编码外, 还会注册 too.darn.old.agetoo.darn.old.age.int 这两个错误编码(即一个是包含属性名,另外一个既包含属性名还包含类型的) . 在 Spring 中这种注册称为注册约定,这样所有的开发者都能按照这种约定来定位错误信息.

有关 MessageCodesResolver 和默认策略的更多信息可分别在 MessageCodesResolverDefaultMessageCodesResolver, 的 javadoc 中找到.

3.3. 操作 bean 和 BeanWrapper

org.springframework.beans 包遵循 Oracle 提供的 JavaBeans 标准,JavaBean 只是一个包含默认无参构造器的类, 它遵循命名约定(举例来说) 名为 bingoMadness 属性将拥有设置方法 setBingoMadness(..) 和获取方法 getBingoMadness(). 有关 JavaBeans 和规范的更多信息,请参考 Oracle 的网站( javabeans) .

beans 包里一个非常重要的类是 BeanWrapper 接口和它的相应实现(BeanWrapperImpl). 引自 javadoc: BeanWrapper 提供了设置和获取属性值(单个或批量) 、 获取属性描述符以及查询属性以确定它们是可读还是可写的功能. BeanWrapper 还提供对嵌套属性的支持,能够不受嵌套深度的限制启用子属性的属性设置. BeanWrapper 还提供了无需目标类代码的支持就能够添加标准 JavaBeans 的 PropertyChangeListenersVetoableChangeListeners 的能力. 最后但同样重要的是, BeanWrapper 支持设置索引属性. 应用程序代码通常不会直接使用 BeanWrapper,而是提供给 DataBinderBeanFactory 使用.

BeanWrapper 顾名思义,它包装了 bean 并对其执行操作. 例如设置和获取属性.

3.3.1. 设置并获取基本和嵌套的属性

设置和获取属性是通过使用 setPropertyValue, 和 getPropertyValues 方法完成的,这些方法重载了 BeanWrapper. Springs javadoc 更详细地描述了它们. JavaBeans 规范具有指示对象属性的约定. 下表显示了这些约定的一些示例:

Table 10. Examples of properties
Expression Explanation

name

表示属性 namegetName()isName()setName(..) 方法相对应

account.name

表示 account 属性的嵌套属性 namegetAccount().setName()getAccount().getName() 相对应.

account[2]

表示索引属性 account 的第_3_个属性. 索引属性可以是 array, list, 其他自然排序的集合.

account[COMPANYNAME]

表示映射属性 account 是键为 COMPANYNAME 的值.

(如果您不打算直接使用 BeanWrapper ,那么下一节对您来说并不重要. 如果您只使用 DataBinderBeanFactory 及其默认实现,那么您应该跳到有关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;
    }
}
Kotlin
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;
    }
}
Kotlin
class Employee {
    var name: String? = null
    var salary: Float? = null
}

以下代码段显示了如何检索和操作实例化 CompaniesEmployees 的某些属性的一些示例:

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");
Kotlin
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 使用 PropertyEditor 的概念来实现 ObjectString 之间的转换,有时使用不同于对象本身的方式来表示属性显得更方便. 例如,Date 可以使用易于阅读的方式(如 String : '2007-14-09'). 还能将易于阅读的形式转换回原来的 Date (甚至做得更好: 转换任何以易于阅读形式输入的日期,然后返回日期对象) . 可以通过注册 java.beans.PropertyEditor 类型的自定义编辑器来实现此行为. 在 BeanWrapper 上注册自定义编辑器,或者在特定的 IoC 容器中注册自定义编辑器(如前一章所述) ,使其了解如何将属性转换为所需类型. 有关 PropertyEditor 的更多信息,请参阅 Oracle的java.beans包的 javadoc

在 Spring 中使用属性编辑的几个示例:

  • 通过使用 PropertyEditor 实现来设置 bean 的属性. 当您使用 java.lang.String 作为您在 XML 文件中声明的某个 bean 的属性的值时, Spring 将(如果相应属性的 setter 具有类参数) 使用 ClassEditor 尝试将参数解析为类对象.

  • 在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种 PropertyEditor 实现来完成的,您可以在 CommandController 的所有子类中手动绑定它们.

Spring 内置了许多 PropertyEditor 用于简化处理. 它们都位于 org.springframework.beans.propertyeditors 包中. 大多数(但不是全部,如下表所示) 默认情况下由 BeanWrapperImpl 注册. 当属性编辑器以某种方式进行配置时,开发者仍可以注册自定义的变体用于覆盖默认的变量. 下表描述了 Spring 提供的各种 PropertyEditor 实现:

Table 11. 内置 PropertyEditor 实现
说明

ByteArrayPropertyEditor

字节数组的编辑器. 将字符串转换为其对应的字节表示形式. BeanWrapperImpl 默认注册.

ClassEditor

将表示类的字符串解析为实际的类,反之亦然. 找不到类时,抛出 IllegalArgumentException. 默认情况下,由 BeanWrapperImpl 注册.

CustomBooleanEditor

Boolean 属性的可自定义属性编辑器. 默认情况下,由 BeanWrapperImpl 注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖.

CustomCollectionEditor

Collection 的属性编辑器,将任何源 Collection 转换为给定的目标 Collection 类型.

CustomDateEditor

java.util.Date 的可自定义属性编辑器,支持自定义 DateFormat. 默认未注册. 必须根据需要使用适当的格式进行用户注册.

CustomNumberEditor

任何 Number 子类的可自定义属性编辑器,例如 Integer, Long, FloatDouble. 默认情况下,由 BeanWrapperImpl 注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖.

FileEditor

将字符串解析为 java.io.File 对象. 默认情况下,由 BeanWrapperImpl 注册.

InputStreamEditor

单向属性编辑器,可以获取字符串并生成(通过中间 ResourceEditorResource) InputStream,以便 InputStream 属性可以直接设置为字符串. 请注意,默认用法不会为您关闭 InputStream. 默认情况下,由 BeanWrapperImpl 注册.

LocaleEditor

可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 [language]_[country]_[variant],与 LocaletoString() 方法相同) .也接受空格作为分隔符,作为下划线的替代。 默认情况下,由 BeanWrapperImpl 注册.

PatternEditor

可以将字符串解析为 java.util.regex.Pattern 对象,反之亦然.

PropertiesEditor

可以将字符串(使用 java.util.Properties 类的 javadoc 中定义的格式进行格式化) 转换为 Properties 对象. 默认情况下,由 BeanWrapperImpl 注册.

StringTrimmerEditor

修剪字符串的属性编辑器. (可选) 允许将空字符串转换为 null. 默认情况下未注册 - 必须是用户注册的.

URLEditor

可以将URL的字符串表示形式解析为实际的 URL 对象. 默认情况下,由 BeanWrapperImpl 注册.

Spring 使用 java.beans.PropertyEditorManager 设置属性编辑器(可能需要) 的搜索路径. 搜索路径还包括 sun.bean.editors,其中包括 Font, Color 和大多数基本类型等类型的 PropertyEditor 实现. 注意,标准的 JavaBeans 架构可以自动发现 PropertyEditor 类(无需显式注册) ,前提是此类与需处理的类位于同一个包,并且与该类具有相同的名称. 并以 Editor 单词结尾. 可以使用以下类和包结构,这足以使 SomethingEditor 类被识别并用作 Something 类型属性的 PropertyEditor.

com
  chank
    pop
      Something
      SomethingEditor // the PropertyEditor for the Something class

请注意,您也可以在此处使用标准 BeanInfo JavaBeans机制(这里描述的是无关紧要的细节) . 以下示例使用 BeanInfo 机制使用关联类的属性显式注册一个或多个 PropertyEditor 实例:

com
  chank
    pop
      Something
      SomethingBeanInfo // the BeanInfo for the Something class

以下引用的 SomethingBeanInfo 类的 Java 源代码将 CustomNumberEditorSomething 类的 age 属性相关联:

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());
        }
    }
}
Kotlin
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 容器最终使用标准 JavaBeans PropertyEditor 实现将这些字符串转换为属性的复杂类型. Spring 预先注册了许多自定义 PropertyEditor 实现(例如,将表示为字符串的类名转换为 Class 对象) . 此外,Java 的标准 JavaBeans PropertyEditor 查找机制允许对类的 PropertyEditor 进行适当的命名,并将其放置在与其提供支持的类相同的包中,以便可以自动找到它.

如果需要注册其他自定义 PropertyEditors,可以使用多种机制. 通常最麻烦也不推荐的策略是手动、简单的使用 ConfigurableBeanFactory 接口的 registerCustomEditor() 方法, 假设有一个 BeanFactory 引用,另一种(稍微更方便) 机制是使用一个名为 CustomEditorConfigurer 的特殊 bean 工厂后置处理器. 尽管您可以将 bean 工厂后置处理器与 BeanFactory 实现一起使用,但 CustomEditorConfigurer 具有嵌套属性设置, 因此我们强烈建议您将它与 ApplicationContext 一起使用,您可以在其中以类似的方式将其部署到任何其他 bean 以及它可以在哪里 自动检测并应用.

请注意,所有的 bean 工厂和应用程序上下文都自动使用了许多内置属性编辑器,在其内部都是使用 BeanWrapper 来进行属性转换的. BeanWrapper 注册的标准属性编辑器列在上一节中 此外,ApplicationContexts 还会覆盖或添加其他编辑器,以适合特定应用程序上下文类型的方式处理资源查找.

标准的 PropertyEditor JavaBeans 实例用于将以字符串表示的属性值转换为属性的实际复杂类型. CustomEditorConfigurer 是一个 bean 后置处理工厂,可用于方便地在 ApplicationContext 中添加额外的 PropertyEditor 实例.

请考虑以下示例,该示例定义名为 ExoticType 的用户类和另一个名为 DependsOnExoticType 的类,该类需要将 ExoticType 设置为属性:

Java
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;
    }
}
Kotlin
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
public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}
Kotlin
import java.beans.PropertyEditorSupport

class ExoticTypeEditor : PropertyEditorSupport() {

    override fun setAsText(text: String) {
        value = ExoticType(text.toUpperCase())
    }
}

最后,以下示例显示如何使用 CustomEditorConfigurerApplicationContext 注册新的 PropertyEditor,然后可以根据需要使用它:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
        </map>
    </property>
</bean>
使用 PropertyEditorRegistrar

使用 Spring 容器注册属性编辑器的另一个策略是创建和使用 PropertyEditorRegistrar. 当需要在多种不同的情况下使用相同的属性编辑器集时,这个接口特别有用,编写相应的注册器并在每个案例中重用. PropertyEditorRegistrar 与另外一个称为 PropertyEditorRegistry 的接口一起工作. 它使用 Spring BeanWrapper(和DataBinder)实现. PropertyEditorRegistrar 在与 CustomEditorConfigurer (本节介绍的)一起使用时特别方便, 它暴露 setPropertyEditorRegistrars(..) 的属性. PropertyEditorRegistrarCustomEditorConfigurer 结合使用可以简单的在 DataBinder 和 Spring MVC 控制之间共享. 它避免了在自定义编辑器上进行同步的需要: PropertyEditorRegistrar需要为每个bean创建尝试创建新的 PropertyEditor 实例.

以下示例显示如何创建自己的 PropertyEditorRegistrar 实现:

Java
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...
    }
}
Kotlin
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...
    }
}

有关 PropertyEditorRegistrar 实现的示例,另请参见 org.springframework.beans.support.ResourceEditorRegistrar. 请注意,在实现 registerCustomEditors(..) 方法时,它会创建每个属性编辑器的新实例.

下一个示例显示如何配置 CustomEditorConfigurer 并将 CustomPropertyEditorRegistrar 的实例注入其中:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>

<bean id="customPropertyEditorRegistrar"
    class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>

最后(与本章的重点有所不同,对于那些使用Spring’s MVC web framework框架的人来说) ,使用 PropertyEditorRegistrars 和数据绑定控制器(SimpleFormController) 可以非常方便. 以下示例在 @InitBinder 方法的实现中使用 PropertyEditorRegistrar:

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
}
Kotlin
@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 注册代码封装在一个类中,然后根据需要在尽可能多的 Controllers 之间共享.

3.4. Spring 类型转换

Spring 3 引入了一个 core.convert 包,它提供了一个通用的类型转换系统. 系统定义了一个用于实现类型转换逻辑的 SPI 和一个用于在运行时执行类型转换的 API. 在 Spring 的容器中,此系统可以用作 PropertyEditor 的替代方法,它将外部 bean 属性值字符串转换为所需的属性类型. 您还可以在需要进行类型转换的应用程序中的任何位置使用公共 API.

3.4.1. SPI 转换器

实现类型转换逻辑的 SPI 是简易的,而且是强类型的. 如以下接口定义所示:

public interface Converter<S, T> {

    T convert(S source);
}

创建自定义转换器都需要实现 Converter 接口,参数 S 是需要转换的类型,T 是转换后的类型. 这个转换器也可以应用在集合或数组上将 S 参数转换为 T 参数. 前提是已经注册了委托数组或集合转换器(DefaultConversionService 默认情况下也是如此) .

对于要 convert(S) 的每个调用,source 参数需保证不为 null. 转换失败时,Converter 可能会引发任意的 unchecked 异常. 具体来说,它应抛出 IllegalArgumentException 以报告无效的 source 值. 请注意确保您的 Converter 实现是线程安全的.

为方便起见,core.convert.support 包中提供了几个转换器实现. 这些包括从字符串到数字和其他常见类型的转换器. 以下清单显示了 StringToInteger 类,它是典型的 Converter 实现:

final class StringToInteger implements Converter<String, Integer> {

    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

3.4.2. 使用 ConverterFactory

当需要集中整个类层次结构的转换逻辑时(例如,从 String 转换为 Enum 对象时) ,您可以实现 ConverterFactory,如以下示例所示:

public interface ConverterFactory<S, R> {

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

参数化 S 为您要转换的类型,R 是需要转换后的类型的基类. 然后实现 getConverter(Class),其中 TR 的子类.

StringToEnumConverterFactory 为例:

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }

    private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}

3.4.3. 使用 GenericConverter

当您需要复杂的 Converter 实现时,请考虑使用 GenericConverter 接口. GenericConverter 具有比 Converter 更灵活但不太强类型的签名,支持在多种源和目标类型之间进行转换. 此外,GenericConverter 可以在实现转换逻辑时使用可用的源和目标字段上下文. 此上下文类允许通过字段注解或在字段签名上声明的一般信息来驱动类型转换. 以下清单显示了 GenericConverter 的接口定义:

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

要实现 GenericConverter,请使用 getConvertibleTypes() 返回支持的 source→target 类型对,然后实现 convert(Object, TypeDescriptor, TypeDescriptor) 方法并编写转换逻辑. 源 TypeDescriptor 提供对保存要转换的值的源字段的访问,目标 TypeDescriptor 提供对要设置转换值的目标字段的访问.

Java 数组和集合之间转换的转换器是 GenericConverter 应用的例子. 其中 ArrayToCollectionConverter 内部声明目标集合类型用于解析集合元素类型的字段. 它允许在目标字段上设置集合之前,将源数组中的每个元素转换为集合元素类型.

因为 GenericConverter 是一个更复杂的SPI接口,所以只有在需要时才应该使用它. 一般使用 ConverterConverterFactory 足以满足基本的类型转换需求.
使用 ConditionalGenericConverter

有时可能只想在特定条件为真时才执行 Converter,例如,在特定注解的目标上使用 Converter,或者,在一个特定的目标类方法(例如 static valueOf 方法) 中执行 Converter. ConditionalGenericConverterGenericConverterConditionalConverter 接口的组合. 允许自定义匹配条件

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

用于持久实体标识符和实体引用之间转换的 IdToEntityConverterConditionalGenericConverter 应用的例子. 如果目标实体类型声明静态查找器方法(如 findAccount(Long)), 那么 IdToEntityConverter 只对匹配的生效. 开发者可以实现 matches(TypeDescriptor, TypeDescriptor) 以执行 finder 方法来检查是否匹配.

3.4.4. ConversionService API

ConversionService 定义了一个统一的 API,用于在运行时执行类型转换逻辑. 转换器通常在以下 Facade 接口后面执行:

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 实现委托其注册的转换器执行类型转换逻辑.

core.convert.support 包中提供了强大的 ConversionService 实现. GenericConversionService 是适用于大多数环境的通用实现. ConversionServiceFactory 提供了一个方便的工厂,用于创建常见的 ConversionService 配置.

3.4.5. 配置 ConversionService

ConversionService 是一个无状态对象,在应用程序启动时就会实例化,可以被多个线程共享. 在 Spring 应用程序中,通常每个 Spring 容器(或 ApplicationContext ) 配置一个 ConversionService 实例. 该 ConversionService 将被 Spring 获取,然后在框架需要执行类型转换时使用. 也可以将 ConversionService 插入任意 bean 并直接调用它.

如果没有向 Spring 注册 ConversionService,则使用基于 PropertyEditor 的原始系统.

要使用 Spring 注册默认的 ConversionService,请添加以下 bean 定义,其 idconversionService:

<bean id="conversionService"
    class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默认的 ConversionService 可以在字符串,数字,枚举,集合,映射和其他常见类型之间进行转换. 要使用您自己的自定义转换器补充或覆盖默认转换器,请设置 converters 属性. 属性值可以任何实现了 Converter, ConverterFactory, 或 GenericConverter 接口的类.

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>

在 Spring MVC 应用程序中使用 ConversionService 也很常见. 请参阅Spring MVC章节中的转换和格式化 .

在某些情况下,您可能希望在转换期间应用格式. 有关使用 FormattingConversionServiceFactoryBean 的详细信息,请参阅 FormatterRegistry SPI.

3.4.6. 使用 ConversionService 编程

要以编程方式使用 ConversionService 实例,您可以像对任何其他 bean 一样注入对它的引用. 以下示例显示了如何执行此操作:

Java
@Service
public class MyService {

    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void doIt() {
        this.conversionService.convert(...)
    }
}
Kotlin
@Service
class MyService(private val conversionService: ConversionService) {

    fun doIt() {
        conversionService.convert(...)
    }
}

对于大多数用例,您可以使用指定 targetTypeconvert 方法,但它不适用于更复杂的类型,例如参数化元素的集合. 例如,如果想使用编程的方式将整数列表转换为字符串列表,则需要提供源和目标类型的正规定义.

幸运的是,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)));
Kotlin
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 会自动注册适合大多数环境的转换器. 这包括集合转换器,基本类型转换器和基本的对象到字符串转换器. 您可以使用 DefaultConversionService 类上的静态 addDefaultConverters 方法向任何 ConverterRegistry 注册相同的转换器.

值类型的转换器可以重用于数组和集合,因此无需创建特定的转换器即可将 SCollection 转换为 TCollection,前提是标准集合处理是合适的.

3.5. Spring 字段格式化

如前一节所述, core.convert 是一种通用类型转换系统. 它提供统一的 ConversionService API 以及强类型转换器 SPI,用于实现从一种类型到另一种类型的转换逻辑. Spring 容器使用此系统绑定 bean 属性值. 此外,Spring Expression Language(SpEL) 和 DataBinder 都使用此系统绑定字段值. 此外,当 SpEL 需要将 Short 类型强转为 Long 类型, 用于试图完成 expression.setValue(Object bean, Object value) 时,那么 core.convert 系统也可以提供这种功能.

现在考虑典型客户端环境(例如 Web 或桌面应用程序) 的类型转换要求. 在这种环境中,在这种环境中,还包括转换成为 String 用于支持视图呈现程序. 此外,还通常需要本地化字符串值. 普通的转化器 SPI 没有提供按照直接进行格式转换的功能. 更通用的 core.convert Converter SPI不能解决此类要求. 为了实现这个功能,Spring 3 添加了方便的 Formatter SPI,它提供了简单强健的的 PropertyEditor 专供客户端环境.

通常, 当需要使用通用类型转换时可以用 Converter SPI. 例如,在 java.util.Datejava.lang.Long 之间进行转换. 在客户端环境(例如Web应用程序) 中工作时,可以使用 Formatter SPI, 并且需要解析和打印本地化的字段值. ConversionService 为两个 SPI 提供统一的类型转换 API.

3.5.1. Formatter SPI

Formatter SPI 实现字段格式化逻辑是简单的,强类型的. 以下清单显示了 Formatter 接口定义:

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter 继承了内置的 PrinterParser 接口. 以下清单显示了这两个接口的定义:

public interface Printer<T> {

    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;

public interface Parser<T> {

    T parse(String clientValue, Locale locale) throws ParseException;
}

如果需要创建自定义的 Formatter,需要实现 Formatter 接口. 参数 T 类型是你需要格式化的类型. 例如,java.util.Date. 实现 print() 操作在客户端本地设置中打印显示的 T 实例. 实现 parse() 操作以从客户端本地设置返回的格式化表示形式分析T的实例. 如果尝试分析失败,Formatter 会抛出 ParseExceptionIllegalArgumentException 异常. 注意确保自定义的 Formatter 是线程安全的.

format 子包提供了多种 Formatter 实现方便使用. number 子包中提供了 NumberStyleFormatter, CurrencyStyleFormatter, 和 PercentStyleFormatter 用于格式化 java.lang.Number(使用 java.text.NumberFormat) . datetime 子包中提供了 DateFormatter 用于格式化 java.util.Date(使用 java.text.DateFormat) .

以下 DateFormatterFormatter 实现的示例:

Java
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;
    }
}
Kotlin
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 Issues 进行贡献.

3.5.2. 基于注解的格式化

字段格式也可以通过字段类型或注解进行配置. 如果要将注解绑定到 Formatter,请实现 AnnotationFormatterFactory. 以下清单显示了 AnnotationFormatterFactory 接口的定义:

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);
}

创建一个实现: . 参数 A 是要与之关联的字段 annotationType 逻辑格式(例如: org.springframework.format.annotation.DateTimeFormat) . Have getFieldTypes() 返回可以在其上使用注解的字段的类型. . Have getPrinter() 返回 Printer 以打印带注解的字段的值. . Have getParser() 返回 Parser 解析注解字段的 clientValue

参数化 A 是将格式逻辑与(例如 org.springframework.format.annotation.DateTimeFormat 关联到字段 annotationType. getFieldTypes() 返回注解可用的字段类型. 使 getPrinter() 返回 Printer 以打印注解字段值. getParser() 返回一个 Parser 以分析注解字段的 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();
            }
        }
    }
}
Kotlin
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;
}
Kotlin
class MyModel(
    @field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal
)
格式注解 API

org.springframework.format.annotation 包中存在可移植格式注解 API. 您可以使用 @NumberFormat 格式化 java.lang.Number 字段,例如 Double and Long . 使用 @DateTimeFormat 格式化 java.util.Date, java.util.Calendar,java.util.Long JSR-310 java.time 字段.

下面的示例使用 @DateTimeFormatjava.util.Date 化为 ISO Date(yyyy-MM-dd):

Java
public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}
Kotlin
class MyModel(
    @DateTimeFormat(iso= ISO.DATE) private val date: Date
)

3.5.3. FormatterRegistry SPI

FormatterRegistry 是一个用于注册格式化程序和转换器的 SPI. FormattingConversionService 适用于大多数环境的 FormatterRegistry 实现. 此实现可以通过编程或以声明的方式配置为可用 FormattingConversionServiceFactoryBean 的 Spring bean. 由于它也实现了 ConversionService,因此可以直接配置用于 Spring 的 DataBinder 和 Spring 的表达式语言(SpEL) .

以下清单显示了 FormatterRegistry:

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

如上所示, Formatters 通过 fieldType 或注解进行注册.

FormatterRegistry SPI 可以集中配置格式规则,避免跨控制器的重复配置. 例如,想要强制所有日期字段都以特定方式格式化,或者具有特定注解的字段以某种方式格式化. 使用共享的 FormatterRegistry,开发者只需一次定义这些规则,即可到处使用.

3.5.4. FormatterRegistrar SPI

FormatterRegistrar 是用于注册格式化器和通过 FormatterRegistry 转换的 SPI:

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

FormatterRegistrar 用于注册多个相关的转换器或格式化器(根据给定的格式化目录注册,例如 Date 格式化) . 在直接注册不能实现时 FormatterRegistrar 就派上用场了, 例如,当格式化程序需要在不同于其自身 <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 格式(此示例不依赖于 Joda-Time 库) :

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 date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}
Kotlin
@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 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 框架提供了 Java Bean验证 API.

3.7.1. Bean验证概述

Bean 验证为 Java 应用程序提供了通过约束声明和元数据进行验证的通用方法. 要使用它,您需要使用声明性验证约束对 domain 模型属性进行注解,然后由运行时强制实施. 有内置的约束,您也可以定义自己的自定义约束.

请考虑以下示例,该示例显示了具有两个属性的简单 PersonForm 模型:

Java
public class PersonForm {
    private String name;
    private int age;
}
Kotlin
class PersonForm(
        private val name: String,
        private val age: Int
)

Bean 允许您为这些属性定义声明性验证约束,如以下示例所示:

Java
public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}
Kotlin
class PersonForm(
    @get:NotNull @get:Size(max=64)
    private val name: String,
    @get:Min(0)
    private val age: Int
)

然后,Bean 验证验证器根据声明的约束来验证此类的实例.

有关 API 的常规信息,请参阅 Bean Validation 网站. 有关默认引用实现的特定功能的信息,请参阅 Hibernate Validator 文档. 要了解如何将 bean 验证提供程序设置为 Spring bean,请继续阅读以下内容.

3.7.2. 配置 bean Validation 提供者

Spring 提供了对 Bean 验证 API 的全面支持,包括将 Bean 验证提供程序作为 Spring Bean 进行引导. 这样,您就可以在应用程序需要验证的任何地方注入 javax.validation.ValidatorFactoryjavax.validation.Validator.

您可以使用 LocalValidatorFactoryBean 将默认 Validator 配置为 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) 应该存在于类路径中并自动检测.

注入Validator

LocalValidatorFactoryBean 实现了 javax.validation.ValidatorFactoryjavax.validation.Validator, 以及 Spring 的 org.springframework.validation.Validator. 您可以将这些接口中的任何一个引用注入到需要调用验证逻辑的 bean 中.

如果您希望直接使用 Bean Validation API,则可以注入对 javax.validation.Validator 的引用,如以下示例所示:

Java
import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}
Kotlin
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;
}
Kotlin
import org.springframework.validation.Validator

@Service
class MyService(@Autowired private val validator: Validator)
配置自定义约束

每个 bean 验证约束由两部分组成:

  • 首先是声明约束及其可配置属性的 @Constraint 注解

  • 实现约束行为的 javax.validation.ConstraintValidator 接口实现.

如果要将声明与实现关联,每个 @Constraint 注解都会引用相应的 ConstraintValidator 实现类. 在运行中,当在 domain 模型中遇到约束注解时,ConstraintValidatorFactory 会将引用的实现实例化.

默认情况下,LocalValidatorFactoryBean 会配置 SpringConstraintValidatorFactory,它会使用 Spring 去创建 ConstraintValidator 实例. 这允许自定义 ConstraintValidators, 就像任何其他 Spring bean 一样,从依赖注入中获益.

下面是自定义 @Constraint 声明的例子,使用 Spring 的依赖注入来管理 ConstraintValidator 的实现:

Java
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
Kotlin
@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;

    // ...
}
Kotlin
import javax.validation.ConstraintValidator

class MyConstraintValidator(private val aDependency: Foo) : ConstraintValidator {

    // ...
}

如前面的示例所示,ConstraintValidator 实现可以将其依赖 @Autowired 与任何其他 Spring bean 一样.

Spring驱动的方法验证

Bean Validation 1.1 支持的方法验证,Hibernate Validator 4.3 支持的自定义扩展都可以通过 MethodValidationPostProcessor 定义集成到 Spring 上下文中. 如下所示:

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 进行注解,还可以选择声明要使用的验证组. 使用 Hibernate Validator 和 Bean Validation 1.1 提供验证的步骤可以查看 MethodValidationPostProcessor的 javadocs

方法验证依赖于 AOP Proxies 目标类,接口上的方法的 JDK 动态代理或 CGLIB 代理。 使用代理有一定的限制,其中一些在 了解 AOP 代理。 另外记得 始终在代理类上使用方法和访问器; 直接访问字段将不起作用。

额外的配置选项

对于大多数情况,默认的 LocalValidatorFactoryBean 配置就足够了. 从消息插入到遍历解析,各种 Bean Validation 构造有许多配置选项. 有关这些选项的更多信息,请参阅 LocalValidatorFactoryBean javadoc.

3.7.3. 配置 DataBinder

从 Spring 3 开始,您可以使用 Validator 配置 DataBinder 实例. 配置完成后,您可以通过调用 binder.validate() 来调用 Validator. 任何验证 Errors 都会自动添加到 binder 的 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();
Kotlin
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 还可以通过 dataBinder.addValidatorsdataBinder.replaceValidators 来配置多个 Validator 实例. 将全局配置的 Bean Validation 与本地在 DataBinder 实例上配置的Spring Validator 相结合,这非常有用. 请参阅 webmvc.html.

3.7.4. Spring MVC 3 验证

请在 Spring MVC 章节 查看 验证.

4. Spring 的表达式语言(SpEL)

Spring Expression Language(简称 "SpEL") 是一种强大的表达式语言,支持在运行时查询和操作对象. 语言语法类似于 Unified EL,但提供了其他功能,最有名的是方法调用和基本字符串模板功能.

尽管还有其他几种 Java 表达式语言,例如 OGNL, MVEL, 和 JBoss,但 SpEL 只是为了向 Spring 社区提供一种支持良好的表达式语言,开发者可以在所有用到 Spring 框架的产品中使用 SpEL. 其语言特性是由使用 Spring 框架项目的需求所驱动的,包括基于 Eclipse 的 Spring Tools for Eclipse 代码支持的需求. 也就是说,SpEL 基于一种抽象实现的技术 API, 允许在需要时集成其他表达式语言来实现功能.

虽然 SpEL 作为 Spring 产品组合中的表达式运算操作的基础,但它并不直接与 Spring 耦合,可以独立使用. 要自包含,本章中的许多示例都使用 SpEL,就像它是一种独立的表达式语言一样. 这时需要创建一些引导作用的基础实现类,例如解释器. 大多数 Spring 用户不需要处理这种基础实现类,只需将表达式字符串作为运算操作即可. SpEL 典型用途的是集成到创建XML或基于注解的 bean 定义中, 例如bean 定义的表达式支持.

本章介绍表达式语言的功能,API 及其语言语法. 在一些地方,InventorSociety 类作为表达式运算操作的目标对象. 这些类声明和用于填充它们的数据列在本章末尾.

表达式语言支持以下功能:

  • 文字表达

  • B 布尔和关系运算符

  • 正则表达式

  • 类表达式

  • 访问属性,数组,list 和 maps

  • 方法调用

  • 关系运算符

  • Assignment

  • 调用构造器

  • bean 的引用

  • 数组的构造

  • 内嵌的 list

  • 内嵌的map

  • 三元表达式

  • 变量

  • 用户自定义函数

  • 集合映射

  • 集合选择

  • 模板表达式

4.1. 使用 Spring 表达式接口的表达式运算

本节介绍 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'.
Kotlin
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 负责运算操作先前定义的表达式字符串. 当分别调用 parser.parseExpressionexp.getValue 时,可能抛出两个异常: ParseExceptionEvaluationException.

SpEL 支持广泛的功能,例如调用方法,访问属性和调用构造函数.

在下面的方法调用示例中,我们在字符串文字上调用 concat 方法:

Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();
1 message 现在的值为 'Hello World!'.
Kotlin
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 该行将文字转换为字节数组
Kotlin
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 给出了字符串的长度.
Kotlin
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 对象并使其成为大写
Kotlin
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). 使用此方法不需要将表达式的值转换为所需的结果类型. 如果该值不能转换为类型 T 或使用注册的类型转换器转换, 则将抛出 EvaluationException 异常.

SpEL 的更常见用法是提供针对特定对象实例(称为根对象) 计算的表达式字符串. 以下示例显示如何从 Inventor 类的实例检索 name 属性或创建布尔条件:

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
Kotlin
// 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 (no reflection)

  • 只读访问的数据绑定属性

  • 读写的数据绑定属性

类型转换

默认情况下,SpEL 使用 Spring 核心类( org.springframework.core.convert.ConversionService )提供的转换服务. 此转换服务附带许多转换器,内置很多常用转换,但也支持扩展. 因此可以添加类型之间的自定义转换. 此外,它具有泛型感知的关键功能. 这意味着在使用表达式中的泛型类型时,SpEL 将尝试转换以维护遇到的任何对象的类型正确性.

这在实践中能得到什么好处? 假设使用 setValue() 的赋值被用于设置 List 属性. 属性的类型实际上是 List<Boolean>,SpEL 会识别列表的元素需要在被放置在其中之前被转换为 Boolean . 以下示例显示了如何执行此操作:

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);
Kotlin
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 将尝试使用元素类型的默认值创建元素设置指定值之前的构造函数. 如果元素类型没有 默认构造函数 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
Kotlin
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 次迭代的微基准测试示例中,使用解析器评估需要 75ms,使用表达式的编译版本只需 3ms.

编译器配置

编译器在默认情况下是关闭的,有两种方法可以打开. 您可以使用解析器配置过程(前面讨论的) 或在将 SpEL 用法嵌入到另一个组件中时使用 Spring 属性来打开它. 本节讨论这两个选项.

编译器可以以三种模式之一操作,这些模式在 org.springframework.expression.spel.SpelCompilerMode 枚举中获取. 模式如下:

  • OFF (default): 编译器已关闭.

  • IMMEDIATE: 在即时模式下,表达式将尽快编译. 这通常在第一次解释运算之后,如果编译的表达式失败(通常是由于类型更改引起的,参看上一节) ,则表达式运算操作的调用者将收到异常.

  • MIXED: 在混合模式下,表达式随着时间的推移在解释模式和编译模式之间静默地切换. 经过一些解释运行后,它们将切换到编译模式,如果编译后的表单出现问题(如上所述改变类型) , 表达式将自动重新切换回解释模式. 稍后,它可能生成另一个编译表单并切换. 基本上,用户进入 IMMEDIATE 模式的异常是内部处理的.

推荐 IMMEDIATE 即时模式,因为 MIXED 模式可能会导致副作用,使得表达式出错. 如果编译的表达式在部分成功之后崩掉,此时可能已经影响了系统状态. 如果发生这种情况,调用者可能不希望它在解释模式下静默地重新运行,这样的话表达式的某部分可能会运行两次.

选择模式后,使用 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);
Kotlin
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 嵌入其他组件内部使用,并且可能无法通过配置对象进行配置. 在这种情况下,可以使用 JVM 系统属性配置 (或者通过 SpringProperties). 属性 spring.expression.compiler.mode 可以设置为 SpelCompilerMode 枚举值之一(off, immediate, 或 mixed) .

编译器限制

从 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(类型为 org.springframework.core.env.Environment)以及 systemProperties 和用于访问运行时环境的 systemEnvironment(类型为 Map<String,Object>).

以下示例显示了对 SpEL 变量对 systemProperties 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;
    }
}
Kotlin
class FieldValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    var defaultLocale: String? = null
}

下面显示了属性 setter 方法的相同配置:

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;
    }
}
Kotlin
class PropertyValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    var defaultLocale: String? = null
}

使用 @Autowired 方法注解的构造方法也可以使用 @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;
    }

    // ...
}
Kotlin
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;
    }

    // ...
}
Kotlin
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();
Kotlin
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. Properties, Arrays, Lists, Maps, 和 Indexers

调用属性的引用是很简单的,只要指定内置的属性值即可. 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);
Kotlin
// 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 + 1900, 并且 分别是 PlaceOfBirth.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);
Kotlin
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)

maps 的内容通过方括号包着文字的键/值定义. 在这种情况下, 由于 officerskeys 是字符串,则可以定义字符字面值:

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");
Kotlin
// 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. 内嵌的 Lists

您可以使用 {} 表示法直接在表达式中表达列表.

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);
Kotlin
// 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. 内嵌 Maps

您还可以使用 {key:value} 表示法直接在表达式中表达 map. 以下示例显示了如何执行此操作:

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);
Kotlin
// 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<, *>

{:} 本身就是一张空 map. 出于性能原因,如果 map 本身由固定文字或其他嵌套常量结构(列表或 map) 组成, 则会创建一个常量来表示表达式(而不是在每次计算时构建新 map) . map 的双引号是可选的(除非 key 包含句点 (.)).上面的示例没有使用双引号的 key.

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);
Kotlin
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);
Kotlin
// 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 Expression Language 支持以下类型的运算符:

关系运算符

使用标准运算符表示法支持关系运算符(等于,不等于,小于,小于或等于,大于,等于或等于) . 以下清单显示了一些运算符示例:

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);
Kotlin
// 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 被视为空(不是零) . 因此,任何其他值始终大于 null ( X > 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);
Kotlin
// 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);
Kotlin
// -- 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
Kotlin
// 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);
Kotlin
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 的实例类型. 也可以使用此运算符调用静态方法. StandardEvaluationContext 使用 TypeLocator 来查找类型, 而 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);
Kotlin
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 包中的类型 (Integer, Float, String,等等是可以直接使用的) 外, 所有类型需要使用全限定类名. 以下示例显示如何使用 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);
Kotlin
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 模式来表示. 变量的设置用到 EvaluationContextsetVariable 方法.

有效的变量名称必须由以下一种或多种支持的组成字符.

  • 字母: A to Z and a to z

  • 数字: 0 to 9

  • 下划线: _

  • dollar 符: $

以下示例显示了如何使用变量.

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"
Kotlin
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);
Kotlin
// 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);
Kotlin
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();
    }
}
Kotlin
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);
Kotlin
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);
Kotlin
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);
Kotlin
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);
Kotlin
val falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String::class.java)

在这种情况下,布尔值 false 会返回字符串值 '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"
Kotlin
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 运算符

Elvis 运算符是三元运算符语法的缩写,用于http://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] 语言. 使用三元运算符语法,您通常必须重复两次变量,如以下示例所示:

String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

可以使用 Elvis 运算符来实现,上面例子的也可以使用如下的形式展现:

Java
ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name);  // 'Unknown'
Kotlin
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
Kotlin
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. 安全的引导运算符

安全的引导运算符用于避免 NullPointerException 异常,这种观念来自 Groovy 语言. 当需要引用一个对象时, 可能需要在访问对象的方法或属性之前验证它是否为 null. 为避免出现这种情况, 安全引导运算符将简单地返回 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!!!
Kotlin
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. 集合的选择

Selection 是一种功能强大的表达语言功能,通过从条目中进行选择,可以将某些源集合转换为另一种集合.

Selection 使用语法是 .?[selectionExpression]. 它会过滤集合并返回一个新的集合,其包含一个原始数据的子集合. 例如,Selection 可以简单地获取 Serbian inventors 的list:

Java
List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "members.?[nationality == 'Serbian']").getValue(societyContext);
Kotlin
val list = parser.parseExpression(
        "members.?[nationality == 'Serbian']").getValue(societyContext) as List<Inventor>

Selection 可以使用在数组,或实现了 java.lang.Iterable 接口的任何类和 map 上.对于数组和列表 选择标准是针对每一个元素,而当选择一个 map 时将会处理每个 map 的 entry(Java 类型 Map.Entry 的对象) ,Map 的 entry 有他的 keyvalue 作为属性访问在 Selection 中使用.

以下表达式将返回一个新的 map,包括原有 map 中所有值小于 27 的条目:

Java
Map newMap = parser.parseExpression("map.?[value<27]").getValue();
Kotlin
val newMap = parser.parseExpression("map.?[value<27]").getValue()

除了返回所有选定元素外, 还可以只检索第一个或最后一个值. 要获得与所选内容匹配的第一个元素语法是 .^[selectionExpression]. 而获取最后一个匹配的选择语法是 .$[selectionExpression].

4.3.17. 集合投影

投影允许集合被一个子表达式处理而且结果是一个新的集合. 投影的语法是 .![projectionExpression]. 通过例子可便于理解,假设有一个 invertors 的 list 并且希望其生产一个叫 cities 的 list, 有效的做法是对每个在 invertor 的 list 调用 'placeOfBirth.city'. 使用投影:

Java
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]");
Kotlin
// returns ['Smiljan', 'Idvor' ]
val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*>

Projection 可以使用在数组,或实现了 java.lang.Iterable 接口的任何类和 map 上 当使用 map 用于处理投影,在这种情况下投影表达式可以对 map 中的每个 entry 进行处理(作为一个 Java 的 Map.Entry) . map 投影的结果是一个 list,包含对每一个 map 条目处理的投影表达式.

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"
Kotlin
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;
    }
}
Kotlin
class TemplateParserContext : ParserContext {

    override fun getExpressionPrefix(): String {
        return "#{"
    }

    override fun getExpressionSuffix(): String {
        return "}"
    }

    override fun isTemplate(): Boolean {
        return true
    }
}

4.4. 例子中用到的类

本节列出了本章示例中使用的类

Inventor.Java
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
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;
    }
}
PlaceOfBirth.kt
class PlaceOfBirth(var city: String, var country: String? = null) {
Society.java
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;
    }
}
Society.kt
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 面向切面编程

面向切面编程(Aspect-oriented Programming 简称 AOP) ,是相对面向对象编程(Object-oriented Programming 简称 OOP )的框架,作为 OOP 的一种功能补充. OOP 主要的模块单元是类(class). 而 AOP 则是切面(aspect) . 切面会将诸如事务管理这样跨越多个类型和对象的关注点模块化(在 AOP 的语义中,这类关注点被称为横切关注点(crosscutting) ) .

AOP 是 Spring 框架重要的组件,虽然 Spring IoC 容器没有依赖 AOP,因此 Spring 不会强迫开发者使用 AOP. 但 AOP 提供了非常棒的功能,用做对 Spring IoC 的补充.

Spring AOP 具有 AspectJ 切点aop-pointcuts-designators

Spring 引入了一种更简单、更强大的方式用来自定义切面,开发者可以选择使用基于模式 schema-based approach 的方式或使用@AspectJ 注解风格方式来定义. 这两种方式都完全支持通知(Advice) 类型和 AspectJ 的切点语义,虽然实际上仍然是使用 Spring AOP 织入(weaving) 的.

本章主要讨论 Spring 框架对基于模式和基于 @AspectJ 的 AOP 支持. 下一章,将讨论底层的 AOP 支持.

AOP 在 Spring Framework 中用于:

  • 提供声明式企业服务,特别是用于替代 EJB 的声明式服务. 最重要的服务是声明式事务管理,这个服务建立在 Spring 的抽象事务管理之上.

  • 允许开发者实现自定义切面,使用 AOP 来完善 OOP 的功能.

如果只打算使用通用的声明式服务或者已有的声明式中间件服务,例如缓冲池(pooling) 那么可以不直接使用 AOP,也可以忽略本章大部分内容.

5.1. AOP 概念

让我们从定义一些核心 AOP 概念和术语开始. 这些术语不是特定于 Spring 的. 不幸的是,AOP 术语不是特别直观. 但是,如果 Spring 使用自己的术语,那将更加令人困惑.

  • Aspect(切面): 指关注点模块化,这个关注点可能会横切多个对象. 事务管理是企业级 Java 应用中有关横切关注点的例子. 在 Spring AOP 中,切面可以使用通用类 基于 schema 的方式的方式或者在普通类中以@AspectJ注解来实现.

  • Join point(连接点):在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点. 在 Spring AOP 中,一个连接点总是代表一个方法的执行.

  • Advice(通知): 在切面的某个特定的连接点上执行的动作. 通知有多种类型,包括 “around”, “before” 和 “after” 等等. 通知的类型将在后面的章节进行讨论. 许多 AOP 框架,包括 Spring 在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链.

  • Pointcut(切点): 匹配连接点的断言. 通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时) . 切点表达式如何和连接点匹配是 AOP 的核心: Spring 默认使用 AspectJ 切点语义.

  • Introduction(引入): 声明额外的方法或者某个类型的字段. Spring 允许引入新的接口(以及一个对应的实现) 到任何被通知的对象上. 例如,可以使用引入来使 bean 实现 IsModified 接口, 以便简化缓存机制(在 AspectJ 社区,引入也被称为内部类型声明(inter) ) .

  • Target object(目标对象): 被一个或者多个切面所通知的对象. 也被称作被通知(advised) 对象. 既然 Spring AOP 是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied) 的对象.

  • AOP proxy(AOP代理): AOP 框架创建的对象,用来实现切面契约(aspect contract) (包括通知方法执行等功能) . 在 Spring 中,AOP 代理可以是 JDK 动态代理或 CGLIB 代理.

  • Weaving(织入): 把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程. 这个过程可以在编译时(例如使用 AspectJ 编译器) 、类加载时或运行时中完成. Spring 和其他纯 Java AOP 框架一样,是在运行时完成织入的.

Spring AOP 包含以下类型的通知:

  • Before advice(前置通知): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常) .

  • After returning advice(后置返回通知): 在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时) .

  • After throwing advice(后置异常通知): 在方法抛出异常退出时执行的通知.

  • After (finally) advice(后置通知(总会执行) ): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出) .

  • Around advice(环绕通知): 环绕连接点的通知,例如方法调用. 这是最强大的一种通知类型,. 环绕通知可以在方法调用前后完成自定义的行为. 它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束.

环绕通知是最常用的一种通知类型. 与 AspectJ 一样,在选择 Spring 提供的通知类型时,团队推荐开发者尽量使用简单的通知类型来实现需要的功能. 例如, 如果只是需要使用方法的返回值来作缓存更新,虽然使用环绕通知也能完成同样的事情,但是仍然推荐使用后置返回通知来代替. 使用最合适的通知类型可以让编程模型变得简单, 还能避免很多潜在的错误. 例如,开发者无需调用于环绕通知(用 JoinPoint) 的 proceed() 方法,也就不会产生调用的问题.

所有通知参数都是静态类型的,因此您可以使用相应类型的通知参数(例如,方法执行的返回值的类型) 而不是 Object 数组.

切点和连接点匹配是 AOP 的关键概念,这个概念让 AOP 不同于其它仅仅提供拦截功能的旧技术. 切入点使得通知可以独立于面向对象的层次结构进行定向. 例如,您可以将一个提供声明式事务管理的通知应用于跨多个对象(例如服务层中的所有业务操作) 的一组方法.

5.2. Spring AOP 的功能和目标

Spring AOP 是用纯 Java 实现的. 不需要特殊的编译过程. Spring AOP 不需要控制类加载器层次结构,因此适合在 servlet 容器或应用程序服务器中使用.

Spring 目前仅支持方法调用的方式作为连接点(在 Spring bean 上通知方法的执行) . 虽然可以在不影响到 Spring AOP 核心 API 的情况下加入对成员变量拦截器支持, 但 Spring 并没有实现成员变量拦截器. 如果需要通知对成员变量的访问和更新连接点,可以考虑其它语言,例如 AspectJ.

Spring 实现 AOP 的方法与其他的框架不同,Spring 并不是要尝试提供最完整 的AOP 实现(尽管 Spring AOP 有这个能力) ,相反地,它其实侧重于提供一种 AOP 与 Spring IoC 容器的整合的实现,用于帮助解决在企业级开发中的常见问题.

因此,例如,Spring Framework 的 AOP 功能通常与 Spring IoC 容器一起使用. 通过使用普通 bean 定义语法来配置切面(尽管 Spring 提供了强大的 "自动代理" 功能) . 这是与其他AOP实现的重要区别. 使用 Spring AOP 无法轻松或高效地完成某些操作,例如建议非常细粒度的对象(通常是域对象) . 在这种情况下,AspectJ 是最佳选择. 但是,我们的经验是,Spring AOP 为适合 AOP 的企业 Java 应用程序中的大多数问题提供了出色的解决方案.

Spring AOP从来没有打算通过提供一种全面的 AOP 解决方案用于取代 AspectJ,我们相信,基于代理的框架(如 Spring AOP) 和完整的框架(如 AspectJ) 都很有价值,而且它们是互补的,而不是竞争. Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,使得所有的 AOP 功能完全融入基于 Spring 的应用体系. 这样的集成不会影响 Spring AOP API 或者 AOP Alliance API. Spring AOP 仍然向后兼容. 有关Spring AOP API的讨论,请参阅 以下章节.

Spring 框架的一个核心原则是非侵入性. 这意味着开发者无需在自身的 业务/domain 模型上被迫引入框架特定的类和接口. 然而,有些时候,Spring 框架可以让开发者选择引入 Spring 框架特定的依赖到业务代码. 给予这种选择的理由是因为在某些情况下它可能是更易读或易于编写某些特定功能. Spring 框架(几乎) 总能给出这样的选择,开发者可以自由地做出明智的决定,选择最适合的特定用例或场景.

与本章相关的一个选择是选择哪种 AOP 框架(以及哪种AOP样式) . 您可以选择 AspectJ,Spring AOP 或两者. 也可以选择 @AspectJ 注解式的方法或 Spring 的 XML 配置方式. 事实上,本章以介绍 @AspectJ 方式为先不应该被视为 Spring 团队倾向于 @AspectJ 的方式胜过 Spring 的 XML 配置方式.

请参阅选择要使用的 AOP 声明样式,以更全面地讨论每种样式的 "为什么和如何进行".

5.3. AOP 代理

Spring 默认使用标准的 JDK 动态代理来作为AOP的代理. 这样任何接口(或者接口的 set) 都可以被代理.

Spring 也支持使用 CGLIB 代理. 对于需要代理类而不是代理接口的时候 CGLIB 代理是很有必要的. 如果业务对象并没有实现接口,默认就会使用 CGLIB 代理 . 此外,面向接口编程也是最佳实践,业务对象通常都会实现一个或多个接口. 此外,还可以强制的使用 CGLIB 代理, 在那些(希望是罕见的) 需要通知没有在接口中声明的方法时,或者当需要传递一个代理对象作为一种具体类型到方法的情况下.

掌握 Spring AOP 是基于代理的这一事实非常重要. 请参阅 AOP 代理,以全面了解此实现细节的实际含义. .

5.4. @AspectJ 注解支持

@AspectJ 会将切面声明为常规 Java 类的注解类型. AspectJ project 引入了 @AspectJ 风格,并作为 AspectJ 5 发行版的一部分. Spring 使用的注解类似于 AspectJ 5, 使用 AspectJ 提供的库用来解析和匹配切点. AOP 运行时仍然是纯粹的 Spring AOP,并不依赖 AspectJ 编译器或编织器.

使用 AspectJ 编译器和织入并允许使用全部基于 AspectJ 语言,并在在 Spring 应用中使用 AspectJ进行了讨论.

5.4.1. 启用 @AspectJ 支持

要在 Spring 配置中使用 @AspectJ 切面,需要启用 Spring 支持,用于根据 @AspectJ 切面配置 Spring AOP,并根据这些切面自动代理 bean (事先判断是否在通知的范围内) . 通过自动代理的意思是: 如果 Spring 确定一个 bean 是由一个或多个切面处理的,将据此为 bean 自动生成代理 bean ,并以拦截方法调用并确保需要执行的通知.

可以使用 XML 或 Java 配置的方式启用 @AspectJ 支持. 不管哪一种方式,您还需要确保 AspectJ 的 aspectjweaver.jar 库位于应用程序的类路径中(版本 1.8 或更高版本) . 此库可在 AspectJ 分发的 lib 目录中或 Maven Central 仓库中找到.

使用 Java 配置启用 @AspectJ 支持

要使用 Java @Configuration 启用 @AspectJ 支持,请添加 @EnableAspectJAutoProxy 注解,如以下示例所示:

Java
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
Kotlin
@Configuration
@EnableAspectJAutoProxy
class AppConfig
使用 XML 配置启用 @AspectJ 支持

要使用基于 XML 的配置启用 @AspectJ 支持,请使用 aop:aspectj-autoproxy 元素,如以下示例所示:

<aop:aspectj-autoproxy/>

这假设您使用基于 XML Schema 配置中描述的 schema 支持. 有关如何在 aop 命名空间中导入标签,请参阅 AOP schema.

5.4.2. 声明切面

启用了 @AspectJ 支持后,在应用程序上下文中定义的任意 bean (有 @Aspect 注解) 的类都将被 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
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}
Kotlin
import org.aspectj.lang.annotation.Aspect;

@Aspect
class NotVeryUsefulAspect

切面(使用 @Aspect 的类) 可以拥有方法和属性,与其他类并无不同. 也可以包括切点、通知和内置类型(即引入) 声明.

通过组件扫描自动检测切面
您可以在 Spring XML 配置中将切面类注册为常规 bean 或者在 @Configuration 类中配置 @Bean 方法 ,Spring 会通过类路径扫描自动检测它们 - 与任何其他 Spring 管理的 bean 相同. 然而注意到 @Aspect 注解对于类的自动探测是不够的, 为此,需要单独添加 @Component ,注解(或自定义注解声明,用作 Spring 组件扫描器的规则之一) .
是否可以作为其他切面的切面通知?
在 Spring AOP 中,不可能将切面本身被作为其他切面的目标. 类上的 @Aspect 注解表明他是一个切面并且排除在自动代理的范围之外.

5.4.3. 声明切点

切点决定了匹配的连接点,从而使我们能够控制通知何时执行. Spring AOP 只支持使用 Spring bean 的方法执行连接点,所以可以将切点看出是匹配 Spring bean 上方法的执行. 切点的声明包含两个部分: 包含名称和任意参数的签名,以及明确需要匹配的方式执行的切点表达式. 在 @AspectJ 注解方式的 AOP 中,一个切点的签名由常规方法定义来提供, 并且切点表达式使用 @Pointcut 注解指定(方法作为切点签名必须有类型为 void 的返回) .

使用例子有助于更好地区分切点签名和切点表达式之间的关系. 以下示例定义名为 anyOldTransfer 的切点,该切点与名为 transfer 的任何方法的执行相匹配:

Java
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
Kotlin
@Pointcut("execution(* transfer(..))") // the pointcut expression
private fun anyOldTransfer() {} // the pointcut signature

切点表达式由 @Pointcut 注解的值是常规的 AspectJ 切点表达式. 关于 AspectJ 切点语言的描述,见 AspectJ Programming Guide (作为扩展, 请参考 AspectJ 5 Developer’s Notebook) 或者 Colyer 著的关于 AspectJ 的书籍. 例如, Eclipse AspectJ,或者参看 Ramnivas Laddad的 AspectJ in Action.

支持切点标识符

Spring AOP 支持使用以下 AspectJ 切点标识符(PCD),用于切点表达式:

  • execution: 用于匹配方法执行连接点. 这是使用 Spring AOP 时使用的主要切点标识符.

  • within: 限制匹配特定类型中的连接点(在使用 Spring AOP 时,只需执行在匹配类型中声明的方法) .

  • this: 在 bean 引用(Spring AOP 代理) 是给定类型的实例的情况下,限制匹配连接点(使用 Spring AOP 时方法的执行) .

  • target: 限制匹配到连接点(使用 Spring AOP 时方法的执行) ,其中目标对象(正在代理的应用程序对象) 是给定类型的实例.

  • args: 限制与连接点的匹配(使用 Spring AOP 时方法的执行) ,其中变量是给定类型的实例.

  • @target: 限制与连接点的匹配(使用 Spring AOP 时方法的执行) ,其中执行对象的类具有给定类型的注解.

  • @args: 限制匹配连接点(使用 Spring AOP 时方法的执行) ,其中传递的实际参数的运行时类型具有给定类型的注解.

  • @within: 限制与具有给定注解的类型中的连接点匹配(使用 Spring AOP 时在具有给定注解的类型中声明的方法的执行) .

  • @annotation: 限制匹配连接点(在 Spring AOP 中执行的方法具有给定的注解) .

其他切点类型

Spring 并没有完全地支持 AspectJ 切点语言声明的切点标识符,包括 call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow,cflowbelow, if, @this, 和 @withincode. 在由 Spring AOP 解释的切点表达式中, 使用这些切点标识符将导致 IllegalArgumentException 异常.

Spring AOP 支持的切点标识符可以在将来的版本中扩展,以支持更多的 AspectJ 切点标识符.

因为 Spring AOP 限制了只匹配方法的连接点执行,所以上面的切点标识符的讨论比在 AspectJ 编程指南中找到的定义要窄. 另外,AspectJ 本身具有基于类型的语义, 并且在执行连接点上,thistarget 都指向同一个对象-即执行方法的对象. Spring AOP 是一个基于代理的系统,区分代理对象本身(绑定到 this) 和代理(绑定到 target) 后的目标对象.

由于 Spring AOP 框架是基于代理的特性,定义的 protected 方法将不会被处理,不管是 JDK 的代理(做不到) 还是 CGLIB 代理(有技术可以实现但是不建议) . 因此,任何给定的切点将只能与 public 方法匹配.

请注意,切点定义通常与任何截获的方法匹配. 如果切点严格意义上是暴露的,即使在通过代理进行潜在非公共交互的 CGLIB 代理方案中,也需要相应地定义切点.

如果需要拦截包括 protected 和 private 方法甚至是构造函数,请考虑使用基于 Spring 驱动的本地 AspectJ 织入而不是 Spring 的基于代理的 AOP 框架. 这构成了不同特性的 AOP 使用模式,所以在做出决定之前一定要先熟悉一下编织.

Spring AOP 支持更多的 PCD 命名 bean. PCD允许将连接点的匹配限制为特定的 Spring bean 或一系列 Spring bean. bean PCD 具有以下形式:

Java
bean(idOrNameOfBean)
Kotlin
bean(idOrNameOfBean)

idOrNameOfBean 标识可以是任意符合 Spring bean 的名字, 提供了使用 * 字符的有限通配符支持,因此,如果为 Spring bean 建立了一些命名约定,则可以编写 bean PCD 表达式来选择它们. 与其他切点标识符的情况一样,PCD bean 可以是 && (and), || (or), and !(negation).

bean PCD 仅在 Spring AOP 中受支持,而在本地 AspectJ 编织中不受支持. 它是 AspectJ 定义的标准 PCD 的 Spring 特定扩展,因此不适用于 @Aspect 模型中声明的切面.

bean PCD 运行在实例级别上(基于 Spring bean 名称概念构建) ,而不是仅在类型级别(这是基于编织的 AOP 所限制的) . 基于实例的切点标识符是 Spring 基于代理的 AOP 框架的特殊功能,它与 Spring bean 工厂紧密集成,通过名称识别特定的 bean 是自然而直接的.

合并切点表达式

您可以使用 &&, ||! 等符号进行合并操作. 也可以通过名字来指向切点表达式. 以下示例显示了三个切入点表达式:

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 如果方法执行在 trading 中,则匹配.
3 tradingOperation 如果方法执行表示 trading 中的任何公共方法,则匹配.
Kotlin
@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 如果方法执行在 trading 中,则匹配.
3 tradingOperation 如果方法执行表示 trading 中的任何公共方法,则匹配.

如上所示,用更小的命名组件构建更复杂的切入点表达式是最佳实践. 当按名称引用切点时,将应用普通的 Java 可见性规则(可以看到相同类型的私有切点,层次结构中受保护的切点,任何位置的公共切点等) . 可见性并不影响切点匹配.

共享通用的切点定义

在处理企业应用程序时,通常需要从几个切面来引用应用程序的模块和特定的操作集. 建议定义一个 CommonPointcuts 切面,以此为目的捕获通用的切点表达式. 这样的切面通常类似于以下示例:

Java
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() {}

}
Kotlin
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() {
    }

}

像这样定义的切点可以用在任何需要切点表达式的地方, 例如,要使服务层具有事务性,您可以编写以下内容:

Kotlin
<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> 元素在 基于 Schema的 AOP 支持中进行了讨论. 事务管理中讨论了事务元素.

Examples

Spring AOP 用户可能最常使用 execution 切点标识符 ,执行表达式的格式为:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)

除返回类型模式(上面片段中的 ret-type-pattern ) 以外的所有部件、名称模式和参数模式都是可选的. 返回类型模式确定要匹配的连接点的方法的返回类型必须是什么. 通常,可以使用 * 作为返回类型模式,它匹配任何返回类型. 只有当方法返回给定类型时,完全限定的类型名称才会匹配. 名称模式与方法名称匹配,可以将 * 通配符用作名称模式的全部或部分. 如果指定声明类型模式,则需要有后缀 .将其加入到名称模式组件中. 参数模式稍微复杂一点. () 匹配没有参数的方法. (..) 匹配任意个数的参数(0个或多个) . ( * )匹配任何类型的单个参数. (*,String) 匹配有两个参数而且第一个参数是任意类型,第二个必须是 String 的方法. 有关更多信息,请参阅AspectJ编程指南的 Language Semantics部分.

以下示例显示了一些常见的切点表达式:

  • 匹配任意公共方法的执行:

        execution(public * *(..))
  • 匹配任意以 set 开始的方法:

        execution(* set*(..))
  • 匹配定义了 AccountService 接口的任意方法:

        execution(* com.xyz.service.AccountService.*(..))
  • 匹配定义在 service 包中的任意方法:

        execution(* com.xyz.service.*.*(..))
  • 匹配定义在 service 包和其子包中的任意方法:

        execution(* com.xyz.service..*.*(..))
  • 匹配在 service 包中的任意连接点(只在 Spring AOP 中的方法执行) :

        within(com.xyz.service.*)
  • 匹配在 service 包及其子包中的任意连接点(只在 Spring AOP 中的方法执行)

        within(com.xyz.service..*)
  • 匹配代理实现了 AccountService 接口的任意连接点(只在 Spring AOP 中的方法执行) :

        this(com.xyz.service.AccountService)
    this 常常以捆绑的形式出现. 见后续的章节讨论如何在 声明通知 中使用代理对象.
  • 匹配当目标对象实现了 AccountService 接口的任意连接点(只在 Spring AOP 中的方法执行) :

        target(com.xyz.service.AccountService)
    target 常常以捆绑的形式出现. 见后续的章节讨论如何在 声明通知 中使用目标对象.
  • 匹配使用了单一的参数,并且参数在运行时被传递时可以 Serializable 的任意连接点(只在 Spring 的 AOP 中的方法执行) :

        args(java.io.Serializable)
    args 常常以捆绑的形式出现.见后续的章节讨论如何在 声明通知 中使用方法参数.

    注意在这个例子中给定的切点不同于 execution(* *(java.io.Serializable)). 如果在运行时传递的参数是可序列化的,则与 execution 匹配,如果方法签名声明单个参数类型为 Serializable,则与 args 匹配.

  • 匹配当目标对象有 @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 也可以以捆绑的形式使用.见后续的章节讨论如何在 声明通知 中使用注解对象.
  • 匹配有单一的参数并且在运行时传入的参数类型有 @Classified 注解的任意连接点(只在 Spring AOP 中的方法执行) :

        @args(com.xyz.security.Classified)
    @args 也可以以捆绑的形式使用.见后续的章节讨论如何在 声明通知 中使用注解对象.
  • 匹配在名为 tradeService 的 Spring bean 上的任意连接点(只在Spring AOP中的方法执行) :

        bean(tradeService)
  • 匹配以 *Service 结尾的 Spring bean 上的任意连接点(只在 Spring AOP 中方法执行) :

        bean(*Service)
编写好的切点

在编译过程中,AspectJ 会尝试和优化匹配性能来处理切点. 检查代码并确定每个连接点是否匹配(静态或动态) 给定切点是一个代价高昂的过程. (动态匹配意味着无法从静态分析中完全确定匹配, 并且将在代码中放置测试,以确定在运行代码时是否存在实际匹配) . 在第一次遇到切点声明时,AspectJ 会将它重写为匹配过程的最佳形式. 这是什么意思? 基本上,切点是在 DNF(析取范式) 中重写的 ,切点的组成部分会被排序,以便先检查那些比较明确的组件. 这意味着开发者不必担心各种切点标识符的性能,并且可以在切点声明中以任何顺序编写.

但是,AspectJ 只能与被它指定的内容协同工作,并且为了获得最佳的匹配性能,开发者应该考虑它们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间. 现有的标识符会自动选择下面三个中的一个 kinded, scoping, 和 contextual:

  • Kinded 选择特定类型的连接点的标识符: execution, get, set, call, 和 handler.

  • Scoping 选择一组连接点的匹配 (可能是许多种类) : withinwithincode

  • Contextual 基于上下文匹配 (或可选绑定) 的标识符: this, target, 和 @annotation

一个写得很好的切入点应该至少包括前两种类型(kinded 和 scoping) . 同时 contextual 标识符或许会被包括如果希望匹配基于连接点上下文或绑定在通知中使用的上下文. 只是提供 kinded 标识符或只提供 contextual 标识符器也能够工作,但是可能影响处理性能(时间和内存的使用) ,浪费了额外的处理和分析时间或空间. scoping 标识符可以快速匹配并且使用 AspectJ 可以快速排除不会被处理的连接点组, 这也说明编写好的切点表达式是很重要的(因为没有明确指定时,它就会 Loop Lookup 循环匹配) .

5.4.4. 声明通知

通知是与切点表达式相关联的概念,可以在切点匹配的方法之前、之后或之间执行. 切点表达式可以是对命名切点的简单引用,也可以是即时声明的切点表达式.

前置通知

您可以使用 @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() {
        // ...
    }
}
Kotlin
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() {
        // ...
    }
}
Kotlin
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() {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning

@Aspect
class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doAccessCheck() {
        // ...
    }
}
在同一切面中当然可以声明多个通知. 在此只是为了迎合讨论的主题而只涉及单个通知.

有些时候需要在通知中获取实际的返回值. 可以使用 @AfterReturning ,并指定 returning 字段如下:

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) {
        // ...
    }
}
Kotlin
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 属性中使用的名字必须和通知方法中的参数名相关,方法执行返回时,返回值作为相应的参数值传递给 advice 方法. returning 子句还限制只匹配那些返回指定类型的值的方法执行(在本例中为 Object,它匹配任何返回值对象) .

请注意,当使用 after-returning 的通知时. 不能返回不同的引用.

后置异常通知

当方法执行并抛出异常时后置异常通知会被执行,需要使用 @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() {
        // ...
    }
}
Kotlin
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) {
        // ...
    }
}
Kotlin
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 属性中使用的名字必须和通知方法中的参数名相关. 当方法执行并抛出异常时,异常将会传递给通知方法作为相关的参数值. 抛出子句还限制与只引发指定类型的异常(在本例中为 DataAccessException) 的方法执行的匹配.

请注意,@AfterThrowing 并不表示常规的异常处理回调. 具体来说,@AfterThrowing 通知方法仅应从连接点 (用户声明的目标方法) 本身接收异常,而不能从随附的 @After/@AfterReturning 方法接收异常.

后置通知(总会执行)

当匹配方法执行之后后置通知(总会执行) 会被执行. 这种情况使用 @After 注解来定义. 后置通知必须被准备来处理正常或异常的返回条件. 通常用于释放资源等等:

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() {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.After

@Aspect
class AfterFinallyExample {

    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doReleaseLock() {
        // ...
    }
}

请注意,AspectJ 中的 @After 建议被定义为 "after finally advice",类似于 try-catch 语句中的 finally 块. 与 @AfterReturning 相反 (仅适用于成功的正常返回) ,它将为从连接点 (用户声明的目标方法) 抛出任何异常,正常返回或异常调用.

环绕通知

最后一种通知是 环绕通知,环绕通知围绕方法执行. 可以在方法执行之前和执行之后执行,并且定义何时做什么,甚至是否真正得到执行. 如果需要在方法执行之前和之后以线程安全的方式 (例如启动和停止计时器) 共享状态

确认可使用的通知形式, 要符合最小匹配原则.

例如,如果 before 通知足以满足您的需求,则不要使用 around 通知。

环绕通知是通过使用 @Around 注解对方法进行注解来声明的。 这个方法应该声明 Object 作为它的返回类型,以及方法的第一个参数 必须是 ProceedingJoinPoint 类型。 在通知方法的主体内,您必须在 ProceedingJoinPoint 上调用 proceed() 以使底层方法。 不带参数调用 proceed() 将导致调用者的调用底层方法时提供原始参数。 这属于高级用途 proceed 方法也可以通过传递 Object[] 数组的值给原方法作为传入参数, 数组中的值将用作调用时的底层方法。

使用 @Around 注解来定义环绕通知,第一个参数必须是 ProceedingJoinPoint 类型的. 在通知中调用 ProceedingJoinPoint 中的 proceed() 方法来引用执行的方法. proceed 方法也可以被调用传递数组对象- 数组的值将会被当作参数在方法执行时被使用. proceed 方法也可以传入 Object[]. 数组中的值在进行时用作方法执行的参数.

在使用 `Object[]` 调用时 `proceed` 的行为与在 AspectJ 编译器编译的环绕通知进行的行为略有不同. 对于使用传统 AspectJ 语言编写的通知, 传递给 `proceed` 的参数数必须与传递给环绕通知的参数数量(不是被连接点处理的参数的数目) 匹配,并且传递的值将 `proceed` 在给定的参数位置取代该值绑定到的实体的连接点的原始值(如果现在无法理解 ,请不要担心) .

Spring 处理的方式是简单的并且基于代理的,会生成更好的匹配语义. 现在只需意识到这两种是有这么一点的不同的即可. 为 Spring 编写的 @AspectJ 方面并在 AspectJ 中使用带有参数的 proceed 编译器和编织器。 有一种方法可以编写出 100% 兼容 Spring AOP 和 AspectJ 的匹配, 在后续的章节中将会讨论 通知的参数.

环绕通知返回的值将会被调用的方法看到,例如,一个简单的缓存切面可以从缓存中返回一个值(如果有的话) ,如果没有则调用 proceed(). 请注意,可以在 around 通知的主体内调用一次,多次或根本不调用. 所有这些都是合法的.

如果您将环绕通知方法的返回类型声明为 voidnullproceed() 将忽略任何调用的结果返回给调用者,因此,建议使用环绕通知方法声明返回 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;
    }
}
Kotlin
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(): 打印方法被通知的有用描述.

See the javadoc for more detail.

传递参数给通知

我们已经看到了如何绑定返回的值或异常值(在返回之后和抛出通知之后使用) . 为了在通知代码段中使用参数值,可以使用绑定 args 的形式. 如果在 args 表达式中使用参数名代替类型名称, 则在调用通知时,要将相关的参数值当作参数传递. 例如,假如在 dao 操作时将 Account 对象作为第一个参数传递给通知,并且需要在通知代码段内访问 Account,可以这样写:

Java
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}
Kotlin
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
fun validateAccount(account: Account) {
    // ...
}

切点表达式的 args(account,..) 部分有两个目的. p 它严格匹配了至少带一个参数的执行方法,并且传递给传递的参数是 Account 实例. 第二,它使得实际的 Account 对象通过 account 参数提供给通知.

另一个方法写法就是先定义切点,然后, "provides" Account 对象给匹配的连接点,有了连接点,那么引用连接点作为切点的通知就能获得 Account 对象的值. 这看起来如下:

Java
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}
Kotlin
@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 注解的注解方法的执行并获取audit 代码:

首先是 @Auditable 注解的定义:

Java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}
Kotlin
@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();
    // ...
}
Kotlin
@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);
}
Kotlin
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
}
Kotlin
@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
}
Kotlin
@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
}
Kotlin
@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
}

如果第一个参数是 JoinPoint, ProceedingJoinPoint, 或 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
}
Kotlin
@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
}
Kotlin
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
fun audit(jp: JoinPoint) {
    // ... use jp
}
  • 使用 argNames 属性有点笨拙,所以如果没有指定 argNames 属性,Spring AOP会查看该类的调试信息,并尝试从局部变量表中确定参数名称. 只要使用调试信息( -g:vars ) 编译类, 就会出现此信息. 使用此标志进行编译的后果是: (1).您的代码将容易被理解(逆向工程. (2). 类文件的大小将会有些大(通常不是什么事). (3). 对非使用本地变量的优化将不会应用于你的编译器. 换句话说,通过使用此标志构建,您应该不会遇到任何困难.

    如果即使没有调试信息,AspectJ 编译器(ajc) 也编译了 @AspectJ 方面,则无需添加 argNames 属性,因为编译器会保留所需的信息.
  • 如果代码是在没有必要的调试信息的情况下编译的,那么 Spring AOP 将尝试推断绑定变量与参数的配对(例如,如果在切点表达式中只绑定了一个变量,并且该通知方法只需要一个参数,此时两者匹配是明显的) . 如果给定了可用信息,变量的绑定是不明确的话,则会引发 AmbiguousBindingException 异常.

  • 如果上述所有策略都失败,则抛出 IllegalArgumentException 异常.

处理参数

前面说过. 将描述如何用在 Spring AOP 和 AspectJ 中一致的参数中编写 proceed 处理函数. 解决方案是确保建议签名按顺序绑定每个方法参数. 以下示例显示了如何执行此操作:

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});
}
Kotlin
@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 相同的优先级规则来确定通知执行的顺序. 拥有最高优先权的通知会途中先"进入"(因此,给定两条前置通知,优先级最高的通知首先运行) . 从连接点"退出",拥有最高优先级的通知最后才运行(退出) ((因此,如果有两个后置通知,那么拥有最高优先级的将在最后运行(退出) ) .

如果在不同切面定义的两个通知都需要在同一个连接点运行,那么除非开发者指定运行的先后,否则执行的顺序是未定义的. 可以通过指定优先级来控制执行顺序. 这也是 Spring 推荐的方式,通过在切面类实现 org.springframework.core.Ordered 接口或使用 @Order 对其进行注解即可. 如果有两个切面,从 Ordered.getOrder()(或注解值) 返回较低值的方面具有较高的优先级.

每一个切面的不同通知类型都应作用于连接点,因此 @AfterThrowing 通知方法不应该随同和 @After/`@AfterReturning`方法接收异常

从 Spring Framework 5.2.7 开始,在相同 @Aspect 类中定义的,需要在同一连接点运行的通知方法将根据其通知类型从高到低的优先级 @Around,@Before ,@After,@AfterReturning,@AfterThrowing (从高到低). 但是请注意,由于 Spring 的 AspectJAfterAdvice 中的实现方式,在同一切面中的任何 @AfterReturning@AfterThrowing 通知方法之后,都会调用 @After 通知方法.遵循 AspectJ 的 @After 的 "after finally advice" 语义.

当在同一切面定义的两条通知都需要在同一个连接点上运行时,排序也是未定义的(因为没有办法通过反射检索Javac编译的类的声明顺序) . 考虑将通知方法与一个通知方法合并,根据每个连接点在每个切面类或将通知切分为切面类,可以在切面级别指定顺序.

当在同一个 @Aspect 类中定义的两个相同类型的通知(例如,两个 @After 通知方法)都需要在同一个连接点上运行时,其顺序是不确定的(因为没有办法通过反射检索 Javac 编译的类的声明顺序).考虑将通知方法与一个通知方法合并,根据每个连接点在每个切面类或将重构为单独的 @Aspect 类,可以在切面级别 通过 Ordered@Order 指定顺序. 考虑将此类建议方法折叠为每个 @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();
    }

}
Kotlin
@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()
    }
}

要实现的接口由注解属性的类型来确定. @DeclareParents 注解的 value 值是 AspectJ 类型模式引过来的. 任何匹配类型的 bean 实现了 UsageTracked 接口。注意上面例子中的前置通知, 服务 bean 可以直接作为 UsageTracked 接口的实现,如果以编程方式访问 bean,您将编写以下内容:

Java
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
Kotlin
val usageTracked = context.getBean("myService") as UsageTracked

5.4.6. 切面实例化模型

这是一个高级主题. 如果您刚刚开始使用 AOP,您可以跳过它直到稍后再了解.

默认情况下,应用程序上下文中的每个切面都有一个实例. AspectJ 将其称为单例实例化模型. 可以使用交替生命周期定义切面. Spring 支持 AspectJ 的 perthispertarget 实例化模型(目前不支持 percflow, percflowbelow, 和 pertypewithin) .

您可以通过在 @Aspect 注解中指定 perthis 子句来声明相关方面. 请考虑以下示例:

Java
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {

    private int someState;

    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}
Kotlin
@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 异常. 这个需求很明显,它跨越了服务层中的多个服务,因此非常适合通过切面来实现.

因为我们想要重试操作,所以我们需要使用环绕通知,以便我们可以多次调用 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;
    }

}
Kotlin
@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
}
Kotlin
@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 {
    // ...
}
Kotlin
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
    // ...
}

5.5. 基于 Schema 的 AOP 支持

如果您更喜欢基于 XML 的格式,Spring 还支持使用新的 aop 命名空间标签定义切面. 完全相同的切点表达式和通知类型在使用 @AspectJ 方式时同样得到支持. 因此,在本节中,我们将重点放在新语法上,并将读者引用到上一节(@AspectJ 注解支持) 中的讨论,以了解编写切点表达式和通知参数的绑定.

要使用本节中描述的 aop 命名空间标签,您需要导入 spring-aop schema,如基于 XML 模式的配置中所述. 有关如何在 aop 命名空间中导入标记,请参阅AOP schema.

在 Spring 配置中,所有 aspect 和 advisor 元素必须放在 <aop:config> 元素中(在应用程序上下文配置中可以有多个 <aop:config> 元素) . <aop:config> 元素可以包含切点,通知者和切面元素(请注意,这些元素必须按此顺序声明) .

<aop:config> 配置样式大量使用了Spring的自动代理 机制.如果已经通过使用 BeanNameAutoProxyCreator 或类似的类使用了显式的自动代理, 则可能会出现问题(如通知还没被编织) . 建议的使用模式是仅使用 <aop:config> 样式或仅使用 AutoProxyCreator 样式,并且永远不要混用它们.

5.5.1. 声明切面

如果使用 schema,那么切面只是在 Spring 应用程序上下文中定义为 bean 的常规 Java 对象.在对象的字段和方法中获取状态和行为,并且在 XML 中获取切点和通知信息.

您可以使用 <aop:aspect> 元素声明方面,并使用 ref 属性引用支持 bean,如以下示例所示:

<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 切点表达式语言,如 @Aspect 注解支持 所述. 如果使用基于 schema 的声明样式,则可以引用在切点表达式内的类型(@Aspects)中定义的命名切点 . 定义上述切入点的另一种方法如下:

<aop:config>

    <aop:pointcut id="businessService"
        expression="com.xyz.myapp.CommonPointcuts.businessService()"/>

</aop:config>

假设有一个 CommonPointcuts 的切面(如共享通用的切点定义一节所述) .

切面声明切点与声明 top-level 切点非常相似,如下例所示:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        ...
    </aop:aspect>

</aop:config>

@AspectJ 方面的方法相同,使用基于 schema 的定义样式声明的切点可能会收集连接点上下文. 例如,以下切点将 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) {
    // ...
}
Kotlin
fun monitor(service: Any) {
    // ...
}

在组合切点表达式中, &amp;&amp; 在 XML 文档中很难处理,因此您可以分别使用 and, ornot 分别用来代替 &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 引用,不能用作命名切点以形成复合切点. 因此,基于 schema 定义样式中的命名切点比 @AspectJ 样式提供的受到更多的限制.

5.5.3. 声明通知

同样的五种通知类型也支持 @AspectJ 样式,并且它们具有完全相同的语义.

前置通知

前置通知很明显是在匹配方法执行之前被调用, 它通过使用 <aop:aspect> 元素在 <aop:aspect> 中声明,如下例所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

这里 dataAccessOperation 是在最外层的(<aop:config>)定义的切点 id. 若要以内联方式定义切点,请将 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. 在数据访问操作之前通知会被执行(当然连接点匹配中的切点), 即切面 bean 的 doAccessCheck 方法会被调用.

后置返回通知

在匹配的方法执行正常完成后返回通知运行. 它在 <aop:aspect> 中以与前置通知相同的方式声明. 以下示例显示了如何声明它:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

@AspectJ 样式一样,可以在通知代码体内获取返回值. 为此,使用 returning 属性定义参数的名字来传递返回值,如以下示例所示:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

doAccessCheck 方法必须声明一个名为 retVal 的参数,此参数的类型约束匹配的方式与 @AfterReturning 所描述的相同. 例如,您可以按如下方式声明方法签名:

Java
public void doAccessCheck(Object retVal) {...
Kotlin
fun doAccessCheck(retVal: Any) {...
后置异常通知

就是匹配的方法运行抛出异常后后置异常通知会运行,它在 <aop:aspect> 中使用 after-throwing 元素声明. 如下例所示:

<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) {...
Kotlin
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
后置通知(总会执行的)

当方法执行完成并退出后,后置通知会被执行(而且是总会被执行). 你可以使用 after 元素声明. 如以下示例所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>
环绕通知

最后一种通知是环绕通知. 环绕通知 "around" 匹配的方法执行运行. 它有机会在方法执行之前和之后进行工作,并确定方法何时、 如何以及甚至是否真正执行. 环绕通知经常用于需要在方法执行前或后在线程安全的情况下共享状态(例如开始和结束时间) .

确认可使用的通知形式, 要符合最小匹配原则.

例如,如果 before 通知足以满足您的需求,则不要使用 around 通知。

您可以使用 aop:around 元素声明环绕通知. 通知方法应该以 Object 作为它的返回类型,第一个参数必须是 ProceedingJoinPoint 类型. 在通知代码体中,调用 ProceedingJoinPoint 实现的 proceed() 会使匹配的方法继续执行. proceed 方法也可以通过传递 Object[] 数组的值给原方法作为传入参数. 有关调用继续使用 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;
}
Kotlin
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
    // start stopwatch
    val retVal = pjp.proceed()
    // stop stopwatch
    return pjp.proceed()
}
通知参数

基于 schema 的声明样式支持所有类型的通知,其方式与 @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
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);
    }
}
Kotlin
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(..) 作为环绕通知来使用,如以下示例所示:

Java
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());
        }
    }
}
Kotlin
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);
    }
}
Kotlin
fun main() {
    val ctx = ClassPathXmlApplicationContext("x/y/plain.xml")
    val person = ctx.getBean("personService") as PersonService
    person.getPerson("Pengo", 12)
}

使用这样的 Boot 类,我们将在标准输出上获得类似于以下内容的输出:

StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)
通知的顺序

当多个通知需要在同一个连接点(执行方法) 执行时,排序规则如 通知的顺序 中所述. 方面之间的优先级是通过将 order 属性添加到 <aop:aspect> 元素或 @Order 注解添加到支持切面的 bean 或通过让 bean 实现 Ordered 接口来确定的.

与在同一 @Aspect 类中定义的通知方法的优先规则相反,当在同一 <aop:aspect> 元素中定义的两条通知都需要在同一连接点上运行时,优先级由中的顺序确定在封闭的 <aop:aspect> 元素中声明的通知元素,从最高优先级到最低优先级.

例如,给定一个环绕通知和一个在同一 <aop:aspect> 元素中定义的,适用于同一连接点的前置通知,以确保环绕通知的优先级高于前置通知的 <aop:around> 元素必须在 <aop:before> 元素之前声明.

根据一般经验,如果发现在同一 <aop:aspect> 元素中定义了多个通知,这些通知适用于同一连接点,请考虑将这些通知方法合并成每个 <aop:aspect> 元素,或将通知重构为单独的 <aop:aspect> 元素,您可以在切面级别进行排序.

5.5.4. 引入

引入(作为 AspectJ 中内部类型的声明) 允许切面定义通知的对象实现给定的接口,并代表这些对象提供该接口的实现.

您可以在 aop:aspect 中使用 aop:declare-parents 元素进行引入. 您可以使用 aop:declare-parents 元素声明匹配类型具有父级(因此名称) . 例如,给定名为 UsageTracked 的接口和名为 DefaultUsageTracked 的接口的实现,以下方面声明服务接口的所有实现者也实现 UsageTracked 接口. (例如,为了通过JMX暴露统计信息. )

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.CommonPointcuts.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

然后,支持 usageTracking bean的类将包含以下方法:

Java
public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}
Kotlin
fun recordUsage(usageTracked: UsageTracked) {
    usageTracked.incrementUseCount()
}

要实现的接口由 implement-interface 属性确定. types-matching 属性的值是 AspectJ 类型模式. 任何匹配类型的 bean 都将实现 UsageTracked 接口. 请注意,在前面的示例的通知中,服务 bean 可以直接用作 UsageTracked 接口的实现. 要以编程方式访问 bean,您可以编写以下代码:

Java
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
Kotlin
val usageTracked = context.getBean("myService") as UsageTracked

5.5.5. 切面实例化模型

唯一受支持的 schema 定义的实例化模型是单例模型,在将来的版本中可能支持其他实例化模型.

5.5.6. 通知者

“advisors” 的概念是在 Spring 1.2 中提出的,能被 AOP 支持. 而在 AspectJ 中没有等价的概念. 通知者就像迷你的切面,包含单一的通知. 通知本身可以通过 bean 来代表,并且必须实现 Spring 中的 Advice Types in Spring 中描述的通知接口之一, 通知者可以利用 AspectJ 的切点表达式

Spring 使用 <aop:advisor> 元素支持通知者概念. 通常会看到它与事务性通知一起使用,它在 Spring 中也有自己的命名空间支持. 以下示例显示了一个通知者:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor
        pointcut-ref="businessService"
        advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

除了前面示例中使用的 pointcut-ref 属性之外,您还可以使用 pointcut 属性来内联定义切点表达式.

如果想将通知排序,可以定义通知者的优先级. 在通知者上可以使用 order 属性来定义 Ordered 值.

5.5.7. AOP Schema 例子

本节说明如何使用 Schema 支持重写 An AOP Example AOP 例子 中的并发锁定失败重试示例.

由于并发问题(例如,死锁失败者) ,业务服务的执行有时会失败. 如果重试该操作,则可能在下次尝试时成功. 对于适合在这种情况下重试的业务服务(不需要返回给用户来解决冲突的幂等操作) . 希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException 异常. 这个需求很明显,它跨越了服务层中的多个服务,因此非常适合通过切面来实现.

因为我们想要重试操作,所以我们需要使用环绕通知,以便我们可以多次调用 proceed. 以下清单显示了基本方面的实现(使用 Schema 支持的常规 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;
    }

}
Kotlin
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 配置. 主要的操作是在 doConcurrentOperation 的环绕通知中. 请注意,请注意,目前,我们将重试逻辑应用于每个 businessService(). 尝试执行时,如果失败了,将产生 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
}
Kotlin
@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 还是全面使用 AspectJ?

使用最简单的方法. Spring AOP 比使用完整的 AspectJ 更 简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/ 编织器. 如果只是需要在 Spring bean 上执行通知操作,那么使用 Spring AOP 是正确的选择. 如果需要的通知不是由 Spring 容器管理的对象(通常是域对象) ,那么就需要使用 AspectJ. 如果想使用通知连接点而不是简单的方法执行,也需要使用 AspectJ(例如,字段获取或设置连接点等) ,则还需要使用 AspectJ.

使用 AspectJ 时,您可以选择 AspectJ 语言语法(也称为 "代码样式") 或 @AspectJ 注解样式. 显然,如果没有使用 Java 5+ 版本那么选择已经确定了…​使用代码方式. 如果切面在你的设计中扮演重要角色,并且想使用针对Eclipse的(AspectJ 开发工具 (AJDT) ) 插件,那么 AspectJ 语言语法是首选项: 它更清晰和更简单,因为语言是专门用于编写切面的. 如果没有使用 Eclipse,或者只有一些切面在应用程序中不起主要作用,那么可能需要考虑使用 @AspectJ 方式,并在 IDE 中使用常规 Java 编译,并加入切面编织阶段构建的脚本.

5.6.2. 选择 @AspectJ 注解还是 Spring AOP 的 XML 配置?

如果您选择使用 Spring AOP,则可以选择 @AspectJ 或 XML 样式. 需要考虑各种权衡.

XML 样式可能是现有 Spring 用户最熟悉的,并且由真正的 POJO 支持. 当使用 AOP 作为一种工具来配置企业服务时, XML 就是一个很好的选择(可以用以下方法测试: 是否认为切入点表达式是想要独立改变的配置的一部分) . 使用 XML 配置的方式,可以从配置中更清楚地了解系统中存在哪些切面.

XML 样式有两个缺点. 首先,它并没有按实现的要求完全封装到单个地方. DRY 原则是说: 在任何知识系统中,应该有一个单一的、明确的、权威的职责. 使用 XML 的样式时,如果要求的知识是实现拆分的 bean 类的声明,并且是配置在文件的 XML 中. 当使用 @AspectJ 的风格实现单一的模块时,切面的信息是封装的. 其次,XML 的样式在能表达的功能方面比 @AspectJ 风格的有更多的限制,只有 "singleton" 切面的实例化模式得到支持,这在XML声明的切点中是不可能的. 例如,在 @AspectJ 样式中,您可以编写如下内容:

Java
@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
Kotlin
@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 的风格支持更多的实例化模式和丰富的切点组合. 它的优点是将切面确保为单元模块化,@AspectJ 的使用对理解切面也很有优势(也很容易接受) , 无论是通过 Spring AOP 还是 AspectJ 的使用 . 所以如果决定需要 AspectJ 的能力解决额外的要求,然后迁移到一个基于 AspectJ 的方法,是非常简单的. Spring 团队建议使用 @AspectJ 的方式.

5.7. 混合切面类型

在实际应用中,完全有可能混合使用 @AspectJ 的切面方式,用于支持自动代理、schema 定义 <aop:aspect>,<aop:advisor> 声明通知者甚至在同一配置中定义使用 Spring 1.2 风格的代理和拦截器. 所有这些都是使用相同的底层支持机制实现的,并且可以愉快地共存.

5.8. 代理策略

Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的代理创建代理目标对象. JDK 中内置了 JDK 动态代理,而 CGLIB 是常见的开源类定义库(重新打包为 spring-core) .

如果要代理的目标对象实现至少一个接口,则使用 JDK 动态代理. 目标类型实现的所有接口都是代理的. 如果目标对象未实现任何接口,则会创建 CGLIB 代理.

如果要强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法) ,您可以这样做. 但是,您应该考虑以下问题:

  • final 声明为 final 的方法不能使用,因为它们不能被覆盖.

  • 从 Spring 4.0 开始,代理对象的构造函数不再被调用两次, 因为 CGLIB 代理实例是通过 Objenesis 创建的. 仅当您的 JVM 执行不允许绕过构造函数,您可能会看到两次调用和 来自 Spring 的 AOP 支持的相应调试日志条目.

要强制使用 CGLIB 代理,请将 <aop:config> 元素的 proxy-target-class 属性的值设置为 true,如下所示:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用 @AspectJ 自动代理支持时强制 CGLIB 代理,请将 <aop:aspectj-autoproxy> 元素的 proxy-target-class 属性设置为 true,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true"/>

多个 <aop:config/> 选择被集合到一个统一的自动代理创建器中运行,它使用了一个强代理设置,这些配置是任意 <aop:config/> 的子代码段(通常是来自不同的 XML bean 定义文件) . 这也适用于 <tx:annotation-driven/><aop:aspectj-autoproxy/>.

要明确的是,在 <tx:annotation-driven/>,<aop:aspectj-autoproxy/><aop:config/> 元素上使用 proxy-target-class="true" 会强制使用 CGLIB 代理 他们.

5.8.1. 理解 AOP 代理

Spring AOP 是基于代理的,在编写自定义切面或使用 Spring 框架提供的任何基于 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...
    }
}
Kotlin
class SimplePojo : Pojo {

    fun foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar()
    }

    fun bar() {
        // some logic...
    }
}

如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图所示:

aop proxy plain pojo call
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();
    }
}
Kotlin
fun main() {
    val pojo = SimplePojo()
    // this is a direct method call on the 'pojo' reference
    pojo.foo()
}

当客户端代码是代理的引用时,事情发生了细微的变化. 请考虑以下图表和代码段:

aop proxy call
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();
    }
}
Kotlin
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()this.foo(), 都会调用这个引用,而不是代理. 这具有重要的意义,这意味着自我调用不会导致与方法调用相关联的通知,从而也不会获得执行的机会.

好的,那要做些什么呢? 最好的方法(这个 “best” , 的,也是迫不得已的) 是重构代码,以便不会发生自我调用. 这确实需要您做一些工作,但这是最好的,最少侵入性的方法. 下一个办法绝对是可怕的,我几乎不愿意指出,正是因为它是如此可怕. 您可以(对我们来说很痛苦) 将类中的逻辑完全绑定到 Spring AOP,如下例所示:

Java
public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}
Kotlin
class SimplePojo : Pojo {

    fun foo() {
        // this works, but... gah!
        (AopContext.currentProxy() as Pojo).bar()
    }

    fun bar() {
        // some logic...
    }
}

这完全将代码与 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();
    }
}
Kotlin
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();
Kotlin
// 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>()

See the javadoc for more information.

5.10. 在 Spring 应用中使用 AspectJ

到目前为止,我们在本章中介绍的所有内容都是纯粹的 Spring AOP. 将介绍如何使用 AspectJ 编译器/编织器代替 AOP,还介绍了超越 Spring AOP 而单独提供的功能.

Spring 有一个小的 AspectJ 切面库,是一个单独管理的 spring-aspects.jar 包. 如果使用到切面那么需要将它添加到类路径中. 在使用Spring 中的 AspectJ 独立注入域对象 和 在 Spring 中使用的 AspectJ 另外的切面 会讨论这个库的内容以及如何使用. 使用 Spring 的 IoC配置 AspectJ 切面 讨论如何依赖于使用 AspectJ 编译器编织的 AspectJ 切面. 最后, 在 在 Spring 框架中使用AspectJ 的加载时织入 将讨论在 Spring 的应用中使用 AspectJ 涉及的编织时机的讨论.

5.10.1. 使用 Spring 中的 AspectJ 独立注入域对象

Spring 容器实例化和配置会在应用程序上下文中定义 bean. 也可以让 bean 工厂配置预先存在的对象,给定一个包含要应用的配置的 bean 定义名称. spring-aspects.jar 包含了注解驱动的切面, 利用这个功能来允许依赖注入到任意对象. 该支持旨在用于在创建任何容器控制之外的对象. 域对象通常属于这一类,因为它们通常是使用 new 的操作符以编程方式创建的,或由 ORM 工具为数据库查询的结果创建的.

@Configurable 注解标记一个类符合 Spring 驱动配置的条件,在最简单的情况下,您可以纯粹使用它作为标记注解,如下例所示:

Java
import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}
Kotlin
import org.springframework.beans.factory.annotation.Configurable

@Configurable
class Account {
    // ...
}

作为这样一个标识接口, Spring 将会为这个注解类型(在例子中是 Account) 利用定义 bean 的方式(典型的原型作用域) 配置一个新实例, 这个实例拥有与完全限定类型相同的名字(com.xyz.myapp.domain.Account). 因为一个 bean 的默认名称是它的类型的完全限定名,这个简便的方式只是省略了它的 id 属性. 如以下示例所示:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果想要显式指定为原型 bean 使用的名称,可以直接在注解执行此操作,如以下示例所示:

Java
import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}
Kotlin
import org.springframework.beans.factory.annotation.Configurable

@Configurable("account")
class Account {
    // ...
}

Spring 现在查找名为 account 的bean定义,并将其用作配置新 Account 实例的定义.

也可以使用自动装配以避免指定一个特定的专用 bean 定义. Spring 将利用 @Configurable 注解的自动装配属性来自动装配 bean,可以使用 @Configurable(autowire=Autowire.BY_NAME 或者 @Configurable(autowire=Autowire.BY_TYPE) 分别自动装配基于名称和基于类型的 bean.作为替代方法,您可以在其类上或方法级别上使用 @Autowired@Inject 能够使用注解驱动的依赖注入. 有关更多详细信息,请参阅 基于注解的容器配置 基于注解的容器配置.

最后,可以使用 Spring 依赖的名为 dependencyCheck 的特性去检查新建的对象引用以及配置对象(例如, @Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)) . 如果将此特性设置为 true,那么 Spring 将在配置之后确认所有属性(非原始或集合) 已被设置.

当然,使用注解本身没有任何作用. 这是 spring-aspects.jar 包中的 AnnotationBeanConfigurerAspect 注解的存在行为. 实质上, 该切面表达的是,一个带有 @Configurable 注解类型的新对象在初始化返回之后,按照注解的属性使用 Spring 配置创建新的对象. 在这种情况下,初始化是指新实例化的对象(例如, 用 new 运算符实例化的对象) 以及正在经历反序列化(例如,通过 readResolve()) 的 Serializable 对象.

上一段的一个关键短语是 "实质".. 在大多数情况下,精确的语义从一个新对象初始化后返回是适合的. "初始化后"意味着依赖将会在对象被构建完毕后注入 , 这意味着依赖在类构造器当中是不能使用的. 如果想依赖的注入发生在构造器执行之前,而且能够用在构造器之中,那么需要像下面这样声明 @Configurable:

Java
@Configurable(preConstruction = true)
Kotlin
@Configurable(preConstruction = true)

您可以在 本附录AspectJ编程指南一书中找到更多有关 AspectJ 的信息

这个注解类型必须使用 AspectJ 编织织入才可以工作 , 开发者可以使用构建组件 Ant 或 Maven 来完成这个任务(AspectJ Development Environment Guide有参考例子) ,或者在装配时织入(请参考 在 Spring 框架中使用AspectJ 的加载时织入) . AnnotationBeanConfigurerAspect 注解本身需要 Spring 来配置(为了获取一个 bean 工厂引用,被用于配置新的对象) . 如果使用基于 Java 的配置, 那么只需将 @EnableSpringConfigured 注解加入到任意的 @Configuration 类中即可,如下所示:

Java
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
Kotlin
@Configuration
@EnableSpringConfigured
class AppConfig {
}

如果基于 XML 配置,那么只要在 Springcontext 命名空间声明中添加 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>
不用通过 bean 的切面配置来激活 @Configurable 处理过程,除非真的想在运行中依赖其语义. 特别地,不要在一个已经在容器上注册过的 Spring bean 上去再去使用 @Configurable 注解. 否则,这个 bean 将会被初始化两次,容器一次,切面一次.
单元测试 @Configurable 的对象

开启 @Configurable 支持的一个目标就是使单元测试独立于域对象,从而没有碰到诸如硬编码查找一样的困难. 如果 @Configurable 注解没有使用 AspectJ 织入那么它就不会对单元测试造成影响, 这样就可以正常地进行 mock 或 stub 测试. 如果 @Configurable 是使用 AspectJ 织入的,那么依然可以在容器之外正常地进行单元测试,但是如果每次都构建一个 @Configurable 对象都会看到警告消息, 它表示此配置并非 Spring 的配置.

多个应用上下文一起工作

AnnotationBeanConfigurerAspect 类在AspectJ中用来实现 @Configurable 支持的单个切面. 单个切面的作用域与静态成员的作用域是相同的, 也就是说每一个类加载器都会定义这个切面的实例类型. 这意味着,如果使用相同的类加载器层来定义多个应用上下文. 那么必须考虑在哪儿定义 @EnableSpringConfigured bean以及在哪个路径存放 spring-aspects.jar 包.

考虑一个典型的 Spring Web 应用程序配置,其中有一个共享的父应用上下文,定义公共业务服务和支持它们所需的所有内容,每个Servlet包含一个子应用上下文, 其中包含特定于Servlet的定义. 所有这些上下文共存于相同的类加载器层次,所以 AnnotationBeanConfigurerAspect 能够持有他们之中的一个的引用. 在这种情况下, 建议在共享的(父) 应用上下文上使用 @EnableSpringConfigured bean 定义,这个定义的服务, 可能想注入到域对象中. 结果是,开发者不能在子上下文(特定的 Servlet) 中使用 @Configurable 去定义域对象的引用bean(也许并不想做些什么) .

在同一个容器部署多个 Web 应用程序时,确保每个 Web 应用程序加载 spring-aspects.jar 类型是在使用自己的加载器引用(例如,通过 WEB-INF/lib) . 如果 spring-aspects.jar 仅在容器的类路径下(也就是装在父母共享的加载器的引用) ,所有 的 Web 应用程序将共享相同的切面实例,而这可能不是你想要的.

5.10.2. 在Spring中使用的AspectJ额外的切面

除了 @Configurable 切面,spring-aspects.jar 还包含 AspectJ 切面,可以用来驱动 Spring 的事务管理,用于注解带 @Transactional 注解的类型和方法 . 这主要是为那些希望在 Spring 容器之外使用 Spring 框架的事务支持的用户而设计的.

解析 @Transactional 注解的切面是 AnnotationTransactionAspect. 当使用这个切面时,必须注解这个实现类(和/或在类的方法上) ,不是接口(如果有的话) 的实现类. AspectJ 遵循 Java 的规则,注解的接口不能被继承.

@Transactional 注解的类指定默认的事务语义的各种公共操作的类.

在类的方法上注解 @Transactional 将会覆盖由给定默认事务语义的注解(如果存在) ,任意可见性的方法都可以被注解,包括私有方法. 直接注解非公共方法是获得执行此类方法的事务划分的唯一方法.

从 Spring Framework 4.2 开始, spring-aspects 提供了类似的切面,为标准的 javax.transaction.Transactional 注解提供了完全相同的功能. 查看 JtaAnnotationTransactionAspect 获取更多细节.

对于希望使用 Spring 配置和事务管理支持但不希望(或不能) 使用注解的 AspectJ 程序员, spring-aspects.jar 还包含可以扩展以提供自定义切点定义的抽象切面. 有关更多信息,请参阅 AbstractBeanConfigurerAspectAbstractTransactionAspect 切面的源码. 作为示例,以下摘录显示了如何使用与完全限定的类名匹配的原型 bean 定义来编写一个切面 ,用于配置 domain 模型中定义的所有对象实例:

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 运行时本身是负责创建和配置切面的, AspectJ 通过 Spring 创建切面取决于 AspectJ 实例化模型的方法(per-xxx 引起的) 的切面使用.

多数的 AspectJ 切面是单例切面. 这些切面的配置非常容易,只需正常地创建一个 bean 定义引用切面的类型,包含 bean 属性 factory-method="aspectOf" . 这保证了 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 的定义和从 spring-aspects.jar 中使用 @Configurable 的支持. 这些工作需要在 AspectJ 运行之后在创建之中去配置切面实例才能成功.

如果想要使用 AspectJ 编写一些 @AspectJ 切面(例如,针对领域模型类型使用加载时编织) 以及希望与 Spring AOP 一起使用的其他 @AspectJ 切面,并且这些切面都使用 Spring 进行配置 . 那么需要告诉 Spring AOP @AspectJ 自动代理支持在配置中定义的 @AspectJ 方面的确切子集应该用于自动代理. 可以通过在 <aop:aspectj-autoproxy/> 元素中声明使用一个或多个 <include/> 元素来完成此操作. 每个 <include/> 元素指定一个名称模式,并且只有名称与至少一个模式相匹配的 bean 才会用于 Spring AOP 自动代理配置. 以下示例显示了如何使用 <include/> 元素:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被 <aop:aspectj-autoproxy/> 元素的名称误导. 使用它会导致创建 Spring AOP 代理. 切面声明的 @AspectJ 方式只是在这里使用,AspectJ 运行时是没有用到的.

5.10.4. 在 Spring 框架中使用 AspectJ 的加载时织入

是指 AspectJ 切面在 JVM 加载类文件时被织入到程序的类文件的过程. 本部分的重点是配置和使用 LTW 在 Spring 框架上的具体内容,本节不是 LTW 的简介. 只有 AspectJ 能够详细地讲述 LTW 的特性和配置(与 Spring 完全没有关系) ,可以参看 LTW section of the AspectJ Development Environment Guide.

Spring 框架在 AspectJ 的 LTW 织入的过程中提供了更细粒度的控制,'Vanilla' AspectJ LTW 是一个高效的使用 Java(1.5+) 的代理,它会在 JVM 启动的时候改变一个 VM 参数. 这是一种 JVM 范围的设置,在某些情况下可能会很适合,但是太粗粒度了. Spring 的 LTW 能够为 LTW 提供类加载前的织入,显然这是一个更细粒度的控制,而且它在 'single-JVM-multiple-application' 的环境下更具意义(在典型的应用程序服务器环境中就是这样做的) .

此外,在特定的环境中(查看在某些环境) ,这种方式可以在对应用程序服务器运行脚本不做任何修改的情形下支持 LTW, 但需要添加 -javaagent:path/to/aspectjweaver.jar(本节稍后将会描述) 或 -javaagent:path/to/org.springframework.instrument-5.3.22.jar(原名为 spring-agent.jar) . 开发人员只需修改构成应用程序上下文的一个或多个文件,以启用加载时编入,而不是依赖通常负责部署配置的管理文件. 例如启动脚本.

到此为止,推销宣传部分已经结束了,那么让我们首先介绍使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中介绍的元素. 有关完整示例,请参阅 Petclinic示例应用程序.

第一个例子

假设您是一名应用程序开发人员,负责诊断系统中某些性能问题的原因. 我们无需打开一个分析工具,而是要打开一个简单的剖析切面,让我们能够很快获得了一些性能指标, 这样我们就可以在随后立即使用更细粒度的分析工具.

这里介绍的例子使用 XML 格式的配置,也可以使用 Java配置@AspectJ 的方式. 特别是 @EnableLoadTimeWeaving 注解可以起 到替代 <context:load-time-weaver/> (详情见下文 ) .

下面是一个用于性能分析的切面,它不需要太花哨,它是一个基于时间的分析器,它使用 @AspectJ 样式的方面声明:

Java
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(){}
}
Kotlin
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(稍后解释) . LTW 是从一个或多个 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>

现在所有必需的材料( aspect, META-INF/aop.xml 文件, Spring 的配置) 都已到位,我们可以使用 main(..) 方法创建以下驱动程序类,以演示 LTW 的运行情况:

Java
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();
    }
}
Kotlin
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()
}

我们还有最后一件事要做. 本节的介绍确实说可以使用 Spring 在每个 ClassLoader 的基础上有选择地打开 LTW,这是事实. 但是,对于此示例,我们使用 Java 代理(随 Spring 提供) 来打开 LTW. 我们使用以下命令来运行前面显示的 Main 类:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent 是一个标志,用于指定和启用 代理程序来检测在 JVM 上运行的程序. Spring Framework 附带了一个代理程序 InstrumentationSavingAgent, 它包装在 spring-instrument.jar 中,它作为前面示例中 -javaagent 参数的值提供.

Main 的输出将如下所示. (前面已经介绍了 Thread.sleep(..) 声明为 calculateEntitlement() 实现使分析器实际上捕获了比 0 毫秒更多的东西(01234 毫秒不是 AOP 引入的开销) ) 下面的清单显示了输出 我们运行我们的探查器时得到了:

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

由于 LTW 是会对 AspectJ 产生影响的,而不是仅仅局限在 Spring 的 beans. 在 Main 程序的轻微变化会产生相同的结果:

Java
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();
    }
}
Kotlin
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 容器,然后在 Spring 的上下文之外创建一个新的 StubEntitlementCalculationService 实例. 分析通知依然会被编织.

不可否认,这个例子很简单. 但是在 Spring 中支持 LTW 的基础都介绍到了,而且为什么使用以及怎样使用配置在后面的章节也将解释.

在这个例子中使用的 ProfilingAspect 可能很基础的,但它非常有用. 是一个开发者可以使用在开发过程中使用开发时间切面的例子, 然后很容易地排除来自应用程序被部署到测试或生产中的因素.
切面

在 LTW 使用的 aspects 必须是 AspectJ 的切面. 它们可以写在 AspectJ 语言本身也可以在 @AspectJ 方式声明. 这意味着 aspects 在 AspectJ 和 Spring AOP 的切面都有效. 此外,编译切面的类需要包含在类路径中.

'META-INF/aop.xml'

使用 AspectJ LTW 的基础设施是一个或多个 META-INF/aop.xml 配置文件,这是在 Java 类路径中的(直接的或者更通常是一个 JAR 文件) .

LTW 部分 AspectJ 参考文档中详细介绍了此文件的结构和内容. 由于 aop.xml 文件是 100% AspectJ,因此我们不在此进一步描述.

需要的类库(JARS)

至少,您需要以下库来使用 Spring Framework 对 AspectJ LTW 的支持:

  • spring-aop.jar

  • aspectjweaver.jar

如果使用Spring 提供的代理程序启用检测,则还需要:

  • spring-instrument.jar

Spring 的配置

Spring 支持 LTW 的关键部件是 LoadTimeWeaver 接口(位于 org.springframework.instrument.classloading 包) ,而这接口有大部分的实现分布在 Spring 中. LoadTimeWeaver 负责添加一个或多个 java.lang.instrument.ClassFileTransformers 到运行时的类装载器中. 这为各种有趣的应用程序打开了大门,其中一个恰好是方面的 LTW.

如果您不熟悉运行时类文件转换的概念,请在继续之前查看 java.lang.instrument 包的 javadoc API 文档. 虽然该文档并不全面,但至少可以看到关键接口和类(供您阅读本节时参考) .

配置一个特定的 ApplicationContext LoadTimeWeaver 就像加入一行代码一样容易. (请注意,几乎可以肯定会将 ApplicationContext 作为的Spring容器- 通常一个 BeanFactory 是不够的,因为LTW的支持利用到 BeanFactoryPostProcessors) .

要启用 Spring Framework 的 LTW 支持,您需要配置 LoadTimeWeaver,通常使用 @EnableLoadTimeWeaving 注解来完成,如下所示:

Java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
Kotlin
@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>

上面的配置自动为你登记了一些特定的基础 beans,例如 LoadTimeWeaverAspectJWeavingEnabler. 默认的 LoadTimeWeaverDefaultContextLoadTimeWeaver 类,它试图装饰并自动检测 LoadTimeWeaver. "自动检测" 的 LoadTimeWeaver 的确切类型取决于您的运行时环境. 下表总结了各种 LoadTimeWeaver 实现:

Table 12. DefaultContextLoadTimeWeaver LoadTimeWeavers
Runtime Environment LoadTimeWeaver implementation

Running in Apache Tomcat

TomcatLoadTimeWeaver

Running in GlassFish (limited to EAR deployments)

GlassFishLoadTimeWeaver

Running in Red Hat’s JBoss AS or WildFly

JBossLoadTimeWeaver

Running in IBM’s WebSphere

WebSphereLoadTimeWeaver

Running in Oracle’s WebLogic

WebLogicLoadTimeWeaver

JVM started with Spring InstrumentationSavingAgent (java -javaagent:path/to/spring-instrument.jar)

InstrumentationLoadTimeWeaver

Fallback, expecting the underlying ClassLoader to follow common conventions (namely addTransformer and optionally a getThrowawayClassLoader method)

ReflectiveLoadTimeWeaver

请注意,该表仅列出使用 DefaultContextLoadTimeWeaver 时自动检测的 LoadTimeWeavers. 您可以准确指定要使用的 LoadTimeWeaver 实现.

使用 Java 配置指定特定的 LoadTimeWeaver 实现 LoadTimeWeavingConfigurer 接口并覆盖 getLoadTimeWeaver() 方法. 以下示例指定 ReflectiveLoadTimeWeaver:

Java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}
Kotlin
@Configuration
@EnableLoadTimeWeaving
class AppConfig : LoadTimeWeavingConfigurer {

    override fun getLoadTimeWeaver(): LoadTimeWeaver {
        return ReflectiveLoadTimeWeaver()
    }
}

如果使用基于 XML 的配置,则可以将完全限定的类名指定为 <context:load-time-weaver/> 元素上的 weaver-class 属性的值. 同样,以下示例指定了 ReflectiveLoadTimeWeaver:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        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 基础结构的机制来添加一个或多个 ClassFileTransformer,执行LTW的实际 ClassFileTransformersClassPreProcessorAgentAdapter(来自 org.aspectj.weaver.loadtime 包) . 有关详细信息,请参阅 ClassPreProcessorAgentAdapter 类的类级 javadoc, 因为编织实际如何实现的细节超出了本文档的范围.

剩下要讨论的配置有一个 final 属性: aspectjWeaving 属性(如果使用 XML,则为 aspectj-weaving) . 此属性控制是否启用 LTW. 它接受三个可能值中的一个,如果该属性不存在,则默认值为 autodetect. 下表总结了三个可能的值:

Table 13. AspectJ织入的属性值
Annotation Value XML Value Explanation

ENABLED

on

AspectJ 编织开启,切面在加载时织入.

DISABLED

off

LTW 已关闭. 没有切面加载时织入.

AUTODETECT

autodetect

如果 Spring LTW 基础结构可以找到至少一个 META-INF/aop.xml 文件,那么 AspectJ 编织就会打开. 否则,它关闭. 这是默认值.

特定环境的配置

最后一部分包含在应用程序服务器 和 Web 容器等环境中使用 Spring LTW 支持时所需的任何其他设置和配置.

Tomcat, JBoss, WebSphere, WebLogic

Tomcat, JBoss/WildFly, IBM WebSphere Application Server 和 Oracle WebLogic Server 提供了一个能够进行本地检测的 ClassLoader. Spring的原生 LTW 利用这种 ClassLoader 实现来实现 AspectJ 织入. 如前所述,您可以通过激活加载时织入来启用 LTW. 具体来说,您无需修改启动脚本即可添加 -javaagent:path/to/spring-instrument.jar.

注意在 JBoss 中, 应用程序服务器的扫描需要禁用,防止它加载的类的应用之前实际上已经开始. 快速的解决方案是增加一个叫 WEB-INF/jboss-scanning.xml 的文档并加入以下内容:

<scanning xmlns="urn:jboss:scanning:1.0"/>
通用的 Java 应用

在不支持现有 LoadTimeWeaver 实现或不受现有 LoadTimeWeaver 实现支持的环境中需要类检测时,使用 JDK 代理可能是唯一的解决方案. 对于这种情况, Spring 提供了 InstrumentationLoadTimeWeaver,它需要 Spring 特有的(但也是非常普通的) VM 代理包 spring-instrument.jar . 通过常见的 @EnableLoadTimeWeaving<context: load-time-weaver /> 设置.

要使用它,必须通过提供以下 JVM 选项来启动带有 Spring 代理的虚拟机:

-javaagent:/path/to/spring-instrument.jar

请注意,这需要修改 JVM 启动脚本,这可能会阻止在应用服务器环境中使用它(具体取决于操作策略) . 就是说,对于每个 JVM 一个应用程序的部署,例如独立 在 Spring Boot 应用程序中,通常无论如何都要控制整个 JVM 设置.

5.11. 更多资源

有关 AspectJ 的更多信息可以在 AspectJ website上找到.

Eclipse AspectJ 由 Adrian Colyer(Addison-Wesley, 2005) 出版 提供了详尽的有关 AspectJ 语言的介绍

AspectJ in Action, 一书的第二版由 Ramnivas Laddad(Manning,2009) 出版,也是强烈推荐的. 这本书的重点是 AspectJ,但也在一定的深度上探讨了普通的 AOP 主题.

6. Spring AOP APIs

前一章介绍了 Spring 使用 @AspectJ 和基于 schema 的切面定义对 AOP 的支持. 在本章中,将讨论较底层的 Spring AOP API 和 AOP 支持. 对于普通应用程序,我们建议将 Spring AOP 与 AspectJ 切入点一起使用,如 前一章所述.

6.1. Spring 中的切点 API

本节描述了 Spring 如何处理切点的关键概念.

6.1.1. 概念

Spring 的切点模式能够让切点独立于通知类型. 针对不同的通知使用相同的切点是可能的.

org.springframework.aop.Pointcut 接口是切点的主要接口,用于特定类和方法的目标通知. 完整的接口如下:

public interface Pointcut {

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();
}

Pointcut 接口分成两部分,允许重用类和方法匹配部分,以及细粒度的组合操作 (例如与另一个方法匹配器执行 “union”) .

ClassFilter 接口是用来限制切点的一组给定的目标类. 如果 matches() 方法总是返回 true,那么表示所有的目标类都将匹配. 以下清单显示了 ClassFilter 接口定义:

public interface ClassFilter {

    boolean matches(Class clazz);
}

MethodMatcher 接口通常更重要. 完整的接口如下所示:

public interface MethodMatcher {

    boolean matches(Method m, Class<?> targetClass);

    boolean isRuntime();

    boolean matches(Method m, Class<?> targetClass, Object... args);
}

matches(Method, Class) 方法用于测试此切点是否曾经匹配到目标类上的给定方法. 在创建 AOP 代理时可以执行此评估,以避免需要对每个方法调用进行测试. 如果对于给定方法这个双参数 matches 方法返回 true,并且 MethodMatcherisRuntime() 方法也返回 true. 则在每次方法调用时都会调用三参数 matches 方法. 这使切点能够在目标通知执行之前,查看传递给方法调用的参数.

大多数 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 类,可以提供一个匹配的 Strings 列表. 如果其中任意一个都是匹配的,则切点将计算将为 true (因此,结果实际上是这些切点的并集) .

以下示例显示如何使用 JdkRegexpMethodPointcut:

<bean id="settersAndAbsquatulatePointcut"
        class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

Spring 提供了一个方便使用的类 RegexpMethodPointcutAdvisor, 它允许引用 Advice (记住 Advice 可能是一个拦截器、前置通知、异常通知等等) . 而在这个类的后面,Spring 也是使用 JdkRegexpMethodPointcut 类的. 使用 RegexpMethodPointcutAdvisor 来简化织入,用作 bean 封装的切点和通知. 如下例所示:

<bean id="settersAndAbsquatulateAdvisor"
        class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="advice">
        <ref bean="beanNameOfAopAllianceInterceptor"/>
    </property>
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

您可以将 RegexpMethodPointcutAdvisor 与任何 Advice 类型一起使用.

基于属性的切点

静态切点的一个重要特征是元数据驱动的切点. 它将使用元数据属性的值,通常是使用源等级的元数据.

动态的切点

与静态切点相比,动态切点的评估成本更高. 它们考虑了方法参数和静态信息. 这意味着必须使用每个方法调用来评估它们,并且不能缓存结果,因为参数会有所不同.

主要的例子是 control flow 切点

控制流切点

Spring 控制流切点在概念上类似于 AspectJ 的 cflow 切点,虽然功能不够它的强大 (目前没有办法指定切点在另一个切点匹配的连接点下面执行) . 控制流切点与当前调用的栈相匹配. 例如,如果连接点是由 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
    }
}
Kotlin
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支持的 "semantic pointcuts" - 例如,"所有更改目标对象中实例变量的方法".

6.2. Spring 的通知 API

接下来介绍 Spring AOP 是怎么样处理通知的

6.2.1. 通知的生命周期

每个通知都是 Spring bean.通知实例可以在所有通知对象之间共享,或者对每个通知对象都是唯一的. 这对应于每个类或每个实例的通知.

单类 (Per-class) 通知是最常用的. 它适用于诸如事务通知者之类的一般性通知. 它不依赖于代理对象的状态或添加新状态,它们只是对方法和参数产生作用.

单实例 (Per-instance) 的通知适合于引入,以支持混合使用.在这种情况下,通知将状态添加到代理对象中.

在同一个 AOP 代理中,可以使用混合共享的和单实例的通知.

6.2.2. Advice Types in Spring

Spring 提供了几种通知类型,并且可以扩展以支持任意通知类型. 本节介绍基本概念和标准通知类型.

Spring中的通知类型

在 Spring 中,最基础的通知类型是拦截环绕通知

Spring 使用方法拦截来满足 AOPAlliance 接口的要求. MethodInterceptor 实现环绕通知应该实现以下接口:

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke() 方法的参数 MethodInvocation 暴露了将要被触发的方法,目标连接点,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;
    }
}
Kotlin
class DebugInterceptor : MethodInterceptor {

    override fun invoke(invocation: MethodInvocation): Any {
        println("Before: invocation=[$invocation]")
        val rval = invocation.proceed()
        println("Invocation returned")
        return rval
    }
}

请注意对 MethodInvocationproceed() 方法的调用. proceed 从拦截器链上进入连接点. 大多数拦截器调用此方法并返回其返回值. 但是, 与任意的环绕通知一样, MethodInterceptor 可以返回不同的值或引发异常,而不是调用 proceed 方法. 但是,如果没有充分的理由,您不希望这样做.

MethodInterceptor 提供与其他 AOP Alliance 兼容的 AOP 实现. 本节其余部分讨论的其他通知类型实现了常见的 AOP 概念,但这特定于使用 Spring 的方式. 尽管使用最具体的通知类型切面总是有优势的,但如果希望在另一个 AOP 框架中运行该切面面,,则应坚持使用 MethodInterceptor 的通知. 请注意,目前切点不会在框架之间进行交互操作, 并且目前的 AOP Alliance 并没有定义切点接口.
前置通知

前置通知是一种简单的通知,它并不需要 MethodInvocation 对象,因为它只会在执行方法前调用.

前置通知的主要优势就是它没有必要去触发 proceed() 方法,因此当拦截器链失败时对它是没有影响的.

以下清单显示了 MethodBeforeAdvice 接口:

public interface MethodBeforeAdvice extends BeforeAdvice {

    void before(Method m, Object[] args, Object target) throws Throwable;
}

(Spring 的 API 设计允许前置通知使用在域上,尽管通常是适用于字段拦截的,而 Spring 也不可能实现它) .

注意 before 方法的返回类型是 void 的. 前置通知可以在连接点执行之前插入自定义行为,但不能更改返回值. 如果前置通知抛出了异常, 将会中止拦截器链的进一步执行,该异常将会传回给拦截器链. 如果它标记了 unchecked,或者是在触发方法的签名上,那么它将直接传递给客户端. 否则,它由 AOP 代理包装在未经检查的异常中.

以下示例显示了 Spring 中的前置通知,该通知计算所有方法调用:

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;
    }
}
Kotlin
class CountingBeforeAdvice : MethodBeforeAdvice {

    var count: Int = 0

    override fun before(m: Method, args: Array<Any>, target: Any?) {
        ++count
    }
}
前置通知可以用在任意的切点上
异常通知

异常通知是在连接点返回后触发的,前提是连接点抛出了异常. Spring 提供了类型化的抛出通知. 请注意,这意味着 org.springframework.aop.ThrowsAdvice 接口不包含任何方法. 它只是标识给定对象实现一个或多个类型化异常通知方法的标识接口,这些应该是以下形式:

afterThrowing([Method, args, target], subclassOfThrowable)

这个方法只有最后一个参数是必需的. 方法签名可以有一个或四个参数,具体取决于通知方法是否对方法和参数有影响. 接下来的两个列表显示了作为异常通知示例的类. .

如果抛出 RemoteException (包括子类) ,则调用以下通知:

Java
public class RemoteThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
}
Kotlin
class RemoteThrowsAdvice : ThrowsAdvice {

    fun afterThrowing(ex: RemoteException) {
        // Do something with remote exception
    }
}

与前面的通知不同,下一个示例声明了四个参数,以便它可以访问被调用的方法,方法参数和目标对象. 如果抛出 ServletException,则调用以下通知:

Java
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}
Kotlin
class ServletThrowsAdviceWithArguments : ThrowsAdvice {

    fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
        // Do something with all arguments
    }
}

最后的示例演示了如何在单个类中使用这两种方法,它能处理 RemoteExceptionServletException 异常. 任何数量的异常通知方法都可以在单个类中进行组合. 以下清单显示了最后一个示例:

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
    }
}
Kotlin
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
    }
}
如果异常通知方法引发了异常,那么它将会重写原始的异常 (即更改为向用户抛出异常) . 覆盖异常通常是 RuntimeException,它与任何方法签名兼容. 但是,如果异常通知方法引发了 checked 异常,那么它必须与目标方法的已声明的异常相匹配,因此在某种程度上耦合到特定的目标方法签名. 不要抛出与目标方法签名不兼容的未声明的 checked 异常
异常通知可以被用在任意切点上
后置返回通知

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;
    }
}
Kotlin
class CountingAfterReturningAdvice : AfterReturningAdvice {

    var count: Int = 0
        private set

    override fun afterReturning(returnValue: Any?, m: Method, args: Array<Any>, target: Any?) {
        ++count
    }
}

此通知不会更改执行路径,如果抛出异常,将抛出拦截器链而不是返回值.

后置返回通知能被任何切点使用
引入通知

Spring 将引入通知看作是一种特殊的拦截器通知

引入通知需要 IntroductionAdvisorIntroductionInterceptor,他们都实现了下面的接口:

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

从 AOP Alliance MethodInterceptor 接口继承的 invoke() 方法也都必须实现引入. 即如果 invoked 方法是一个引入接口, 引入拦截器将会负责处理这个方法的调用-它无法触发 proceed().

引入通知不能与任何切点一起使用,因为它只适用于类级别,而不是方法级别. 开发者只能使用 IntroductionAdvisor 的引入通知,它具有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

    ClassFilter getClassFilter();

    void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

    Class<?>[] getInterfaces();
}

在这里如果没有与 MethodMatcher 相关的引入通知类. 也就不会有 Pointcut . 此时,只有 filtering 类是符合逻辑的.

getInterfaces() 方法返回通知者的引入接口

validateInterfaces() 方法在内部使用,可以查看引入接口是否可以由配置的 IntroductionInterceptor 实现.

考虑 Spring 测试套件中的一个示例,并假设我们要将以下接口引入一个或多个对象:

Java
public interface Lockable {
    void lock();
    void unlock();
    boolean locked();
}
Kotlin
interface Lockable {
    fun lock()
    fun unlock()
    fun locked(): Boolean
}

这个说明是混合型的. 我们希望可以将无论是什么类型的通知对象都转成 Lockable,这样可以调用它的 lock 和 unlock 方法. 如果调用的是 lock() 方法,希望所有的 setter 方法都抛出 LockedException 异常. 因此,可以添加一个切面,它提供了对象不可变的能力,而不需要对它有任何了解. AOP 的一个很好的例子: a good example of AOP.

首先,我们需要一个可以完成繁重工作的 IntroductionInterceptor. 在这种情况下,我们扩展了 org.springframework.aop.support.DelegatingIntroductionInterceptor 类更方便. 我们可以直接实现 IntroductionInterceptor,但使用 DelegatingIntroductionInterceptor 最适合大多数情况.

DelegatingIntroductionInterceptor 设计是为了将引入委托让给引入接口真正的实现类,从而隐藏了拦截器去做这个事. 可以使用构造函数参数将委托设置为任何对象. 默认委托 (当使用无参数构造函数时) 时是 this 的. 因此,在下面的示例中, 委托是 DelegatingIntroductionInterceptor 中的 LockMixin 子类. 给定一个委托 (默认是它本身) , DelegatingIntroductionInterceptor 实例将查找委托(非 IntroductionInterceptor) 实现的所有接口,并支持对其中任何一个的引入. 子类(如 LockMixin) 可以调用 suppressInterface(Class intf) 方法来控制不应该暴露的接口. 但是,无论 IntroductionInterceptor 准备支持多少接口,使用 IntroductionAdvisor 都可以控制实际暴露的接口. 引入接口将隐藏目标对同一接口的任何实现.

因此, 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);
    }

}
Kotlin
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. 一个更复杂的示例可能会引用引入拦截器 (被定义为原型) ,在这种情况下,没有与 LockMixin 相关的配置,因此我们使用 new 创建它. 以下示例显示了我们的 LockMixinAdvisor 类:

Java
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

    public LockMixinAdvisor() {
        super(new LockMixin(), Lockable.class);
    }
}
Kotlin
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)

我们可以非常简单地应用这个通知者,因为它不需要配置. (但是,没有 IntroductionAdvisor 就不可能使用 IntroductionInterceptor. ) 与通常的引入一样, 通知者必须是个单实例 (per-instance) ,因为它是有状态的. 需要为每个通知的对象创建每一个不同的 LockMixinAdvisor 实例和 LockMixin. 通知者也包括通知对象状态的一部分

可以使用 Advised.addAdvisor() 方法或在在XML配置中 (推荐此法) 编写通知者,这与其他任何的通知者一样. 下面讨论的所有代理创建选项, 包括自动代理创建,都正确处理了引入和有状态的mixin.

6.3. Spring 中通知者的 API

在 Spring 中,一个通知者就是一个切面,一个仅包含与单个通知对象关联的切点表达式.

除了引入是一个特殊的例子外,通知者能够用于所有的通知上. org.springframework.aop.support.DefaultPointcutAdvisor 类是最常使用的通知者类. 它可以与 MethodInterceptor, BeforeAdviceThrowsAdvice 一起使用.

在同一个 AOP 代理中,可以在 Spring 中混合使用通知者和通知类型. 例如,可以在一个代理配置中同时使用环绕通知、异常通知和前置通知. Spring 自动创建必要的拦截链.

6.4. 使用 ProxyFactoryBean 来创建 AOP 代理

如果你为业务对象使用 Spring IoC 容器 (一个 ApplicationContextBeanFactory) (同时也应该这么做!) , 那么可能希望用到其中一个 Spring 的 AOP FactoryBean. (请记住,工厂 bean 引入了一个间接层,让它创建一个不同类型的对象. )

Spring AOP 支持也使用到了工厂 bean

在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean. 这将完全控制切点和应用的通知及顺序. 但是,如果不需要这样的控制,可以有更简单的选项.

6.4.1. 基础设置

ProxyFactoryBean 与其他Spring FactoryBean 的实现一样,引入了一个间接层. 如果定义了一个名为 fooProxyFactoryBean, 那么引用 foo 的对象不是 ProxyFactoryBean 实例本身,而是由 ProxyFactoryBean 实现的 getObject() 方法创建的对象. 此方法将创建一个用于包装目标对象的 AOP 代理

使用 ProxyFactoryBean 或另一个 IoC 识别类来创建 AOP 代理的最重要的好处之一是,它意味着建议和切点也可以由 IoC 容器管理. 这是一个强大的功能,能够实现其他AOP框架无法实现的方法. 例如,通知本身可以引用应用程序对象 (除了目标,它应该在任何 AOP 框架中可用) ,这得益于依赖注入提供的所有可插入功能.

6.4.2. JavaBean 属性

与 Spring 提供的大多数 FactoryBean 实现一样,ProxyFactoryBean 类本身就是一个 JavaBean. 其属性用于:

一些关键属性继承自 org.springframework.aop.framework.ProxyConfig (Spring中所有AOP代理工厂的超类) . 这些关键属性包括以下内容:

  • proxyTargetClass: 如果目标类需要代理,而不是目标类的接口时,则为 true. 如果此属性值设置为 true,则会创建 CGLIB 代理 (但另请参阅基于 JDK 和 CGLIB 的代理) .

  • optimize: 控制是否将积极的优化应用于通过 CGLIB 创建的代理. 除非您完全了解相关 的 AOP 代理如何处理优化,否则不要随意使用此设置. 当前仅用于 CGLIB 代理. 它对 JDK 动态代理无效.

  • frozen: 如果代理配置被 frozen,则不再允许对配置进行更改. 这既可以作为一种轻微的优化,也适用于当不希望调用方在创建代理后能够操作代理 (通过 Advised 接口) 的情况. 此属性的默认值为 false,因此如果允许添加其他的通知的话可以更改.

  • exposeProxy: 确定当前代理是否应在 ThreadLocal 中暴露,以便目标可以访问它. 如果目标需要获取代理,并且 exposeProxy 属性设置为 true. 则目标可以使用 AopContext.currentProxy() 方法.

ProxyFactoryBean 特有的其他属性包括以下内容:

  • proxyInterfaces: 字符串接口名称的数组. 如果未提供此项,将使用目标类的 CGLIB 代理 ( 基于 JDK 和 CGLIB 的代理) .

  • interceptorNames: 要提供的通知者、拦截器或其他通知名称的字符串数组. 在先到先得的服务基础上,Ordering (顺序) 是重要的. 也就是说, 列表中的第一个拦截器将首先拦截调用.

    这些名称是当前工厂中的 bean 名称,包括来自上级工厂的 bean 名称. 不能在这里提及 bean 的引用,因为这样做会导致 ProxyFactoryBean 忽略通知的单例.

    可以追加一个带有星号()的拦截器名称. 这将导致应用程序中的所有被 匹配的通知者 bean 的名称都会被匹配上. 您可以在使用 全局通知者中中找到使用此功能的示例.

  • singleton: 工厂强制返回单个对象,无论调用 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 属性指定拦截器列表. 请注意,即使 ProxyFactoryBeanproxyTargetClass 属性被设置为 false,也会创建 CGLIB 的代理. (显然,这个 false 是没有意义的,最好从 bean 定义中删除,因为它充其量是冗余的,而且是最容易产生混乱) .

如果目标类实现了一个 (或多个) 接口,那么所创建代理的类型取决于 ProxyFactoryBean 的配置.

如果 ProxyFactoryBeanproxyTargetClass 属性已设置为 true,则会创建基于 CGLIB 的代理. 这是有道理的,并且符合最少惊喜的原则. 即使 ProxyFactoryBeanproxyInterfaces 属性已设置为一个或多个完全限定的接口名称,proxyTargetClass 属性设置为 true 这一事实也会导致基于 CGLIB 的代理生效.

如果 ProxyFactoryBeanproxyInterfaces 属性已设置为一个或多个完全限定的接口名称,则会创建基于 JDK 的代理. 创建的代理实现 proxyInterfaces 属性中指定的所有接口. 如果目标类恰好实现了比 proxyInterfaces 属性中指定的更多的接口,那么这一切都很好,但是这些附加接口将不会由返回的代理实现.

如果 ProxyFactoryBeanproxyInterfaces 属性具有没有被设置,而目标类确实实现一个或多个接口,则 ProxyFactoryBean 将自动检测选择,当目标类实际上至少实现一个接口. 将创建 JDK 代理. 实际上代理的接口将是目标类实现的所有接口. 事实上,这与简单地提供了目标类实现到 proxyInterfaces 属性的每个接口的列表相同. 但是,这明显减轻了负担,还避免配置错误.

6.4.4. 代理接口

首先看一下 ProxyFactoryBean 简单的例子,这个例子包含:

  • 将被代理的目标 bean,下面示例中的 personTarget bean定义

  • 一个 Advisor (通知者) 和一个 Interceptor (拦截器) ,用于提供通知.

  • 指定目标对象( personTarget bean)的 AOP 代理 bean 和要代理的接口,以及要应用的通知.

以下清单显示了该示例:

<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,它必须能够返回独立的代理实例. 如果任意的通知者本身是原型的, 那么就需要返回一个独立的实例,所以有必要从工厂获得原型实例. 只保存一个引用是不够的.

前面显示的 person bean定义可以用来代替 Person 实现,如下所示:

Java
Person person = (Person) factory.getBean("person");
Kotlin
val person = factory.getBean("person") as Person;

与普通 Java 对象一样,同一 IoC 上下文中的其他 bean 可以表达对它的强类型依赖. 以下示例显示了如何执行此操作:

<bean id="personUser" class="com.mycompany.PersonUser">
    <property name="person"><ref bean="person"/></property>
</bean>

此示例中的 PersonUser 类将暴露类型为 Person 的属性. 就它而言,可以透明地使用 AOP 代理来代替 “real” 的 person 实现. 但是,它的类将是动态代理类. 可以将其转换为 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>

这样做的好处是只有一个 Person 类型的对象,如果想阻止应用程序上下文的用户获得对 un-advised 对象的引用,或者需要避免使用 Spring IoC 自动装配的任何含糊不清的情况, 那么这个对象就很有用. ProxyFactoryBean 定义是自包含的,这也是一个好处. 但是,有时能够从工厂获得 un-advised 目标可能是一个优势 (例如,在某些测试场景中) .

6.4.5. 代理类

如果需要代理一个类而不是一个或多个接口,又该怎么办?

考虑上面的例子,没有 Person 接口,需要给一个没有实现任何业务接口的 Person 类提供通知. 在这种情况下,您可以将 Spring 配置为使用 CGLIB 代理而不是动态代理. 简单设置 ProxyFactoryBeanproxyTargetClass 属性为 true. 尽管最佳实践是面向接口编程,不是类. 但在处理遗留代码时, 通知不实现接口的类的能力可能会非常有用 (一般来说,Spring 不是规定性的. 虽然它可以很容易地应用好的实践,但它避免强制使用特定的方法) .

如果你愿意,即使有接口,也可以强制使用 CGLIB 代理.

CGLIB 代理的原理是在运行时生成目标类的子类. Spring 配置这个生成的子类用了委托的方法来调用原始的对象,在通知的编织中,子类被用于实现装饰者模式.

CGLIB 代理通常对于用户应当是透明的,然而还有需考虑一些问题:

  • Final 方法不能被 advised,因为它们不能被覆盖.

  • 无需添加 CGLIB 到项目的类路径中,从 Spring 3.2 开始,CGLIB 被重新打包并包含在 spring-core JAR中. 换句话说,基于 CGLIB 的 AOP "开箱即用",JDK 动态代理也是如此.

CGLIB 代理和动态代理之间几乎没有性能差异. 从 Spring 1.0 开始,动态代理略快一些. 但是,这可能会在未来发生变化. 在这种情况下,性能不应该是决定性的考虑因素.

6.4.6. 使用 “Global” (全局)的通知者

通过将星号追加到拦截器名称上,所有与星号前面部分匹配的 bean 名称的通知者都将添加到通知者链中. 如果需要添加一组标准的全局 ( "global") 通知者,这可能会派上用场. 以下示例定义了两个全局的通知者程序:

<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 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 定义标记为抽象的 (abstract) ,如前所述,这样它就不会被实例化. 应用程序上下文 (但不是简单的 bean 工厂) 将默认提前实例化所有的单例. 因此,重要的是 (至少对于单例 bean) ,如果有一个 (父级) bean 定义,只打算将它用作模板,而这个定义指定一个类,必须确保将抽象 (abstract) 属性设置为 true, 否则应用程序上下文将实际尝试提前实例化它.

6.6. 使用 ProxyFactory 编程创建AOP代理

使用 Spring 以编程的方式创建 AOP 代理是很容易的. 这样允许在不依赖于 Spring IoC 的情况下使用 Spring AOP.

目标对象实现的接口将自动代理. 下面的代码显示了使用一个拦截器和一个通知者创建目标对象的代理的过程:

Java
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
Kotlin
val factory = ProxyFactory(myBusinessInterfaceImpl)
factory.addAdvice(myMethodInterceptor)
factory.addAdvisor(myAdvisor)
val tb = factory.proxy as MyBusinessInterface

第一步是构建一个类型为 org.springframework.aop.framework.ProxyFactory 的对象. 可以使用目标对象创建此对象. 如前面的示例所示,或者在指定的接口中进行代理而不是构造器.

开发者可以添加通知 (使用拦截器作为一种专用的通知) 和/或通知者,并在 ProxyFactory 的生命周期中进行操作. 如果添加 IntroductionInterceptionAroundAdvisor,则可以使代理实现其他接口.

ProxyFactory 上还有一些便捷的方法 (从 AdvisedSupport 类继承的) ,允许开发者添加其他通知类型,例如前置和异常通知. AdvisedSupportProxyFactoryProxyFactoryBean 的超类

将 AOP 代理创建与 IoC 框架集成是多数应用程序的最佳实践,因此强烈建议从 Java 代码中外部配置使用 AOP

6.7. 处理被通知对象

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();
Kotlin
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 的切点封装. 因此,如果添加了 MethodInterceptor,则返回的通知者将是 DefaultPointcutAdvisor 返回来的 MethodInterceptor 和与所有类和方法匹配的切点.

addAdvisor() 方法可用于添加任意的 Advisor. 通常,持有切点和通知的通知者是通用的 DefaultPointcutAdvisor 类,它可以用于任意通知或切点 (但不能用于引入) .

默认情况下, 即使已经创建了代理,也可以添加或删除通知者或拦截器. 唯一的限制是无法添加或删除引入通知者,因为来自工厂的现有代理将不会展示接口的变化. (开发者可以从工厂获取新的代理,以避免这种问题) .

将 AOP 代理转换为通知接口并检查和操作其 Advisor 的简单示例 :

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);
Kotlin
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 标志. 在这种情况下,通知的 isFrozen() 方法将返回 true,任何通过添加或删除修改通知的尝试都将导致 AopConfigException 异常. 在某些情况下冻结通知的对象状态的功能很有用 (例如,防止调用代码删除安全拦截器) . 如果已知的运行时通知不需要修改的话,它也可以在 Spring 1.1 中使用以获得最好的优化.

6.8. 使用自动代理功能

到目前为止,上面的章节已经介绍了使用 ProxyFactoryBean 或类似的工厂 bean 显式地创建 AOP 代理.

Spring 还支持使用 “auto-proxy” (自动代理) 的 bean 定义, 允许自动代理选择 bean 定义.这是建立在 Spring 的 Bean 后置处理器基础上的,它允许修改任何 bean 定义作为容器加载.

在这个模式下,可以在 XML bean 定义文件中设置一些特殊的 bean 定义,用来配置基础的自动代理. 这允许开发者只需声明符合自动代理的目标即可,开发者无需使用 ProxyFactoryBean.

有两种方法可以做到这一点:

  • 使用在当前上下文中引用特定 bean 的自动代理创建器

  • 自动代理创建的一个特例值得单独考虑: 由源代码级别的元数据属性驱动的自动代理创建.

6.8.1. 自动代理 bean 的定义

本节介绍 org.springframework.aop.framework.autoproxy 包提供的自动代理创建器.

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator 类是一个 BeanPostProcessor 的实现,它会自动为具有匹配文本值或通配符的名称的 bean 创建 AOP 代理. 以下示例显示如何创建 BeanNameAutoProxyCreator 的 bean :

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="jdk*,onlyJdk"/>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>

ProxyFactoryBean 一样,它拥有 interceptorNames 属性而不是持有拦截器列表,以便为原型通知者提供正确的行为. 通知者和任意的通知类型都可命名为 “interceptors”.

与普通的自动代理一样,使用 BeanNameAutoProxyCreator 的主要目的是能将相同的配置同时或共享地应用于多个对象,此时配置是最少的. 将声明性事务应用于多个对象是很普遍的例子.

在上例中,名称匹配的 Bean 定义 (例如 jdkMyBeanonlyJdk) 是带有目标类的、普通的、老式的 bean 定义. AOP 代理由 BeanNameAutoProxyCreator 自动创建. 相同的通知也适用于所有匹配到的 bean. 注意,如果使用通知着 (而不是上述示例中的拦截器) ,那么切点可能随bean的不同用处而变化.

DefaultAdvisorAutoProxyCreator

DefaultAdvisorAutoProxyCreator 是另一个更通用、功能更强大的自动代理创建器. 它会在当前的上下文中自动用于符合条件的通知者,而无需在自动代理通知者的 bean 定义中包含特定的 bean 名称. 它具有 BeanNameAutoProxyCreator 相同的配置,以及避免重复定义的有点.

使用此机制涉及:

  • 指定 DefaultAdvisorAutoProxyCreator bean定义

  • 在相同或相关上下文中指定任意数量的通知者. 注意,这里必须是通知者,而不是拦截器或其他通知类型. 这种约束是必需的,因为必须引入对切点的评估, 以检查每个通知是否符合候选 bean 定义的要求.

DefaultAdvisorAutoProxyCreator 将自动评估包含在每个通知者中的切点,以查看它是否适用于每个业务对象 (如示例中的 businessObject1businessObject2 ) 的通知 (如果有的话) .

这意味着可以将任意数量的通知者自动用于每个业务对象. 如果任意通知者都没有一个切点与业务对象中的任何方法匹配,那么对象将不会被代理. 当为新的业务对象添加了 bean 定义时,如果需要这些对象都将被自动代理.

一般来说,自动代理具有使调用方或依赖无法获取 un-advised 对象的优点. 在这个 ApplicationContext 调用 getBean("businessObject1") 方法将返回 AOP 代理, 而不是目标业务对象. (前面显示的 "inner bean" 语义也提供了这种好处) .

以下示例创建一个 DefaultAdvisorAutoProxyCreator bean 以及本节中讨论的其他元素:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
    <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>

<bean id="businessObject1" class="com.mycompany.BusinessObject1">
    <!-- Properties omitted -->
</bean>

<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>

如果希望对多个业务对象适用相同的通知,那么 DefaultAdvisorAutoProxyCreator 类会显得非常有用. 一旦基础架构已定义,就可以简单地添加新的业务对象, 而不必再设置特定的代理配置. 还可以很容易地删除其他切面,例如跟踪或性能监视切面 , 这样对配置的更改最小.

DefaultAdvisorAutoProxyCreator 提供对过滤器 (filtering) 的支持 (使用命名约定,以便只评估某些通知者,允许在同一工厂中使用多个不同配置的 AdvisorAutoProxyCreators) 和排序. 通知者可以实现 org.springframework.core.Ordered 接口,以确保正确的排序,如果需要排序的话. 上面的例子中使用的 TransactionAttributeSourceAdvisor 类具有具有可配置的排序值, 默认的设置是无序的.

6.9. 使用 TargetSource 实现

Spring 提供了 TargetSource 概念,定义在 org.springframework.aop.TargetSource 接口中. 这个接口用于返回目标对象实现的连接点. 每次AOP代理处理方法调用时,都会要求目标实例进行 TargetSource 实现.

使用 Spring AOP 的开发者通常无需直接使用 TargetSource,一般都是提供了支持池,热部署和用于其他复杂目标的强大手段. 例如,池化的 TargetSource 可以为每个调用返回一个不同的目标实例,并使用一个池来管理实例.

如果未指定 TargetSource,则使用默认实现来包装本地对象. 每次调用都会返回相同的目标 (正如您所期望的那样) .

将下来介绍 Spring 提供的标准目标源 (target sources) ,以及如何使用.

当使用自定义的 target source,目标通常需要配置成原型而不是单例的 bean 定义. 这允许 Spring 按需时创建新的目标实例

6.9.1. Hot-swappable Target Sources

org.springframework.aop.target.HotSwappableTargetSource 的存在是为了允许切换 AOP 代理的目标.

改变目标源的目标会立即有效,HotSwappableTargetSource 是线程安全的.

可以通过 HotSwappableTargetSource 上的 swap() 方法更改目标,如下所示:

Java
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
Kotlin
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() 方法改变了 swappable bean 的目标. 持有对该 bean 引用的客户端将不会察觉到目标的更改,但会马上开始处理新目标.

虽然这个例子没有添加任何通知 , 也没有必要添加通知来使用 TargetSource,当然任意的 TargetSource 都可以和任意的通知一起使用.

6.9.2. 创建目标源池

使用池化的目标源为无状态会话 EJB 提供了类似的编程模型,它维护了相同实例池,调用方法将会释放池中的对象.

Spring 池和 SLSB 池有一个关键的区别是: Spring 池可以应用于任意 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 能够实现按需创建目标的新实例,用于扩展池. 请参阅 AbstractPoolingTargetSource 以及用于其属性信息的具体子类. maxSize 是最基本的,并且始终保证存在.

在这种情况下, myInterceptor 是需要在相同的 IoC 上下文中定义的拦截器的名称. 但是,无需指定拦截器来使用池. 如果只希望使用池化功能而不需要通知,那么可以不设置 interceptorNames 属性.

可以对 Spring 进行配置,以便将任意池对象强制转换到 org.springframework.aop.target.PoolingConfig 接口,从而引入暴露的,有关池的配置和当前大小的信息. 此时需要像下面这样定义通知者:

<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="poolTargetSource"/>
    <property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>

这个通知者是通过在 AbstractPoolingTargetSource 类上调用一个方便的方法获得的,因此可以调用 MethodInvokingFactoryBean. 通知者的名字 (在这里是 poolConfigAdvisor) 必须包含在拦截器名字的列表中,ProxyFactoryBean 暴露了池化的对象.

Java
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
Kotlin
val conf = beanFactory.getBean("businessObject") as PoolingConfig
println("Max pool size is " + conf.maxSize)
池化的无状态服务对象一般是没有必要的. 一般这种选择不是默认的,因为大多数无状态的对象本质上是线程安全的,并且如果资源是缓存的话,其实例池化是有问题的.

使用自动代理可以创建更简单的池,可以设置任何自动代理创建者使用的 TargetSource .

6.9.3. 原型目标源

设置 “prototype” 目标源与合并 TargetSource 类似. 在这种情况下,每个方法调用都会创建一个新的目标实例. 尽管在现代 JVM 中创建新对象的成本并不高, 但是连接新对象 (满足其 IoC 依赖性) 的成本可能会更高. 因此,如果没有很好的理由,不应该使用这种方法.

为此, 可以修改上面显示的 poolTargetSource 定义,如下所示 (为清晰起见,我们还更改了名称) :

<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
    <property name="targetBeanName" ref="businessObjectTarget"/>
</bean>

唯一的属性是目标 bean 的名称. 在 TargetSource 实现中使用继承来确保一致的命名. 与池化目标源一样,目标 bean 必须是原型 bean 定义.

6.9.4. ThreadLocal 的目标源

如果您需要为每个传入请求创建一个对象 (每个线程) ,ThreadLocal 目标源很有用. ThreadLocal 的概念提供了一个 JDK 范围的工具,用于透明地将资源与线程存储在一起. 设置 ThreadLocalTargetSource 几乎与其他类型的目标源设置一样. 如下例所示:

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
</bean>
当在多线程和多类加载器环境中错误地使用它们时,ThreadLocal 会带来严重的问题 (可能导致内存泄漏) . 您应该始终考虑将 threadlocal 包装在其他类中,并且永远不要直接使用 ThreadLocal 本身 (除了在包装类中) . 另外,应该始终记得正确设置和取消设置 (后者只需调用 ThreadLocal.set(null) 方法) 线程的本地资源. 在任何情况下都应该写取消设置,如果不取消将会出问题. Spring 的 ThreadLocal 支持此设置并且应当被考虑支持使用 ThreadLocal 而不是手动操作代码.

6.10. 定义新的通知类型

Spring AOP 被设计为可扩展的. 尽管拦截器实施机制目前只在内部使用,但除了围绕通知拥有开箱即用的拦截器之外,还可以支持任意的通知类型,例如前置、异常和后置返回的通知.

org.springframework.aop.framework.adapter 包是一个 SPI 包,允许在不改变核心框架的情况下添加新的自定义通知类型. 自定义通知类型的唯一约束是它必须实现 org.aopalliance.aop.Advice 标识接口.

org.springframework.aop.framework.adapter javadoc 获取更多信息.

7. Null-safety

尽管 Java 不允许使用类型系统来表示 null 安全,但 Spring 框架现在加入了 org.springframework.lang 包,并提供了以下注解,用来声明 API 和字段的 null 特性:

  • @Nullable: 其中特定参数、返回值或字段可以为 null.

  • @NonNull: 注解指示特定参数,返回值或字段不能为 null(对于 @NonNullApi@NonNullFields 适用的参数和返回值不需要)

  • @NonNullApi: 在包级别将非 null 声明为参数和返回值的默认行为

  • @NonNullFields: 在包级别将非 null 声明为字段的默认行为

Spring 框架用到这些注解,但它们也可以用于任意基于 Spring 的 Java 项目中,用来声明 null 安全的 API 和可选的 null 安全字段. null 特性对于泛型类型参数、varargs 参数和数组元素是不受支持的, 但可能会包含在即将发布的版本中,请参阅 SPR-15942 以获取最新信息. 在 Spring 框架发行版(包括小版本) 之间. 可以将 fine-tuned 声明为 null. 在方法体内判定类型是否为 null 超出了它的能力范围.

像 Reactor 或者 Spring Data 这样的库也用到了 null 安全的 API.

7.1. 用例

Spring API 除了为 null 提供了显式的声明外,IDE 还可以使用这些注解(如 IDEA 或 Eclipse) 为与 null 安全相关的 Java 开发人员提供有用的警告,用于避免运行时出现 NullPointerException.

在 Kotlin 项目中也使用到 Spring API 的 null 安全特性,因为 Kotlin 本身支持 null-safety. Kotlin 支持文档提供了更多的详细信息.

7.2. JSR-305 元注解

Spring 注解是被 JSR 305 注解的元注解(一个潜在的但广泛使用的JSR) . JSR 305 元注解允许工具供应商(如 IDEA 或 Kotlin) 以通用方式提供安全支持,而无需为 Spring 注解提供硬编码支持.

为了使用 Spring 的 null 安全 API,其实不需要也不建议在项目类路径中添加 JSR 305 依赖. 而只有在其代码库中使用null安全标识的项目(例如 Spring 基本库) 即可,应该将 com.google.code.findbugs:jsr305:3.0.2 添加到 Gradle 的 compileOnly 或 Maven 的 provided 配置中,就可以避免编译警告了.

8. 数据缓冲区和编解码器

Java NIO 虽然提供了 ByteBuffer,但许多库在顶层构建自己的字节缓冲区 API,尤其是对于重用缓冲区和/或使用直接缓冲区有利于性能的网络操作. 例如, Netty 具有 ByteBuf 层次结构, Undertow 使用 XNIO,Jetty 使用带有回调的池化字节缓冲区,等等. spring-core 模块提供了一组抽象来处理各种字节缓冲 API,如下所示:

8.1. DataBufferFactory

DataBufferFactory 以两种方式之一创建数据缓冲区:

  1. 分配新的数据缓冲区,可选择预先指定容量(如果已知) ,即使 DataBuffer 的实现可以按需增长和缩小,这也更有效.

  2. 包装现有的 byte[]java.nio.ByteBuffer,并使用 DataBuffer 实现来修饰给定的数据,且不涉及分配.

请注意,WebFlux 应用程序不直接创建 DataBufferFactory,而是通过 ServerHttpResponse 或客户端的 ClientHttpRequest 访问它. 工厂类型取决于底层客户端或服务器,例如 Reactor Netty 的 NettyDataBufferFactory ,其他的 DefaultDataBufferFactory.

8.2. DataBuffer

DataBuffer 接口提供与 java.nio.ByteBuffer 类似的操作,但也带来了一些额外的好处,其中一些受 Netty ByteBuf 的启发. 以下是部分好处清单:

  • 可以独立的读写,即不需要调用 flip() 来在读写之间交替.

  • java.lang.StringBuilder 一样,按需扩展容量.

  • 通过 PooledDataBuffer 缓冲区和引用计数.

  • java.nio.ByteBuffer, InputStreamOutputStream 的形式查看缓冲区.

  • 确定给定字节的索引或最后一个索引.

8.3. PooledDataBuffer

正如 Javadoc ByteBuffer 中所解释的,字节缓冲区可以是直接缓冲区,也可以是非直接缓冲区. 直接缓冲区可以驻留在 Java 堆之外,这样就无需复制本地 I/O 操作. 这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但是创建和释放它们也更加昂贵,这导致了池化缓冲区的想法.

PooledDataBufferDataBuffer 的扩展,它有助于引用计数,这对于字节缓冲池是必不可少的. 它是如何工作的? 当分配 PooledDataBuffer 时,引用计数为 1. 调用 retain() 递增计数,而对 release() 的调用则递减计数. 只要计数大于 0,就保证缓冲区不被释放. 当计数减少到 0 时,可以释放池化缓冲区,这实际上可能意味着缓冲区的保留内存返回到内存池.

请注意,不是直接对 PooledDataBuffer 进行操作,在大多数情况下,最好使用 DataBufferUtils 中的方法, 只有当它是 PooledDataBuffer 的实例时才应用 releaseretainDataBuffer.

8.4. DataBufferUtils

DataBufferUtils 提供了许多用于操作数据缓冲区的实用方法:

  • 将数据缓冲区流加入单个缓冲区中,可能只有零拷贝,例如 通过复合缓冲区,如果底层字节缓冲区 API 支持.

  • InputStream 或 NIO Channel 转化为 Flux<DataBuffer>, 反之亦然, 将 Publisher<DataBuffer> 转化为 OutputStream 或 NIO Channel.

  • 如果缓冲区是 PooledDataBuffer 的实例,则释放或保留 DataBuffer 的方法.

  • 从字节流中跳过或取出,直到特定的字节数.

8.5. Codecs

org.springframework.core.codec 包提供以下策略接口:

  • EncoderPublisher<T> 编码为数据缓冲区流.

  • DecoderPublisher<DataBuffer> 为更高级别的对象流.

spring-core 模块提供 byte[], ByteBuffer, DataBuffer, Resource, 和 String 编码器和解码器实现. spring-web 模块增加了 Jackson JSON, Jackson Smile, JAXB2, Protocol Buffers 和其他编码器和解码器. 请参阅 WebFlux 部分中的Codecs.

8.6. 使用 DataBuffer

使用数据缓冲区时,必须特别注意确保缓冲区被释放,因为它们可能被pooled. 我们将使用编解码器来说明它是如何工作的,但概念更普遍适用. 让我们看看内部编解码器必须在内部管理数据缓冲区.

Decoder 是在创建更高级别对象之前读取输入数据缓冲区的最后一个,因此必须按如下方式释放它们:

  1. 如果 Decoder 只是读取每个输入缓冲区并准备立即释放它,它可以通过 DataBufferUtils.release(dataBuffer) 来实现.

  2. 如果 Decoder 使用 FluxMono 运算符(如 flatMap,reduce 等) 在内部预取和缓存数据项,或者正在使用诸如 filter, skip 和其他省略项的运算符, 则必须将 doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release) 添加到组合链中,以确保在丢弃之前释放这些缓冲区,可能还会导致错误或取消信号.

  3. 如果 Decoder 以任何其他方式保持一个或多个数据缓冲区,则必须确保在完全读取时释放它们,或者在读取和释放高速缓存数据缓冲区之前发生错误或取消信号.

请注意, DataBufferUtils#join 提供了一种安全有效的方法,可将数据缓冲区流聚合到单个数据缓冲区中. 同样,skipUntilByteCounttakeUntilByteCount 是解码器使用的其他安全方法.

Encoder 分配其他人必须读取(和释放) 的数据缓冲区. 所以 Encoder 没什么可做的. 但是,如果在使用数据填充缓冲区时发生序列化错误,则 Encoder 必须注意释放数据缓冲区. 例如:

Java
DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
    // serialize and populate buffer..
    release = false;
}
finally {
    if (release) {
        DataBufferUtils.release(buffer);
    }
}
return buffer;
Kotlin
val buffer = factory.allocateBuffer()
var release = true
try {
    // serialize and populate buffer..
    release = false
} finally {
    if (release) {
        DataBufferUtils.release(buffer)
    }
}
return buffer

Encoder 的使用者负责释放它接收的数据缓冲区. 在WebFlux应用程序中,Encoder 的输出用于写入 HTTP 服务器响应或客户端 HTTP 请求, 在这种情况下,释放数据缓冲区是代码写入服务器响应或客户端的责任. 请求.

请注意,在 Netty 上运行时,可以使用调试选项来 排除缓冲区泄漏.

9. Logging

从 Spring Framework 5.0 开始, Spring 在 spring-jcl 模块中附带了自己的 Commons Logging 实现.该实现检查 Log4j 2.x 是否存在在类路径中的 API 和 SLF4J 1.7 API, 并使用其中的第一个作为日志记录的实现, 回溯到 Java 平台的核心日志记录工具 (也包括 如果 Log4j 2.x 和 SLF4J 都不可用, 则称为 JULjava.util.logging.

将 Log4j 2.x 或 Logback (或其他 SLF4J 提供程序) 放在您的类路径中, 无需任何额外操作, 并让框架自动适应您的选择. 有关更多信息, 请参见 Spring Boot Logging Reference Documentation.

Spring 的 Commons Logging 仅用于基础结构的日志记录,核心框架的扩展为目的.

对于应用程序代码中的日志记录需求, 建议直接使用 Log4j 2.x, SLF4J 或 JUL.

可以通过 org.apache.commons.logging.LogFactory 检索 Log 实现. 如下:

Java
public class MyBean {
    private final Log log = LogFactory.getLog(getClass());
    // ...
}
Kotlin
class MyBean {
  private val log = LogFactory.getLog(javaClass)
  // ...
}

10. 附录

10.1. XML Schemas

附录的这一部分列出了与核心容器相关的 XML Schemas.

10.1.1. util Schema

顾名思义,util 标签处理常见的实用程序配置问题,例如配置集合,引用常量等等. 要使用 util schema 中的标记,您需要在 Spring XML 配置文件的顶部有以下声明. 下面代码段中的文本引用了正确的 schema,以便您可以使用 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>

上述配置使用 Spring FactoryBean 实现( FieldRetrievingFactoryBean) ,将 bean 上的隔离属性的值设置为 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 用于检索静态或非静态字段值. 它通常用于检索 public static final 常量,然后可用于为另一个 bean 设置属性值或构造函数的参数.

以下示例显示了如何使用 staticField 属性暴露 static 字段:

<bean id="myField"
        class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
    <property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>

还有一个便捷使用形式,其中 static 字段被指定为 bean 名称,如以下示例所示:

<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
        class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>

这确实意味着 bean id 不再有任何选择(因此引用它的任何其他 bean 也必须使用这个更长的名称) , 但这种形式定义非常简洁,非常方便用作内部 bean ,因为不必为 bean 引用指定 id,如下例所示:

<bean id="..." class="...">
    <property name="isolation">
        <bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
                class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
    </property>
</bean>

您还可以访问另一个 bean 的非静态(实例) 字段,如 FieldRetrievingFactoryBean类的 API 文档中所述

在 Spring 中,将枚举值作为属性或构造函数参数注入到 bean 中非常容易,因为您实际上并不需要做任何事情,也不知道 Spring 内部的任何内容(甚至包括 FieldRetrievingFactoryBean 相似的类) . 以下示例枚举显示了注入枚举值的容易程度:

Java
public enum PersistenceContextType {

    TRANSACTION,
    EXTENDED
}
Kotlin
enum class PersistenceContextType {

    TRANSACTION,
    EXTENDED
}

现在考虑下面的 PersistenceContextType 类型的 setter 和相应的 bean 定义:

Java
public class Client {

    private PersistenceContextType persistenceContextType;

    public void setPersistenceContextType(PersistenceContextType type) {
        this.persistenceContextType = type;
    }
}
Kotlin
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"/>

上述配置使用 Spring FactoryBean 实现(PropertyPathFactoryBean) 创建名为 testBean.age 的 bean(类型为 int) ,其值等于 testBean bean 的 age 属性.

现在考虑以下示例,它添加了一个 <util:property-path/> 元素:

<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>

<property-path/> 元素的 path 属性的值遵循 beanName.beanProperty 的形式. 在这种情况下,它会获取名为 testBean 的bean的 age 属性. 该 age 属性值是 10.

使用 <util:property-path/> 设置Bean属性或构造函数参数

PropertyPathFactoryBean 是一个用于计算给定目标对象的属性路径的 FactoryBean . 目标对象可以直接指定,也可以通过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>

在以下示例中,path 被内部 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>

上述配置使用 Spring FactoryBean 实现(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>pechorin@hero.org</value>
            <value>raskolnikov@slums.org</value>
            <value>stavrogin@gov.org</value>
            <value>porfiry@gov.org</value>
        </list>
    </property>
</bean>

上述配置使用 Spring FactoryBean 实现(ListFactoryBean) 创建 java.util.List 实例,并使用从提供的 sourceList 获取的值对其进行初始化.

以下示例使用 <util:list/> 元素进行更简洁的表示:

<!-- creates a java.util.List instance with the supplied values -->
<util:list id="emails">
    <value>pechorin@hero.org</value>
    <value>raskolnikov@slums.org</value>
    <value>stavrogin@gov.org</value>
    <value>porfiry@gov.org</value>
</util:list>

您还可以使用 <util:list/> 元素上的 list-class 属性显式控制实例化和填充的 List 的确切类型. 例如,如果我们确实需要实例化 java.util.LinkedList,我们可以使用以下配置:

<util:list id="emails" list-class="java.util.LinkedList">
    <value>jackshaftoe@vagabond.org</value>
    <value>eliza@thinkingmanscrumpet.org</value>
    <value>vanhoek@pirate.org</value>
    <value>d'Arcachon@nemesis.org</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="pechorin@hero.org"/>
            <entry key="raskolnikov" value="raskolnikov@slums.org"/>
            <entry key="stavrogin" value="stavrogin@gov.org"/>
            <entry key="porfiry" value="porfiry@gov.org"/>
        </map>
    </property>
</bean>

上述配置使用 Spring FactoryBean 实现(MapFactoryBean) 创建一个 java.util.Map 实例,该实例使用从提供的 'sourceMap' 获取的键值对进行初始化.

以下示例使用 <util:map/> 元素进行更简洁的表示:

<!-- creates a java.util.Map instance with the supplied key-value pairs -->
<util:map id="emails">
    <entry key="pechorin" value="pechorin@hero.org"/>
    <entry key="raskolnikov" value="raskolnikov@slums.org"/>
    <entry key="stavrogin" value="stavrogin@gov.org"/>
    <entry key="porfiry" value="porfiry@gov.org"/>
</util:map>

您还可以使用 <util:map/> 元素上的 'map-class' 属性显式控制实例化和填充的 Map 的确切类型. 例如,如果我们真的需要实例化 java.util.TreeMap ,我们可以使用以下配置:

<util:map id="emails" map-class="java.util.TreeMap">
    <entry key="pechorin" value="pechorin@hero.org"/>
    <entry key="raskolnikov" value="raskolnikov@slums.org"/>
    <entry key="stavrogin" value="stavrogin@gov.org"/>
    <entry key="porfiry" value="porfiry@gov.org"/>
</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>pechorin@hero.org</value>
            <value>raskolnikov@slums.org</value>
            <value>stavrogin@gov.org</value>
            <value>porfiry@gov.org</value>
        </set>
    </property>
</bean>

上述配置使用 Spring FactoryBean 实现( SetFactoryBean) 创建一个 java.util.Set 实例,该实例使用从提供的 sourceSet 获取的值进行初始化.

以下示例使用 <util:set/> 元素进行更简洁的表示:

<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
    <value>pechorin@hero.org</value>
    <value>raskolnikov@slums.org</value>
    <value>stavrogin@gov.org</value>
    <value>porfiry@gov.org</value>
</util:set>

您还可以使用 <util:set/> 元素上的 set-class 属性显式控制实例化和填充的 Set 的确切类型. 例如,如果我们确实需要实例化 java.util.TreeSet ,我们可以使用以下配置:

<util:set id="emails" set-class="java.util.TreeSet">
    <value>pechorin@hero.org</value>
    <value>raskolnikov@slums.org</value>
    <value>stavrogin@gov.org</value>
    <value>porfiry@gov.org</value>
</util:set>

如果未提供 set-class 属性,则容器将选择 Set 实现.

10.1.2. aop Schema

aop 标签用于配置 Spring 中的所有 AOP,包括 Spring 自己的基于代理的 AOP 框架和 Spring 与 AspectJ AOP 框架的集成. 这些标签在为面向切面的编程一章中全面介绍.

为了完整性起见,要使用 aop schema 中的标签,您需要在 Spring XML 配置文件的顶部有以下 xsd: 以下代码段中的文本引用了正确的 schema,以便您可以使用 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 Schema

context 标签处理与管道(plumbing) )有关的 ApplicationContext 配置- 也就是说,通常不是对最终用户很重要的 bean,而是在 Spring 中执行大量 "grunt" 工作的 bean. 例如 BeanfactoryPostProcessors. 以下代码段引用了正确的 schema,以便您可以使用 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 ,您可以自己明确定义一个.

使用 <annotation-config/>

此元素激活 Spring 基础结构以检测bean类中的注解:

  • Spring 的 @Configuration 模式

  • @Autowired/@Inject@Value@Lookup

  • JSR-250 的 @Resource, @PostConstruct and @PreDestroy (if available)

  • JAX-WS 的 @WebServiceRef 和 EJB 3 的 @EJB (if available)

  • JPA’s @PersistenceContext@PersistenceUnit (if available)

  • Spring 的 @EventListener

或者,您可以选择显式激活这些注解的各个 BeanPostProcessors.

这个元素没有激活处理Spring的@Transactional注解. 使用<tx:annotation-driven/> 来激活Spring的@Transactional注解. enabled 来激活caching 注解
使用 <component-scan/>

此元素在 基于注解的容器配置 中进行了详细说明.

使用 <load-time-weaver/>

此元素在AspectJ 的加载时织入进行了详细说明.

使用 <spring-configured/>

此元素在 使用 Spring 中的 AspectJ 独立注入域对象中进行了详细说明.

使用 <mbean-export/>

此元素在 配置基于注解的 MBean 的导出 中进行了详细说明.

10.1.4. Beans Schema

最后,但并非最不重要的是,beans schema标签. 这些都是相同的标签,已经在 Spring 框架中崭露头角. 此处不显示 bean 架构中各种标签的示例, 因为它们在依赖性和配置的细节(甚至在chapter整个章节) 中有相当全面的介绍.

请注意,您可以向 <bean/> XML 定义添加零个或多个键值对. 如果有的话,使用这些额外的元数据完成的工作完全取决于您自己的自定义逻辑 (因此,如果您按照 XML Schema Authoring 的附录中所述编写自己的自定义元素自定义元素,通常只能使用它.

以下示例显示了周围 <bean/> 上下文中的 <meta/> 元素(请注意,没有任何逻辑可以解释它,元数据实际上是无用的) .

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 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 Schema 创建

从版本 2.0 开始,Spring 就为定义和配置 bean 的基本 Spring XML格式的可扩展性提供了一种机制. 本节介绍如何编写自己的自定义 XML bean 定义解析器并将这些解析器集成到 Spring IoC 容器中.

为了便于使用架构感知的XML编辑器编写配置文件,Spring 的可扩展XML配置机制基于 xml Schema. 如果您对 Spring 当前的 XML 配置扩展不熟悉,则应首先阅读标题为 XML Schemas 的附录.

要创建新的XML配置扩展:

  1. Author编写 xml 的 schema 来描述您的自定义元素.

  2. Code编写自定义 NamespaceHandler 实现.

  3. Code 编写一个或多个 BeanDefinitionParser 实现(这是完成实际工作的地方) .

  4. Register 使用 Spring 注册

下面是对每个步骤的描述.对于本例,我们将创建一个 XML 扩展(一个自定义 XML 元素) ,它允许我们以一种简单的方式配置 SimpleDateFormat 类型的对象(在 java.text 包中) . 当我们完成后,我们将能够定义类型 SimpleDateFormat 定义如下:

<myns:dateformat id="dateFormat"
    pattern="yyyy-MM-dd HH:mm"
    lenient="true"/>

(不要担心这个例子过于简单,后面还有很多的案例.第一个简单的案例的目的是完成基本步骤的调用)

10.2.1. 编写 Schema

创建一个用于 Spring 的 IoC 容器的 XML 配置扩展,首先要创建一个 XML Schema 来描述扩展. 对于我们的示例,我们使用以下 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 命名空间.

前面的 schema 允许我们使用 <myns:dateformat/> 元素直接在XML应用程序上下文文件中配置 SimpleDateFormat 对象,如以下示例所示:

<myns:dateformat id="dateFormat"
    pattern="yyyy-MM-dd HH:mm"
    lenient="true"/>

请注意,在我们创建基础结构类之后,前面的XML代码段与以下XML代码段基本相同:

<bean id="dateFormat" class="java.text.SimpleDateFormat">
    <constructor-arg value="yyyy-HH-dd HH:mm"/>
    <property name="lenient" value="true"/>
</bean>

前两个片段中的第二个在容器中创建一个 bean(由名称为 SimpleDateFormat 类型的 dateFormat 标识) ,并设置了几个属性.

基于 schema 创建的配置格式可以与带有 schema 感知的 XML 编辑器的 IDE 集成. 使用正确的创建模式,可以让用户再几个配置选择之间进行自由切换(其实说的就是 eclipse 编辑 XML 的多种视图) .

10.2.2. 编写 NamespaceHandler

除了 schema 之外,我们需要一个 NamespaceHandler 来解析 Spring 在解析配置文件时遇到的这个特定命名空间的所有元素.对于此示例, NamespaceHandler 应该负责解析 myns:dateformat 元素.

NamespaceHandler 接口有三个方法:

  • init(): 允许初始化 NamespaceHandler ,在使用处理程序之前此方法将被 Spring 调用.

  • BeanDefinition parse(Element, ParserContext): 当 Spring 遇到 top-level 元素(不嵌套在 bean 定义或其他命名空间中)时调用. 此方法可以注册 bean 定义本身和/或返回bean定义.

  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext): 当 Spring 遇到不同命名空间的属性或嵌套元素时调用. 一个或多个 bean 定义的装饰将被使用, (例如) 与Spring支持的作用域一起使用.我们将首先写一个简单的例子,不使用装饰器,之后我们在一个更高级的例子中展示装饰.

尽管完全可以为整个命名空间编写自己的 NamespaceHandler (从而提供分析命名空间中每个元素的代码) . 但通常情况下,Spring XML 配置文件中的每个顶级 XML 元素都会生成一个 bean 定义(在我们的例子中, 单个 <myns:dateformat/> 元素导致单个 SimpleDateFormat 定义) . Spring 具有许多支持此方案的便捷类.在本例中,我们将使用 NamespaceHandlerSupport 类:

Java
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class MyNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
    }
}
Kotlin
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

如果 NamespaceHandler 遇到了已映射到特定bean定义分析器(在本例中为 dateformat )的类型的 XML 元素,则将使用 BeanDefinitionParser.换言之, BeanDefinitionParser 负责分析在架构中定义的一个不同的顶级 XML 元素. 在解析器中,我们将可以访问 XML 元素(以及它的子组件) 以便我们能够解析我们的自定义 XML 内容.如下面的示例所示:

Java
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 提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的许多基本工作.
2 我们提供 AbstractSingleBeanDefinitionParser 超类,其类型是我们的单个 BeanDefinition 所代表的类型.
Kotlin
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 提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的许多基本工作.
2 我们提供 AbstractSingleBeanDefinitionParser 超类,其类型是我们的单个 BeanDefinition 所代表的类型.

在这个简单的例子中,这就是我们需要做的一切.我们的单个 BeanDefinition 的创建由 AbstractSingleBeanDefinitionParser 超类处理,bean 定义的唯一标识符的提取和设置也是如此.

10.2.4. 注册处理器和 schema

编码完成.剩下要做的就是让 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 中的字符需要使用反斜杠进行转义.)

键值对的第一部分(key)是与自定义命名空间扩展关联的URI,需要与自定义 XSD schema 中指定的 targetNamespace 属性的值完全匹配

编写 'META-INF/spring.schemas'

称为 spring.schemas 的属性文件包含 xml schema 位置(与 xml 文件中的 schema 声明一起使用,将 schema 用作 xsi:schemaLocation 属性的一部分) 到类路径资源的映射. 这个文件需要阻止 Spring 使用绝对的默认的 EntityResolver 及要求网络访问来接收 schema 文件.如果在此属性文件中指定映射,Spring 将在类路径中搜索 schema(在本例中为 org.springframework.samples.xml 包中的 myns.xsd) . 以下代码段显示了我们需要为自定义schema添加的行:

http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd

(请记住: 必须转义 : 字符.)

建议您在类路径上的 NamespaceHandlerBeanDefinitionParser 类旁边部署XSD文件(或多个文件) .

10.2.5. 在 Spring XML 配置中使用自定义扩展

使用您自己已经实现的自定义扩展,与使用 Spring 提供的 "自定义" 扩展是没有区别的.在下面的示例中,可以使用 Spring XML 配置文件,以前的步骤开发自定义的 <dateformat/> 元素:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:myns="http://www.mycompany.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 属性的 setter 方法. 这使得使用 setter 注入为 components 类配置bean定义变得困难(或者说是不可能的) . 以下清单显示了 Component 类:

Java
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;
    }
}
Kotlin
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 属性的 setter 属性.以下清单显示了这样的自定义 FactoryBean:

Java
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;
    }
}
Kotlin
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 schema 来定义自定义标记的结构,如下面的清单所示:

<?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
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
    }
}
Kotlin
import org.springframework.beans.factory.xml.NamespaceHandlerSupport

class ComponentNamespaceHandler : NamespaceHandlerSupport() {

    override fun init() {
        registerBeanDefinitionParser("component", ComponentBeanDefinitionParser())
    }
}

接下来是自定义 BeanDefinitionParser.请记住,我们正在创建描述 ComponentFactoryBeanBeanDefinition.以下清单显示了我们的自定义 BeanDefinitionParser:

Java
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);
    }
}
Kotlin
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 基础结构中注册各种部件,如下所示:

# in 'META-INF/spring.handlers'
http\://www.foo.example/schema/component=com.foo.ComponentNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.example/schema/component/component.xsd=com/foo/component.xsd
自定义 “Normal” 元素的属性

编写自己的自定义分析器和关联的部件并不难,但有时它不是正确的做法. 请考虑您需要将元数据添加到已经存在的 bean 定义的情况. 在这种情况下,你当然不想要去写你自己的整个自定义扩展, 相反,您只想向现有的 bean 定义元素添加一个附加属性.

另一个例子,假设您要为服务对象定义一个 bean (它不知道它) 正在访问群集 JCache,并且您希望确保命名的 JCache 实例在周围的群集.以下清单显示了这样一个定义:

<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
        jcache:cache-name="checking.account">
    <!-- other dependencies here... -->
</bean>

然后,我们可以在解析 'jcache:cache-name' 属性时创建另一个 BeanDefinition.这个 BeanDefinition 将为我们初始化命名的 JCache. 我们还可以修改 'checkingAccountService' 的现有 BeanDefinition,以便它依赖于这个新的 JCache 初始化 BeanDefinition.以下清单显示了我们的 JCacheInitializer:

Java
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...
    }
}
Kotlin
class JCacheInitializer(private val name: String) {

    fun initialize() {
        // lots of JCache API calls to initialize the named cache...
    }
}

现在我们可以转到自定义扩展.首先,我们需要编写描述自定义属性的 XSD schema,如下所示:

<?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
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class JCacheNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        super.registerBeanDefinitionDecoratorForAttribute("cache-name",
            new JCacheInitializingBeanDefinitionDecorator());
    }

}
Kotlin
import org.springframework.beans.factory.xml.NamespaceHandlerSupport

class JCacheNamespaceHandler : NamespaceHandlerSupport() {

    override fun init() {
        super.registerBeanDefinitionDecoratorForAttribute("cache-name",
                JCacheInitializingBeanDefinitionDecorator())
    }

}

接下来,我们需要创建解析器.请注意,在这种情况下,因为我们要解析 XML 属性,所以我们编写 BeanDefinitionDecorator 而不是 BeanDefinitionParser.以下清单显示了我们的 BeanDefinitionDecorator:

Java
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;
    }
}
Kotlin
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.handlersMETA-INF/spring.schemas 文件来注册 Spring XML 基础结构中的各种 artifacts ,如下所示:

# in 'META-INF/spring.handlers'
http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd

10.3. Application Startup Steps

附录的此部分列出了核心容器所使用的现有 StartupSteps .

有关每个启动步骤的名称和详细信息不是公共合同的一部分, 并且 随时可能更改; 这被视为核心容器的实现细节, 并且将遵循它的行为会改变.
Table 14. Application startup steps defined in the core container
Name Description Tags

spring.beans.instantiate

Instantiation of a bean and its dependencies.

beanName the name of the bean, beanType the type required at the injection point.

spring.beans.smart-initialize

Initialization of SmartInitializingSingleton beans.

beanName the name of the bean.

spring.context.annotated-bean-reader.create

Creation of the AnnotatedBeanDefinitionReader.

spring.context.base-packages.scan

Scanning of base packages.

packages array of base packages for scanning.

spring.context.beans.post-process

Beans post-processing phase.

spring.context.bean-factory.post-process

Invocation of the BeanFactoryPostProcessor beans.

postProcessor the current post-processor.

spring.context.beandef-registry.post-process

Invocation of the BeanDefinitionRegistryPostProcessor beans.

postProcessor the current post-processor.

spring.context.component-classes.register

Registration of component classes through AnnotationConfigApplicationContext#register.

classes array of given classes for registration.

spring.context.config-classes.enhance

Enhancement of configuration classes with CGLIB proxies.

classCount count of enhanced classes.

spring.context.config-classes.parse

Configuration classes parsing phase with the ConfigurationClassPostProcessor.

classCount count of processed classes.

spring.context.refresh

Application context refresh phase.