核心
1. IoC 容器
本章介绍 Spring 的控制反转(IoC)容器。
1.1. Spring IoC 容器与 Bean 简介
本章介绍 Spring 框架对控制反转(IoC)原则的实现。IoC 也被称为依赖注入(DI)。它是一种过程,通过该过程,对象仅通过构造函数参数、工厂方法参数,或在对象实例被构造或从工厂方法返回之后所设置的属性来定义其依赖关系(即它们所协作的其他对象)。容器在创建 bean 时会注入这些依赖项。这一过程从根本上说是反过来的(因此得名“控制反转”),即由 bean 自身通过直接构造类或使用服务定位器(Service Locator)模式等机制来控制其依赖项的实例化或定位。
org.springframework.beans 和 org.springframework.context 包是 Spring Framework IoC 容器的基础。BeanFactory 接口提供了一种高级配置机制,能够管理任何类型的对象。ApplicationContext 是 BeanFactory 的子接口。它增加了:
-
更轻松地与 Spring 的 AOP 功能集成
-
消息资源处理(用于国际化)
-
事件发布
-
用于Web应用程序的、特定于应用层的上下文,例如
WebApplicationContext。
简而言之,BeanFactory 提供了配置框架和基本功能,而 ApplicationContext 则增加了更多企业级特定功能。ApplicationContext 是 BeanFactory 的完整超集,本章在描述 Spring 的 IoC 容器时仅使用它。有关使用 BeanFactory 而非 ApplicationContext, 的更多信息,请参阅 《BeanFactory》。
在 Spring 中,构成应用程序主干并由 Spring IoC 容器管理的对象称为 bean。bean 是由 Spring IoC 容器实例化、装配和管理的对象。除此之外,bean 也只是应用程序中的众多对象之一。bean 及其之间的依赖关系通过容器所使用的配置元数据进行体现。
1.2. 容器概述
org.springframework.context.ApplicationContext 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。该容器通过读取配置元数据来获取有关要实例化、配置和组装哪些对象的指令。配置元数据可以采用 XML、Java 注解或 Java 代码的形式。它允许你表达构成应用程序的对象以及这些对象之间复杂的相互依赖关系。
Spring 提供了 ApplicationContext 接口的多种实现。在独立应用程序中,通常会创建
ClassPathXmlApplicationContext
或 FileSystemXmlApplicationContext 的实例。
虽然 XML 一直是定义配置元数据的传统格式,但您可以通过提供少量 XML 配置来声明式地启用对这些额外元数据格式的支持,从而指示容器使用 Java 注解或代码作为元数据格式。
在大多数应用场景中,通常不需要显式编写用户代码来实例化一个或多个 Spring IoC 容器。例如,在 Web 应用场景中,应用程序的 web.xml 文件中通常只需大约八行(左右)样板化的 Web 描述符 XML 配置就足够了(参见Web 应用程序的便捷 ApplicationContext 实例化)。如果您使用Spring Tools for Eclipse(基于 Eclipse 的开发环境),只需几次鼠标点击或按键操作,即可轻松创建此类样板配置。
下图展示了 Spring 工作原理的高层视图。您的应用程序类与配置元数据相结合,这样在创建并初始化 ApplicationContext 之后,您就拥有了一个完全配置好且可运行的系统或应用程序。
1.2.1. 配置元数据
如上图所示,Spring IoC 容器使用一种配置元数据。这种配置元数据代表了作为应用程序开发人员,您如何告诉 Spring 容器去实例化、配置和组装应用程序中的对象。
配置元数据传统上以一种简单直观的 XML 格式提供, 本章大部分内容都使用这种格式来阐述 Spring IoC 容器的核心概念和特性。
| 基于 XML 的元数据并不是唯一允许的配置元数据形式。 Spring IoC 容器本身与实际编写这种配置元数据所采用的格式完全解耦。如今,许多开发者为其 Spring 应用选择基于 Java 的配置。 |
有关在 Spring 容器中使用其他形式元数据的信息,请参见:
-
基于注解的配置:Spring 2.5 引入了对基于注解的配置元数据的支持。
-
基于 Java 的配置:从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多功能已成为核心 Spring Framework 的一部分。因此,您可以使用 Java 而非 XML 文件在应用程序类外部定义 Bean。要使用这些新功能,请参阅
@Configuration、@Bean、@Import以及@DependsOn注解。
Spring 配置至少包含一个,通常包含多个由容器管理的 bean 定义。基于 XML 的配置元数据将这些 bean 配置为顶级 <bean/> 元素内部的 <beans/> 元素。Java 配置通常在带有 @Bean 注解的类中使用带有 @Configuration 注解的方法。
这些 bean 定义对应于构成您应用程序的实际对象。
通常,您会定义服务层对象、数据访问对象(DAO)、表示层对象(例如 Struts Action 实例)、
基础设施对象(例如 Hibernate SessionFactories、JMS Queues 等)。
通常不会在容器中配置细粒度的领域对象,因为创建和加载领域对象通常是 DAO 和业务逻辑的职责。
不过,您可以使用 Spring 与 AspectJ 的集成来配置那些在 IoC 容器控制范围之外创建的对象。
参见使用 AspectJ 通过 Spring 对领域对象进行依赖注入。
以下示例展示了基于 XML 的配置元数据的基本结构:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
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 等)加载配置元数据。
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
|
在了解了 Spring 的 IoC 容器之后,你可能希望进一步了解 Spring 的 |
以下示例展示了服务层对象的 (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 类以及两个数据访问对象(类型分别为 JpaAccountDao 和 JpaItemDao,基于 JPA 对象关系映射标准)组成。property name 元素引用的是 JavaBean 属性的名称,而 ref 元素引用的是另一个 bean 定义的名称。id 与 ref 元素之间的这种关联表达了协作对象之间的依赖关系。有关配置对象依赖项的详细信息,请参阅依赖项。
组合基于 XML 的配置元数据
将 Bean 定义分散在多个 XML 文件中可能是很有用的。通常,每个单独的 XML 配置文件代表架构中的一个逻辑层或模块。
您可以使用应用上下文的构造函数从所有这些 XML 片段中加载 bean 定义。该构造函数接受多个 Resource 位置,如上一节所示。或者,也可以使用一个或多个 <import/> 元素从其他一个或多个文件中加载 bean 定义。以下示例展示了如何实现这一点:
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>
在前面的示例中,外部 bean 定义从三个文件加载:services.xml、messageSource.xml 和 themeSource.xml。所有位置路径都是相对于执行导入的定义文件而言的,因此 services.xml 必须与执行导入的文件位于同一目录或类路径位置,而 messageSource.xml 和 themeSource.xml 必须位于导入文件所在位置下方的 resources 目录中。如您所见,开头的斜杠会被忽略。然而,鉴于这些路径是相对路径,最好完全不要使用斜杠。被导入文件的内容(包括顶层的 <beans/> 元素)必须是符合 Spring Schema 的有效 XML bean 定义。
|
可以(但不推荐)使用相对路径“../”引用父目录中的文件。这样做会创建对当前应用程序外部文件的依赖。尤其不建议在 你始终可以使用完全限定的资源位置,而不是相对路径:例如, |
该命名空间本身提供了导入指令功能。除了基本的 bean 定义之外,Spring 还在一系列 XML 命名空间中提供了更多配置特性——例如 context 和 util 命名空间。
Groovy Bean 定义 DSL
作为外部化配置元数据的另一个示例,Bean 定义也可以使用 Spring 的 Groovy Bean 定义 DSL(领域特定语言)来表达,这种 DSL 在 Grails 框架中广为人知。 通常,此类配置位于一个“.groovy”文件中,其结构如下例所示:
beans {
dataSource(BasicDataSource) {
driverClassName = "org.hsqldb.jdbcDriver"
url = "jdbc:hsqldb:mem:grailsDB"
username = "sa"
password = ""
settings = [mynew:"setting"]
}
sessionFactory(SessionFactory) {
dataSource = dataSource
}
myService(MyService) {
nestedBean = { AnotherBean bean ->
dataSource = dataSource
}
}
}
这种配置方式在很大程度上等同于 XML bean 定义,甚至支持 Spring 的 XML 配置命名空间。它还允许通过 importBeans 指令导入 XML bean 定义文件。
1.2.3. 使用容器
ApplicationContext 是一个高级工厂的接口,能够维护不同 bean 及其依赖关系的注册表。通过使用方法
T getBean(String name, Class<T> requiredType),你可以获取 bean 的实例。
ApplicationContext 允许你读取 bean 定义并访问它们,如下例所示:
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// use configured instance
List<String> userList = service.getUsernameList();
import org.springframework.beans.factory.getBean
// create and configure beans
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
// retrieve configured instance
val service = context.getBean<PetStoreService>("petStore")
// use configured instance
var userList = service.getUsernameList()
使用 Groovy 配置时,引导过程看起来非常相似。它使用了一个不同的上下文实现类,该类支持 Groovy(但也能够理解 XML bean 定义)。 以下示例展示了 Groovy 配置:
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");
val context = GenericGroovyApplicationContext("services.groovy", "daos.groovy")
最灵活的变体是 GenericApplicationContext 与读取器委托(reader delegates)结合使用——例如,与用于 XML 文件的 XmlBeanDefinitionReader 结合使用,如下例所示:
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();
val context = GenericApplicationContext()
XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml")
context.refresh()
你也可以像下面示例所示,对 Groovy 文件使用 GroovyBeanDefinitionReader:
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();
val context = GenericApplicationContext()
GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy")
context.refresh()
你可以在同一个ApplicationContext中混合使用这些读取器委托,从多种配置源读取 bean 定义。
然后,您可以使用 getBean 方法来获取 Bean 的实例。ApplicationContext 接口还提供了其他一些用于获取 Bean 的方法,但理想情况下,您的应用程序代码绝不应使用这些方法。实际上,您的应用程序代码完全不应调用 getBean() 方法,从而完全不依赖于 Spring 的 API。例如,Spring 与 Web 框架的集成支持对各种 Web 框架组件(如控制器和 JSF 管理的 Bean)进行依赖注入,允许您通过元数据(例如自动装配注解)声明对特定 Bean 的依赖。
1.3. Bean 概述
Spring IoC 容器管理一个或多个 bean。这些 bean 是通过您提供给容器的配置元数据创建的(例如,以 XML <bean/> 定义的形式)。
在容器内部,这些 Bean 定义以 BeanDefinition 对象的形式表示,其中包含(除其他信息外)以下元数据:
-
一个带包限定的类名:通常是所定义 Bean 的实际实现类。
-
Bean 行为配置元素,用于声明 Bean 在容器中应如何行为(作用域、生命周期回调等)。
-
对其他 Bean 的引用,这些 Bean 是当前 Bean 完成其工作所必需的。这些引用也被称为协作者(collaborators)或依赖项(dependencies)。
-
在新创建的对象中需要设置的其他配置项——例如,连接池的大小限制,或用于管理连接池的 bean 所使用的连接数量。
此元数据转换为一组属性,这些属性构成了每个 bean 的定义。 下表描述了这些属性:
| 属性 | 详见…… |
|---|---|
类 |
|
姓名 |
|
作用域 |
|
构造函数参数 |
|
属性 |
|
自动装配模式 |
|
延迟初始化模式 |
|
初始化方法 |
|
销毁方法 |
除了包含如何创建特定 bean 的信息的 bean 定义之外,ApplicationContext 的实现还允许注册在容器外部(由用户)创建的现有对象。这是通过 getBeanFactory() 方法访问 ApplicationContext 的 BeanFactory 来实现的,该方法返回的是 BeanFactory 的 DefaultListableBeanFactory 实现。DefaultListableBeanFactory 通过 registerSingleton(..) 和 registerBeanDefinition(..) 方法支持此类注册。然而,典型的应用程序仅使用通过常规 bean 定义元数据定义的 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 类型。请注意,尽管 XML 解析器不再强制约束,但容器仍然会强制执行 Bean id 的唯一性。
您并非必须为 bean 提供 name 或 id。如果您未显式指定 name 或 id,容器会为该 bean 生成一个唯一的名称。然而,如果您希望通过 ref 元素或服务定位器(Service Locator)风格的查找方式按名称引用该 bean,则必须提供一个名称。
不提供名称的原因通常与使用内部 bean和自动装配协作者(autowiring collaborators)有关。
在类路径中启用组件扫描时,Spring 会为未命名的组件生成 Bean 名称,遵循前面描述的规则:本质上是取简单类名,并将其首字母转换为小写。然而,在(不常见)的特殊情况下,如果类名包含多个字符,且前两个字符均为大写,则原始大小写格式将被保留。这些规则与 java.beans.Introspector.decapitalize(Spring 在此处使用该方法)所定义的规则相同。 |
在 Bean 定义之外为 Bean 设置别名
在 bean 定义本身中,你可以通过组合使用 id 属性指定的一个名称(最多一个)以及 name 属性中任意数量的其他名称,为该 bean 提供多个名称。这些名称可以作为同一 bean 的等效别名,在某些场景下非常有用,例如允许应用程序中的每个组件使用特定于自身组件的 bean 名称来引用一个公共依赖。
然而,仅在实际定义 bean 的地方指定所有别名并不总是足够的。
有时,我们希望为在其他位置定义的 bean 引入一个别名。
这在大型系统中很常见,此类系统的配置被拆分到各个子系统中,每个子系统都拥有自己的一组对象定义。
在基于 XML 的配置元数据中,你可以使用 <alias/> 元素来实现这一点。以下示例展示了如何进行此操作:
<alias name="fromName" alias="toName"/>
在这种情况下,一个名为 fromName 的 bean(位于同一容器中)在使用此别名定义后,也可以被称为 toName。
例如,子系统 A 的配置元数据可能通过名称 subsystemA-dataSource 引用一个 DataSource。子系统 B 的配置元数据可能通过名称 subsystemB-dataSource 引用一个 DataSource。当构建同时使用这两个子系统的主应用程序时,主应用程序通过名称 myApp-dataSource 引用该 DataSource。为了让这三个名称都指向同一个对象,你可以在配置元数据中添加以下别名定义:
<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>
现在,每个组件和主应用程序都可以通过一个唯一且保证不会与其他任何定义冲突的名称(实际上创建了一个命名空间)来引用 dataSource,而它们所引用的是同一个 bean。
1.3.2. 实例化 Bean
Bean 定义本质上是用于创建一个或多个对象的配方。 当容器被要求提供某个命名的 Bean 时,它会查看该 Bean 的配方,并使用该 Bean 定义所封装的配置元数据来创建(或获取)一个实际的对象。
如果你使用基于 XML 的配置元数据,则需在 class 元素的 <bean/> 属性中指定要实例化的对象类型(或类)。该 class 属性(在内部,它是 Class 实例上的一个 BeanDefinition 属性)通常是必需的。(关于例外情况,请参见使用实例工厂方法进行实例化和Bean 定义继承。)
你可以通过以下两种方式之一来使用 Class 属性:
-
通常,用于指定在容器通过反射调用构造函数直接创建 bean 时所要构造的 bean 类,这在某种程度上等同于使用
new运算符的 Java 代码。 -
在较为少见的情况下,当容器通过调用某个类的
static工厂方法来创建 bean 时,需指定包含该static工厂方法的实际类。通过调用static工厂方法所返回的对象类型可以是同一个类,也可以是完全不同的另一个类。
使用构造函数进行实例化
当你通过构造函数方式创建一个 bean 时,所有普通的类都可以被 Spring 使用并与其兼容。也就是说,所开发的类不需要实现任何特定的接口,也不需要以某种特定的方式进行编码。只需指定该 bean 的类就足够了。然而,根据你对该特定 bean 所使用的 IoC 类型,你可能需要一个默认(无参)构造函数。
Spring 的 IoC 容器几乎可以管理你希望它管理的任何类。它并不局限于管理标准的 JavaBean。大多数 Spring 用户更倾向于使用真正的 JavaBean,即仅包含一个默认(无参)构造函数,并具有与容器中属性相对应的适当 setter 和 getter 方法。你也可以在容器中使用更特殊的、非 Bean 风格的类。例如,如果你需要使用一个完全不符合 JavaBean 规范的遗留连接池,Spring 同样可以对其进行管理。
使用基于 XML 的配置元数据,您可以按如下方式指定您的 bean 类:
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
有关向构造函数提供参数(如果需要)的机制,以及在对象构造完成后设置对象实例属性的详细信息,请参阅 依赖注入。
使用静态工厂方法实例化
在定义通过静态工厂方法创建的 bean 时,请使用 class 属性指定包含该 static 工厂方法的类,并使用名为 factory-method 的属性指定工厂方法本身的名称。你应该能够调用此方法(可选择传入参数,如后文所述),并返回一个活动的对象,该对象随后将被当作通过构造函数创建的一样进行处理。
此类 bean 定义的一种用途是调用遗留代码中的 static 工厂方法。
以下 bean 定义指定通过调用工厂方法来创建该 bean。该定义并未指定返回对象的类型(class),而仅指定了包含工厂方法的类。在本例中,createInstance() 方法必须是一个静态方法。以下示例展示了如何指定工厂方法:
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
以下示例展示了一个可与上述 bean 定义配合使用的类:
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
class ClientService private constructor() {
companion object {
private val clientService = ClientService()
fun createInstance() = clientService
}
}
有关向工厂方法提供(可选)参数的机制,以及在对象从工厂返回后设置对象实例属性的详细信息,请参阅依赖项与配置详解。
使用实例工厂方法进行实例化
与通过静态工厂方法进行实例化类似,使用实例工厂方法进行实例化会调用容器中一个现有 bean 的非静态方法来创建新 bean。要使用此机制,请将 class 属性留空,并在 factory-bean 属性中指定当前(或父级、祖先)容器中包含用于创建对象的实例方法的那个 bean 的名称。再通过 factory-method 属性设置工厂方法本身的名称。以下示例展示了如何配置这样的 bean:
<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>
<!-- the bean to be created via the factory bean -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
以下示例展示了对应的类:
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}
class DefaultServiceLocator {
companion object {
private val clientService = ClientServiceImpl()
}
fun createClientServiceInstance(): ClientService {
return clientService
}
}
一个工厂类也可以包含多个工厂方法,如下例所示:
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
<bean id="accountService"
factory-bean="serviceLocator"
factory-method="createAccountServiceInstance"/>
以下示例展示了对应的类:
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
private static AccountService accountService = new AccountServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
public AccountService createAccountServiceInstance() {
return accountService;
}
}
class DefaultServiceLocator {
companion object {
private val clientService = ClientServiceImpl()
private val accountService = AccountServiceImpl()
}
fun createClientServiceInstance(): ClientService {
return clientService
}
fun createAccountServiceInstance(): AccountService {
return accountService
}
}
这种方法表明,工厂 Bean 本身可以通过依赖注入(DI)进行管理和配置。参见依赖与配置详解。
确定 Bean 的运行时类型
特定 bean 的运行时类型并不容易确定。在 bean 元数据定义中指定的类仅是一个初始的类引用,它可能与一个声明的工厂方法结合使用,或者本身是一个 FactoryBean 类,这些情况都可能导致 bean 的实际运行时类型有所不同;或者在使用实例级工厂方法的情况下(此时通过指定的 factory-bean 名称进行解析),该类甚至可能根本没有设置。此外,AOP 代理可能会使用基于接口的代理包装 bean 实例,从而仅暴露目标 bean 实际类型所实现的接口(而非其具体类型)。
要确定特定 bean 的实际运行时类型,推荐的方法是针对指定的 bean 名称调用 BeanFactory.getType。该方法会考虑上述所有情况,并返回与对相同 bean 名称调用 BeanFactory.getBean 所得到的对象类型。
1.4. 依赖项
一个典型的企业级应用程序并非由单个对象(在 Spring 术语中称为 bean)组成。即使是最简单的应用程序,也包含多个协同工作的对象,为最终用户呈现出一个结构一致的应用程序。接下来的部分将说明如何从定义若干独立的 bean 定义,逐步构建出一个完整的应用程序,其中各个对象相互协作以实现特定目标。
1.4.1. 依赖注入
依赖注入(DI)是一种对象仅通过构造函数参数、工厂方法参数,或在对象实例被构造完成或从工厂方法返回后所设置的属性来定义其依赖关系(即它们所协作的其他对象)的过程。容器在创建 bean 时会注入这些依赖项。这一过程从根本上说是对 bean 自身通过直接构造类或使用服务定位器(Service Locator)模式来控制其依赖项的实例化或定位方式的反转(因此得名“控制反转”)。
通过依赖注入(DI)原则,代码变得更加简洁;当对象被提供其依赖项时,解耦效果也更为显著。对象无需主动查找其依赖项,也不需要知道依赖项的位置或具体类。因此,您的类变得更易于测试,尤其是在依赖项基于接口或抽象基类的情况下,这使得在单元测试中可以使用桩(stub)或模拟(mock)实现。
DI(依赖注入)主要有两种形式:基于构造函数的依赖注入和基于 Setter 的依赖注入。
基于构造器的依赖注入
基于构造函数的依赖注入(DI)是通过容器调用一个带有若干参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的 static 工厂方法来构造 bean 几乎是等效的,本文对构造函数参数和 static 工厂方法参数的讨论方式是类似的。以下示例展示了一个只能通过构造函数注入进行依赖注入的类:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private final MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
// a constructor so that the Spring container can inject a MovieFinder
class SimpleMovieLister(private val movieFinder: MovieFinder) {
// business logic that actually uses the injected MovieFinder is omitted...
}
请注意,这个类没有任何特殊之处。它是一个普通的 Java 对象(POJO),不依赖于容器特定的接口、基类或注解。
构造函数参数解析
构造函数参数的解析匹配是通过参数的类型进行的。如果一个 bean 定义中的构造函数参数不存在潜在的歧义,那么在实例化该 bean 时,将按照 bean 定义中声明构造函数参数的顺序,将这些参数依次传递给相应的构造函数。请考虑以下类:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
package x.y
class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree)
假设 ThingTwo 和 ThingThree 类之间不存在继承关系,则不会产生潜在的歧义。因此,以下配置可以正常工作,您无需在 <constructor-arg/> 元素中显式指定构造函数参数的索引或类型。
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
</beans>
当引用另一个 bean 时,类型是已知的,因此可以进行匹配(如前面的示例所示)。当使用简单类型时,例如
<value>true</value>,Spring 无法确定该值的类型,因此在没有额外帮助的情况下无法按类型进行匹配。请考虑以下类:
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private final int years;
// The Answer to Life, the Universe, and Everything
private final String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
package examples
class ExampleBean(
private val years: Int, // Number of years to calculate the Ultimate Answer
private val ultimateAnswer: String // The Answer to Life, the Universe, and Everything
)
在上述场景中,如果你通过使用 type 属性显式指定构造函数参数的类型,容器就可以对简单类型进行类型匹配,如下例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
您可以使用 index 属性显式指定构造函数参数的索引,如下例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
除了消除多个简单值的歧义之外,指定索引还可以解决构造函数具有两个相同类型参数时的歧义问题。
| 索引从0开始。 |
您也可以使用构造函数参数名称来进行值的消歧,如下例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
请注意,为了使此功能开箱即用,您的代码必须在启用调试标志(debug flag)的情况下进行编译,这样 Spring 才能从构造函数中查找参数名称。 如果您无法或不想使用调试标志编译代码,则可以使用 @ConstructorProperties JDK 注解来显式指定构造函数参数的名称。此时,示例类将需要如下所示:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
package examples
class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)
基于 Setter 的依赖注入
基于 setter 的依赖注入(DI)是通过容器在调用无参构造函数或无参 static 工厂方法实例化你的 bean 之后,再调用该 bean 的 setter 方法来完成的。
以下示例展示了一个只能通过纯 setter 注入方式进行依赖注入的类。该类是标准的 Java 类,是一个不依赖于容器特定接口、基类或注解的 POJO(普通 Java 对象)。
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
class SimpleMovieLister {
// a late-initialized property so that the Spring container can inject a MovieFinder
lateinit var movieFinder: MovieFinder
// business logic that actually uses the injected MovieFinder is omitted...
}
ApplicationContext 支持对其管理的 Bean 进行基于构造函数和基于设置器的依赖注入(DI)。它还支持在已通过构造函数方式注入部分依赖后,再进行基于设置器的依赖注入。您可以将依赖配置为 BeanDefinition 的形式,并结合 PropertyEditor 实例使用,以将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不会直接(即以编程方式)使用这些类,而是使用 XML bean 定义、带注解的组件(即使用 @Component、@Controller 等注解的类),或基于 Java 的 @Configuration 类中的 @Bean 方法。这些来源随后会在内部被转换为 BeanDefinition 实例,并用于加载整个 Spring IoC 容器实例。
依赖解析过程
容器按如下方式执行 bean 的依赖解析:
-
ApplicationContext是通过配置元数据创建并初始化的,这些配置元数据描述了所有的 bean。配置元数据可以通过 XML、Java 代码或注解来指定。 -
对于每个 bean,其依赖项以属性、构造函数参数或静态工厂方法的参数(如果你使用静态工厂方法而非普通构造函数)的形式进行表达。当实际创建该 bean 时,这些依赖项会被提供给该 bean。
-
每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。
-
每个属性或构造函数参数的值都会从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将字符串格式提供的值转换为所有内置类型,例如
int、long、String、boolean等。
Spring 容器在创建容器时会验证每个 bean 的配置。 然而,bean 的属性本身并不会被设置,直到该 bean 实际被创建时才进行赋值。 单例作用域(singleton-scoped)且设置为预实例化(pre-instantiated,默认行为)的 bean 会在容器创建时就被实例化。作用域的定义请参见 Bean 作用域。否则, bean 仅在其被请求时才会被创建。创建一个 bean 可能会引发一系列 bean 的创建, 因为该 bean 的依赖项、依赖项的依赖项(依此类推)都会被创建并注入。 请注意,这些依赖项之间的解析不匹配问题可能会较晚才暴露出来—— 也就是说,直到受影响的 bean 首次被创建时才会显现。
你通常可以信赖 Spring 做出正确的选择。它会在容器加载时检测配置问题,
例如引用不存在的 Bean 和循环依赖。Spring 在实际创建 Bean 时才会尽可能晚地设置属性并解析依赖关系。这意味着,一个已正确加载的 Spring 容器在您请求某个对象时,如果在创建该对象或其某个依赖项时出现问题(例如,由于缺少属性或属性无效,导致该 bean 抛出异常),则可能随后抛出异常。这就是为什么 ApplicationContext 的实现默认会预先实例化单例 bean,以避免某些配置问题的可见性可能被延迟。在创建这些bean之前投入一些初始时间来
分配内存,并且只有在需要它们时才实际使用它们,
你可以在创建ApplicationContext bean 时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,使单例 Bean 延迟初始化,而不是被急切实例化。
如果没有循环依赖存在,当一个或多个协作 Bean 被注入到某个依赖 Bean 中时,每个协作 Bean 都会在被注入之前完成全部配置。这意味着,如果 Bean A 依赖于 Bean B,Spring IoC 容器会在调用 Bean A 的 setter 方法之前,先完全配置好 Bean B。换句话说,该 Bean 会被实例化(如果不是预先实例化的单例),其依赖项会被设置,并且相关的生命周期方法(例如配置的初始化方法或InitializingBean 回调方法)会被调用。
依赖注入示例
以下示例使用基于 XML 的配置元数据进行基于 setter 的依赖注入(DI)。Spring XML 配置文件的一小部分内容如下所示,用于指定一些 bean 定义:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
以下示例展示了对应的 ExampleBean 类:
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
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 类:
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public ExampleBean(
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
this.beanOne = anotherBean;
this.beanTwo = yetAnotherBean;
this.i = i;
}
}
class ExampleBean(
private val beanOne: AnotherBean,
private val beanTwo: YetAnotherBean,
private val i: Int)
在 bean 定义中指定的构造函数参数将用作 ExampleBean 构造函数的参数。
现在考虑这个示例的一个变体:Spring 不再使用构造函数,而是被指示调用一个 static 工厂方法来返回该对象的实例:
<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
<constructor-arg ref="anotherExampleBean"/>
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
以下示例展示了对应的 ExampleBean 类:
public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}
// a static factory method; the arguments to this method can be
// considered the dependencies of the bean that is returned,
// regardless of how those arguments are actually used.
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
class ExampleBean private constructor() {
companion object {
// a static factory method; the arguments to this method can be
// considered the dependencies of the bean that is returned,
// regardless of how those arguments are actually used.
fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean {
val eb = ExampleBean (...)
// some other operations...
return eb
}
}
}
对static工厂方法的参数通过<constructor-arg/>元素提供,
其方式与实际使用构造函数时完全相同。工厂方法所返回的类的类型
不必与包含该static工厂方法的类属于同一类型(尽管在此示例中它们是相同的)。
实例(非静态)工厂方法可以以基本相同的方式使用(区别仅在于使用factory-bean属性
而非class属性),因此我们在此不再赘述这些细节。
1.4.2. 依赖与配置详解
如前一节所述,您可以将 bean 的属性和构造函数参数定义为对其他托管 bean(协作者)的引用,或定义为内联值。Spring 基于 XML 的配置元数据为此目的在其 <property/> 和 <constructor-arg/> 元素内支持子元素类型。
直接值(基本类型、字符串等)
value 元素的 <property/> 属性以人类可读的字符串形式指定一个属性或构造函数参数。Spring 的转换服务用于将这些值从 String 类型转换为属性或参数的实际类型。以下示例展示了各种被设置的值:
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="misterkaoli"/>
</bean>
以下示例使用 p-命名空间 来实现更加简洁的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="root"
p:password="misterkaoli"/>
</beans>
上述 XML 更加简洁。然而,除非你使用支持在创建 bean 定义时自动完成属性的 IDE(例如 IntelliJ IDEA 或 Eclipse 的 Spring Tools),否则拼写错误只能在运行时而非设计时被发现。强烈推荐使用此类 IDE 辅助功能。
你也可以按如下方式配置一个 java.util.Properties 实例:
<bean id="mappings"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<!-- typed as a java.util.Properties -->
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
</value>
</property>
</bean>
Spring 容器通过使用 JavaBeans 的 <value/> 机制,将 java.util.Properties 元素内的文本转换为一个 PropertyEditor 实例。这是一种便捷的简写方式,也是 Spring 团队少数倾向于使用嵌套的 <value/> 元素而非 value 属性风格的场景之一。
这idref元素
idref 元素只是一种防错的方式,用于将容器中另一个 bean 的 id(字符串值,而非引用)传递给 <constructor-arg/> 或 <property/> 元素。以下示例展示了如何使用它:
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
前面的 bean 定义片段在运行时与以下片段完全等效:
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>
第一种形式优于第二种形式,因为使用 idref 标签可以让容器在部署时验证所引用的、具有指定名称的 bean 是否确实存在。而在第二种形式中,传递给 targetName bean 的 client 属性的值不会经过任何验证。拼写错误只有在实际实例化 client bean 时才会被发现(很可能导致严重后果)。如果 client bean 是一个原型(prototype) bean,那么此类拼写错误及其引发的异常可能在容器部署很久之后才会被发现。
在 4.0 版本的 beans XSD 中,local 元素上的 idref 属性不再受支持,因为它相较于普通的 bean 引用已不再提供额外价值。在升级到 4.0 schema 时,请将现有的 idref local 引用改为 idref bean。 |
一个常见的使用场景(至少在 Spring 2.0 之前的版本中)是,在 <idref/> 的 bean 定义中配置 AOP 拦截器 时,ProxyFactoryBean 元素能带来实际价值。当你指定拦截器名称时使用 <idref/> 元素,可以防止拼错拦截器的 ID。
对其他 Bean 的引用(协作者)
ref 元素是 <constructor-arg/> 或 <property/>
定义元素内部的最后一个元素。在这里,您将某个 bean 的指定属性值设置为对容器所管理的另一个 bean(即协作者)的引用。
被引用的 bean 是当前 bean(其属性将被设置)的一个依赖项,并会在设置该属性之前按需进行初始化。(如果该协作者是一个单例 bean,则可能已经被容器初始化过了。)
所有引用最终都是对另一个对象的引用。其作用域和验证取决于您是通过 bean 属性还是 parent 属性来指定目标对象的 ID 或名称。
通过 bean 标签的 <ref/> 属性指定目标 bean 是最通用的形式,它允许创建对同一容器或父容器中任意 bean 的引用,而不管该 bean 是否位于同一个 XML 文件中。bean 属性的值可以与目标 bean 的 id 属性相同,也可以与目标 bean 的 name 属性中的某个值相同。以下示例展示了如何使用 ref 元素:
<ref bean="someBean"/>
通过 parent 属性指定目标 bean 会创建一个对当前容器的父容器中某个 bean 的引用。parent 属性的值可以与目标 bean 的 id 属性相同,也可以是目标 bean 的 name 属性中的任意一个值。目标 bean 必须位于当前容器的父容器中。当您拥有一组容器层级结构,并希望使用一个与父容器中现有 bean 同名的代理来包装该 bean 时,应主要使用这种 bean 引用方式。以下两段代码示例展示了如何使用 parent 属性:
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
</property>
<!-- insert other configuration and dependencies as required here -->
</bean>
在 4.0 版本的 beans XSD 中,local 元素上的 ref 属性不再受支持,因为它相较于普通的 bean 引用已不再提供额外价值。在升级到 4.0 schema 时,请将现有的 ref local 引用改为 ref bean。 |
内部 Bean
<bean/> 或 <property/> 元素内部的 <constructor-arg/> 元素定义了一个内部 bean,如下例所示:
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
内部 bean 定义不需要指定 ID 或名称。即使指定了,容器也不会将该值用作标识符。容器在创建时也会忽略 scope 标志,因为内部 bean 始终是匿名的,并且总是随外部 bean 一起创建。无法独立访问内部 bean,也无法将其注入到除包含它的外部 bean 之外的其他协作 bean 中。
作为一种边界情况,可以从自定义作用域中接收销毁回调——例如,对于一个包含在单例 bean 中的请求作用域内部 bean。该内部 bean 实例的创建与其所包含的 bean 相关联,但销毁回调使其能够参与到请求作用域的生命周期中。这种情况并不常见。通常,内部 bean 只是简单地共享其包含 bean 的作用域。
集合
<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">[email protected]</prop>
<prop key="support">[email protected]</prop>
<prop key="development">[email protected]</prop>
</props>
</property>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>
映射(map)的键或值,或者集合(set)的值,也可以是以下任意元素:
bean | ref | idref | list | set | map | props | value | null
集合合并
Spring 容器还支持集合的合并。应用程序开发者可以定义一个父级的 <list/>、<map/>、<set/> 或 <props/> 元素,并让子级的 <list/>、<map/>、<set/> 或 <props/> 元素继承并覆盖父集合中的值。也就是说,子集合的值是父集合与子集合元素合并后的结果,其中子集合的元素会覆盖父集合中指定的值。
本节关于合并的内容讨论了父-子 bean 机制。不熟悉父 bean 和子 bean 定义的读者在继续阅读之前,可能希望先阅读相关章节。
以下示例演示了集合合并:
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">[email protected]</prop>
<prop key="support">[email protected]</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- the merge is specified on the child collection definition -->
<props merge="true">
<prop key="sales">[email protected]</prop>
<prop key="support">[email protected]</prop>
</props>
</property>
</bean>
<beans>
请注意,在 child Bean 定义的 adminEmails 属性的 <props/> 元素上使用了 merge=true 属性。当容器解析并实例化 child Bean 时,生成的实例将包含一个 adminEmails Properties 集合,该集合是子级的 adminEmails 集合与父级的 adminEmails 集合合并后的结果。以下列表展示了最终结果:
子 Properties 集合的值集会继承父 <props/> 中的所有属性元素,并且子集合中 support 键对应的值会覆盖父集合中的对应值。
这种合并行为同样适用于 <list/>、<map/> 和 <set/> 集合类型。对于 <list/> 元素的特定情况,与 List 集合类型相关的语义(即ordered值集合的概念)得以保留。父级的值位于所有子级列表的值之前。对于 Map、Set 和 Properties 集合类型,不存在顺序。因此,对于容器内部使用的关联 Map、Set 和 Properties 实现类型所基于的集合类型,不生效任何顺序语义。
集合合并的局限性
您不能合并不同的集合类型(例如 Map 和 List)。如果您尝试这样做,系统会抛出相应的 Exception。必须在较低层级的、继承的子定义上指定 merge 属性。在父集合定义上指定 merge 属性是多余的,不会产生预期的合并效果。
强类型集合
随着 Java 5 中泛型类型的引入,您可以使用强类型集合。
也就是说,您可以声明一个 Collection 类型,使其只能包含
(例如)String 元素。如果您使用 Spring 将一个强类型的 Collection 依赖注入到某个 bean 中,
就可以利用 Spring 的类型转换支持,使得在将元素添加到 Collection 之前,
这些强类型 Collection 实例中的元素会被转换为适当的类型。
以下 Java 类和 bean 定义展示了如何实现这一点:
public class SomeClass {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
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>
当 accounts bean 的 something 属性准备进行注入时,通过反射可以获取强类型 Map<String, Float> 中元素类型的泛型信息。因此,Spring 的类型转换基础设施能够识别各个值元素的类型为 Float,并将字符串值(9.99、2.75 和 3.99)转换为实际的 Float 类型。
空值和空字符串值
Spring 将属性等的空参数视为空的 Strings。以下基于 XML 的配置元数据片段将 email 属性设置为空 String 值("")。
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
前面的示例等同于以下 Java 代码:
exampleBean.setEmail("");
exampleBean.email = ""
<null/> 元素用于处理 null 值。以下清单展示了一个示例:
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
上述配置等效于以下 Java 代码:
exampleBean.setEmail(null);
exampleBean.email = null
使用 p-命名空间的 XML 快捷方式
p 命名空间允许你使用 bean 元素的属性(而不是嵌套的 <property/> 元素)来描述你的属性值、协作的 bean,或者两者兼有。
Spring 支持可扩展的配置格式(通过命名空间),
这些格式基于 XML Schema 定义。beans 配置格式(本章所讨论的)
是在一个 XML Schema 文档中定义的。然而,p-命名空间并未在 XSD 文件中定义,
仅存在于 Spring 的核心中。
以下示例展示了两个 XML 片段(第一个使用标准 XML 格式,第二个使用 p-命名空间),它们解析后的结果相同:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="[email protected]"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="[email protected]"/>
</beans>
该示例展示了在 bean 定义中使用 p-命名空间的一个名为 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" 作为
属性来实现完全相同的功能。在此情况下,spouse 是属性名,
而 -ref 部分表明这不是一个直接的值,而是对另一个 bean 的
引用。
p 命名空间不如标准 XML 格式灵活。例如,声明属性引用的格式会与以 Ref 结尾的属性发生冲突,而标准 XML 格式则不会出现此问题。我们建议您谨慎选择使用的方式,并告知您的团队成员,以避免在同一 XML 文档中同时使用这三种方式。 |
使用 c-命名空间的 XML 快捷方式
与使用 p-命名空间的 XML 快捷方式类似,Spring 3.1 引入的 c-命名空间允许使用内联属性来配置构造函数参数,而不是使用嵌套的 constructor-arg 元素。
以下示例使用 c: 命名空间来实现与基于构造函数的依赖注入相同的功能:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
<!-- traditional declaration with optional argument names -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="[email protected]"/>
</bean>
<!-- c-namespace declaration with argument names -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="[email protected]"/>
</beans>
c: 命名空间与 p: 命名空间采用相同的约定(对 bean 引用使用后缀 -ref),通过参数名称来设置构造函数参数。同样,即使它未在 XSD schema 中定义(它存在于 Spring 核心内部),也需要在 XML 文件中进行声明。
对于极少数构造函数参数名称不可用的情况(通常是因为字节码在编译时未包含调试信息),你可以退而使用参数索引,如下所示:
<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
c:_2="[email protected]"/>
由于 XML 语法的限制,索引表示法要求必须带有前导 _,
因为 XML 属性名不能以数字开头(尽管某些 IDE 允许这样做)。
类似的索引表示法也可用于 <constructor-arg> 元素,
但通常不常用,因为在该处声明的自然顺序通常已足够。 |
在实践中,构造函数解析 机制在匹配参数方面非常高效,因此除非确实需要,我们建议在整个配置中使用名称表示法。
复合属性名称
在设置 Bean 属性时,您可以使用复合属性名或嵌套属性名,只要路径中除最终属性名以外的所有组成部分都不为 null 即可。请考虑以下 Bean 定义:
<bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>
something Bean 拥有一个 fred 属性,该属性包含一个 bob 属性,而该属性又包含一个 sammy
属性,并且最终的 sammy 属性被设置为值 123。为了使此机制正常工作,在 Bean 构造完成后,
something 的 fred 属性以及 fred 的 bob 属性不得为 null。否则,将抛出 NullPointerException。
1.4.3. 使用depends-on
如果一个 bean 是另一个 bean 的依赖项,通常意味着将一个 bean 设置为另一个 bean 的属性。您通常可以在基于 XML 的配置元数据中使用 <ref/>
元素 来实现这一点。然而,有时 bean 之间的依赖关系并不那么直接。例如,当需要触发类中的静态初始化程序时(比如用于数据库驱动程序注册)。depends-on 属性可以显式强制一个或多个 bean 在使用该元素的 bean 初始化之前先进行初始化。以下示例使用 depends-on 属性来表达对单个 bean 的依赖:
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
要表达对多个 Bean 的依赖关系,请将一个包含 Bean 名称的列表作为 depends-on 属性的值(逗号、空格和分号均可作为有效分隔符):
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
depends-on 属性既可以指定初始化时的依赖关系,也可以(仅针对 单例 bean)指定相应的销毁时依赖关系。与某个 bean 定义了 depends-on 关系的依赖 bean 会在该 bean 自身被销毁之前先被销毁。因此,depends-on 也可以控制关闭顺序。 |
1.4.4. 延迟初始化的 Bean
默认情况下,ApplicationContext 的实现会在初始化过程中急切地创建并配置所有单例 Bean。通常,这种预先实例化是可取的,因为配置或环境中的错误能够立即被发现,而不是等到数小时甚至数天之后。当这种行为不可取时,可以通过将 Bean 定义标记为懒加载(lazy-initialized)来阻止单例 Bean 的预实例化。一个懒加载的 Bean 会告知 IoC 容器:仅在首次请求该 Bean 时才创建其实例,而不是在启动时就创建。
在 XML 中,此行为由 lazy-init 元素上的 <bean/> 属性控制,如下例所示:
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>
当上述配置被 ApplicationContext 加载时,lazy bean 在 ApplicationContext 启动时不会被急切实例化,而 not.lazy bean 则会被急切实例化。
然而,当一个延迟初始化的 bean 是一个非延迟初始化的单例 bean 的依赖项时,ApplicationContext 会在启动时创建该延迟初始化的 bean,因为它必须满足单例 bean 的依赖关系。该延迟初始化的 bean 会被注入到其他地方一个非延迟初始化的单例 bean 中。
您还可以通过在 default-lazy-init 元素上使用 <beans/> 属性,在容器级别控制延迟初始化,如下例所示:
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
1.4.5. 自动装配协作者
Spring 容器可以自动装配协作 bean 之间的依赖关系。你可以让 Spring 通过检查 ApplicationContext 的内容,自动为你的 bean 解析其协作者(其他 bean)。自动装配具有以下优势:
-
自动装配可以显著减少指定属性或构造函数参数的需求。(本章其他部分讨论的其他机制,例如 bean 模板,在这方面也很有价值。)
-
自动装配可以在对象演进时更新配置。例如,如果你需要向某个类添加一个依赖项,该依赖项可以自动得到满足,而无需你修改配置。因此,在开发阶段,自动装配尤其有用;同时,当代码库变得更加稳定时,你仍然可以选择切换到显式装配。
在使用基于 XML 的配置元数据时(参见依赖注入),您可以通过 autowire 元素的 <bean/> 属性为 bean 定义指定自动装配模式。自动装配功能共有四种模式。您可以为每个 bean 单独指定自动装配方式,从而选择对哪些 bean 进行自动装配。下表描述了这四种自动装配模式:
| 模式 | 说明 |
|---|---|
|
(默认)不进行自动装配。Bean 的引用必须通过 |
|
按属性名称进行自动装配。Spring 会查找与需要自动装配的属性同名的 bean。例如,如果一个 bean 定义被设置为按名称自动装配,并且它包含一个 |
|
如果容器中恰好存在一个与属性类型匹配的 bean,则允许该属性被自动装配。如果存在多个这样的 bean,将抛出一个致命异常,表明您不能对该 bean 使用 |
|
类似于 |
在使用 byType 或 constructor 自动装配模式时,您可以装配数组和类型化的集合。在这种情况下,容器中所有与期望类型匹配的自动装配候选 Bean 都会被提供以满足该依赖。如果期望的键类型为 Map,您还可以自动装配强类型的 String 实例。自动装配的 Map 实例的值包含所有与期望类型匹配的 Bean 实例,而该 Map 实例的键则包含对应的 Bean 名称。
自动装配的局限性和缺点
当在整个项目中一致地使用自动装配时,其效果最佳。如果通常不使用自动装配,而仅对一两个 Bean 定义使用它来进行装配,可能会让开发人员感到困惑。
考虑自动装配的局限性和缺点:
-
在
property和constructor-arg设置中的显式依赖始终会覆盖自动装配。您无法对简单属性(例如基本类型、Strings和Classes,以及这些简单属性的数组)进行自动装配。此限制是刻意设计的。 -
自动装配不如显式装配精确。尽管如前表所述,Spring 在存在可能导致意外结果的歧义时会谨慎避免猜测。您由 Spring 管理的对象之间的关系将不再被显式记录。
-
接线信息可能无法提供给那些从 Spring 容器生成文档的工具。
-
容器中可能存在多个 bean 定义与要自动装配的 setter 方法或构造函数参数所指定的类型相匹配。对于数组、集合或
Map实例而言,这不一定会造成问题。然而,对于期望单一值的依赖项,这种歧义不会被随意解决。如果没有唯一的 bean 定义可用,则会抛出异常。
在后一种情况下,你有几个选项:
从自动装配中排除 Bean
在单个 Bean 的基础上,您可以将某个 Bean 排除在自动装配之外。在 Spring 的 XML 格式中,将 <bean/> 元素的 autowire-candidate 属性设置为 false。容器会使该特定的 Bean 定义对自动装配基础设施不可用(包括注解风格的配置,例如 @Autowired)。
autowire-candidate 属性仅用于影响基于类型的自动装配。
它不会影响通过名称进行的显式引用,即使指定的 Bean 未标记为自动装配候选者,这些引用依然会被解析。
因此,如果名称匹配,按名称自动装配仍然会注入该 Bean。 |
您还可以基于与 Bean 名称的模式匹配来限制自动装配候选者。顶层的 <beans/> 元素在其 default-autowire-candidates 属性中接受一个或多个模式。例如,若要将自动装配候选者的范围限制为名称以 Repository 结尾的任意 Bean,请提供值 *Repository。若要指定多个模式,请以逗号分隔的列表形式定义它们。对于某个 Bean 定义的 true 属性显式设置为 false 或 autowire-candidate 的情况,该设置始终优先。对于这类 Bean,上述模式匹配规则将不再适用。
这些技术适用于那些你永远不希望被自动装配注入到其他 bean 中的 bean。这并不意味着被排除的 bean 本身不能通过自动装配进行配置。而是说,该 bean 本身不会作为自动装配其他 bean 的候选对象。
1.4.6. 方法注入
在大多数应用场景中,容器中的大部分 bean 都是 单例(singleton)的。当一个单例 bean 需要与另一个单例 bean 协作,或者一个非单例 bean 需要与另一个非单例 bean 协作时,通常的做法是将其中一个 bean 定义为另一个 bean 的属性来处理这种依赖关系。然而,当 bean 的生命周期不同时,就会出现问题。假设单例 bean A 需要在每次调用其方法时使用一个非单例(原型,prototype)bean B。容器只会创建一次单例 bean A,因此只有一次机会设置其属性。这样一来,容器无法在每次需要时都为 bean A 提供一个新的 bean B 实例。
一种解决方案是放弃部分控制反转。您可以通过实现 ApplicationContextAware 接口,使 Bean A 感知容器,并通过向容器发起 getBean("B") 调用,在 Bean A 每次需要时请求(通常是新的)Bean B 实例。以下示例展示了这种方法:
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;
// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple
// Spring-API imports
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
class CommandManager : ApplicationContextAware {
private lateinit var applicationContext: ApplicationContext
fun process(commandState: Map<*, *>): Any {
// grab a new instance of the appropriate Command
val command = createCommand()
// set the state on the (hopefully brand new) Command instance
command.state = commandState
return command.execute()
}
// notice the Spring API dependency!
protected fun createCommand() =
applicationContext.getBean("command", Command::class.java)
override fun setApplicationContext(applicationContext: ApplicationContext) {
this.applicationContext = applicationContext
}
}
上述做法并不理想,因为业务代码感知到了 Spring 框架并与之耦合。方法注入(Method Injection)是 Spring IoC 容器的一项较为高级的特性,可让你干净利落地处理此类用例。
查找方法注入
查找方法注入(Lookup method injection)是指容器能够重写容器所管理的 bean 上的方法,并返回容器中另一个已命名 bean 的查找结果。这种查找通常涉及原型(prototype)作用域的 bean,如前一节所述场景所示。Spring 框架通过使用 CGLIB 库进行字节码生成,动态创建一个子类来重写该方法,从而实现这种方法注入。
|
在前面代码片段中的 CommandManager 类中,
Spring 容器会动态地重写 createCommand()
方法的实现。正如重构后的示例所示,CommandManager 类没有任何 Spring 依赖:
package fiona.apple;
// no more Spring imports!
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
package fiona.apple
// no more Spring imports!
abstract class CommandManager {
fun process(commandState: Any): Any {
// grab a new instance of the appropriate Command interface
val command = createCommand()
// set the state on the (hopefully brand new) Command instance
command.state = commandState
return command.execute()
}
// okay... but where is the implementation of this method?
protected abstract fun createCommand(): Command
}
在包含要注入方法的客户端类中(本例中为 CommandManager),该要注入的方法需要具有如下形式的签名:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果该方法是abstract的,则动态生成的子类会实现该方法。
否则,动态生成的子类将重写原始类中定义的具体方法。请考虑以下示例:
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
标识为 commandManager 的 bean 在每次需要 createCommand() bean 的新实例时,都会调用其自身的 myCommand 方法。如果确实需要原型作用域,你必须小心地将 myCommand bean 部署为原型(prototype)。如果它是一个单例(singleton),则每次都会返回同一个 myCommand bean 实例。
或者,在基于注解的组件模型中,您可以通过 @Lookup 注解声明一个查找方法,如下例所示:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
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 根据查找方法声明的返回类型进行解析:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract Command createCommand();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
val command = createCommand()
command.state = commandState
return command.execute()
}
@Lookup
protected abstract fun createCommand(): Command
}
请注意,通常应为这类带注解的查找方法声明一个具体的存根(stub)实现,以确保它们与 Spring 的组件扫描规则兼容,因为默认情况下抽象类会被忽略。此限制不适用于显式注册或显式导入的 Bean 类。
|
访问不同作用域目标 Bean 的另一种方式是通过 您可能还会发现 |
任意方法替换
与查找方法注入相比,另一种用处较小的方法注入形式是:能够用另一个方法实现来替换托管 Bean 中的任意方法。在你真正需要此功能之前,可以安全地跳过本节的其余内容。
使用基于 XML 的配置元数据,您可以使用 replaced-method 元素为已部署的 Bean 将现有方法的实现替换为另一个实现。请考虑以下类,该类包含一个名为 computeValue 的方法,我们希望重写该方法:
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
class MyValueCalculator {
fun computeValue(input: String): String {
// some real code...
}
// some other methods...
}
一个实现 org.springframework.beans.factory.support.MethodReplacer 接口的类提供了新的方法定义,如下例所示:
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
class ReplacementComputeValue : MethodReplacer {
override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
// get the input value, work with it, and return a computed result
val input = args[0] as String;
...
return ...;
}
}
用于部署原始类并指定方法重写的 bean 定义将类似于以下示例:
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- arbitrary method replacement -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
你可以在 <arg-type/> 元素内使用一个或多个 <replaced-method/> 元素,以指明被重写方法的方法签名。只有当方法被重载且类中存在多个变体时,才需要指定参数的签名。为方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,以下所有写法都与 java.lang.String 匹配:
java.lang.String
String
Str
由于参数的数量通常足以区分每个可能的选项,因此这个快捷方式可以节省大量输入,让你只需键入匹配参数类型的最短字符串即可。
1.5. Bean 作用域
当你创建一个 bean 定义时,你实际上是在创建一个用于生成该 bean 定义所指定类的实际实例的“配方”。将 bean 定义视为一种配方这一概念非常重要,因为这意味着,就像使用类一样,你可以从单个配方中创建多个对象实例。
你不仅可以控制从特定 bean 定义创建的对象所注入的各种依赖项和配置值,还可以控制从该 bean 定义创建的对象的作用域。这种方法强大而灵活,因为你可以通过配置来选择所创建对象的作用域,而不必在 Java 类级别硬编码对象的作用域。Bean 可以被定义为部署在多种作用域之一。
Spring 框架支持六种作用域,其中四种仅在使用 Web 感知的 ApplicationContext 时才可用。你还可以创建自定义作用域。
下表描述了所支持的作用域:
| 作用域 | 描述 |
|---|---|
(默认)将单个 bean 定义的作用域限定为每个 Spring IoC 容器对应一个对象实例。 |
|
将单个 bean 定义的作用域限定为任意数量的对象实例。 |
|
将单个 bean 定义的作用域限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都会基于同一个 bean 定义创建各自独立的 bean 实例。此作用域仅在支持 Web 的 Spring |
|
将单个 bean 定义的作用域限定为 HTTP |
|
将单个 bean 定义的作用域限定为 |
|
将单个 bean 定义的作用域限定为 |
自 Spring 3.0 起,提供了线程作用域(thread scope),但默认情况下未注册。有关更多信息,请参阅
SimpleThreadScope 的文档。
关于如何注册此作用域或任何其他自定义作用域的说明,请参阅
使用自定义作用域。 |
1.5.1. 单例作用域
Spring 容器仅管理单例 bean 的一个共享实例,所有请求 ID 或 IDs 与该 bean 定义相匹配的 bean 时,都会返回这同一个特定的 bean 实例。
换句话说,当你定义一个 bean 定义并将其作用域设置为单例(singleton)时,Spring IoC 容器会根据该 bean 定义精确地创建一个对象实例。这个唯一的实例会被存储在单例 bean 的缓存中,此后所有对该命名 bean 的请求和引用都会返回该缓存中的对象。下图展示了单例作用域的工作方式:
Spring 中单例 bean 的概念与《四人帮》(Gang of Four, GoF)设计模式一书中所定义的单例模式有所不同。GoF 单例模式将对象的作用域硬编码,使得每个 ClassLoader 只能创建某个特定类的一个且仅一个实例。而 Spring 单例的作用域最好被描述为“每个容器、每个 bean”。这意味着,如果你在单个 Spring 容器中为某个特定类定义了一个 bean,那么 Spring 容器将根据该 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. 原型作用域
Bean 部署的非单例(prototype)作用域会导致每次请求该特定 Bean 时都创建一个新的 Bean 实例。也就是说,当该 Bean 被注入到另一个 Bean 中,或者你通过容器上的 getBean() 方法调用来请求它时,都会创建新实例。通常情况下,你应该对所有有状态的 Bean 使用 prototype 作用域,而对无状态的 Bean 使用 singleton 作用域。
下图展示了 Spring 的原型作用域:
(数据访问对象(DAO)通常不会被配置为原型(prototype),因为典型的 DAO 不保存任何会话状态。对我们来说,复用单例(singleton)图表的核心部分更为简便。)
以下示例在 XML 中将一个 bean 定义为原型(prototype):
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其它作用域不同,Spring 并不管理原型(prototype)Bean 的完整生命周期。容器会实例化、配置并以其他方式组装一个原型对象,然后将其交给客户端,而不再保留该原型实例的任何记录。因此,尽管初始化生命周期回调方法会对所有对象(无论其作用域如何)进行调用,但对于原型 Bean 而言,所配置的销毁生命周期回调方法则不会被调用。客户端代码必须自行清理原型作用域的对象,并释放这些原型 Bean 所持有的昂贵资源。若希望 Spring 容器释放原型作用域 Bean 所持有的资源,可以尝试使用自定义的Bean 后处理器(bean post-processor),该处理器持有需要清理的 Bean 的引用。
在某些方面,Spring 容器对于原型作用域(prototype-scoped)Bean 的作用相当于替代了 Java 的 new 操作符。在此之后的所有生命周期管理都必须由客户端自行处理。(有关 Spring 容器中 Bean 生命周期的详细信息,请参阅生命周期回调。)
1.5.3. 具有原型作用域依赖的单例 Bean
当你在单例作用域的 Bean 中使用依赖于原型作用域 Bean 的情况时,请注意:依赖关系是在实例化时解析的。因此,如果你将一个原型作用域的 Bean 依赖注入到一个单例作用域的 Bean 中,那么会先实例化一个新的原型 Bean,然后将其依赖注入到该单例 Bean 中。这个原型实例是唯一一个会被提供给该单例作用域 Bean 的实例。
然而,假设你希望单例作用域的 bean 在运行时反复获取原型作用域 bean 的新实例。你不能将原型作用域的 bean 通过依赖注入的方式注入到单例 bean 中,因为这种注入只会在 Spring 容器实例化单例 bean 并解析和注入其依赖项时发生一次。如果你在运行时需要多次获取原型 bean 的新实例,请参阅 方法注入(Method Injection)。
1.5.4. 请求、会话、应用程序和 WebSocket 作用域
request、session、application 和 websocket 作用域仅在使用支持 Web 的 Spring ApplicationContext 实现(例如 XmlWebApplicationContext)时才可用。如果在普通的 Spring IoC 容器(如 ClassPathXmlApplicationContext)中使用这些作用域,则会抛出一个 IllegalStateException,提示未知的 Bean 作用域。
初始 Web 配置
为了支持在 request、session、application 和
websocket 级别(即 Web 作用域 Bean)对 Bean 进行作用域管理,在定义 Bean 之前需要进行一些简单的初始配置。(标准作用域 singleton 和 prototype 则不需要此初始设置。)
您如何完成此初始设置取决于您特定的 Servlet 环境。
如果你在 Spring Web MVC 中访问作用域 Bean,实际上就是在由 Spring DispatcherServlet 处理的请求中访问,那么无需进行任何特殊设置。DispatcherServlet 已经暴露了所有相关状态。
如果你使用的是 Servlet 2.5 的 Web 容器,并且请求是在 Spring 的 DispatcherServlet 之外处理的(例如,使用 JSF 或 Struts 时),你需要注册 org.springframework.web.context.request.RequestContextListener 这个 ServletRequestListener。
对于 Servlet 3.0 及以上版本,可以通过使用 WebApplicationInitializer 接口以编程方式完成此操作。或者,对于较旧的容器,可以在你的 Web 应用程序的 web.xml 文件中添加以下声明:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
或者,如果你的监听器设置存在问题,可以考虑使用 Spring 的
RequestContextFilter。该过滤器的映射取决于所处的 Web 应用程序配置,因此你需要根据实际情况进行相应调整。以下代码清单展示了 Web 应用程序中的过滤器部分:
<web-app>
...
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
DispatcherServlet、RequestContextListener 和 RequestContextFilter 的作用完全相同,即将 HTTP 请求对象绑定到处理该请求的 Thread 上。这使得请求作用域(request-scoped)和会话作用域(session-scoped)的 Bean 在调用链的后续环节中可以被访问。
请求作用域
请考虑以下用于 bean 定义的 XML 配置:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring 容器会为每个 HTTP 请求,根据 LoginAction bean 定义创建一个新的 loginAction bean 实例。也就是说,loginAction bean 的作用域是 HTTP 请求级别的。你可以随意更改所创建实例的内部状态,因为从同一个 loginAction bean 定义创建的其他实例不会看到这些状态变化。这些状态变更仅针对单个请求。当请求处理完成后,该请求作用域的 bean 就会被丢弃。
在使用基于注解的组件或 Java 配置时,可以使用 @RequestScope 注解将组件指定为 request 作用域。以下示例展示了如何实现这一点:
@RequestScope
@Component
public class LoginAction {
// ...
}
@RequestScope
@Component
class LoginAction {
// ...
}
会话作用域
请考虑以下用于 bean 定义的 XML 配置:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring 容器使用 userPreferences Bean 定义,为单个 HTTP Session 的生命周期创建 UserPreferences Bean 的新实例。换句话说,userPreferences Bean 实际上是在 HTTP Session 级别进行作用域划分的。与请求作用域的 Bean 一样,您可以随意更改所创建实例的内部状态,而无需担心其他同样使用该 userPreferences Bean 定义创建的实例的 HTTP Session 会看到这些状态变化,因为这些变化特定于各个独立的 HTTP Session。当该 HTTP Session 最终被丢弃时,作用于该特定 HTTP Session 的 Bean 也会被丢弃。
使用基于注解的组件或 Java 配置时,可以使用 @SessionScope 注解将组件指定为 session 作用域。
@SessionScope
@Component
public class UserPreferences {
// ...
}
@SessionScope
@Component
class UserPreferences {
// ...
}
应用作用域
请考虑以下用于 bean 定义的 XML 配置:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring 容器在整个 Web 应用程序中仅使用 AppPreferences bean 定义创建一次 appPreferences bean 的新实例。也就是说,appPreferences bean 的作用域限定在 ServletContext 级别,并作为常规的 ServletContext 属性进行存储。这在某种程度上类似于 Spring 的单例(singleton)bean,但在两个重要方面有所不同:它是每个 ServletContext 对应一个单例,而不是每个 Spring ApplicationContext(在任意给定的 Web 应用程序中可能存在多个 ServletContext)对应一个单例;并且它实际上是对外暴露的,因此可作为 8 属性被直接访问和查看。
在使用基于注解的组件或 Java 配置时,您可以使用
@ApplicationScope 注解将组件分配给 application 作用域。以下示例展示了如何实现这一点:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
作为依赖的作用域 Bean
Spring IoC 容器不仅管理对象(Bean)的实例化,还负责协作对象(或依赖项)的装配。如果你想将一个 HTTP 请求作用域的 Bean 注入到另一个生命周期更长的 Bean 中,可以选择注入一个 AOP 代理来代替该作用域 Bean。也就是说,你需要注入一个代理对象,该代理对象公开与作用域对象相同的公共接口,但还能从相应的作用域(例如 HTTP 请求)中获取真实的目标对象,并将方法调用委托给该真实对象。
|
您也可以在作用域为 当对一个作用域为 此外,作用域代理(scoped proxies)并不是以生命周期安全的方式访问较短作用域中 Bean 的唯一方法。您还可以将注入点(即构造函数或 setter 参数,或自动装配的字段)声明为 作为一种扩展变体,您可以声明 此功能在 JSR-330 中的变体称为 |
以下示例中的配置仅有一行,但理解其背后的“为什么”和“如何做”同样重要:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/> (1)
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.something.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
| 1 | 定义代理的那行代码。 |
要创建此类代理,需在作用域 bean 的定义中插入一个子元素 <aop:scoped-proxy/>(参见选择要创建的代理类型和基于 XML Schema 的配置)。
为什么 request、session 以及自定义作用域的 bean 定义需要 <aop:scoped-proxy/> 元素呢?
请考虑以下单例(singleton)bean 的定义,并将其与前述作用域所需的定义进行对比(注意,以下所示的 userPreferences bean 定义目前是不完整的):
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
在前面的示例中,单例 bean(userManager)被注入了一个对 HTTP Session 作用域 bean(userPreferences)的引用。此处的关键点在于,userManager bean 是一个单例:它在整个容器中仅被实例化一次,并且其依赖项(在此例中只有一个,即 userPreferences bean)也仅被注入一次。这意味着 userManager bean 始终操作的是完全相同的 userPreferences 对象(即最初注入给它的那个对象)。
当你将一个生命周期较短的 bean 注入到一个生命周期较长的 bean 中时(例如,将一个 HTTP Session 作用域的协作 bean 作为依赖注入到单例 bean 中),这不是你想要的行为。而你需要一个单一的userManager
对象,并且,在一个HTTP Session
的生命周期内,你需要一个针对该HTTP Session
的具体userPreferences
对象。因此,容器会创建一个对象,该对象公开与 UserPreferences 类完全相同的公共接口(理想情况下是一个 UserPreferences 实例),该对象可以从作用域机制(HTTP 请求、Session 等)中获取真实的 UserPreferences 对象。容器将此代理对象注入到userManager豆中,而该豆并不知道这个UserPreferences引用是一个代理。在该示例中,当一个UserManager实例调用其通过依赖注入获取的UserPreferences对象的方法时,实际上是在调用代理对象上的方法。该代理随后从(在这种情况下)HTTP 获取实际的
UserPreferences 对象,并将方法调用委托给检索到的实际
UserPreferences 对象。
因此,在将 request- 和 session-scoped 的 Bean 注入到协作对象中时,您需要如下所示的(正确且完整)配置:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
选择要创建的代理类型
默认情况下,当 Spring 容器为使用 <aop:scoped-proxy/> 元素标记的 bean 创建代理时,会生成一个基于 CGLIB 的类代理。
|
CGLIB 代理仅拦截公共方法调用!不要在这样的代理上调用非公共方法,因为它们不会被委派给实际的作用域目标对象。 |
或者,你可以通过将 false 元素的 proxy-target-class 属性值设为 <aop:scoped-proxy/>,来配置 Spring 容器为这类作用域 Bean 创建基于 JDK 接口的标准代理。使用基于 JDK 接口的代理意味着你无需在应用程序的类路径中添加额外的库即可实现此类代理。然而,这也意味着该作用域 Bean 的类必须至少实现一个接口,并且所有注入该作用域 Bean 的协作对象都必须通过其某个接口来引用该 Bean。以下示例展示了一个基于接口的代理:
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.stuff.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
有关选择基于类的代理还是基于接口的代理的更详细信息,请参阅代理机制。
1.5.5. 自定义作用域
Bean 的作用域机制是可扩展的。你可以定义自己的作用域,甚至重新定义已有的作用域,尽管后者被认为是不良实践,而且你不能覆盖内置的 singleton(单例)和 prototype(原型)作用域。
创建自定义作用域
要将自定义作用域集成到 Spring 容器中,您需要实现
org.springframework.beans.factory.config.Scope 接口,该接口在本节中进行了描述。关于如何实现您自己的作用域,可以参考 Spring Framework 自身提供的 Scope
实现,以及 Scope Javadoc,
其中更详细地解释了您需要实现的方法。
Scope 接口提供了四种方法,用于从作用域中获取对象、从作用域中移除对象,以及销毁这些对象。
例如,会话作用域(session scope)的实现会返回一个会话作用域的 Bean(如果该 Bean 不存在,则该方法会创建一个新的 Bean 实例,并将其绑定到会话中以供将来引用)。以下方法从底层作用域中返回该对象:
Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any
例如,会话作用域(session scope)的实现会从底层会话中移除该会话作用域的 bean。应当返回该对象,但如果找不到指定名称的对象,则可以返回 null。以下方法会从底层作用域中移除该对象:
Object remove(String name)
fun remove(name: String): Any
以下方法注册一个回调,当作用域被销毁或作用域中的指定对象被销毁时,该作用域应调用此回调:
void registerDestructionCallback(String name, Runnable destructionCallback)
fun registerDestructionCallback(name: String, destructionCallback: Runnable)
有关销毁回调的更多信息,请参阅javadoc或 Spring 作用域的实现。
以下方法用于获取底层作用域的会话标识符:
String getConversationId()
fun getConversationId(): String
该标识符在每个作用域中都是不同的。对于会话作用域的实现,此标识符可以是会话标识符。
使用自定义作用域
在你编写并测试一个或多个自定义的 Scope 实现之后,你需要让 Spring 容器感知到这些新的作用域。以下方法是向 Spring 容器注册新 Scope 的核心方法:
void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)
该方法声明在 ConfigurableBeanFactory 接口中,而大多数 Spring 自带的具体 BeanFactory 实现都通过其 ApplicationContext 属性提供了对该接口的访问。
registerScope(..) 方法的第一个参数是与作用域关联的唯一名称。Spring 容器本身中此类名称的示例包括 singleton 和 prototype。registerScope(..) 方法的第二个参数是你希望注册并使用的自定义 Scope 实现的实际实例。
假设你编写了自己的自定义 Scope 实现,然后按照下一个示例所示进行注册。
下一个示例使用了 SimpleThreadScope,该类包含在 Spring 中,但默认情况下并未注册。对于您自己实现的自定义 Scope,其配置步骤也是相同的。 |
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
val threadScope = SimpleThreadScope()
beanFactory.registerScope("thread", threadScope)
然后,您可以创建符合自定义 Scope 作用域规则的 bean 定义,如下所示:
<bean id="..." class="..." scope="thread">
通过自定义的 Scope 实现,您不仅限于以编程方式注册作用域。您还可以使用 Scope 类以声明式的方式注册 CustomScopeConfigurer,如下例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="thing2" class="x.y.Thing2" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="thing1" class="x.y.Thing1">
<property name="thing2" ref="thing2"/>
</bean>
</beans>
当你在 <aop:scoped-proxy/> 实现的 <bean> 声明中放置 FactoryBean 时,被作用域限定的是工厂 Bean 本身,而不是从 getObject() 方法返回的对象。 |
1.6. 自定义 Bean 的性质
Spring 框架提供了多个接口,可用于自定义 Bean 的特性。本节将这些接口按如下方式分组:
1.6.1. 生命周期回调
要与容器对 bean 生命周期的管理进行交互,您可以实现 Spring 的 InitializingBean 和 DisposableBean 接口。容器会分别为这两个接口调用 afterPropertiesSet() 和 destroy() 方法,以便让 bean 在初始化和销毁时执行特定的操作。
|
JSR-250 如果你不想使用 JSR-250 注解,但仍希望降低耦合度,可以考虑使用 |
在内部,Spring 框架使用 BeanPostProcessor 的实现来处理它能找到的任何回调接口,并调用相应的方法。如果你需要 Spring 默认未提供的自定义功能或其他生命周期行为,你可以自己实现一个 BeanPostProcessor。更多信息,请参见容器扩展点。
除了初始化和销毁回调之外,Spring 管理的对象还可以实现 Lifecycle 接口,以便这些对象能够参与到由容器自身生命周期驱动的启动和关闭过程中。
本节介绍了生命周期回调接口。
初始化回调
org.springframework.beans.factory.InitializingBean 接口允许一个 bean 在容器为其设置完所有必要属性之后执行初始化工作。InitializingBean 接口定义了一个单一的方法:
void afterPropertiesSet() throws Exception;
我们建议您不要使用 InitializingBean 接口,因为它会使代码不必要地与 Spring 耦合。或者,我们建议使用
@PostConstruct 注解或指定一个 POJO 初始化方法。在基于 XML 的配置元数据情况下,
您可以使用 init-method 属性来指定具有无参且返回值为 void 签名的方法名称。对于 Java 配置,您可以使用
@Bean 的 initMethod 属性。请参阅 接收生命周期回调。考虑以下示例:
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {
public void init() {
// do some initialization work
}
}
class ExampleBean {
fun init() {
// do some initialization work
}
}
前面的示例几乎与以下示例(包含两个代码清单)具有完全相同的效果:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {
@Override
public void afterPropertiesSet() {
// do some initialization work
}
}
class AnotherExampleBean : InitializingBean {
override fun afterPropertiesSet() {
// do some initialization work
}
}
然而,前面两个示例中的第一个并没有将代码与 Spring 耦合。
销毁回调
实现 org.springframework.beans.factory.DisposableBean 接口可让一个 bean 在其所在的容器被销毁时收到回调通知。DisposableBean 接口仅定义了一个方法:
void destroy() throws Exception;
我们建议您不要使用 DisposableBean 回调接口,因为它会使代码与 Spring 产生不必要的耦合。或者,我们建议使用 @PreDestroy 注解,或指定一个由 Bean 定义支持的泛型方法。对于基于 XML 的配置元数据,您可以在 <bean/> 上使用 destroy-method 属性。对于 Java 配置,您可以使用 @Bean 的 destroyMethod 属性。请参阅 接收生命周期回调。考虑以下定义:
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class ExampleBean {
public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
class ExampleBean {
fun cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
上述定义的效果几乎与以下定义完全相同:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {
@Override
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
class AnotherExampleBean : DisposableBean {
override fun destroy() {
// do some destruction work (like releasing pooled connections)
}
}
然而,上述两个定义中的第一个并未将代码与 Spring 耦合。
您可以将 <bean> 元素的 destroy-method 属性赋予一个特殊的 (inferred) 值,该值会指示 Spring 自动检测特定 Bean 类中的公共 close 或 shutdown 方法。(因此,任何实现了 java.lang.AutoCloseable 或 java.io.Closeable 的类都将匹配。)您也可以将此特殊的 (inferred) 值设置在 <beans> 元素的 default-destroy-method 属性上,从而将此行为应用于整组 Bean(请参阅 默认初始化和销毁方法)。请注意,这是 Java 配置下的默认行为。 |
默认初始化和销毁方法
当你编写初始化和销毁方法回调时,如果不使用 Spring 特定的 InitializingBean 和 DisposableBean 回调接口,通常会编写诸如 init()、initialize()、dispose() 等名称的方法。理想情况下,这类生命周期回调方法的名称应在整个项目中标准化,以便所有开发人员使用相同的方法名,从而确保一致性。
你可以配置 Spring 容器,使其在每个 bean 上“查找”指定名称的初始化和销毁回调方法。这意味着,作为应用程序开发者,你可以编写应用程序类,并使用名为 init() 的初始化回调方法,而无需在每个 bean 定义中配置 init-method="init" 属性。当 bean 被创建时(并遵循前面描述的标准生命周期回调契约),Spring IoC 容器会调用该方法。此功能还强制实施了一种统一的初始化和销毁方法回调的命名规范。
假设你的初始化回调方法名为 init(),而销毁回调方法名为 destroy()。那么你的类将类似于以下示例中的类:
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
// this is (unsurprisingly) the initialization callback method
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}
class DefaultBlogService : BlogService {
private var blogDao: BlogDao? = null
// this is (unsurprisingly) the initialization callback method
fun init() {
if (blogDao == null) {
throw IllegalStateException("The [blogDao] property must be set.")
}
}
}
然后,您可以在类似于以下的 bean 中使用该类:
<beans default-init-method="init">
<bean id="blogService" class="com.something.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>
</beans>
顶层 default-init-method 元素上的 <beans/> 属性的存在,
会使 Spring IoC 容器将 bean 类中名为 init 的方法识别为初始化方法回调。
当 bean 被创建并装配完成后,如果该 bean 类包含这样的方法,
它将在适当的时机被调用。
你可以通过在顶层 default-destroy-method 元素上使用 <beans/> 属性,以类似的方式(即在 XML 中)配置销毁方法回调。
如果现有的 bean 类已经包含与约定命名不一致的回调方法,你可以通过在 init-method 元素本身中使用 destroy-method 和 <bean/> 属性(即在 XML 中)来指定方法名,从而覆盖默认行为。
Spring 容器保证在为 Bean 注入所有依赖项之后,立即调用已配置的初始化回调方法。因此,该初始化回调是在原始 Bean 引用上调用的,这意味着此时 AOP 拦截器等尚未应用到该 Bean 上。容器会先完整地创建目标 Bean,然后再应用 AOP 代理(例如)及其拦截器链。如果目标 Bean 和代理是分别定义的,您的代码甚至可以直接与原始的目标 Bean 交互,从而绕过代理。因此,若将拦截器应用于 init 方法,就会造成不一致,因为这样做会将目标 Bean 的生命周期与其代理或拦截器耦合在一起,当您的代码直接与原始目标 Bean 交互时,会产生奇怪的语义。
组合生命周期机制
从 Spring 2.5 开始,您有三种选项来控制 bean 的生命周期行为:
-
InitializingBean和DisposableBean回调接口 -
自定义
init()和destroy()方法 -
@PostConstruct和@PreDestroy注解。您可以组合这些机制来控制指定的 Bean。
如果为一个 bean 配置了多种生命周期机制,并且每种机制都配置了不同的方法名,那么每个配置的方法将按照本注释之后列出的顺序依次执行。然而,如果在多个生命周期机制中配置了相同的方法名——例如,将初始化方法配置为 init()——则该方法只会执行一次,如前一节所述。 |
为同一个 bean 配置了多种生命周期机制,并且它们具有不同的初始化方法,其调用顺序如下:
-
使用
@PostConstruct注解的方法 -
afterPropertiesSet()方法,由InitializingBean回调接口定义 -
一个自定义配置的
init()方法
销毁方法按照相同的顺序被调用:
-
使用
@PreDestroy注解的方法 -
destroy()方法,由DisposableBean回调接口定义 -
一个自定义配置的
destroy()方法
启动和关闭回调
Lifecycle 接口定义了任何具有自身生命周期需求(例如启动和停止某些后台进程)的对象所需的基本方法:
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
任何由 Spring 管理的对象都可以实现 Lifecycle 接口。这样,当 ApplicationContext 本身接收到启动和停止信号时(例如在运行时进行停止/重启操作),它会将这些调用级联传递给该上下文中定义的所有 Lifecycle 实现。这是通过委托给一个 LifecycleProcessor 来完成的,如下列代码所示:
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}
请注意,LifecycleProcessor 本身是 Lifecycle 接口的扩展。它还额外添加了两个方法,用于响应上下文的刷新和关闭事件。
|
请注意,常规的 此外,请注意停止通知并不保证在销毁之前发出。
在正常关闭过程中,所有 |
启动和关闭调用的顺序可能非常重要。如果任意两个对象之间存在“depends-on”(依赖)关系,则依赖方在其所依赖的对象之后启动,并在其所依赖的对象之前停止。然而,有时直接的依赖关系并不明确,你可能只知道某一类型的对象应当在另一类型的对象之前启动。在这种情况下,SmartLifecycle 接口提供了另一种选择,即其父接口 getPhase() 中定义的 Phased 方法。以下代码清单展示了 Phased 接口的定义:
public interface Phased {
int getPhase();
}
以下代码清单展示了 SmartLifecycle 接口的定义:
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}
启动时,相位(phase)值最低的对象最先启动;停止时,则按相反的顺序执行。因此,一个实现了 SmartLifecycle 接口且其 getPhase() 方法返回 Integer.MIN_VALUE 的对象,将会在启动时最早启动,在停止时最晚停止。在相位值范围的另一端,相位值为 Integer.MAX_VALUE 表示该对象应当最后启动、最先停止(很可能是因为它依赖于其他进程先运行起来)。在考虑相位值时,还需注意:任何未实现 Lifecycle 接口的普通 SmartLifecycle 对象,默认相位值为 0。因此,任何负的相位值都表示该对象应在这些标准组件之前启动(并在它们之后停止),而任何正的相位值则正好相反。
SmartLifecycle 接口中定义的 run() 方法接受一个回调参数。任何实现类都必须在其自身的关闭流程完成后调用该回调的 LifecycleProcessor 方法。这在必要时可实现异步关闭,因为 DefaultLifecycleProcessor 接口的默认实现类 lifecycleProcessor 会等待每个阶段中的对象组调用该回调,最长等待时间为其设定的超时值。默认情况下,每个阶段的超时时间为 30 秒。您可以通过在上下文中定义一个名为 5 的 bean 来覆盖默认的生命周期处理器实例。如果您只想修改超时时间,则只需定义如下内容即可:
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
<!-- timeout value in milliseconds -->
<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>
如前所述,LifecycleProcessor接口定义了用于上下文刷新和关闭的回调方法。后者在上下文关闭时驱动关闭过程,就像显式调用了stop()一样。另一方面,'refresh' 回调使SmartLifecycle bean 的另一个功能成为可能。当上下文被刷新(即所有对象都被实例化和初始化之后),该回调会被触发。此时,默认生命周期处理器会检查每个SmartLifecycle对象的isAutoStartup()方法返回的布尔值。如果true,那么在那个点该对象将会启动,而不是等待显式调用上下文或其自身的start()方法(与上下文刷新不同,标准上下文实现中的上下文启动不会自动发生)。phase值和任何“depends-on”关系确定了启动顺序,如前所述。
在非 Web 应用中优雅地关闭 Spring IoC 容器
|
本节仅适用于非 Web 应用程序。Spring 的基于 Web 的 |
如果你在非 Web 应用环境中使用 Spring 的 IoC 容器(例如,在富客户端桌面环境中),请向 JVM 注册一个关闭钩子(shutdown hook)。这样做可以确保应用优雅地关闭,并调用你的单例 Bean 上相应的销毁方法,从而释放所有资源。你仍然必须正确地配置和实现这些销毁回调。
要注册一个关闭钩子,请调用 registerShutdownHook() 接口中声明的 ConfigurableApplicationContext 方法,如下例所示:
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Boot {
public static void main(final String[] args) throws Exception {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// add a shutdown hook for the above context...
ctx.registerShutdownHook();
// app runs here...
// main method exits, hook is called prior to the app shutting down...
}
}
import org.springframework.context.support.ClassPathXmlApplicationContext
fun main() {
val ctx = ClassPathXmlApplicationContext("beans.xml")
// add a shutdown hook for the above context...
ctx.registerShutdownHook()
// app runs here...
// main method exits, hook is called prior to the app shutting down...
}
1.6.2. ApplicationContextAware和BeanNameAware
当 ApplicationContext 创建一个实现了
org.springframework.context.ApplicationContextAware 接口的对象实例时,该实例会被注入一个指向该 ApplicationContext 的引用。以下代码清单展示了 ApplicationContextAware 接口的定义:
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
因此,Bean 可以通过 ApplicationContext 接口或将引用强制转换为该接口的已知子类(例如 ConfigurableApplicationContext,它公开了额外功能),以编程方式操作创建它们的 ApplicationContext。一种用途是以编程方式检索其他 Bean。有时这种能力很有用。然而,通常情况下,您应该避免这样做,因为它会将代码与 Spring 耦合,并且不符合控制反转(Inversion of Control)风格,在控制反转风格中,协作者作为属性提供给 Bean。ApplicationContext 的其他方法提供了对文件资源的访问、发布应用程序事件以及访问 MessageSource 的功能。这些额外功能在 ApplicationContext 的其他功能 中进行了描述。
自动装配是获取ApplicationContext引用的另一种替代方案。传统的constructor和byType自动装配模式(如自动装配协作者中所述)可以分别为构造函数参数或 Setter 方法参数提供类型为ApplicationContext的依赖项。为了获得更大的灵活性,包括支持字段和多个参数方法的自动装配,请使用基于注解的自动装配功能。如果启用该功能,当相关字段、构造函数或方法带有@Autowired注解时,ApplicationContext将被自动装配到期望ApplicationContext类型的字段、构造函数参数或方法参数中。更多信息请参阅使用@Autowired。
当 ApplicationContext 创建一个实现了
org.springframework.beans.factory.BeanNameAware 接口的类时,该类会被提供一个引用,指向其关联对象定义中指定的名称。以下代码清单展示了 BeanNameAware 接口的定义:
public interface BeanNameAware {
void setBeanName(String name) throws BeansException;
}
该回调在普通 bean 属性填充之后、但在初始化回调(例如 InitializingBean.afterPropertiesSet() 或自定义的 init-method)之前被调用。
1.6.3. 其他Aware接口
除了 ApplicationContextAware 和 BeanNameAware(前面已讨论过)之外,
Spring 还提供了多种 Aware 回调接口,允许 Bean 向容器表明它们需要某种基础设施依赖。
通常来说,接口名称就表明了其所依赖的类型。下表总结了最重要的 Aware 接口:
| 姓名 | 注入的依赖 | 详见…… |
|---|---|---|
|
声明 |
|
|
封闭的 |
|
|
用于加载 Bean 类的类加载器。 |
|
|
声明 |
|
|
声明该 bean 的 bean 名称。 |
|
|
资源适配器所运行的容器 |
|
|
定义用于在加载时处理类定义的织入器。 |
|
|
用于解析消息的已配置策略(支持参数化和国际化)。 |
|
|
Spring JMX 通知发布器。 |
|
|
用于底层资源访问的已配置加载器。 |
|
|
当前容器所运行的 |
|
|
当前容器所运行的 |
再次注意,使用这些接口会将您的代码与 Spring API 耦合,并且不符合控制反转(Inversion of Control)的风格。因此,我们建议仅在需要以编程方式访问容器的基础架构 bean 中使用它们。
1.7. Bean 定义继承
一个 bean 定义可以包含大量配置信息,包括构造函数参数、属性值以及容器特定的信息,例如初始化方法、静态工厂方法名称等。子 bean 定义会从父定义中继承配置数据。子定义可以根据需要覆盖某些值或添加其他值。使用父子 bean 定义可以节省大量输入工作。实际上,这是一种模板形式。
如果你以编程方式使用 ApplicationContext 接口,子 bean 定义由 ChildBeanDefinition 类表示。大多数用户不会在这一层直接使用它们。相反,他们通常在诸如 ClassPathXmlApplicationContext 这样的类中以声明方式配置 bean 定义。当你使用基于 XML 的配置元数据时,可以通过使用 parent 属性来指定一个子 bean 定义,并将父 bean 指定为该属性的值。以下示例展示了如何实现这一点:
<bean id="inheritedTestBean" abstract="true"
class="org.springframework.beans.TestBean">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithDifferentClass"
class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBean" init-method="initialize"> (1)
<property name="name" value="override"/>
<!-- the age property value of 1 will be inherited from parent -->
</bean>
| 1 | 注意 parent 属性。 |
如果子 bean 定义未指定自己的 bean 类,则会使用父定义中的 bean 类,但也可以覆盖它。在后一种情况下,子 bean 的类必须与父类兼容(即必须能够接受父类的属性值)。
子 bean 定义会从父 bean 继承作用域、构造函数参数值、属性值以及方法重写,并可选择性地添加新值。您所指定的任何作用域、初始化方法、销毁方法或 static 工厂方法设置,都会覆盖父 bean 中相应的设置。
其余的设置始终从子定义中获取:depends on(依赖)、autowire mode(自动装配模式)、dependency check(依赖检查)、singleton(单例)和 lazy init(懒加载)。
前面的示例通过使用 abstract 属性,显式地将父 bean 定义标记为抽象。如果父定义未指定类,则必须显式地将父 bean 定义标记为 abstract,如下例所示:
<bean id="inheritedTestBeanWithoutClass" abstract="true">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBeanWithoutClass" init-method="initialize">
<property name="name" value="override"/>
<!-- age will inherit the value of 1 from the parent bean definition-->
</bean>
父 bean 无法单独实例化,因为它本身是不完整的,并且还被显式标记为 abstract(抽象)。当一个 bean 定义被标记为 abstract 时,它只能作为一种纯粹的模板 bean 定义,用作子 bean 定义的父定义。如果尝试单独使用这种 abstract 父 bean——例如在另一个 bean 的 ref 属性中引用它,或通过父 bean 的 ID 显式调用 getBean() 方法——都会返回错误。同样,容器内部的 preInstantiateSingletons() 方法也会忽略那些被定义为 abstract 的 bean 定义。
ApplicationContext 默认会预先实例化所有单例(singleton)bean。因此,如果你有一个(父级)bean 定义仅打算用作模板,并且该定义指定了一个类,那么你必须确保将 abstract 属性设置为 true(至少对于单例 bean 而言这一点很重要)。否则,应用上下文实际上会(尝试)预先实例化这个被标记为 abstract 的 bean。 |
1.8. 容器扩展点
通常,应用程序开发人员不需要继承 ApplicationContext 的实现类。相反,可以通过插入特殊集成接口的实现来扩展 Spring IoC 容器。接下来的几节将介绍这些集成接口。
1.8.1. 通过使用自定义 BeanBeanPostProcessor
BeanPostProcessor 接口定义了回调方法,您可以实现这些方法以提供自己的(或覆盖容器默认的)实例化逻辑、依赖解析逻辑等。如果您希望在 Spring 容器完成 bean 的实例化、配置和初始化之后执行某些自定义逻辑,可以插入一个或多个自定义的 BeanPostProcessor 实现。
您可以配置多个 BeanPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanPostProcessor 实例的运行顺序。
仅当 BeanPostProcessor 实现了 Ordered 接口时,才能设置此属性。如果您编写自己的 BeanPostProcessor,也应考虑实现
Ordered 接口。有关更多详细信息,请参阅
BeanPostProcessor
和 Ordered 接口的 Javadoc。另请参阅关于
以编程方式注册 BeanPostProcessor 实例 的说明。
|
要更改实际的 Bean 定义(即定义 Bean 的蓝图),
您需要改用 |
org.springframework.beans.factory.config.BeanPostProcessor 接口正好包含两个回调方法。当此类作为后处理器注册到容器中时,对于容器所创建的每个 bean 实例,该后处理器都会从容器接收到两次回调:一次在容器初始化方法(例如 InitializingBean.afterPropertiesSet() 或任何声明的 init 方法)被调用之前,另一次在所有 bean 初始化回调之后。后处理器可以对 bean 实例执行任意操作,包括完全忽略该回调。通常,bean 后处理器会检查回调接口,或者可能使用代理包装 bean。一些 Spring AOP 基础设施类就是作为 bean 后处理器实现的,以提供代理包装逻辑。
ApplicationContext 会自动检测配置元数据中定义的、实现了 BeanPostProcessor 接口的所有 bean。ApplicationContext 会将这些 bean 注册为后处理器,以便在创建 bean 时能够调用它们。Bean 后处理器可以像其他任何 bean 一样部署到容器中。
请注意,当通过配置类中的 BeanPostProcessor 工厂方法声明一个 @Bean 时,该工厂方法的返回类型应当是其实现类本身,或者至少是 org.springframework.beans.factory.config.BeanPostProcessor 接口,以明确表明该 bean 的后处理器特性。否则,ApplicationContext 将无法在完全创建该 bean 之前通过类型自动检测到它。由于 BeanPostProcessor 需要在上下文早期就被实例化,以便应用于上下文中其他 bean 的初始化过程,因此这种早期的类型检测至关重要。
以编程方式注册
虽然推荐的 BeanPostProcessor 实例BeanPostProcessor 注册方式是通过ApplicationContext 自动检测(如前所述),但您可以使用addBeanPostProcessor方法,针对ConfigurableBeanFactory以编程方式注册它们。当您在注册前需要评估条件逻辑,或者甚至在层次结构中跨上下文复制 Bean 后置处理器时,这种方式非常有用。然而请注意,以编程方式添加的BeanPostProcessor实例不遵循Ordered接口。在此情况下,注册顺序决定了执行顺序。另请注意,以编程方式注册的BeanPostProcessor实例始终会在通过自动检测注册的实例之前被处理,无论是否存在显式的排序。 |
BeanPostProcessor 实例与 AOP 自动代理实现 对于任何此类 bean,您应会看到一条信息日志消息: 如果你通过自动装配(autowiring)或 |
以下示例展示了如何在 BeanPostProcessor 中编写、注册和使用 ApplicationContext 实例。
示例:你好,世界,BeanPostProcessor-style
第一个示例展示了基本用法。该示例展示了一个自定义的BeanPostProcessor实现,它在容器创建每个 bean 时调用该 bean 的toString()方法,并将结果字符串打印到系统控制台。
以下清单展示了自定义 BeanPostProcessor 实现类的定义:
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// simply return the instantiated bean as-is
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean; // we could potentially return any object reference here...
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
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 应用程序运行了前面的代码和配置:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
Messenger messenger = ctx.getBean("messenger", Messenger.class);
System.out.println(messenger);
}
}
import org.springframework.beans.factory.getBean
fun main() {
val ctx = ClassPathXmlApplicationContext("scripting/beans.xml")
val messenger = ctx.getBean<Messenger>("messenger")
println(messenger)
}
前述应用程序的输出类似于以下内容:
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961 org.springframework.scripting.groovy.GroovyMessenger@272961
1.8.2. 使用 a 自定义配置元数据BeanFactoryPostProcessor
我们接下来要查看的扩展点是
org.springframework.beans.factory.config.BeanFactoryPostProcessor。
该接口的语义与 BeanPostProcessor 类似,但有一个主要区别:
BeanFactoryPostProcessor 作用于 bean 的配置元数据。
也就是说,Spring IoC 容器允许 BeanFactoryPostProcessor 在容器实例化除
BeanFactoryPostProcessor 实例之外的任何 bean 之前,
读取配置元数据并可能对其进行修改。
您可以配置多个 BeanFactoryPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanFactoryPostProcessor 实例的运行顺序。
但是,仅当 BeanFactoryPostProcessor 实现了 Ordered 接口时,才能设置此属性。如果您编写自己的 BeanFactoryPostProcessor,也应该考虑实现 Ordered 接口。有关更多详细信息,请参阅
BeanFactoryPostProcessor
和 Ordered 接口的 Javadoc。
|
如果您想要更改实际的 bean 实例(即从配置元数据创建的对象),那么您需要改用 此外, |
当在 ApplicationContext 内部声明时,Bean 工厂后处理器(bean factory post-processor)会自动运行,以对定义容器的配置元数据进行修改。Spring 提供了多个预定义的 Bean 工厂后处理器,例如 PropertyOverrideConfigurer 和 PropertySourcesPlaceholderConfigurer。你也可以使用自定义的 BeanFactoryPostProcessor,例如用于注册自定义的属性编辑器。
ApplicationContext 会自动检测部署到其中的、实现了 BeanFactoryPostProcessor 接口的任何 bean。它会在适当的时机将这些 bean 用作 bean 工厂后处理器。你可以像部署其他任意 bean 一样部署这些后处理器 bean。
与BeanPostProcessor一样,通常你不希望将BeanFactoryPostProcessor配置为延迟初始化。如果没有其他 bean 引用某个Bean(Factory)PostProcessor,那么该后置处理器根本不会被实例化。因此,即使你将其标记为延迟初始化,该设置也会被忽略,并且即使你在Bean(Factory)PostProcessor元素的声明中将default-lazy-init属性设置为true,<beans />仍会被提前(急切地)实例化。 |
示例:类名替换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>
该示例展示了从外部 Properties 文件中配置的属性。在运行时,
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 不仅会在您指定的 Properties 文件中查找属性。默认情况下,如果在指定的属性文件中找不到某个属性,它还会检查 Spring 的 Environment 属性和标准的 Java System 属性。
|
你可以使用
如果该类在运行时无法解析为一个有效的类,则在即将创建该 bean 时解析会失败,对于非延迟初始化(non-lazy-init)的 bean,这发生在 |
示例:PropertyOverrideConfigurer
PropertyOverrideConfigurer 是另一种 Bean 工厂后置处理器,与 PropertySourcesPlaceholderConfigurer 类似。但与后者不同的是,原始定义中的 Bean 属性可以具有默认值,甚至完全不设置值。如果用于覆盖的 Properties 文件中没有包含某个 Bean 属性的条目,则使用默认的上下文定义。
请注意,bean 定义本身并不知道它被覆盖了,因此从 XML 定义文件中无法立即看出正在使用覆盖配置器(override configurer)。如果有多个 PropertyOverrideConfigurer 实例为同一个 bean 属性定义了不同的值,由于覆盖机制的作用,最后一个定义的值将生效。
属性文件的配置行采用以下格式:
beanName.property=value
以下列表展示了该格式的一个示例:
dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql:mydb
此示例文件可与包含名为 dataSource 的 bean 的容器定义一起使用,该 bean 具有 driver 和 url 属性。
只要路径中除最终要被覆盖的属性之外的每个组件都已非空(通常由构造函数初始化),复合属性名称也受支持。在以下示例中,sammy bean 的 bob 属性的 fred 属性的 tom 属性被设置为标量值 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 容器实例化逻辑的一个可插拔接入点。如果你有复杂的初始化代码,用 Java 表达比使用(可能非常冗长的)XML 更为合适,那么你可以创建自己的 FactoryBean,在该类中编写复杂的初始化逻辑,然后将自定义的 FactoryBean 插入到容器中。
FactoryBean<T> 接口提供了三个方法:
-
T getObject():返回此工厂所创建对象的一个实例。该实例可能是共享的,具体取决于此工厂返回的是单例(singleton)还是原型(prototype)。 -
boolean isSingleton():如果此true返回单例对象,则返回FactoryBean,否则返回false。该方法的默认实现返回true。 -
Class<?> getObjectType():返回getObject()方法所返回的对象类型,如果该类型事先未知,则返回null。
FactoryBean 的概念和接口在 Spring 框架的许多地方都有使用。Spring 自带的 FactoryBean 接口实现就超过 50 种。
当您需要向容器请求实际的FactoryBean实例本身,而不是它生成的 bean 时,在调用ApplicationContext的getBean()方法时,需在 bean 的id前加上与符号(&)。因此,对于给定的FactoryBean,其id为myBean,在容器上调用getBean("myBean")将返回FactoryBean的产物,而调用getBean("&myBean")则返回FactoryBean实例本身。
1.9. 基于注解的容器配置
另一种替代 XML 配置的方式是基于注解的配置,它依赖字节码元数据来装配组件,而不是使用尖括号声明。开发者不再使用 XML 来描述 bean 的装配,而是通过在相关的类、方法或字段声明上使用注解,将配置直接移入组件类本身。如 示例:AutowiredAnnotationBeanPostProcessor 中所述,将 BeanPostProcessor 与注解结合使用是扩展 Spring IoC 容器的常见方式。例如,Spring 2。0 引入了通过 @Required 注解强制要求属性的可能性。Spring
2.5 版本使得可以采用相同的通用方法来驱动 Spring 的依赖注入。本质上,@Autowired 注解提供了与 自动装配协作者 中描述相同的功能,但具有更细粒度的控制和更广泛的适用性。Spring 2。5 还添加了对 JSR-250 注解的支持,例如
@PostConstruct 和 @PreDestroy。Spring 3.0 添加了对 javax.inject 包中包含的 JSR-330(Java 依赖注入)注解的支持,例如 @Inject 和 @Named。有关这些注解的详细信息可以在
相关部分中找到。
|
注解注入在 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/> 元素会隐式注册以下后处理器:
|
|
1.9.1. @Required
@Required 注解应用于 Bean 属性的 setter 方法,如下例所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Required
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
class SimpleMovieLister {
@Required
lateinit var movieFinder: MovieFinder
// ...
}
此注解表明,受影响的 Bean 属性必须在配置时通过 Bean 定义中的显式属性值或通过自动装配进行填充。如果该 Bean 属性未被填充,容器将抛出异常。这样可以实现快速且明确的失败机制,避免后续出现 NullPointerException 等类似问题。我们仍然建议您在 Bean 类本身中加入断言(例如,在初始化方法中)。这样做即使在容器外部使用该类时,也能强制确保这些必需的引用和值。
|
|
|
|
1.9.2. 使用@Autowired
|
JSR 330 的 |
您可以将 @Autowired 注解应用于构造函数,如下例所示:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
class MovieRecommender @Autowired constructor(
private val customerPreferenceDao: CustomerPreferenceDao)
|
从 Spring Framework 4.3 开始,如果目标 bean 一开始只定义了一个构造函数,则该构造函数上的 |
您也可以将 @Autowired 注解应用于传统的 setter 方法,如下例所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
class SimpleMovieLister {
@Autowired
lateinit var movieFinder: MovieFinder
// ...
}
你也可以将该注解应用于具有任意名称和多个参数的方法上,如下例所示:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
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 应用于字段,甚至可以将其与构造函数混合使用,如下例所示:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
private MovieCatalog movieCatalog;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
class MovieRecommender @Autowired constructor(
private val customerPreferenceDao: CustomerPreferenceDao) {
@Autowired
private lateinit var movieCatalog: MovieCatalog
// ...
}
|
请确保您的目标组件(例如 对于通过 XML 定义的 bean 或通过类路径扫描发现的组件类,容器通常能提前知道其具体类型。然而,对于 |
你还可以通过在期望接收该类型数组的字段或方法上添加 ApplicationContext 注解,指示 Spring 从 @Autowired 中提供所有特定类型的 bean,如下例所示:
public class MovieRecommender {
@Autowired
private MovieCatalog[] movieCatalogs;
// ...
}
class MovieRecommender {
@Autowired
private lateinit var movieCatalogs: Array<MovieCatalog>
// ...
}
对于类型化的集合也同样适用,如下例所示:
public class MovieRecommender {
private Set<MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
class MovieRecommender {
@Autowired
lateinit var movieCatalogs: Set<MovieCatalog>
// ...
}
|
如果你希望数组或列表中的项按特定顺序排序,你的目标 Bean 可以实现 你可以在目标类级别以及 请注意,标准的 |
即使是有泛型类型的 Map 实例,只要其期望的键类型为 String,也可以被自动装配。
该 Map 的值包含所有符合期望类型的 Bean,而键则包含相应的 Bean 名称,如下例所示:
public class MovieRecommender {
private Map<String, MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
class MovieRecommender {
@Autowired
lateinit var movieCatalogs: Map<String, MovieCatalog>
// ...
}
默认情况下,当给定注入点没有匹配的候选 Bean 可用时,自动装配会失败。对于声明的数组、集合或映射,至少需要有一个匹配的元素。
默认行为是将带有注解的方法和字段视为表示必需的依赖项。你可以更改此行为,如以下示例所示,通过将注入点标记为非必需(即,将 required 注解中的 @Autowired 属性设置为 false),使框架跳过无法满足的注入点:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired(required = false)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
class SimpleMovieLister {
@Autowired(required = false)
var movieFinder: MovieFinder? = null
// ...
}
如果某个非必需方法的依赖项(或在具有多个参数的情况下,其任一依赖项)不可用,则该方法根本不会被调用。在类似情况下,非必需字段也不会被填充,而是保留其默认值。
注入的构造函数和工厂方法参数属于一种特殊情况,因为 required 注解中的 @Autowired 属性在此处具有略微不同的含义,这是由于 Spring 的构造函数解析算法可能会处理多个构造函数。默认情况下,构造函数和工厂方法参数实际上是必需的,但在只有一个构造函数的场景下存在一些特殊规则,例如多元素注入点(数组、集合、映射)在没有匹配的 Bean 可用时会解析为空实例。这使得一种常见的实现模式成为可能:所有依赖项都可以在一个唯一的多参数构造函数中声明——例如,声明为一个不带 @Autowired 注解的单一公共构造函数。
|
任何给定 Bean 类的构造函数中,只能有一个构造函数将 推荐使用 |
或者,您也可以通过 Java 8 的 java.util.Optional 来表达某个特定依赖项的非必需性,如下例所示:
public class SimpleMovieLister {
@Autowired
public void setMovieFinder(Optional<MovieFinder> movieFinder) {
...
}
}
从 Spring Framework 5.0 开始,您还可以使用任意类型的 @Nullable 注解(可以是任何包中的注解,例如来自 JSR-305 的 javax.annotation.Nullable),或者直接利用 Kotlin 内置的空安全支持:
public class SimpleMovieLister {
@Autowired
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
...
}
}
class SimpleMovieLister {
@Autowired
var movieFinder: MovieFinder? = null
// ...
}
您也可以对众所周知的可解析依赖项使用 @Autowired:BeanFactory、ApplicationContext、Environment、ResourceLoader、
ApplicationEventPublisher 和 MessageSource。这些接口及其扩展接口(例如 ConfigurableApplicationContext 或 ResourcePatternResolver)会
自动解析,无需任何特殊配置。以下示例演示了如何自动装配一个 ApplicationContext 对象:
public class MovieRecommender {
@Autowired
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
class MovieRecommender {
@Autowired
lateinit var context: ApplicationContext
// ...
}
|
|
1.9.3. 微调基于注解的自动装配功能,使用@Primary
由于按类型自动装配可能会导致多个候选 Bean,因此通常需要对选择过程进行更精细的控制。实现这一目标的方法之一是使用 Spring 的 @Primary 注解。@Primary 表示当多个 Bean 都符合单值依赖的自动装配条件时,应优先选择该特定的 Bean。如果在所有候选 Bean 中恰好存在一个主 Bean(primary bean),那么它就会被作为自动装配的值。
考虑以下配置,它将 firstMovieCatalog 定义为首选的 MovieCatalog:
@Configuration
public class MovieConfiguration {
@Bean
@Primary
public MovieCatalog firstMovieCatalog() { ... }
@Bean
public MovieCatalog secondMovieCatalog() { ... }
// ...
}
@Configuration
class MovieConfiguration {
@Bean
@Primary
fun firstMovieCatalog(): MovieCatalog { ... }
@Bean
fun secondMovieCatalog(): MovieCatalog { ... }
// ...
}
通过上述配置,以下 MovieRecommender 将被自动装配为使用 firstMovieCatalog:
public class MovieRecommender {
@Autowired
private MovieCatalog movieCatalog;
// ...
}
class MovieRecommender {
@Autowired
private lateinit var movieCatalog: MovieCatalog
// ...
}
对应的 Bean 定义如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean class="example.SimpleMovieCatalog" primary="true">
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
1.9.4. 使用限定符微调基于注解的自动装配
@Primary 是在存在多个实例但可确定一个主要候选者时,通过类型进行自动装配的有效方式。当你需要对选择过程进行更精细的控制时,可以使用 Spring 的 @Qualifier 注解。你可以将限定符值与特定参数关联起来,从而缩小类型匹配的范围,使得为每个参数选择特定的 Bean。在最简单的情况下,这可以是一个简单的描述性值,如下例所示:
public class MovieRecommender {
@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;
// ...
}
class MovieRecommender {
@Autowired
@Qualifier("main")
private lateinit var movieCatalog: MovieCatalog
// ...
}
您也可以在单个构造函数参数或方法参数上指定 @Qualifier 注解,如下例所示:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(@Qualifier("main") MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
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 的名称被视为默认的限定符值。因此,你可以将 Bean 定义为 id 为 main,而不使用嵌套的限定符元素,从而得到相同的匹配结果。然而,尽管你可以使用此约定通过名称引用特定的 Bean,@Autowired 本质上仍是基于类型的注入,并可选择性地使用语义限定符。这意味着,即使有 Bean 名称作为后备,限定符值在类型匹配的集合中始终具有缩小范围的语义,而不会在语义上表示对某个唯一 Bean id 的引用。良好的限定符值应为 main、EMEA 或 persistent,它们表达了特定组件的某些特征,这些特征独立于 Bean 的 id;而在匿名 Bean 定义(如前面示例中的情况)下,Bean 的 8 可能是自动生成的。
限定符(Qualifiers)也适用于类型化的集合,如前所述——例如,适用于Set<MovieCatalog>。在这种情况下,所有符合所声明限定符的匹配 Bean 都会被注入为一个集合。这意味着限定符不必是唯一的,而是构成了一种过滤条件。例如,你可以定义多个具有相同限定符值“action”的MovieCatalog Bean,它们都会被注入到一个使用Set<MovieCatalog>注解的@Qualifier("action")中。
|
在类型匹配的候选 Bean 中,让限定符(qualifier)值根据目标 Bean 的名称进行选择,不需要在注入点使用 |
话虽如此,但如果你打算通过名称来实现基于注解的依赖注入,请不要主要使用 @Autowired,即使它能够在类型匹配的候选 Bean 中根据 Bean 名称进行选择。相反,应使用 JSR-250 的 @Resource 注解,该注解在语义上被定义为通过其唯一名称来识别特定的目标组件,而声明的类型在匹配过程中是无关紧要的。@Autowired 具有截然不同的语义:它首先按类型筛选出候选 Bean,然后仅在这些类型匹配的候选 Bean 中考虑指定的 String 限定符值(例如,将一个名为 account 的限定符与带有相同限定符标签的 Bean 进行匹配)。
对于本身被定义为集合、Map 或数组类型的 Bean,@Resource 是一个很好的解决方案,可以通过唯一的名称引用特定的集合或数组 Bean。
然而,从 Spring 4.3 版本开始,只要元素类型信息在 Map 方法的返回类型签名或集合的继承层次结构中得以保留,你也可以通过 Spring 的 @Autowired 类型匹配算法来匹配集合、@Bean 和数组类型。
在这种情况下,你可以使用限定符(qualifier)值在相同类型的集合之间进行选择,如前一段所述。
从 4.3 版本开始,@Autowired 也会考虑自引用(self references)进行注入(即指向当前正在被注入的 bean 的引用)。请注意,自注入仅作为一种后备方案。对其他组件的常规依赖始终具有更高优先级。从这个意义上说,自引用不会参与常规的候选 bean 选择过程,因此尤其不会被视为主候选(primary)。相反,它们的优先级始终是最低的。在实践中,您应仅将自引用作为最后的手段(例如,通过 bean 的事务代理调用同一实例上的其他方法)。在这种场景下,建议将受影响的方法提取到一个单独的委托 bean 中。或者,您也可以使用 @Resource,它可以通过 bean 的唯一名称获取指向当前 bean 的代理。
|
尝试注入来自同一配置类中 |
@Autowired 可用于字段、构造函数和多参数方法,允许在参数级别通过限定符注解进行缩小匹配范围。相比之下,@Resource 仅支持字段和带单个参数的 bean 属性 setter 方法。
因此,如果你的注入目标是构造函数或多参数方法,应坚持使用限定符。
您可以创建自己的自定义限定符注解。为此,请定义一个注解,并在您的定义中包含 @Qualifier 注解,如下例所示:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {
String value();
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Genre(val value: String)
然后,您可以在自动装配的字段和参数上提供自定义限定符,如下例所示:
public class MovieRecommender {
@Autowired
@Genre("Action")
private MovieCatalog actionCatalog;
private MovieCatalog comedyCatalog;
@Autowired
public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
this.comedyCatalog = comedyCatalog;
}
// ...
}
class MovieRecommender {
@Autowired
@Genre("Action")
private lateinit var actionCatalog: MovieCatalog
private lateinit var comedyCatalog: MovieCatalog
@Autowired
fun setComedyCatalog(@Genre("Comedy") comedyCatalog: MovieCatalog) {
this.comedyCatalog = comedyCatalog
}
// ...
}
接下来,您可以提供候选 Bean 定义的相关信息。您可以在 <qualifier/> 标签内添加 <bean/> 子元素,并指定 type 和 value 以匹配您自定义的限定符注解。其中,4 会与注解的完整限定类名进行匹配。或者,为方便起见,如果不存在名称冲突的风险,您也可以使用简短的类名。以下示例展示了这两种方式:
<?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 中提供限定符元数据。具体请参见使用注解提供限定符元数据。
在某些情况下,使用不带值的注解就足够了。当注解用于更通用的目的,并可应用于多种不同类型的依赖项时,这种方式非常有用。例如,您可能提供一个离线目录,在没有互联网连接时可以进行搜索。首先,定义一个简单的注解,如下例所示:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Offline
然后将注解添加到需要自动装配的字段或属性上,如下例所示:
public class MovieRecommender {
@Autowired
@Offline (1)
private MovieCatalog offlineCatalog;
// ...
}
| 1 | 此行添加了 @Offline 注解。 |
class MovieRecommender {
@Autowired
@Offline (1)
private lateinit var offlineCatalog: MovieCatalog
// ...
}
| 1 | 此行添加了 @Offline 注解。 |
现在,bean 定义只需要一个限定符 type,如下例所示:
<bean class="example.SimpleMovieCatalog">
<qualifier type="Offline"/> (1)
<!-- inject any dependencies required by this bean -->
</bean>
| 1 | 此元素用于指定限定符。 |
你还可以定义自定义的限定符注解,这些注解除了或代替简单的 value 属性外,还可以接受命名的属性。如果在需要自动装配的字段或参数上指定了多个属性值,则 Bean 定义必须匹配所有这些属性值,才能被视为自动装配的候选者。例如,请考虑以下注解定义:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {
String genre();
Format format();
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MovieQualifier(val genre: String, val format: Format)
在这种情况下,Format 是一个枚举,定义如下:
public enum Format {
VHS, DVD, BLURAY
}
enum class Format {
VHS, DVD, BLURAY
}
要自动装配的字段使用了自定义限定符进行注解,并包含两个属性的值:genre 和 format,如下例所示:
public class MovieRecommender {
@Autowired
@MovieQualifier(format=Format.VHS, genre="Action")
private MovieCatalog actionVhsCatalog;
@Autowired
@MovieQualifier(format=Format.VHS, genre="Comedy")
private MovieCatalog comedyVhsCatalog;
@Autowired
@MovieQualifier(format=Format.DVD, genre="Action")
private MovieCatalog actionDvdCatalog;
@Autowired
@MovieQualifier(format=Format.BLURAY, genre="Comedy")
private MovieCatalog comedyBluRayCatalog;
// ...
}
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 的定义应包含匹配的限定符(qualifier)值。本示例还演示了你可以使用 Bean 的元属性(meta attributes),而不必使用 <qualifier/> 元素。如果存在 <qualifier/> 元素及其属性,则它们具有优先权;但如果未提供此类限定符(如以下示例中的最后两个 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"
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 泛型类型作为一种隐式的限定形式。例如,假设你有以下配置:
@Configuration
public class MyConfiguration {
@Bean
public StringStore stringStore() {
return new StringStore();
}
@Bean
public IntegerStore integerStore() {
return new IntegerStore();
}
}
@Configuration
class MyConfiguration {
@Bean
fun stringStore() = StringStore()
@Bean
fun integerStore() = IntegerStore()
}
假设前述的 Bean 实现了一个泛型接口(例如 Store<String> 和
Store<Integer>),你可以对 @Autowire 接口使用 Store 注解,此时泛型类型将被用作限定符,如下例所示:
@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean
@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean
@Autowired
private lateinit var s1: Store<String> // <String> qualifier, injects the stringStore bean
@Autowired
private lateinit var s2: Store<Integer> // <Integer> qualifier, injects the integerStore bean
泛型限定符在自动装配列表、Map 实例和数组时同样适用。以下示例自动装配了一个泛型 List:
// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;
// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private lateinit var s: List<Store<Integer>>
1.9.6. 使用CustomAutowireConfigurer
CustomAutowireConfigurer
是一个BeanFactoryPostProcessor,它允许您注册自己的自定义限定符注解类型,即使它们没有使用 Spring 的@Qualifier注解进行标注。
以下示例展示了如何使用CustomAutowireConfigurer:
<bean id="customAutowireConfigurer"
class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
<property name="customQualifierTypes">
<set>
<value>example.CustomQualifier</value>
</set>
</property>
</bean>
AutowireCandidateResolver 通过以下方式确定自动装配候选者:
-
每个 bean 定义的
autowire-candidate值 -
default-autowire-candidates元素上可用的任何<beans/>模式 -
@Qualifier注解的存在以及通过CustomAutowireConfigurer注册的任何自定义注解
当存在多个符合条件的自动装配候选 Bean 时,“主候选”(primary)的确定规则如下:如果这些候选 Bean 中恰好有一个 Bean 定义的 primary 属性被设置为 true,则该 Bean 将被选中。
1.9.7. 使用注入@Resource
Spring 还支持在字段或 Bean 属性的 setter 方法上使用 JSR-250 的 @Resource 注解(javax.annotation.Resource)进行注入。
这是 Java EE 中的常见模式:例如,在 JSF 管理的 Bean 和 JAX-WS 端点中。
Spring 也为 Spring 管理的对象支持这种模式。
@Resource 带有一个 name 属性。默认情况下,Spring 将该值解释为要注入的 bean 名称。换句话说,它遵循按名称(by-name)的语义,如下例所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource(name="myMovieFinder") (1)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
| 1 | 此行注入了一个 @Resource。 |
class SimpleMovieLister {
@Resource(name="myMovieFinder") (1)
private lateinit var movieFinder:MovieFinder
}
| 1 | 此行注入了一个 @Resource。 |
如果没有显式指定名称,则默认名称将从字段名或 setter 方法派生。对于字段,它采用字段名称;对于 setter 方法,它采用 bean 的属性名称。以下示例将把名为 movieFinder 的 bean 注入到其 setter 方法中:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
class SimpleMovieLister {
@Resource
private lateinit var movieFinder: MovieFinder
}
使用注解提供的名称将由ApplicationContext解析为 Bean 名称,而CommonAnnotationBeanPostProcessor知晓该ApplicationContext。
如果您显式配置 Spring 的
SimpleJndiBeanFactory,
则可以通过 JNDI 解析这些名称。然而,我们建议依赖默认行为,
并使用 Spring 的 JNDI 查找功能以保持间接性层级。 |
在仅使用 @Resource 且未显式指定名称的特殊情况下,与 @Autowired 类似,@Resource 会查找主要类型匹配项,而不是特定名称的 bean,并解析一些已知的可解析依赖项:即 BeanFactory、ApplicationContext、ResourceLoader、ApplicationEventPublisher 和 MessageSource 接口。
因此,在下面的示例中,customerPreferenceDao 字段首先查找名为 “customerPreferenceDao” 的 bean,然后回退到类型 CustomerPreferenceDao 的主类型匹配:
public class MovieRecommender {
@Resource
private CustomerPreferenceDao customerPreferenceDao;
@Resource
private ApplicationContext context; (1)
public MovieRecommender() {
}
// ...
}
| 1 | context 字段会根据已知的可解析依赖类型 ApplicationContext 进行注入。 |
class MovieRecommender {
@Resource
private lateinit var customerPreferenceDao: CustomerPreferenceDao
@Resource
private lateinit var context: ApplicationContext (1)
// ...
}
| 1 | context 字段会根据已知的可解析依赖类型 ApplicationContext 进行注入。 |
1.9.8. 使用@Value
@Value 通常用于注入外部化属性:
@Component
public class MovieRecommender {
private final String catalog;
public MovieRecommender(@Value("${catalog.name}") String catalog) {
this.catalog = catalog;
}
}
@Component
class MovieRecommender(@Value("\${catalog.name}") private val catalog: String)
使用以下配置:
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }
@Configuration
@PropertySource("classpath:application.properties")
class AppConfig
以及以下的 application.properties 文件:
catalog.name=MovieCatalog
在这种情况下,catalog 参数和字段将等于 MovieCatalog 的值。
Spring 提供了一个默认的宽松型嵌入值解析器。它会尝试解析属性值,如果无法解析,则会将属性名称(例如 ${catalog.name})作为值注入。如果您希望对不存在的值保持严格控制,则应声明一个 PropertySourcesPlaceholderConfigurer bean,如下例所示:
@Configuration
public class AppConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
@Configuration
class AppConfig {
@Bean
fun propertyPlaceholderConfigurer() = PropertySourcesPlaceholderConfigurer()
}
使用 JavaConfig 配置 PropertySourcesPlaceholderConfigurer 时,@Bean 方法必须是 static 的。 |
使用上述配置可确保在任何 ${} 占位符无法解析时,Spring 初始化失败。此外,还可以使用诸如 setPlaceholderPrefix、setPlaceholderSuffix 或 setValueSeparator 等方法来自定义占位符。
Spring Boot 默认配置了一个 PropertySourcesPlaceholderConfigurer bean,该 bean 会从 application.properties 和 application.yml 文件中获取属性。 |
Spring 提供的内置转换器支持允许自动处理简单的类型转换(例如转换为 Integer 或 int)。多个以逗号分隔的值可以无需额外操作自动转换为 String 数组。
可以按如下方式提供默认值:
@Component
public class MovieRecommender {
private final String catalog;
public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
this.catalog = catalog;
}
}
@Component
class MovieRecommender(@Value("\${catalog.name:defaultCatalog}") private val catalog: String)
Spring 的 BeanPostProcessor 在底层使用 ConversionService 来处理将 String 中的 @Value 值转换为目标类型的过程。如果你想为自己的自定义类型提供转换支持,可以像下面示例那样提供你自己的 ConversionService bean 实例:
@Configuration
public class AppConfig {
@Bean
public ConversionService conversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addConverter(new MyCustomConverter());
return conversionService;
}
}
@Configuration
class AppConfig {
@Bean
fun conversionService(): ConversionService {
return DefaultFormattingConversionService().apply {
addConverter(MyCustomConverter())
}
}
}
当 @Value 包含一个 SpEL 表达式 时,其值将在运行时动态计算,如下例所示:
@Component
public class MovieRecommender {
private final String catalog;
public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
this.catalog = catalog;
}
}
@Component
class MovieRecommender(
@Value("#{systemProperties['user.catalog'] + 'Catalog' }") private val catalog: String)
SpEL 还支持使用更复杂的数据结构:
@Component
public class MovieRecommender {
private final Map<String, Integer> countOfMoviesPerCatalog;
public MovieRecommender(
@Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
}
}
@Component
class MovieRecommender(
@Value("#{{'Thriller': 100, 'Comedy': 300}}") private val countOfMoviesPerCatalog: Map<String, Int>)
1.9.9. 使用@PostConstruct和@PreDestroy
CommonAnnotationBeanPostProcessor 不仅识别 @Resource 注解,
还支持 JSR-250 生命周期注解:javax.annotation.PostConstruct 和
javax.annotation.PreDestroy。从 Spring 2.5 开始引入对这些注解的支持,
为初始化回调和销毁回调中描述的生命周期回调机制提供了一种替代方案。
只要在 Spring CommonAnnotationBeanPostProcessor 中注册了 ApplicationContext,
带有这些注解之一的方法就会在其生命周期的相应阶段被调用,
其调用时机与对应的 Spring 生命周期接口方法或显式声明的回调方法相同。
在以下示例中,缓存在初始化时被预先填充,并在销毁时被清空:
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
class CachingMovieLister {
@PostConstruct
fun populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
fun clearMovieCache() {
// clears the movie cache upon destruction...
}
}
有关组合使用各种生命周期机制的效果的详细信息,请参阅 组合生命周期机制。
|
与 |
1.10. 类路径扫描与受管组件
大多数本章中的示例使用XML来指定在Spring容器中生成的每个BeanDefinition配置元数据。前一节
(基于注解的容器配置) 展示了如何通过源代码级别的注解提供大量的配置元数据。然而,在这些示例中,即使如此,“基础”bean定义也明确地在XML文件中定义,而注解仅驱动依赖注入。本节描述了一种选项,即通过扫描类路径隐式检测候选组件。候选组件是指匹配过滤条件且已注册有对应bean定义的类。这消除了使用XML进行bean注册的需求。相反,您可以使用注解(例如,@Component),AspectJ类型表达式或您自己的自定义过滤条件来选择哪些类具有被注册到容器中的bean定义。
|
从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多功能已成为 Spring 框架核心的一部分。这使得你可以使用 Java 代码而非传统的 XML 文件来定义 Bean。请查看 |
1.10.1. @Component以及更多的构造型注解
@Repository 注解是用于标记任何扮演仓库角色(也称为数据访问对象或 DAO)的类的注解。该注解的用途之一是自动进行异常转换,如异常转换中所述。
Spring 提供了更多的构造型注解:@Component、@Service 和
@Controller。@Component 是任何由 Spring 管理的组件的通用刻板印象。@Repository, @Service, 和 @Controller 是 @Component 在更多具体用例中的特化(分别在持久层、服务层和表现层)。因此,您可以使用 @Component 注解您的组件类,但改用 @Repository、@Service 或 @Controller 进行注解,会使您的类更适合由工具处理或与切面关联。例如,这些构造型注解是切点(pointcuts)的理想目标。@Repository, @Service, 和 @Controller 也可以在未来的 Spring 框架版本中携带额外的语义。因此,如果你在选择用于服务层的版本时,在使用@Component或@Service之间进行选择,@Service显然是更好的选择。同样地,如之前所述,@Repository 已经被支持作为自动异常转换在持久层中的标记。
1.10.2. 使用元注解和组合注解
Spring 提供的许多注解都可以在你自己的代码中用作元注解。元注解是指可以应用到另一个注解上的注解。
例如,前面提到的 #beans-stereotype-annotations 注解就通过元注解的方式标注了 @Component,如下例所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component (1)
public @interface Service {
// ...
}
| 1 | @Component 注解使得 @Service 被以与 @Component 相同的方式处理。 |
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component (1)
annotation class Service {
// ...
}
| 1 | @Component 注解使得 @Service 被以与 @Component 相同的方式处理。 |
你还可以组合元注解来创建“组合注解”。例如,Spring MVC 中的 @RestController 注解就是由 @Controller 和 @ResponseBody 组合而成的。
此外,组合注解还可以选择性地重新声明元注解中的属性,以允许进行自定义。当你只想暴露元注解属性的一个子集时,这尤其有用。例如,Spring 的 @SessionScope 注解将作用域名称硬编码为 session,但仍允许自定义 proxyMode。以下代码清单展示了 SessionScope 注解的定义:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
@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:
@Service
@SessionScope
public class SessionScopedService {
// ...
}
@Service
@SessionScope
class SessionScopedService {
// ...
}
你也可以覆盖 proxyMode 的值,如下例所示:
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
class SessionScopedUserService : UserService {
// ...
}
有关更多详细信息,请参阅 Spring 注解编程模型 维基页面。
1.10.3. 自动检测类并注册 Bean 定义
Spring 能够自动检测带有构造型注解的类,并向 BeanDefinition 注册相应的 ApplicationContext 实例。例如,以下两个类符合此类自动检测的条件:
@Service
public class SimpleMovieLister {
private MovieFinder movieFinder;
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Service
class SimpleMovieLister(private val movieFinder: MovieFinder)
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}
@Repository
class JpaMovieFinder : MovieFinder {
// implementation elided for clarity
}
要自动检测这些类并注册相应的 Bean,你需要在你的 @ComponentScan 类上添加
@Configuration 注解,并将 basePackages 属性设置为这两个类的公共父包。(或者,你也可以指定一个以逗号、分号或空格分隔的列表,其中包含每个类各自的父包。)
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig {
// ...
}
为简洁起见,前面的示例本可以使用注解的 value 属性(即 @ComponentScan("org.example"))。 |
以下替代方案使用 XML:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example"/>
</beans>
使用 <context:component-scan> 会隐式启用
<context:annotation-config> 的功能。通常在使用 <context:annotation-config> 时,
无需再包含 <context:component-scan> 元素。 |
|
扫描类路径(classpath)包需要类路径中存在相应的目录条目。当你使用 Ant 构建 JAR 文件时,请确保不要启用 JAR 任务的仅文件(files-only)开关。此外,在某些环境中,基于安全策略,类路径目录可能无法被访问——例如,在 JDK 1.7.0_45 及更高版本上运行的独立应用程序(这需要在你的清单文件中设置 'Trusted-Library'——参见https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。 在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常能按预期正常工作。
但是,请确保您的组件类已在 |
此外,当你使用 AutowiredAnnotationBeanPostProcessor 元素时,CommonAnnotationBeanPostProcessor 和 2 都会被隐式包含。这意味着这两个组件会被自动检测并装配在一起——完全无需在 XML 中提供任何 Bean 配置元数据。
你可以通过将 AutowiredAnnotationBeanPostProcessor 属性的值设为 CommonAnnotationBeanPostProcessor,来禁用 annotation-config 和 false 的注册。 |
1.10.4. 使用过滤器自定义扫描
默认情况下,只有使用 @Component、@Repository、@Service、@Controller、
@Configuration 注解,或使用本身被 @Component 注解的自定义注解标注的类,才会被检测为候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。将它们作为 @ComponentScan 注解的 includeFilters 或 excludeFilters 属性添加(或在 XML 配置中作为 <context:component-scan> 元素的 <context:include-filter /> 或
<context:exclude-filter /> 子元素添加)。每个过滤器元素都需要 type 和 expression 属性。
下表描述了过滤选项:
| 过滤器类型 | 示例表达式 | 描述 |
|---|---|---|
注解(默认) |
|
一个在目标组件的类型级别上存在或元存在的注解。 |
可赋值的 |
|
目标组件可赋值(继承或实现)的类(或接口)。 |
aspectj |
|
一个 AspectJ 类型表达式,用于匹配目标组件。 |
正则表达式 |
|
一个正则表达式,用于匹配目标组件的类名。 |
自定义 |
|
|
以下示例展示了忽略所有 @Repository 注解并改用“存根(stub)”仓库的配置:
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = [Filter(type = FilterType.REGEX, pattern = [".*Stub.*Repository"])],
excludeFilters = [Filter(Repository::class)])
class AppConfig {
// ...
}
以下清单展示了等效的 XML:
<beans>
<context:component-scan base-package="org.example">
<context:include-filter type="regex"
expression=".*Stub.*Repository"/>
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
</beans>
您也可以通过在注解上设置 useDefaultFilters=false 或将 use-default-filters="false" 作为 <component-scan/> 元素的属性来禁用默认过滤器。这将有效禁用对使用 @Component、@Repository、@Service、@Controller、@RestController 或 @Configuration 标注或元标注的类的自动检测。 |
1.10.5. 在组件中定义 Bean 元数据
Spring 组件也可以向容器提供 Bean 定义元数据。你可以使用与在带有 @Bean 注解的类中定义 Bean 元数据时相同的 @Configuration 注解来实现这一点。以下示例展示了如何进行此操作:
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// Component method implementation omitted
}
}
@Component
class FactoryMethodComponent {
@Bean
@Qualifier("public")
fun publicInstance() = TestBean("publicInstance")
fun doWork() {
// Component method implementation omitted
}
}
上述类是一个 Spring 组件,其 doWork() 方法中包含应用程序特定的代码。然而,它同时也提供了一个 Bean 定义,该定义具有一个工厂方法,引用了 publicInstance() 方法。@Bean 注解用于标识该工厂方法以及其他 Bean 定义属性,例如通过 @Qualifier 注解指定的限定符值。可以在方法级别指定的其他注解还包括 @Scope、@Lazy 以及自定义的限定符注解。
除了用于组件初始化之外,你还可以将 @Lazy 注解用在标有 @Autowired 或 @Inject 的注入点上。在此上下文中,它会导致注入一个延迟解析的代理。然而,这种代理方式的功能相当有限。对于更复杂的延迟交互,特别是与可选依赖项结合使用时,我们推荐改用 ObjectProvider<MyTargetBean>。 |
如前所述,支持自动装配字段和方法,并且还额外支持对 @Bean 方法进行自动装配。以下示例展示了如何实现这一点:
@Component
public class FactoryMethodComponent {
private static int i;
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
// use of a custom qualifier and autowiring of method parameters
@Bean
protected TestBean protectedInstance(
@Qualifier("public") TestBean spouse,
@Value("#{privateInstance.age}") String country) {
TestBean tb = new TestBean("protectedInstance", 1);
tb.setSpouse(spouse);
tb.setCountry(country);
return tb;
}
@Bean
private TestBean privateInstance() {
return new TestBean("privateInstance", i++);
}
@Bean
@RequestScope
public TestBean requestScopedInstance() {
return new TestBean("requestScopedInstance", 3);
}
}
@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 自动装配为另一个名为 age 的 bean 上 privateInstance 属性的值。Spring 表达式语言(SpEL)元素通过 #{ <expression> } 语法定义该属性的值。对于 @Value 注解,表达式解析器已预先配置为在解析表达式文本时查找 bean 名称。
从 Spring Framework 4.3 起,您还可以声明一个类型为 InjectionPoint(或其更具体的子类:DependencyDescriptor)的工厂方法参数,以访问触发当前 Bean 创建的请求注入点。
请注意,这仅适用于 Bean 实例的实际创建过程,而不适用于现有实例的注入。因此,此功能对原型(prototype)作用域的 Bean 最有意义。对于其他作用域,工厂方法只会看到在给定作用域中触发新 Bean 实例创建的那个注入点
(例如,触发延迟初始化的单例(lazy singleton)Bean 创建的依赖项)。
在此类场景中,您可以谨慎地使用所提供的注入点元数据。
以下示例展示了如何使用 InjectionPoint:
@Component
public class FactoryMethodComponent {
@Bean @Scope("prototype")
public TestBean prototypeInstance(InjectionPoint injectionPoint) {
return new TestBean("prototypeInstance for " + injectionPoint.getMember());
}
}
@Component
class FactoryMethodComponent {
@Bean
@Scope("prototype")
fun prototypeInstance(injectionPoint: InjectionPoint) =
TestBean("prototypeInstance for ${injectionPoint.member}")
}
普通 Spring 组件中的 @Bean 方法与其在 Spring @Configuration 类中的对应方法处理方式不同。区别在于,@Component 类不会通过 CGLIB 进行增强以拦截方法和字段的调用。CGLIB 代理是这样一种机制:在 @Bean 类的 @Configuration 方法内部调用方法或字段时,会创建指向协作对象的 bean 元数据引用。这些方法并非以普通的 Java 语义被调用,而是通过容器进行调用,从而提供 Spring bean 常规的生命周期管理和代理功能,即使通过编程方式调用 @Bean 方法来引用其他 bean 也是如此。相比之下,在普通的 @Bean 类中调用 @Component 方法内的方法或字段时,则遵循标准的 Java 语义,不会应用特殊的 CGLIB 处理或其他约束。
|
您可以将 对静态
最后,单个类可以包含多个用于同一 bean 的 |
1.10.6. 命名自动检测的组件
当一个组件在扫描过程中被自动检测到时,其 Bean 名称由该扫描器所知的 BeanNameGenerator 策略生成。默认情况下,任何包含名称 @Component 的 Spring 刻板印象注解(@Repository、@Service、@Controller 和 value)都会将该名称提供给相应的 Bean 定义。
如果此类注解未包含名称 value,或者对于任何其他被检测到的组件(例如通过自定义过滤器发现的组件),默认的 bean 名称生成器将返回非限定类名的小写形式。例如,如果检测到以下组件类,则其名称将分别为 myMovieLister 和 movieFinderImpl:
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Service("myMovieLister")
class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
@Repository
class MovieFinderImpl : MovieFinder {
// ...
}
如果您不希望依赖默认的 Bean 命名策略,可以提供自定义的
Bean 命名策略。首先,实现
BeanNameGenerator
接口,并确保包含一个默认的无参构造函数。然后,在配置扫描器时提供完全限定类名,如下面的注解和 Bean 定义示例所示。
如果由于多个自动检测到的组件具有相同的非限定类名(即类名相同但位于不同包中的类)而引发命名冲突,你可能需要配置一个 BeanNameGenerator,使其在生成 Bean 名称时默认使用完全限定类名。从 Spring Framework 5.2.3 起,可以使用位于 FullyQualifiedAnnotationBeanNameGenerator 包中的 org.springframework.context.annotation 来实现此目的。 |
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"], nameGenerator = MyNameGenerator::class)
class AppConfig {
// ...
}
<beans>
<context:component-scan base-package="org.example"
name-generator="org.example.MyNameGenerator" />
</beans>
通常情况下,当其他组件可能需要显式引用该名称时,请考虑使用注解来指定名称。另一方面,当容器负责自动装配(wiring)时,自动生成的名称就已足够。
1.10.7. 为自动检测的组件提供作用域
与一般的 Spring 管理组件一样,自动检测到的组件默认且最常见的作用域是 singleton。然而,有时你需要一个不同的作用域,这可以通过 @Scope 注解来指定。你可以在注解中提供作用域的名称,如下例所示:
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
// ...
}
@Scope 注解仅在具体的 bean 类(针对带注解的组件)或工厂方法(针对 @Bean 方法)上进行内省。与 XML bean 定义不同,这里没有 bean 定义继承的概念,类层级的继承关系在元数据层面是无关紧要的。 |
有关 Spring 上下文中“request”或“session”等 Web 特定作用域的详细信息,请参阅请求、会话、应用和 WebSocket 作用域。与这些作用域的预置注解类似,您也可以使用 Spring 的元注解(meta-annotation)方式组合自己的作用域注解:例如,一个使用@Scope("prototype")进行元注解的自定义注解,还可以选择声明自定义的作用域代理模式。
若要提供自定义的作用域解析策略,而不是依赖基于注解的方法,您可以实现
ScopeMetadataResolver
接口。请务必包含一个默认的无参构造函数。然后在配置扫描器时,您可以提供完全限定的类名,如下面的注解和 Bean 定义示例所示: |
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"], scopeResolver = MyScopeResolver::class)
class AppConfig {
// ...
}
<beans>
<context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>
在使用某些非单例作用域时,可能需要为作用域对象生成代理。其原因在作用域 Bean 作为依赖项一节中有详细说明。
为此,no 元素提供了一个 interfaces 属性。该属性有三个可选值:targetClass、4 和 5。例如,以下配置将生成标准的 JDK 动态代理:
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"], scopedProxy = ScopedProxyMode.INTERFACES)
class AppConfig {
// ...
}
<beans>
<context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>
1.10.8. 使用注解提供限定符元数据
@Qualifier 注解在使用限定符微调基于注解的自动装配一节中进行了讨论。
该部分中的示例展示了如何使用 @Qualifier 注解和自定义限定符注解,
在解析自动装配候选 Bean 时提供细粒度的控制。由于这些示例基于 XML Bean 定义,
限定符元数据是通过在 XML 中 qualifier 元素下使用 meta 或 bean
子元素提供给候选 Bean 定义的。当依赖类路径扫描来自动检测组件时,
你可以在候选类上使用类型级别的注解来提供限定符元数据。以下三个示例演示了这一技术:
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Qualifier("Action")
class ActionMovieCatalog : MovieCatalog
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Genre("Action")
class ActionMovieCatalog : MovieCatalog {
// ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Offline
class CachingMovieCatalog : MovieCatalog {
// ...
}
| 与大多数基于注解的替代方案一样,请记住,注解元数据是绑定到类定义本身的,而使用 XML 则允许同一类型的多个 Bean 在其限定符元数据上有所差异,因为该元数据是按实例(per-instance)提供的,而不是按类(per-class)提供的。 |
1.10.9. 生成候选组件索引
虽然类路径扫描速度非常快,但通过在编译时创建一个静态的候选组件列表,可以进一步提升大型应用的启动性能。在此模式下,所有作为组件扫描目标的模块都必须使用此机制。
您现有的 @ComponentScan 或 <context:component-scan/> 指令必须保持不变,以请求上下文在特定包中扫描候选组件。当 ApplicationContext 检测到此类索引时,它会自动使用该索引,而不是扫描类路径。 |
要生成索引,请为每个包含作为组件扫描指令目标的组件的模块添加一个额外的依赖项。以下示例展示了如何在 Maven 中实现这一点:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.2.25.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>
对于 Gradle 4.5 及更早版本,应将依赖项声明在 compileOnly 配置中,如下例所示:
dependencies {
compileOnly "org.springframework:spring-context-indexer:5.2.25.RELEASE"
}
对于 Gradle 4.6 及更高版本,应将依赖项声明在 annotationProcessor 配置中,如下例所示:
dependencies {
annotationProcessor "org.springframework:spring-context-indexer:5.2.25.RELEASE"
}
spring-context-indexer 工件会生成一个 META-INF/spring.components 文件,并将其包含在 jar 文件中。
在 IDE 中使用此模式时,必须将 spring-context-indexer 注册为注解处理器,以确保在候选组件更新时索引保持最新。 |
当在类路径中找到 META-INF/spring.components 文件时,索引会自动启用。如果某些库(或用例)的索引部分可用,但无法为整个应用程序构建索引,您可以通过将 spring.index.ignore 设置为 true 回退到常规的类路径排列(就像完全不存在索引一样),这可以通过 JVM 系统属性或通过 SpringProperties 机制来实现。 |
1.11. 使用 JSR 330 标准注解
从 Spring 3.0 开始,Spring 提供了对 JSR-330 标准注解(依赖注入)的支持。这些注解的扫描方式与 Spring 注解相同。要使用它们,您需要在类路径中包含相关的 JAR 文件。
|
如果你使用 Maven,
|
1.11.1. 依赖注入与@Inject和@Named
你可以使用 @Autowired 代替 @javax.inject.Inject,如下所示:
import javax.inject.Inject;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.findMovies(...);
// ...
}
}
import javax.inject.Inject
class SimpleMovieLister {
@Inject
lateinit var movieFinder: MovieFinder
fun listMovies() {
movieFinder.findMovies(...)
// ...
}
}
与 @Autowired 一样,你可以在字段级别、方法级别和构造函数参数级别使用 @Inject。此外,你可以将注入点声明为 Provider,从而通过调用 Provider.get() 按需访问作用域较短的 Bean,或延迟访问其他 Bean。以下示例提供了前述示例的一个变体:
import javax.inject.Inject;
import javax.inject.Provider;
public class SimpleMovieLister {
private Provider<MovieFinder> movieFinder;
@Inject
public void setMovieFinder(Provider<MovieFinder> movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.get().findMovies(...);
// ...
}
}
import javax.inject.Inject
class SimpleMovieLister {
@Inject
lateinit var movieFinder: MovieFinder
fun listMovies() {
movieFinder.findMovies(...)
// ...
}
}
如果你想为应注入的依赖项使用限定名称,
应使用 @Named 注解,如下例所示:
import javax.inject.Inject;
import javax.inject.Named;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
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) {
// ...
}
}
public class SimpleMovieLister {
@Inject
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
// ...
}
}
class SimpleMovieLister {
@Inject
var movieFinder: MovieFinder? = null
}
1.11.2. @Named和@ManagedBean:的标准等效项@Component注解
你可以使用 @Component 或 @javax.inject.Named 来代替 javax.annotation.ManagedBean,如下例所示:
import javax.inject.Inject;
import javax.inject.Named;
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
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 也可以以类似的方式使用,如下例所示:
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
import javax.inject.Inject
import javax.inject.Named
@Named
class SimpleMovieLister {
@Inject
lateinit var movieFinder: MovieFinder
// ...
}
当你使用 @Named 或 @ManagedBean 时,可以像使用 Spring 注解一样以完全相同的方式进行组件扫描,如下例所示:
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig {
// ...
}
与 @Component 不同,JSR-330 的 @Named 和 JSR-250 的 ManagedBean
注解是不可组合的。您应使用 Spring 的构造型(stereotype)模型来构建自定义的组件注解。 |
1.11.3. JSR-330 标准注解的局限性
当你使用标准注解时,应了解某些重要功能不可用,如下表所示:
| Spring | javax.inject.* | javax.inject 限制 / 注释 |
|---|---|---|
@Autowired |
@Inject |
|
@Component |
@Named / @ManagedBean |
JSR-330 并未提供一种可组合的模型,仅提供了一种标识命名组件的方式。 |
@Scope("singleton") |
@Singleton |
JSR-330 的默认作用域类似于 Spring 的 |
@Qualifier |
@Qualifier / @Named |
|
@Value |
- |
无等效项 |
@Required |
- |
无等效项 |
@Lazy |
- |
无等效项 |
ObjectFactory |
提供者 |
|
1.12. 基于 Java 的容器配置
本节介绍如何在 Java 代码中使用注解来配置 Spring 容器。内容包括以下主题:
1.12.1. 基本概念:@Bean和@Configuration
Spring 新的 Java 配置支持中的核心组件是使用 @Configuration 注解的类和使用 @Bean 注解的方法。
@Bean 注解用于指示一个方法实例化、配置并初始化一个新的对象,该对象将由 Spring IoC 容器进行管理。对于熟悉 Spring 的 <beans/> XML 配置的开发者来说,@Bean 注解所起的作用与 <bean/> 元素相同。你可以在任何 Spring 的 @Bean 中使用带有 @Component 注解的方法,但它们最常与 @Configuration 配置类一起使用。
使用 @Configuration 注解一个类,表明其主要用途是作为 bean 定义的来源。此外,@Configuration 类允许通过在同一类中调用其他 @Bean 方法来定义 bean 之间的依赖关系。
最简单的 @Configuration 类如下所示:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
@Configuration
class AppConfig {
@Bean
fun myService(): MyService {
return MyServiceImpl()
}
}
前面的 AppConfig 类等同于以下 Spring <beans/> XML:
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
以下章节将深入讨论 @Bean 和 @Configuration 注解。
但首先,我们先介绍使用基于 Java 的配置来创建 Spring 容器的各种方式。
1.12.2. 使用以下方法实例化 Spring 容器AnnotationConfigApplicationContext
以下章节记录了 Spring 3.0 中引入的 Spring AnnotationConfigApplicationContext。这一功能强大的 ApplicationContext 实现不仅能够接受 @Configuration 类作为输入,还能够接受普通的 @Component 类以及带有 JSR-330 元数据注解的类。
当提供 @Configuration 类作为输入时,@Configuration 类本身会被注册为一个 bean 定义,同时该类中所有声明的 @Bean 方法也会被注册为 bean 定义。
当提供 @Component 和 JSR-330 注解的类时,它们会被注册为 bean 定义,并假定在这些类中必要之处使用了诸如 @Autowired 或 @Inject 等依赖注入(DI)元数据。
简单构造
与使用 Spring XML 文件作为实例化 ClassPathXmlApplicationContext 的输入类似,您也可以在实例化 @Configuration 时使用 AnnotationConfigApplicationContext 类作为输入。这样就可以完全不使用 XML 来使用 Spring 容器,如下例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
import org.springframework.beans.factory.getBean
fun main() {
val ctx = AnnotationConfigApplicationContext(AppConfig::class.java)
val myService = ctx.getBean<MyService>()
myService.doStuff()
}
如前所述,AnnotationConfigApplicationContext 并不仅限于仅与 @Configuration 类一起使用。任何带有 @Component 注解或 JSR-330 注解的类都可以作为构造函数的输入参数,如下例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
import org.springframework.beans.factory.getBean
fun main() {
val ctx = AnnotationConfigApplicationContext(MyServiceImpl::class.java, Dependency1::class.java, Dependency2::class.java)
val myService = ctx.getBean<MyService>()
myService.doStuff()
}
前面的示例假定 MyServiceImpl、Dependency1 和 Dependency2 使用了 Spring 依赖注入注解,例如 @Autowired。
以编程方式使用构建容器register(Class<?>…)
你可以使用无参构造函数来实例化一个 AnnotationConfigApplicationContext,
然后通过 register() 方法对其进行配置。这种方法在以编程方式构建
AnnotationConfigApplicationContext 时特别有用。以下
示例展示了如何实现这一点:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
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 类添加注解:
@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig {
// ...
}
| 1 | 此注解启用组件扫描。 |
@Configuration
@ComponentScan(basePackages = ["com.acme"]) (1)
class AppConfig {
// ...
}
| 1 | 此注解启用组件扫描。 |
|
有经验的 Spring 用户可能熟悉以下示例中所示的来自 Spring
|
在前面的示例中,com.acme 包会被扫描,以查找所有带有 @Component 注解的类,并将这些类注册为 Spring 容器中的 bean 定义。AnnotationConfigApplicationContext 提供了 scan(String…) 方法,以支持相同的组件扫描功能,如下例所示:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
fun main() {
val ctx = AnnotationConfigApplicationContext()
ctx.scan("com.acme")
ctx.refresh()
val myService = ctx.getBean<MyService>()
}
请记住,@Configuration 类通过 #beans-meta-annotations 进行了元注解,因此它们是组件扫描的候选对象。在前面的示例中,假设 AppConfig 声明在 com.acme 包(或其任意子包)中,那么在调用 scan() 时它会被自动发现。在调用 refresh() 时,其所有 @Bean 方法都会被处理,并作为 bean 定义注册到容器中。 |
支持基于 Web 的应用程序,使用AnnotationConfigWebApplicationContext
存在一种 WebApplicationContext 类型的 AnnotationConfigApplicationContext,即 AnnotationConfigWebApplicationContext。在配置 Spring 的 ContextLoaderListener Servlet 监听器、Spring MVC 的 DispatcherServlet 等组件时,可以使用此实现。以下 web.xml 片段配置了一个典型的 Spring MVC Web 应用程序(注意其中 contextClass 上下文参数和初始化参数的使用):
<web-app>
<!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
instead of the default XmlWebApplicationContext -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<!-- Configuration locations must consist of one or more comma- or space-delimited
fully-qualified @Configuration classes. Fully-qualified packages may also be
specified for component-scanning -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.AppConfig</param-value>
</context-param>
<!-- Bootstrap the root application context as usual using ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Declare a Spring MVC DispatcherServlet as usual -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
instead of the default XmlWebApplicationContext -->
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<!-- Again, config locations must consist of one or more comma- or space-delimited
and fully-qualified @Configuration classes -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.web.MvcConfig</param-value>
</init-param>
</servlet>
<!-- map all requests for /app/* to the dispatcher servlet -->
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
1.12.3. 使用@Bean注解
@Bean 是一个方法级别的注解,是 XML 中 <bean/> 元素的直接对应物。
该注解支持 <bean/> 提供的一些属性,例如:
-
name.
你可以在使用 @Bean 注解或 @Configuration 注解的类中使用 @Component 注解。
声明 Bean
要声明一个 bean,你可以使用 @Bean 注解对一个方法进行标注。你使用此方法在 ApplicationContext 中注册一个 bean 定义,其类型由该方法的返回值指定。默认情况下,bean 的名称与方法名相同。以下示例展示了一个 @Bean 方法声明:
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
@Configuration
class AppConfig {
@Bean
fun transferService() = TransferServiceImpl()
}
上述配置与以下 Spring XML 配置完全等效:
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
这两种声明都会在 transferService 中创建一个名为 ApplicationContext 的 bean,并将其绑定到一个类型为 TransferServiceImpl 的对象实例上,如下图所示:
transferService -> com.acme.TransferServiceImpl
你也可以将你的 @Bean 方法声明为返回一个接口(或基类)类型,如下例所示:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
@Configuration
class AppConfig {
@Bean
fun transferService(): TransferService {
return TransferServiceImpl()
}
}
然而,这会将高级类型预测的可见性限制为指定的接口类型(TransferService)。此时,容器只有在受影响的单例 bean 被实例化之后,才能获知其完整类型(TransferServiceImpl)。
非延迟初始化的单例 bean 会按照其声明顺序进行实例化,因此,当另一个组件尝试通过未声明的类型进行匹配时(例如 @Autowired TransferServiceImpl),
你可能会看到不同的类型匹配结果——这种匹配只有在 transferService bean 被实例化之后才会成功解析。
如果你始终通过声明的服务接口来引用你的类型,那么你的 @Bean 返回类型可以安全地遵循这一设计决策。然而,对于实现了多个接口的组件,或可能通过其实现类型被引用的组件,更安全的做法是尽可能声明最具体的返回类型(至少要具体到引用该 Bean 的注入点所要求的程度)。 |
Bean 依赖
一个使用 @Bean 注解的方法可以拥有任意数量的参数,用于描述构建该 bean 所需的依赖项。例如,如果我们的 TransferService 需要一个 AccountRepository,我们可以通过方法参数来实现该依赖,如下例所示:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
class AppConfig {
@Bean
fun transferService(accountRepository: AccountRepository): TransferService {
return TransferServiceImpl(accountRepository)
}
}
该解析机制与基于构造函数的依赖注入非常相似。更多详细信息,请参阅相关章节。
接收生命周期回调
使用 @Bean 注解定义的任何类都支持标准的生命周期回调,并可使用 JSR-250 中的 @PostConstruct 和 @PreDestroy 注解。更多详细信息,请参见JSR-250 注解。
标准的 Spring 生命周期回调也完全受支持。如果一个 Bean 实现了 InitializingBean、DisposableBean 或 Lifecycle 接口,容器将调用它们各自对应的方法。
标准的 *Aware 接口集(例如 BeanFactoryAware、
BeanNameAware、
MessageSourceAware、
ApplicationContextAware 等)也得到完全支持。
@Bean 注解支持指定任意的初始化和销毁回调方法,类似于 Spring XML 中 init-method 元素上的 destroy-method 和 bean 属性,如下例所示:
public class BeanOne {
public void init() {
// initialization logic
}
}
public class BeanTwo {
public void cleanup() {
// destruction logic
}
}
@Configuration
public class AppConfig {
@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}
@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
class BeanOne {
fun init() {
// initialization logic
}
}
class BeanTwo {
fun cleanup() {
// destruction logic
}
}
@Configuration
class AppConfig {
@Bean(initMethod = "init")
fun beanOne() = BeanOne()
@Bean(destroyMethod = "cleanup")
fun beanTwo() = BeanTwo()
}
|
默认情况下,使用 Java 配置定义的 Bean,如果包含公共的 你可能希望对通过 JNDI 获取的资源默认执行此操作,因为其生命周期是在应用程序外部管理的。特别是对于 以下示例展示了如何防止对 Java
Kotlin
此外,对于 |
对于上述示例中的 BeanOne,在构造期间直接调用 init() 方法同样是有效的,如下例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
// ...
}
@Configuration
class AppConfig {
@Bean
fun beanOne() = BeanOne().apply {
init()
}
// ...
}
| 当你直接使用 Java 编程时,你可以对你的对象执行任何操作,并不总是需要依赖容器的生命周期。 |
指定 Bean 作用域
Spring 提供了 @Scope 注解,以便你可以指定 Bean 的作用域。
使用@Scope注解
您可以指定使用 @Bean 注解定义的 Bean 应具有特定的作用域。您可以使用在Bean 作用域部分中指定的任何标准作用域。
默认作用域是 singleton,但你可以使用 @Scope 注解来覆盖它,如下例所示:
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
@Configuration
class MyConfiguration {
@Bean
@Scope("prototype")
fun encryptor(): Encryptor {
// ...
}
}
@Scope和scoped-proxy
Spring 通过作用域代理(scoped proxies)提供了一种便捷的方式来处理作用域依赖。
在使用 XML 配置时,创建此类代理最简单的方法是使用 <aop:scoped-proxy/> 元素。
当使用 Java 配置并通过 @Scope 注解配置 Bean 时,可通过 proxyMode 属性提供同等支持。
默认值为 ScopedProxyMode.DEFAULT,通常表示除非在组件扫描(component-scan)指令级别配置了其他默认值,否则不应创建作用域代理。
您可以指定 ScopedProxyMode.TARGET_CLASS、ScopedProxyMode.INTERFACES 或 ScopedProxyMode.NO。
如果你将 XML 参考文档中的作用域代理示例(参见
作用域代理)移植到我们使用 Java 的 @Bean 中,
其代码类似于以下内容:
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
return new UserPreferences();
}
@Bean
public Service userService() {
UserService service = new SimpleUserService();
// a reference to the proxied userPreferences bean
service.setUserPreferences(userPreferences());
return service;
}
// 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 属性覆盖此功能,如下例所示:
@Configuration
public class AppConfig {
@Bean("myThing")
public Thing thing() {
return new Thing();
}
}
@Configuration
class AppConfig {
@Bean("myThing")
fun thing() = Thing()
}
Bean 别名
正如在命名 Bean中所讨论的,有时希望为单个 Bean 赋予多个名称,这又称为 Bean 别名。为此,name 注解的 @Bean 属性接受一个字符串数组。以下示例展示了如何为一个 Bean 设置多个别名:
@Configuration
public class AppConfig {
@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}
@Configuration
class AppConfig {
@Bean("dataSource", "subsystemA-dataSource", "subsystemB-dataSource")
fun dataSource(): DataSource {
// instantiate, configure and return DataSource bean...
}
}
Bean 描述
有时,提供更详细的 Bean 文本描述会很有帮助。当 Bean 被暴露出来(例如通过 JMX)用于监控目的时,这一点尤其有用。
要为 @Bean 添加描述,您可以使用
@Description
注解,如下例所示:
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}
}
@Configuration
class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
fun thing() = Thing()
}
1.12.4. 使用@Configuration注解
@Configuration 是一个类级别的注解,用于指示某个对象是 Bean 定义的来源。@Configuration 类通过带有 @Bean 注解的方法来声明 Bean。在 @Configuration 类上调用 @Bean 方法也可用于定义 Bean 之间的依赖关系。有关一般性介绍,请参阅 基本概念:@Bean 和 @Configuration。
注入 Bean 之间的依赖关系
当 Bean 之间存在依赖关系时,表达这种依赖关系就像让一个 Bean 方法调用另一个 Bean 方法一样简单,如下例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
@Configuration
class AppConfig {
@Bean
fun beanOne() = BeanOne(beanTwo())
@Bean
fun beanTwo() = BeanTwo()
}
在前面的示例中,beanOne 通过构造函数注入获得对 beanTwo 的引用。
这种声明 Bean 之间依赖关系的方法仅在 @Bean 方法被声明于 @Configuration 类中时才有效。您不能通过普通的 @Component 类来声明 Bean 之间的依赖关系。 |
查找方法注入
如前所述,查找方法注入是一项高级功能,应尽量少用。它适用于单例作用域的 bean 依赖于原型作用域 bean 的场景。使用 Java 进行此类配置为实现该模式提供了一种自然的方式。以下示例展示了如何使用查找方法注入:
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
// grab a new instance of the appropriate Command interface
val command = createCommand()
// set the state on the (hopefully brand new) Command instance
command.setState(commandState)
return command.execute()
}
// okay... but where is the implementation of this method?
protected abstract fun createCommand(): Command
}
通过使用 Java 配置,您可以创建一个 CommandManager 的子类,在该子类中,抽象方法 createCommand() 被重写,以查找一个新的(原型)命令对象。以下示例展示了如何实现这一点:
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
AsyncCommand command = new AsyncCommand();
// inject dependencies here as required
return command;
}
@Bean
public CommandManager commandManager() {
// return new anonymous implementation of CommandManager with createCommand()
// overridden to return a new prototype Command object
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
}
}
@Bean
@Scope("prototype")
fun asyncCommand(): AsyncCommand {
val command = AsyncCommand()
// inject dependencies here as required
return command
}
@Bean
fun commandManager(): CommandManager {
// return new anonymous implementation of CommandManager with createCommand()
// overridden to return a new prototype Command object
return object : CommandManager() {
override fun createCommand(): Command {
return asyncCommand()
}
}
}
关于基于 Java 的配置内部工作原理的更多信息
请考虑以下示例,其中展示了带有 @Bean 注解的方法被调用了两次:
@Configuration
public class AppConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}
@Configuration
class AppConfig {
@Bean
fun clientService1(): ClientService {
return ClientServiceImpl().apply {
clientDao = clientDao()
}
}
@Bean
fun clientService2(): ClientService {
return ClientServiceImpl().apply {
clientDao = clientDao()
}
}
@Bean
fun clientDao(): ClientDao {
return ClientDaoImpl()
}
}
clientDao() 方法在 clientService1() 中被调用了一次,在 clientService2() 中也被调用了一次。
由于该方法会创建一个新的 ClientDaoImpl 实例并返回它,通常你会预期得到两个实例(每个服务各一个)。这显然会带来问题:在 Spring 中,实例化的 Bean 默认具有 singleton(单例)作用域。这就是魔法所在:所有 @Configuration 类在启动时都会通过 CGLIB 被动态生成子类。在这个子类中,子方法在调用父类方法并创建新实例之前,会首先检查容器中是否存在任何已缓存的(作用域内的)Bean。
| 根据您的 Bean 的作用域不同,其行为可能会有所不同。这里我们讨论的是单例(singleton)。 |
|
从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的类路径中,因为 CGLIB 类已被重新打包到 |
|
由于 CGLIB 在启动时动态添加功能,因此存在一些限制。特别是,配置类不能是 final 的。然而,从 4.3 版本开始,配置类可以包含任意构造函数,包括使用 如果你希望避免 CGLIB 带来的任何限制,可以考虑将你的 |
1.12.5. 组合基于 Java 的配置
Spring 的基于 Java 的配置功能允许你组合注解,从而降低配置的复杂性。
使用@Import注解
正如在 Spring XML 文件中使用 <import/> 元素来帮助模块化配置一样,@Import 注解允许从另一个配置类中加载 @Bean 定义,如下例所示:
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
@Configuration
class ConfigA {
@Bean
fun a() = A()
}
@Configuration
@Import(ConfigA::class)
class ConfigB {
@Bean
fun b() = B()
}
现在,在实例化上下文时,不再需要同时指定 ConfigA.class 和 ConfigB.class,而只需显式提供 ConfigB,如下例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
import org.springframework.beans.factory.getBean
fun main() {
val ctx = AnnotationConfigApplicationContext(ConfigB::class.java)
// now both beans A and B will be available...
val a = ctx.getBean<A>()
val b = ctx.getBean<B>()
}
这种方法简化了容器的实例化,因为只需要处理一个类,而不必在构建过程中记住大量可能存在的@Configuration类。
从 Spring Framework 4.2 起,@Import 注解也支持引用普通的组件类,这与 AnnotationConfigApplicationContext.register 方法类似。
如果你希望通过少量配置类作为入口点来显式定义所有组件,从而避免使用组件扫描,这一特性将特别有用。 |
在导入的组件上注入依赖@Bean定义
前面的示例可以正常工作,但过于简单。在大多数实际场景中,Bean 在不同的配置类之间会相互依赖。使用 XML 时,这并不是一个问题,因为不涉及编译器,你可以声明 ref="someBean",并信任 Spring 在容器初始化期间自行解析。而使用 @Configuration 类时,Java 编译器会对配置模型施加约束,即对其他 Bean 的引用必须符合有效的 Java 语法。
幸运的是,解决这个问题很简单。正如我们之前所讨论的,
@Bean 方法可以拥有任意数量的参数,用于描述该 bean 的依赖项。请考虑以下更贴近实际的场景:其中包含多个 @Configuration
类,每个类都依赖于其他类中声明的 bean:
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
import org.springframework.beans.factory.getBean
@Configuration
class ServiceConfig {
@Bean
fun transferService(accountRepository: AccountRepository): TransferService {
return TransferServiceImpl(accountRepository)
}
}
@Configuration
class RepositoryConfig {
@Bean
fun accountRepository(dataSource: DataSource): AccountRepository {
return JdbcAccountRepository(dataSource)
}
}
@Configuration
@Import(ServiceConfig::class, RepositoryConfig::class)
class SystemTestConfig {
@Bean
fun dataSource(): DataSource {
// return new DataSource
}
}
fun main() {
val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
// everything wires up across configuration classes...
val transferService = ctx.getBean<TransferService>()
transferService.transfer(100.00, "A123", "C456")
}
还有另一种方式可以实现相同的结果。请记住,@Configuration 类最终也只是容器中的另一个 bean:这意味着它们可以像其他任何 bean 一样利用 @Autowired 和 @Value 注入以及其他特性。
|
请确保以这种方式注入的依赖项仅限于最简单的类型。 此外,要特别小心通过 |
以下示例展示了如何将一个 bean 自动装配到另一个 bean:
@Configuration
public class ServiceConfig {
@Autowired
private AccountRepository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
private final DataSource dataSource;
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
import org.springframework.beans.factory.getBean
@Configuration
class ServiceConfig {
@Autowired
lateinit var accountRepository: AccountRepository
@Bean
fun transferService(): TransferService {
return TransferServiceImpl(accountRepository)
}
}
@Configuration
class RepositoryConfig(private val dataSource: DataSource) {
@Bean
fun accountRepository(): AccountRepository {
return JdbcAccountRepository(dataSource)
}
}
@Configuration
@Import(ServiceConfig::class, RepositoryConfig::class)
class SystemTestConfig {
@Bean
fun dataSource(): DataSource {
// return new DataSource
}
}
fun main() {
val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
// everything wires up across configuration classes...
val transferService = ctx.getBean<TransferService>()
transferService.transfer(100.00, "A123", "C456")
}
@Configuration 类中的构造函数注入仅在 Spring Framework 4.3 及更高版本中受支持。另请注意,如果目标 bean 仅定义了一个构造函数,则无需指定 @Autowired。 |
在前述场景中,使用 @Autowired 能够很好地工作并提供所需的模块化,但要确切判断自动装配的 bean 定义是在哪里声明的,仍然有些模糊。例如,作为一名正在查看 ServiceConfig 的开发人员,你如何确切知道 @Autowired AccountRepository 这个 bean 是在哪里声明的呢?这一点在代码中并未显式体现,而这可能完全可以接受。请记住,Spring Tools for Eclipse 提供了可视化工具,可以生成图表来展示所有组件是如何连接的,这或许已经足够满足你的需求。此外,你的 Java IDE 也能轻松找到 AccountRepository 类型的所有声明和使用位置,并快速向你展示返回该类型的 @Bean 方法所在的位置。
在无法接受这种歧义性、并且希望直接从 IDE 中实现从一个 @Configuration 类导航到另一个类的情况下,请考虑自动装配配置类本身。以下示例展示了如何实现这一点:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@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 类在一定程度上得到缓解。请考虑以下示例:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
public interface RepositoryConfig {
@Bean
AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}
@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
import org.springframework.beans.factory.getBean
@Configuration
class ServiceConfig {
@Autowired
private lateinit var repositoryConfig: RepositoryConfig
@Bean
fun transferService(): TransferService {
return TransferServiceImpl(repositoryConfig.accountRepository())
}
}
@Configuration
interface RepositoryConfig {
@Bean
fun accountRepository(): AccountRepository
}
@Configuration
class DefaultRepositoryConfig : RepositoryConfig {
@Bean
fun accountRepository(): AccountRepository {
return JdbcAccountRepository(...)
}
}
@Configuration
@Import(ServiceConfig::class, DefaultRepositoryConfig::class) // import the concrete config!
class SystemTestConfig {
@Bean
fun dataSource(): DataSource {
// return DataSource
}
}
fun main() {
val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
val transferService = ctx.getBean<TransferService>()
transferService.transfer(100.00, "A123", "C456")
}
现在,ServiceConfig 与具体的 DefaultRepositoryConfig 实现之间实现了松耦合,并且 IDE 内置的工具仍然有效:你可以轻松获取 RepositoryConfig 实现类的类型层次结构。通过这种方式,浏览 @Configuration 类及其依赖关系就与通常浏览基于接口的代码的过程没有区别了。
如果你想影响某些 Bean 的启动创建顺序,可以考虑将其中一些声明为 @Lazy(在首次访问时创建,而不是在启动时创建),或者使用 @DependsOn 注解指定依赖于其他特定的 Bean(确保在当前 Bean 创建之前,先创建这些指定的其他 Bean,这超出了当前 Bean 直接依赖关系所隐含的顺序)。 |
条件包含@Configuration类或@Bean方法
根据某些任意的系统状态,有条件地启用或禁用整个 @Configuration 类,甚至单个 @Bean 方法,通常是很有用的。一个常见的例子是使用 @Profile 注解,仅在 Spring Environment 中启用了特定配置文件时才激活相应的 Bean(详见Bean 定义配置文件)。
@Profile 注解实际上是通过使用一个更灵活的注解实现的,该注解称为 @Conditional。
@Conditional 注解指示在注册 @Bean 之前应咨询特定的 org.springframework.context.annotation.Condition 实现。
Condition 接口的实现提供了一个 matches(…) 方法,该方法返回 true 或 false。例如,以下代码清单展示了 Condition 注解实际使用的 @Profile 实现:
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Read the @Profile annotation attributes
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
// Read the @Profile annotation attributes
val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name)
if (attrs != null) {
for (value in attrs["value"]!!) {
if (context.environment.acceptsProfiles(Profiles.of(*value as Array<String>))) {
return true
}
}
return false
}
return true
}
请参阅 @Conditional
javadoc 以获取更多详情。
结合 Java 和 XML 配置
Spring 的 @Configuration 类支持并非旨在完全替代 Spring XML。某些功能(例如 Spring XML 命名空间)仍然是配置容器的理想方式。在 XML 使用起来更方便或必不可少的情况下,您可以选择以下两种方式之一:要么采用“以 XML 为中心”的方式,例如使用 ClassPathXmlApplicationContext 来实例化容器;要么采用“以 Java 为中心”的方式,使用 AnnotationConfigApplicationContext 并结合 @ImportResource 注解按需导入 XML。
以 XML 为中心的使用@Configuration类
或许更倾向于通过 XML 来引导 Spring 容器,并以临时的方式包含 @Configuration 类。例如,在一个已大量使用 Spring XML 的现有代码库中,按需创建 @Configuration 类并从现有的 XML 文件中引入它们会更加简便。本节稍后将介绍在这种“以 XML 为中心”的场景中使用 @Configuration 类的选项。
请记住,@Configuration 个类最终是容器中的 Bean 定义。在本系列示例中,我们创建了一个名为 AppConfig 的 @Configuration 类,并将其作为 <bean/> 定义包含在 system-test-config.xml 中。由于启用了 <context:annotation-config/>,容器能够识别 @Configuration 注解,并正确处理在 AppConfig 中声明的 @Bean 方法。
以下示例展示了一个普通的 Java 配置类:
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}
@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=
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
fun main() {
val ctx = ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml")
val transferService = ctx.getBean<TransferService>()
// ...
}
在 system-test-config.xml 文件中,AppConfig 的 <bean/> 没有声明 id 元素。虽然这样做是可以接受的,但却是不必要的,因为没有其他 bean 引用它,而且也不太可能通过名称从容器中显式获取它。
同样,DataSource bean 始终是按类型自动装配的,因此显式指定 bean 的 id 并非严格必需。 |
由于 @Configuration 注解本身通过元注解的方式标注了 @Component,因此带有 @Configuration 注解的类会自动成为组件扫描的候选对象。使用与前一个示例中描述的相同场景,我们可以重新定义 system-test-config.xml 以利用组件扫描功能。
请注意,在这种情况下,我们无需显式声明
<context:annotation-config/>,因为 <context:component-scan/> 已经启用了相同的功能。
以下示例展示了修改后的 system-test-config.xml 文件:
<beans>
<!-- picks up and registers AppConfig as a bean definition -->
<context:component-scan base-package="com.acme"/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
@Configuration以类为中心的 XML 使用方式,适用于@ImportResource
在以 @Configuration 类作为容器配置主要机制的应用程序中,通常仍有必要使用至少少量的 XML。在这些场景下,你可以使用 @ImportResource 注解,并仅定义所需的最少 XML 内容。这样做可以实现一种“以 Java 为中心”的容器配置方式,并将 XML 的使用降至最低限度。以下示例(包含一个配置类、一个定义 bean 的 XML 文件、一个属性文件以及 main 类)展示了如何使用 @ImportResource 注解来实现按需使用 XML 的“以 Java 为中心”的配置:
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
}
@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=
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
import org.springframework.beans.factory.getBean
fun main() {
val ctx = AnnotationConfigApplicationContext(AppConfig::class.java)
val transferService = ctx.getBean<TransferService>()
// ...
}
1.13. 环境抽象
Environment 接口是集成在容器中的一个抽象,用于建模应用环境的两个关键方面:配置文件(profiles)和 属性(properties)。
配置文件(profile)是一个命名的、逻辑上的 Bean 定义分组,仅在指定配置文件处于激活状态时才会向容器注册。无论 Bean 是通过 XML 还是注解定义的,都可以将其分配给某个配置文件。Environment 对象与配置文件相关的作用在于:确定当前哪些配置文件(如果有的话)处于激活状态,以及哪些配置文件(如果有的话)应默认处于激活状态。
属性在几乎所有应用程序中都扮演着重要角色,其来源多种多样:属性文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、临时的Properties对象、Map对象等等。Environment对象与属性相关的作用是为用户提供一个便捷的服务接口,用于配置属性源并从中解析属性。
1.13.1. Bean 定义配置文件
Bean 定义配置文件(Bean definition profiles)在核心容器中提供了一种机制,允许在不同的环境中注册不同的 Bean。这里的“环境”一词对不同用户可能有不同的含义,而该特性可适用于多种使用场景,包括:
-
在开发环境中使用内存数据源,而在 QA 或生产环境中则通过 JNDI 查找同一个数据源。
-
仅在将应用程序部署到性能环境时注册监控基础设施。
-
为客户 A 与客户 B 的部署注册自定义实现的 Bean。
考虑一个实际应用中的第一个使用场景,该场景需要一个DataSource。在测试环境中,配置可能类似于以下内容:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build()
}
现在考虑如何将此应用程序部署到 QA 或生产环境中,假设该应用程序的数据源已注册到生产应用服务器的 JNDI 目录中。我们的 dataSource Bean 现在如下所示:
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
问题在于如何根据当前环境在使用这两种变体之间进行切换。多年来,Spring 用户已经设计出多种实现方式,通常依赖于系统环境变量与包含 <import/> 占位符的 XML ${placeholder} 语句相结合,这些占位符会根据环境变量的值解析为正确的配置文件路径。Bean 定义 Profiles 是 Spring 核心容器提供的一项功能,用于解决这一问题。
如果我们对前面示例中所示的环境相关 Bean 定义用例进行泛化,最终就会产生一种需求:在某些上下文中注册特定的 Bean 定义,而在其他上下文中则不注册。可以说,在情况 A 中,你希望注册一组特定的 Bean 定义(即某个配置文件),而在情况 B 中则注册另一组不同的配置文件。我们首先更新配置以反映这一需求。
使用@Profile
@Profile 注解允许您指示组件在一个或多个指定配置文件激活时有资格进行注册。使用我们前面的示例,我们可以将 dataSource 配置重写如下:
@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
@Configuration
@Profile("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()
}
}
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
@Configuration
@Profile("production")
class JndiDataConfig {
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
}
如前所述,使用 @Bean 方法时,通常会选择采用编程式 JNDI 查找,即使用 Spring 提供的 JndiTemplate/JndiLocatorDelegate 辅助类,或如前所示直接使用 JNDI 的 InitialContext,而不是使用 JndiObjectFactoryBean 变体,因为后者会迫使你将返回类型声明为 FactoryBean 类型。 |
配置文件字符串可以包含一个简单的配置文件名称(例如,production)或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,production & us-east)。配置文件表达式中支持以下运算符:
-
!: 配置文件的逻辑“非” -
&:配置文件的逻辑“与” -
|: 配置文件的逻辑“或”
你不能在不使用括号的情况下混合使用 & 和 | 运算符。例如,
production & us-east | eu-central 不是一个有效的表达式。它必须写成
production & (us-east | eu-central)。 |
您可以将 @Profile 用作元注解,以创建自定义的组合注解。以下示例定义了一个自定义的 @Production 注解,您可以将其作为 @Profile("production") 的直接替代品使用:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
如果 @Configuration 类被标记为 @Profile,则除非一个或多个指定的配置文件处于激活状态,否则与该类关联的所有 @Bean 方法和 @Import 注解都将被绕过。如果 @Component 或 @Configuration 类被标记为 @Profile({"p1", "p2"}),则除非配置文件 'p1' 或 'p2' 已被激活,否则该类不会被注册或处理。如果给定的配置文件前缀带有非运算符(!),则仅当该配置文件未激活时,带注解的元素才会被注册。例如,给定 @Profile({"p1", "!p2"}),如果配置文件 'p1' 处于激活状态,或者配置文件 'p2' 未处于激活状态,则将发生注册。 |
@Profile 也可以在方法级别上声明,以仅包含配置类中的某一个特定 bean(例如,用于某个特定 bean 的替代变体),如下例所示:
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") (1)
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Bean("dataSource")
@Profile("production") (2)
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
| 1 | standaloneDataSource 方法仅在 development 配置文件中可用。 |
| 2 | jndiDataSource 方法仅在 production 配置文件中可用。 |
@Configuration
class AppConfig {
@Bean("dataSource")
@Profile("development") (1)
fun standaloneDataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build()
}
@Bean("dataSource")
@Profile("production") (2)
fun jndiDataSource() =
InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
| 1 | standaloneDataSource 方法仅在 development 配置文件中可用。 |
| 2 | jndiDataSource 方法仅在 production 配置文件中可用。 |
|
在 如果你想定义具有不同 profile 条件的替代 bean,可以使用不同的 Java 方法名,并通过 |
XML Bean 定义配置文件
其对应的 XML 形式是 profile 元素的 <beans> 属性。我们前面的示例配置可以重写为以下两个 XML 文件:
<beans profile="development"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
也可以避免这种拆分,而是在同一个文件中嵌套 <beans/> 元素,如下例所示:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="development">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
spring-bean.xsd 已被限制为仅允许此类元素出现在文件的末尾。这应在不造成 XML 文件杂乱的前提下,提供所需的灵活性。
|
XML 对应的配置不支持前面描述的 profile 表达式。然而,可以通过使用
在前面的示例中,如果 |
激活配置文件
现在我们已经更新了配置,但仍需告诉 Spring 哪个配置文件(profile)处于激活状态。如果我们现在启动示例应用程序,将会看到抛出一个 NoSuchBeanDefinitionException 异常,因为容器找不到名为 dataSource 的 Spring Bean。
可以通过多种方式激活一个配置文件(profile),但最直接的方式是通过 Environment 获取的 ApplicationContext API 以编程方式进行激活。以下示例展示了如何实现这一点:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
val ctx = AnnotationConfigApplicationContext().apply {
environment.setActiveProfiles("development")
register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
refresh()
}
此外,您还可以通过 spring.profiles.active 属性以声明方式激活配置中心(profiles),该属性可以通过系统环境变量、JVM 系统属性、web.xml 中的 Servlet 上下文参数进行指定,甚至可以作为 JNDI 中的一个条目(请参阅 PropertySource 抽象)。在集成测试中,可以通过在 spring-test 模块中使用 @ActiveProfiles 注解来声明激活的配置中心(请参阅 使用环境配置中心的上下文配置)。
请注意,Profile 并非“非此即彼”的选项。你可以同时激活多个 Profile。在编程方式下,你可以向 setActiveProfiles() 方法传入多个 Profile 名称,该方法接受 String… 可变参数。以下示例激活了多个 Profile:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")
以声明方式,spring.profiles.active 可以接受一个以逗号分隔的配置文件名称列表,如下例所示:
-Dspring.profiles.active="profile1,profile2"
默认配置档案
默认配置文件(default profile)表示默认启用的配置文件。请考虑以下示例:
@Configuration
@Profile("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build();
}
}
@Configuration
@Profile("default")
class DefaultDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build()
}
}
如果没有激活任何配置文件,则会创建 dataSource。你可以将此视为为一个或多个 bean 提供默认定义的一种方式。如果启用了任意配置文件,则默认配置文件将不生效。
你可以通过在 setDefaultProfiles() 上调用 Environment 方法,或者以声明方式使用 spring.profiles.default 属性来更改默认配置文件的名称。
1.13.2. PropertySource抽象
Spring 的 Environment 抽象提供了在可配置的属性源层次结构上进行搜索的操作。请参见以下代码清单:
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
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())。
这些默认属性源适用于 StandardEnvironment,用于独立应用程序。StandardServletEnvironment
会填充额外的默认属性源,包括 Servlet 配置和 Servlet 上下文参数。它还可以选择性地启用 JndiPropertySource。
详情请参阅 Javadoc。 |
具体来说,当你使用 StandardEnvironment 时,如果在运行时存在名为 env.containsProperty("my-property") 的系统属性或 my-property 环境变量,则调用 my-property 将返回 true。
|
执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用 对于一个常见的
|
最重要的是,整个机制是可配置的。也许你有一个自定义的属性来源,希望将其集成到此搜索中。为此,请实现并实例化你自己的 PropertySource,并将其添加到当前 PropertySources 的 Environment 集合中。以下示例展示了如何实现这一点:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())
在前面的代码中,MyPropertySource 已以最高优先级被添加到搜索中。如果它包含 my-property 属性,则该属性将被检测并返回,优先于任何其他 PropertySource 中的 my-property 属性。MutablePropertySources
API 提供了多种方法,可用于精确操作属性源集合。
1.13.3. 使用@PropertySource
@PropertySource 注解提供了一种便捷且声明式的机制,用于向 Spring 的 Environment 添加 PropertySource。
给定一个名为 app.properties 的文件,其中包含键值对 testbean.name=myTestBean,
以下 @Configuration 类以如下方式使用 @PropertySource:
调用 testBean.getName() 将返回 myTestBean:
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
@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 占位符都会根据已注册到环境中的属性源进行解析,如下例所示:
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
@Configuration
@PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties")
class AppConfig {
@Autowired
private lateinit var env: Environment
@Bean
fun testBean() = TestBean().apply {
name = env.getProperty("testbean.name")!!
}
}
假设 my.placeholder 已存在于某个已注册的属性源中(例如系统属性或环境变量),则该占位符将被解析为对应的值。如果不存在,则使用 default/path 作为默认值。如果未指定默认值且属性无法被解析,则会抛出一个 IllegalArgumentException 异常。
@PropertySource 注解根据 Java 8 的约定是可重复的。
然而,所有此类 @PropertySource 注解必须在同一层级上声明,
要么直接声明在配置类上,要么作为元注解包含在同一个自定义注解中。
不建议混合使用直接注解和元注解,因为直接注解会有效地覆盖元注解。 |
1.13.4. 语句中的占位符解析
历史上,元素中占位符的值只能通过 JVM 系统属性或环境变量进行解析。如今情况已不再如此。由于 Environment 抽象在整个容器中得到了集成,因此很容易通过它来路由占位符的解析过程。这意味着你可以按任意方式配置解析流程:可以更改系统属性和环境变量的搜索优先级,甚至完全移除它们;也可以根据需要将自己的属性源添加到解析过程中。
具体来说,只要 customer 属性在 Environment 中可用,以下语句无论该属性在何处定义都能正常工作:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>
1.14. 注册一个LoadTimeWeaver
LoadTimeWeaver 被 Spring 用于在类加载到 Java 虚拟机(JVM)时动态转换这些类。
要启用加载时织入(load-time weaving),您可以将 @EnableLoadTimeWeaving 注解添加到您的某个 @Configuration 类中,如下例所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
@Configuration
@EnableLoadTimeWeaving
class AppConfig
或者,对于 XML 配置,您可以使用 context:load-time-weaver 元素:
<beans>
<context:load-time-weaver/>
</beans>
一旦为 ApplicationContext 完成配置,该 ApplicationContext 中的任何 Bean 都可以实现 LoadTimeWeaverAware,从而获得对加载时织入器实例的引用。这在结合 Spring 的 JPA 支持 时特别有用,因为 JPA 类转换可能需要加载时织入。
请参阅 LocalContainerEntityManagerFactoryBean 的 Javadoc 以获取更多详细信息。有关 AspectJ 加载时织入的更多信息,请参阅 Spring Framework 中使用 AspectJ 进行加载时织入。
1.15. 其他功能ApplicationContext
正如在章节介绍中所讨论的,org.springframework.beans.factory包提供了用于以编程方式管理和操作 Bean 的基本功能。org.springframework.context包增加了ApplicationContext接口,该接口除了扩展其他接口以提供更多面向应用框架风格的功能外,还扩展了BeanFactory接口。许多人完全以声明式的方式使用ApplicationContext,甚至不以编程方式创建它,而是依赖诸如ContextLoader之类的支持类,在 Java EE Web 应用程序的正常启动过程中自动实例化一个ApplicationContext。
为了以更加面向框架的风格增强 BeanFactory 的功能,context 包还提供了以下功能:
-
通过
MessageSource接口以国际化(i18n)风格访问消息。 -
通过
ResourceLoader接口访问资源,例如 URL 和文件。 -
事件发布,即通过
ApplicationListener接口向实现ApplicationEventPublisher接口的 Bean 发布事件。 -
通过
HierarchicalBeanFactory接口加载多个(分层的)上下文,使每个上下文专注于某一层,例如应用程序的 Web 层。
1.15.1. 使用进行国际化MessageSource
ApplicationContext 接口扩展了一个名为 MessageSource 的接口,因此提供了国际化(“i18n”)功能。Spring 还提供了 HierarchicalMessageSource 接口,该接口可以分层解析消息。这些接口共同构成了 Spring 实现消息解析的基础。这些接口中定义的方法包括:
-
String getMessage(String code, Object[] args, String default, Locale loc):用于从MessageSource中检索消息的基本方法。当在指定的区域设置(locale)下未找到对应消息时,将使用默认消息。传入的任何参数都将作为替换值,利用标准库提供的MessageFormat功能进行处理。 -
String getMessage(String code, Object[] args, Locale loc):本质上与前一个方法相同,但有一个区别:无法指定默认消息。如果找不到该消息,则会抛出NoSuchMessageException异常。 -
String getMessage(MessageSourceResolvable resolvable, Locale locale):前面方法中使用的所有属性也都被封装在一个名为MessageSourceResolvable的类中,你可以将该类与本方法一起使用。
当加载 ApplicationContext 时,它会自动在上下文中搜索一个名为 MessageSource 的 messageSource bean。如果找到这样的 bean,所有对上述方法的调用都会被委托给该消息源。如果没有找到消息源,ApplicationContext 会尝试在其父上下文中查找是否存在同名的 bean。如果存在,则使用该 bean 作为 MessageSource。如果 ApplicationContext 无法找到任何消息源,则会实例化一个空的 DelegatingMessageSource,以便能够接受对上述方法的调用。
Spring 提供了三种 MessageSource 实现:ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 和 StaticMessageSource。它们都实现了 HierarchicalMessageSource 接口,以支持嵌套消息功能。StaticMessageSource 很少使用,但它提供了以编程方式向消息源添加消息的途径。以下示例展示了 ResourceBundleMessageSource:
<beans>
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>format</value>
<value>exceptions</value>
<value>windows</value>
</list>
</property>
</bean>
</beans>
该示例假定您在类路径中定义了三个名为 format、exceptions 和 windows 的资源包。
任何解析消息的请求都将通过 JDK 标准方式,使用 ResourceBundle 对象来解析消息。
为便于说明,假设上述资源包文件中的两个文件内容如下:
# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.
下一个示例展示了一个用于运行 MessageSource 功能的程序。
请记住,所有 ApplicationContext 实现同时也是 MessageSource
实现,因此可以强制转换为 MessageSource 接口。
public static void main(String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
System.out.println(message);
}
fun main() {
val resources = ClassPathXmlApplicationContext("beans.xml")
val message = resources.getMessage("message", null, "Default", Locale.ENGLISH)
println(message)
}
上述程序的输出结果如下:
Alligators rock!
简而言之,MessageSource 定义在一个名为 beans.xml 的文件中,该文件位于类路径的根目录下。messageSource 的 bean 定义通过其 basenames 属性引用了多个资源包。传递给 basenames 属性的列表中包含三个文件,它们分别位于类路径的根目录下,名称依次为 format.properties、exceptions.properties 和 windows.properties。
下一个示例展示了传递给消息查找的参数。这些参数会被转换为String对象,并插入到查找消息中的占位符位置。
<beans>
<!-- this MessageSource is being used in a web application -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="exceptions"/>
</bean>
<!-- lets inject the above MessageSource into this POJO -->
<bean id="example" class="com.something.Example">
<property name="messages" ref="messageSource"/>
</bean>
</beans>
public class Example {
private MessageSource messages;
public void setMessages(MessageSource messages) {
this.messages = messages;
}
public void execute() {
String message = this.messages.getMessage("argument.required",
new Object [] {"userDao"}, "Required", Locale.ENGLISH);
System.out.println(message);
}
}
class Example {
lateinit var messages: MessageSource
fun execute() {
val message = messages.getMessage("argument.required",
arrayOf("userDao"), "Required", Locale.ENGLISH)
println(message)
}
}
调用 execute() 方法后产生的输出如下所示:
The userDao argument is required.
关于国际化(“i18n”),Spring 的各种 MessageSource 实现遵循与标准 JDK ResourceBundle 相同的区域设置解析和回退规则。简而言之,继续沿用前面定义的 messageSource 示例,如果你想针对英国(en-GB)区域设置解析消息,则应分别创建名为 format_en_GB.properties、exceptions_en_GB.properties 和 windows_en_GB.properties 的文件。
通常,区域设置(locale)的解析由应用程序所处的环境进行管理。在以下示例中,用于解析(英式)消息的区域设置是手动指定的:
# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.
public static void main(final String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("argument.required",
new Object [] {"userDao"}, "Required", Locale.UK);
System.out.println(message);
}
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 的引用。在 ApplicationContext 中定义的任何实现了 MessageSourceAware 接口的 bean,在创建和配置时都会被注入该应用上下文的 MessageSource。
由于 Spring 的 MessageSource 基于 Java 的 ResourceBundle,它不会合并具有相同基名称的资源包,而只会使用找到的第一个资源包。
具有相同基名称的后续消息资源包将被忽略。 |
作为 ResourceBundleMessageSource 的替代方案,Spring 提供了一个
ReloadableResourceBundleMessageSource 类。该变体支持相同的捆绑包
文件格式,但比基于标准 JDK 的
ResourceBundleMessageSource 实现更加灵活。特别是,它允许从任何 Spring 资源位置(不仅限于类路径)读取文件,并支持捆绑包属性文件的热重载(同时在两次加载之间高效缓存它们)。
有关详细信息,请参阅 ReloadableResourceBundleMessageSource
Javadoc。 |
1.15.2. 标准事件和自定义事件
ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。如果一个实现了 ApplicationListener 接口的 Bean 被部署到上下文中,那么每当有 ApplicationEvent 发布到该 ApplicationContext 时,该 Bean 就会收到通知。本质上,这就是标准的观察者(Observer)设计模式。
从 Spring 4.2 开始,事件基础设施得到了显著改进,提供了基于注解的模型,
以及发布任意事件(即不一定继承自ApplicationEvent的对象)的能力。
当发布此类对象时,我们会自动将其包装为一个事件。 |
下表描述了 Spring 提供的标准事件:
| 事件 | 说明 |
|---|---|
|
当 |
|
当通过 |
|
当通过 |
|
当通过 |
|
一个特定于 Web 的事件,用于通知所有 Bean 一个 HTTP 请求已被处理完毕。该事件在请求完成后发布。此事件仅适用于使用 Spring 的 |
|
|
你也可以创建并发布自己的自定义事件。以下示例展示了一个简单的类,该类继承了 Spring 的 ApplicationEvent 基类:
public class BlockedListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlockedListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
class BlockedListEvent(source: Any,
val address: String,
val content: String) : ApplicationEvent(source)
要发布自定义的 ApplicationEvent,请在 publishEvent() 上调用 ApplicationEventPublisher 方法。通常,这是通过创建一个实现 ApplicationEventPublisherAware 接口的类,并将其注册为 Spring Bean 来完成的。以下示例展示了这样一个类:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blockedList;
private ApplicationEventPublisher publisher;
public void setBlockedList(List<String> blockedList) {
this.blockedList = blockedList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blockedList.contains(address)) {
publisher.publishEvent(new BlockedListEvent(this, address, content));
return;
}
// send email...
}
}
class EmailService : ApplicationEventPublisherAware {
private lateinit var blockedList: List<String>
private lateinit var publisher: ApplicationEventPublisher
fun setBlockedList(blockedList: List<String>) {
this.blockedList = blockedList
}
override fun setApplicationEventPublisher(publisher: ApplicationEventPublisher) {
this.publisher = publisher
}
fun sendEmail(address: String, content: String) {
if (blockedList!!.contains(address)) {
publisher!!.publishEvent(BlockedListEvent(this, address, content))
return
}
// send email...
}
}
在配置阶段,Spring 容器会检测到 EmailService 实现了
ApplicationEventPublisherAware 接口,并自动调用
setApplicationEventPublisher() 方法。实际上,传入的参数就是 Spring
容器本身。您通过其 ApplicationEventPublisher 接口与应用上下文进行交互。
要接收自定义的 ApplicationEvent,您可以创建一个实现 ApplicationListener 接口的类,并将其注册为 Spring Bean。以下示例展示了这样一个类:
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
class BlockedListNotifier : ApplicationListener<BlockedListEvent> {
lateinit var notificationAddres: String
override fun onApplicationEvent(event: BlockedListEvent) {
// notify appropriate parties via notificationAddress...
}
}
请注意,ApplicationListener 已使用您的自定义事件类型(上述示例中的 BlockedListEvent)进行了泛型参数化。这意味着 onApplicationEvent() 方法可以保持类型安全,无需进行任何向下转型。您可以注册任意数量的事件监听器,但请注意,默认情况下,事件监听器会同步接收事件。这意味着 publishEvent() 方法会阻塞,直到所有监听器完成事件处理。这种同步且单线程方式的一个优势是:当监听器接收到事件时,如果存在事务上下文,它将在发布者的事务上下文中运行。如果需要其他事件发布策略,请参阅 Spring 的 ApplicationEventMulticaster 接口和 SimpleApplicationEventMulticaster 实现的 Javadoc,以了解配置选项。
以下示例展示了用于注册和配置上述每个类的 bean 定义:
<bean id="emailService" class="example.EmailService">
<property name="blockedList">
<list>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</list>
</property>
</bean>
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
<property name="notificationAddress" value="[email protected]"/>
</bean>
综合起来,当调用 sendEmail() bean 的 emailService 方法时,如果存在任何应被阻止的电子邮件消息,就会发布一个类型为 BlockedListEvent 的自定义事件。blockedListNotifier bean 被注册为一个 ApplicationListener,并接收该 BlockedListEvent 事件,此时它可以通知相关方。
| Spring 的事件机制旨在实现同一应用上下文内 Spring Bean 之间的简单通信。然而,对于更复杂的企业集成需求,独立维护的 Spring Integration 项目提供了完整支持,可用于构建轻量级、面向模式的事件驱动架构,该架构基于广为人知的 Spring 编程模型。 |
基于注解的事件监听器
你可以通过使用 @EventListener 注解,在任何托管 Bean 的方法上注册一个事件监听器。BlockedListNotifier 可以重写如下:
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
class BlockedListNotifier {
lateinit var notificationAddress: String
@EventListener
fun processBlockedListEvent(event: BlockedListEvent) {
// notify appropriate parties via notificationAddress...
}
}
该方法签名再次声明了它所监听的事件类型, 但这次使用了灵活的方法名,且无需实现特定的监听器接口。 只要实际的事件类型在其实现层次结构中能够解析泛型参数, 就可以通过泛型进一步限定事件类型。
如果你的方法需要监听多个事件,或者你希望完全不使用参数来定义该方法,也可以直接在注解本身上指定事件类型。以下示例展示了如何实现这一点:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
@EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class)
fun handleContextStart() {
// ...
}
还可以通过使用定义 SpEL 表达式 的注解中的 condition 属性来添加额外的运行时过滤,该表达式必须匹配才能实际调用特定事件的方法。
以下示例展示了如何重写我们的通知器,使其仅在事件的 content 属性等于 my-event 时才被调用:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
@EventListener(condition = "#blEvent.content == 'my-event'")
fun processBlockedListEvent(blEvent: BlockedListEvent) {
// notify appropriate parties via notificationAddress...
}
每个 SpEL 表达式都在一个专用的上下文中进行求值。下表列出了提供给该上下文的项,以便您在条件事件处理中使用它们:
| 姓名 | 位置 | 描述 | 例举 |
|---|---|---|---|
事件 |
根对象 |
实际的 |
|
参数数组 |
根对象 |
用于调用该方法的参数(作为对象数组)。 |
|
参数名称 |
评估上下文 |
任意方法参数的名称。如果由于某些原因名称不可用(例如,因为编译后的字节码中没有调试信息),也可以使用 |
|
请注意,#root.event 可让您访问底层事件,即使您的方法签名实际上引用的是所发布的任意对象。
如果你需要在处理另一个事件的结果中发布一个事件,可以更改方法签名以返回应发布的事件,如下例所示:
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
@EventListener
fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
| 此功能不支持 异步监听器。 |
handleBlockedListEvent() 方法会为其处理的每个 ListUpdateEvent 发布一个新的 BlockedListEvent。如果你需要发布多个事件,可以改为返回一个 Collection 或事件数组。
异步监听器
如果您希望特定的监听器异步处理事件,可以复用
常规 @Async 支持。
以下示例展示了如何实现:
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent is processed in a separate thread
}
@EventListener
@Async
fun processBlockedListEvent(event: BlockedListEvent) {
// BlockedListEvent is processed in a separate thread
}
使用异步事件时,请注意以下限制:
-
如果异步事件监听器抛出
异常,它不会传播给调用者。请参阅AsyncUncaughtExceptionHandler以获取更多详细信息。 -
异步事件监听器方法无法通过返回值发布后续事件。如果您需要在处理结果中发布另一个事件,请注入一个
ApplicationEventPublisher以手动发布该事件。
排序监听器
如果你需要让一个监听器在另一个监听器之前被调用,可以在方法声明上添加 @Order 注解,如下例所示:
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
@EventListener
@Order(42)
fun processBlockedListEvent(event: BlockedListEvent) {
// notify appropriate parties via notificationAddress...
}
通用事件
你也可以使用泛型来进一步定义事件的结构。考虑使用 EntityCreatedEvent<T>,其中 T 是实际被创建实体的类型。例如,你可以创建以下监听器定义,仅接收针对 EntityCreatedEvent 的 Person:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}
@EventListener
fun onPersonCreated(event: EntityCreatedEvent<Person>) {
// ...
}
由于类型擦除,这种方式仅在所发布的事件能够解析事件监听器所过滤的泛型参数时才有效(例如:class PersonCreatedEvent extends EntityCreatedEvent<Person> { … })。
在某些情况下,如果所有事件都遵循相同的结构(如前例中的事件那样),这种做法可能会变得相当繁琐。在这种情况下,您可以实现 ResolvableTypeProvider 接口,以向框架提供超出运行时环境所提供的类型信息。以下事件展示了如何实现这一点:
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
super(entity);
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
}
}
class EntityCreatedEvent<T>(entity: T) : ApplicationEvent(entity), ResolvableTypeProvider {
override fun getResolvableType(): ResolvableType? {
return ResolvableType.forClassWithGenerics(javaClass, ResolvableType.forInstance(getSource()))
}
}
这不仅适用于ApplicationEvent,也适用于你作为事件发送的任意对象。 |
1.15.3. 便捷访问底层资源
为了更佳地使用和理解应用上下文,您应熟悉 Spring 的 Resource 抽象,如资源(Resources)一节所述。
应用程序上下文是一个 ResourceLoader,可用于加载 Resource 对象。
Resource 本质上是 JDK 中 java.net.URL 类的一个功能更丰富的版本。
实际上,Resource 的实现会在适当的情况下包装一个 java.net.URL 实例。
Resource 能以透明的方式从几乎任何位置获取底层资源,包括类路径、文件系统位置、任何可通过标准 URL 描述的位置,以及其他一些变体。
如果资源位置字符串是一个不带任何特殊前缀的简单路径,则这些资源的具体来源取决于实际的应用程序上下文类型。
您可以配置部署到应用上下文中的 Bean,使其实现特殊的回调接口 ResourceLoaderAware,这样在初始化时会自动回调该 Bean,并将应用上下文本身作为 ResourceLoader 传入。
您还可以暴露类型为 Resource 的属性,用于访问静态资源。
这些属性会像其他任何属性一样被注入。您可以将这些 Resource 属性指定为简单的 String 路径,在 Bean 部署时依赖从这些字符串自动转换为实际的 Resource 对象。
提供给 ApplicationContext 构造函数的位置路径(一个或多个)实际上是资源字符串,以简单形式表示时,会根据具体的上下文实现进行相应处理。例如,ClassPathXmlApplicationContext 会将一个简单的路径视为类路径(classpath)位置。您还可以在位置路径(资源字符串)前加上特殊前缀,以强制从类路径或 URL 加载定义,而不管实际的上下文类型是什么。
1.15.4. 便于 Web 应用程序的 ApplicationContext 实例化
你可以通过声明式的方式创建 ApplicationContext 实例,例如使用 ContextLoader。当然,你也可以通过编程方式,使用某个 ApplicationContext 的实现类来创建 ApplicationContext 实例。
你可以通过使用 ApplicationContext 来注册一个 ContextLoaderListener,如下例所示:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
该监听器会检查 contextConfigLocation 参数。如果该参数不存在,监听器将默认使用 /WEB-INF/applicationContext.xml。当该参数存在时,监听器会使用预定义的分隔符(逗号、分号和空白字符)对 String 进行分割,并将分割后的值作为搜索应用程序上下文的位置。同时也支持 Ant 风格的路径模式。
例如:/WEB-INF/*Context.xml(用于匹配 Context.xml 目录下所有以 WEB-INF 结尾的文件)和 /WEB-INF/**/*Context.xml(用于匹配 WEB-INF 目录下任意子目录中所有此类文件)。
1.15.5. 部署 SpringApplicationContext作为 Java EE RAR 文件
可以将 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 的 WorkManager 抽象与应用服务器的 JCA TaskExecutor 进行交互。
有关 RAR 部署所涉及的配置详情,请参阅
SpringContextResourceAdapter
类的 Javadoc。
将 Spring ApplicationContext 简单部署为 Java EE RAR 文件:
-
将所有应用程序类打包到一个 RAR 文件中(RAR 文件实际上是一个标准的 JAR 文件,只是文件扩展名不同)。
-
将所有必需的库 JAR 文件添加到 RAR 归档文件的根目录中。
-
添加一个
META-INF/ra.xml部署描述符(如SpringContextResourceAdapter的 Javadoc 中所示) 以及相应的 Spring XML Bean 定义文件(通常为META-INF/applicationContext.xml)。 -
将生成的 RAR 文件放入您的应用服务器的部署目录中。
此类 RAR 部署单元通常是自包含的。它们不会向外部暴露组件,甚至不会向同一应用程序的其他模块暴露。基于 RAR 的 ApplicationContext 通常通过与其它模块共享的 JMS 目的地进行交互。例如,基于 RAR 的 ApplicationContext 还可以调度某些任务,或对文件系统中新文件(或类似事件)作出响应。如果它需要允许来自外部的同步访问,则可以(例如)导出 RMI 端点,供同一台机器上的其他应用模块使用。 |
1.16.BeanFactory
BeanFactory API 为 Spring 的 IoC 功能提供了底层基础。
其具体的契约主要用于与 Spring 的其他部分以及相关的第三方框架进行集成,
而其 DefaultListableBeanFactory 实现则是高层级 GenericApplicationContext 容器中的一个关键委托组件。
BeanFactory 及其相关接口(例如 BeanFactoryAware、InitializingBean、
DisposableBean)是其他框架组件的重要集成点。
由于无需任何注解甚至反射,它们能够实现容器与其组件之间非常高效的交互。
应用级别的 Bean 也可以使用相同的回调接口,但通常更倾向于使用声明式的依赖注入方式,
无论是通过注解还是通过编程式配置。
请注意,核心的 BeanFactory API 层及其 DefaultListableBeanFactory 实现并不对配置格式或要使用的任何组件注解做出假设。所有这些形式都通过扩展(例如 XmlBeanDefinitionReader 和 AutowiredAnnotationBeanPostProcessor)引入,并以共享的 BeanDefinition 对象作为核心元数据表示进行操作。
这正是 Spring 容器如此灵活和可扩展的根本所在。
1.16.1. BeanFactory or ApplicationContext?
本节解释了 BeanFactory 和 ApplicationContext 容器层级之间的区别,以及它们对启动过程的影响。
除非有充分的理由不这样做,否则你应该使用 ApplicationContext,其中 GenericApplicationContext 及其子类 AnnotationConfigApplicationContext 是用于自定义引导的常见实现。这些类是 Spring 核心容器在所有常见用途下的主要入口点:加载配置文件、触发类路径扫描、以编程方式注册 bean 定义和带注解的类,以及(从 5.0 版本起)注册函数式 bean 定义。
由于 ApplicationContext 包含了 BeanFactory 的全部功能,因此通常推荐使用 BeanFactory,而非普通的 ApplicationContext,除非在需要完全控制 bean 处理的场景下。在 GenericApplicationContext(例如 DefaultListableBeanFactory 实现)中,会通过约定(即通过 bean 名称或 bean 类型——特别是后处理器)自动检测多种类型的 bean,而普通的 6 则对任何特殊 bean 都不作区分。
对于许多扩展容器功能,例如注解处理和 AOP 代理,
BeanPostProcessor 扩展点 至关重要。
如果您仅使用普通的 DefaultListableBeanFactory,这些后处理器默认情况下不会被检测并激活。这种情况可能会令人困惑,因为
您的 Bean 配置实际上没有任何问题。相反,在这种场景下,
需要通过额外的设置来完全引导容器。
下表列出了 BeanFactory 和 ApplicationContext 接口及其实现所提供的功能。
| 特性 | BeanFactory |
ApplicationContext |
|---|---|---|
Bean 的实例化/装配 |
是的 |
是的 |
集成的生命周期管理 |
No |
是的 |
自动注册 |
No |
是的 |
自动注册 |
No |
是的 |
便捷的 |
No |
是的 |
内置的 |
No |
是的 |
要显式地向 DefaultListableBeanFactory 注册一个 Bean 后置处理器,
你需要以编程方式调用 addBeanPostProcessor,如下例所示:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions
// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());
// now start using the factory
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 方法,如下例所示:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));
// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
// now actually do the replacement
cfg.postProcessBeanFactory(factory);
val factory = DefaultListableBeanFactory()
val reader = XmlBeanDefinitionReader(factory)
reader.loadBeanDefinitions(FileSystemResource("beans.xml"))
// bring in some property values from a Properties file
val cfg = PropertySourcesPlaceholderConfigurer()
cfg.setLocation(FileSystemResource("jdbc.properties"))
// now actually do the replacement
cfg.postProcessBeanFactory(factory)
在这两种情况下,显式注册的步骤都较为繁琐,因此在基于 Spring 的应用程序中,通常更倾向于使用各种 ApplicationContext 的变体,而不是直接使用 DefaultListableBeanFactory,尤其是在典型的企业级环境中,当需要依赖 BeanFactoryPostProcessor 和 BeanPostProcessor 实例来扩展容器功能时更是如此。
|
|
2. 资源
本章介绍 Spring 如何处理资源,以及如何在 Spring 中使用资源。内容包括以下主题:
2.1. 简介
Java 标准的 java.net.URL 类以及针对各种 URL 前缀的标准处理器,不幸的是,并不足以满足所有对底层资源的访问需求。例如,目前没有标准化的 URL 实现可用于访问需要从类路径(classpath)中获取的资源,或相对于 ServletContext 的资源。尽管可以为特定的 URL 前缀注册新的处理器(类似于已有的 http: 等前缀的处理器),但这一过程通常相当复杂,而且 URL 接口仍然缺少一些理想的功能,例如用于检查所指向资源是否存在的方法。
2.2. 资源接口
Spring 的 Resource 接口旨在提供一个功能更强大的接口,用于抽象对底层资源的访问。以下代码展示了 Resource 接口的定义:
public interface Resource extends InputStreamSource {
boolean exists();
boolean isOpen();
URL getURL() throws IOException;
File getFile() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
interface Resource : InputStreamSource {
fun exists(): Boolean
val isOpen: Boolean
val url: URL
val file: File
@Throws(IOException::class)
fun createRelative(relativePath: String): Resource
val filename: String
val description: String
}
如 Resource 接口的定义所示,它扩展了 InputStreamSource 接口。以下代码清单展示了 InputStreamSource 接口的定义:
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
interface InputStreamSource {
val inputStream: InputStream
}
Resource 接口中一些最重要的方法包括:
-
getInputStream():定位并打开资源,返回一个用于从该资源读取数据的InputStream。每次调用都应返回一个新的InputStream。调用方负责关闭该流。 -
exists():返回一个boolean值,指示该资源在物理形式上是否实际存在。 -
isOpen():返回一个boolean值,指示此资源是否表示一个带有已打开流的句柄。如果返回true,则该InputStream不能被多次读取,必须仅读取一次,然后关闭以避免资源泄漏。对于所有常规的资源实现,此方法均返回false,只有InputStreamResource例外。 -
getDescription():返回此资源的描述信息,用于在处理该资源时输出错误信息。这通常是资源的完整限定文件名或实际 URL。
其他方法允许您获取表示该资源的实际URL或File对象(前提是底层实现兼容并支持该功能)。
Spring 框架本身在许多需要资源的方法签名中广泛使用 Resource 抽象作为参数类型。此外,某些 Spring API 中的其他方法(例如各种 ApplicationContext 实现类的构造函数)接受一个 String 类型的参数,该字符串在未加修饰或简单形式下会被用于创建适合该上下文实现的 Resource;或者,通过在 String 路径上添加特殊前缀,允许调用者指定必须创建并使用某个特定的 Resource 实现。
尽管 Resource 接口在 Spring 框架内部及其使用中非常常见,但即使你的代码完全不了解或不关心 Spring 的其他部分,也可以将其作为通用工具类直接用于访问资源。虽然这样做会使你的代码与 Spring 耦合,但实际上仅耦合到这一小组工具类。这些工具类可作为 URL 的更强大替代品,并且可以视为与其他用于此目的的库具有同等地位。
Resource 抽象并没有取代原有的功能,
而是在可能的情况下对其进行封装。例如,UrlResource 封装了一个 URL,并使用
被封装的 URL 来完成其工作。 |
2.3. 内置资源实现
Spring 包含以下 Resource 实现:
2.3.1. UrlResource
UrlResource 包装了一个 java.net.URL,可用于访问任何通常可通过 URL 访问的对象,例如文件、HTTP 目标、FTP 目标等。所有 URL 都具有标准化的 String 表示形式,通过使用适当的标准化前缀来区分不同类型的 URL。这包括用于访问文件系统路径的 file:、通过 HTTP 协议访问资源的 http:、通过 FTP 访问资源的 ftp: 等。
UrlResource 可通过 Java 代码显式地使用 UrlResource 构造函数来创建,
但通常在调用接受一个 String 参数(该参数用于表示路径)的 API 方法时被隐式创建。
在后一种情况下,JavaBeans 的 PropertyEditor 最终决定要创建哪种类型的 Resource。
如果路径字符串包含它所熟知的前缀(例如 classpath:),
它就会为该前缀创建一个相应的专用 Resource。
然而,如果它无法识别该前缀,则会假定该字符串是一个标准的 URL 字符串,并创建一个 UrlResource。
2.3.2. ClassPathResource
此类表示应从类路径(classpath)中获取的资源。它使用线程上下文类加载器、指定的类加载器或指定的类来加载资源。
如果类路径资源位于文件系统中,此 Resource 实现支持将其解析为 java.io.File;但对于位于 JAR 文件中且尚未被(Servlet 引擎或当前运行环境)解压到文件系统的类路径资源,则不支持该解析方式。为了解决这一问题,各种 Resource 实现始终支持将其解析为 java.net.URL。
ClassPathResource 可通过 Java 代码显式地使用 ClassPathResource 构造函数来创建,但通常在调用接受一个用于表示路径的 String 参数的 API 方法时被隐式创建。在后一种情况下,JavaBeans 的 PropertyEditor 会识别字符串路径中的特殊前缀 classpath:,并据此创建一个 ClassPathResource。
2.3.3. FileSystemResource
这是用于 Resource 和 java.io.File 句柄的 java.nio.file.Path 实现。
它支持解析为 File 和 URL。
2.3.4. ServletContextResource
这是针对 Resource 资源的 ServletContext 实现,它在相关 Web 应用程序的根目录内解析相对路径。
它始终支持流访问和 URL 访问,但仅当 Web 应用程序归档文件被解压且资源实际存在于文件系统上时,才允许 java.io.File 访问。该归档是否被解压并位于文件系统上,还是直接从 JAR 包中访问,抑或从数据库等其他位置(这是可以设想的)访问,实际上取决于 Servlet 容器。
2.4.ResourceLoader
ResourceLoader 接口应由能够返回(即加载)Resource 实例的对象实现。以下代码清单展示了 ResourceLoader 接口的定义:
public interface ResourceLoader {
Resource getResource(String location);
}
interface ResourceLoader {
fun getResource(location: String): Resource
}
所有应用上下文都实现了 ResourceLoader 接口。因此,所有应用上下文都可用于获取 Resource 实例。
当你在特定的应用上下文(application context)上调用 getResource() 方法,并且指定的位置路径没有特定的前缀时,你会得到一个适合该特定应用上下文的 Resource 类型。例如,假设有以下代码片段在一个 ClassPathXmlApplicationContext 实例上执行:
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
val template = ctx.getResource("some/resource/path/myTemplate.txt")
对于 ClassPathXmlApplicationContext,上述代码会返回一个 ClassPathResource。如果对 FileSystemXmlApplicationContext 实例运行相同的方法,则会返回一个 FileSystemResource。对于 WebApplicationContext,则会返回一个 ServletContextResource。同样地,它也会为每种上下文返回相应的对象。
因此,你可以以适合特定应用程序上下文的方式加载资源。
另一方面,您也可以通过指定特殊的 ClassPathResource 前缀来强制使用 classpath:,而不管应用上下文的类型如何,如下例所示:
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt")
同样,你可以通过指定任意标准的 UrlResource 前缀来强制使用 java.net.URL。以下两个示例分别使用了 file 和 http 前缀:
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
val template = ctx.getResource("file:///some/resource/path/myTemplate.txt")
Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");
val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt")
下表总结了将 String 对象转换为 Resource 对象的策略:
| 前缀 | 例举 | 说明 |
|---|---|---|
classpath: |
|
从类路径加载。 |
文件: |
已从文件系统加载为 |
|
http: |
作为 |
|
(none) |
|
取决于底层的 |
2.5.ResourceLoaderAware接口
ResourceLoaderAware 接口是一个特殊的回调接口,用于标识那些期望被注入 ResourceLoader 引用的组件。以下代码清单展示了 ResourceLoaderAware 接口的定义:
public interface ResourceLoaderAware {
void setResourceLoader(ResourceLoader resourceLoader);
}
interface ResourceLoaderAware {
fun setResourceLoader(resourceLoader: ResourceLoader)
}
当一个类实现了 ResourceLoaderAware 接口并被部署到应用上下文(作为 Spring 管理的 Bean)中时,该应用上下文会将其识别为 ResourceLoaderAware。随后,应用上下文会调用 setResourceLoader(ResourceLoader) 方法,并将自身作为参数传入(请记住,Spring 中所有的应用上下文都实现了 ResourceLoader 接口)。
由于 ApplicationContext 是一个 ResourceLoader,该 Bean 也可以实现
ApplicationContextAware 接口,并直接使用所提供的应用上下文来加载资源。
然而,通常情况下,如果你仅需要资源加载功能,最好使用专门的 ResourceLoader
接口。这样,代码只会耦合到资源加载接口(可视为一种工具接口),而不会耦合到整个 Spring
ApplicationContext 接口。
在应用程序组件中,您也可以依赖 ResourceLoader 的自动装配,作为实现 ResourceLoaderAware 接口的替代方案。“传统”的 constructor 和 byType 自动装配模式(如 自动装配协作者 中所述)能够分别为构造函数参数或 setter 方法参数提供 ResourceLoader。为了获得更大的灵活性(包括自动装配字段和多参数方法的能力),请考虑使用基于注解的自动装配功能。在这种情况下,只要相关的字段、构造函数或方法带有 @Autowired 注解,ResourceLoader 就会被自动装配到期望 ResourceLoader 类型的字段、构造函数参数或方法参数中。更多信息,请参阅 使用 @Autowired。
2.6. 资源作为依赖
如果 Bean 本身将通过某种动态过程来确定并提供资源路径,那么该 Bean 使用 ResourceLoader 接口来加载资源可能是合理的。例如,考虑加载某种模板的情形,其中所需的特定资源取决于用户的角色。如果资源是静态的,则完全避免使用 ResourceLoader 接口更为合理,此时应让 Bean 暴露其所需的 Resource 属性,并期望这些属性被注入到 Bean 中。
之所以能够轻松地注入这些属性,是因为所有的应用上下文都会注册并使用一个特殊的 JavaBeans PropertyEditor,它可以将 String 类型的路径转换为 Resource 对象。因此,如果 myBean 有一个类型为 Resource 的 template 属性,就可以通过一个简单的字符串来配置该资源,如下例所示:
<bean id="myBean" class="...">
<property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>
请注意,资源路径没有前缀。因此,由于应用程序上下文本身将被用作ResourceLoader,该资源将通过ClassPathResource、FileSystemResource或ServletContextResource进行加载,具体取决于上下文的确切类型。
如果你需要强制使用特定的Resource类型,可以使用前缀。
以下两个示例展示了如何强制使用ClassPathResource和
UrlResource(后者用于访问文件系统中的文件):
<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>
2.7. 应用上下文与资源路径
本节介绍如何使用资源创建应用上下文,包括适用于 XML 的快捷方式、如何使用通配符以及其他相关细节。
2.7.1. 构建应用上下文
应用程序上下文的构造函数(针对特定的应用程序上下文类型)通常接受一个字符串或字符串数组作为资源的位置路径,例如组成上下文定义的 XML 文件。
当此类位置路径没有前缀时,从该路径构建并用于加载 Bean 定义的特定 Resource 类型取决于具体的应用上下文,并与其相适应。例如,请考虑以下创建 ClassPathXmlApplicationContext 的示例:
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
val ctx = ClassPathXmlApplicationContext("conf/appContext.xml")
Bean 定义是从类路径(classpath)加载的,因为使用了 ClassPathResource。然而,请考虑以下示例,它创建了一个 FileSystemXmlApplicationContext:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/appContext.xml");
val ctx = FileSystemXmlApplicationContext("conf/appContext.xml")
现在,bean 定义是从文件系统位置加载的(在本例中,是相对于当前工作目录)。
请注意,在位置路径上使用特殊的 Resource 前缀或标准 URL 前缀会覆盖用于加载定义的默认 1 类型。请参见以下示例:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml")
使用 FileSystemXmlApplicationContext 从类路径(classpath)加载 bean 定义。然而,它仍然是一个
FileSystemXmlApplicationContext。如果随后将其用作 ResourceLoader,
任何未加前缀的路径仍将被视为文件系统路径。
构建中ClassPathXmlApplicationContext实例 — 快捷方式
ClassPathXmlApplicationContext 提供了多个构造函数,以便于便捷地进行实例化。其基本思路是:您可以仅提供一个字符串数组,其中只包含 XML 文件的文件名(不包含前导路径信息),同时提供一个 Class 对象。ClassPathXmlApplicationContext 将从所提供的类中推导出路径信息。
考虑以下目录结构:
com/
foo/
services.xml
daos.xml
MessengerService.class
以下示例展示了如何实例化一个 ClassPathXmlApplicationContext 实例,该实例由类路径下名为 services.xml 和 daos.xml 的文件中定义的 bean 组成:
ApplicationContext ctx = new ClassPathXmlApplicationContext(
new String[] {"services.xml", "daos.xml"}, MessengerService.class);
val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "daos.xml"), MessengerService::class.java)
请参阅 ClassPathXmlApplicationContext
javadoc,了解各种构造函数的详细信息。
2.7.2. 应用程序上下文构造函数资源路径中的通配符
应用程序上下文构造函数值中的资源路径可以是简单路径(如前所示),每条路径都与一个目标 Resource 一一对应;或者,也可以包含特殊的 "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 等),则会从该 URL 获取一个 java.io.File 对象,并通过遍历文件系统来解析通配符。对于 jar URL 的情况,解析器会从中获取一个 java.net.JarURLConnection,或者手动解析 jar URL,然后遍历 jar 文件的内容以解析通配符。
对可移植性的影响
如果指定的路径已经是文件 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*: 前缀,如下例所示:
ApplicationContext ctx =
new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");
val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml")
该特殊前缀指定必须获取所有与给定名称匹配的类路径资源(在内部,这本质上是通过调用ClassLoader.getResources(…)实现的),然后将它们合并以形成最终的应用上下文定义。
通配符 classpath 依赖于底层类加载器(classloader)的 getResources() 方法。由于当今大多数应用服务器都提供了自己的类加载器实现,其行为可能会有所不同,尤其是在处理 JAR 文件时。一个简单的测试方法是使用类加载器从 classpath 中的 JAR 文件内加载某个文件,以验证 classpath* 是否有效:
getClass().getClassLoader().getResources("<someFileInsideTheJar>")。请尝试用两个不同位置中同名的文件进行此测试。如果返回了不正确的结果,请查阅应用服务器的文档,查看是否有影响类加载器行为的相关设置。 |
你也可以将 classpath*: 前缀与路径其余部分中的 PathMatcher 模式结合起来使用(例如,classpath*:META-INF/*-beans.xml)。在这种情况下,解析策略相当简单:对路径中最后一个非通配符段调用 ClassLoader.getResources() 方法,以获取类加载器层次结构中所有匹配的资源,然后针对每个资源,使用前面所述的相同 PathMatcher 解析策略来处理通配符子路径。
与通配符相关的其他说明
请注意,classpath*: 与 Ant 风格的模式结合使用时,除非目标文件实际位于文件系统中,否则只有在模式开始前至少包含一个根目录时才能可靠地工作。这意味着像 classpath*:*.xml 这样的模式可能无法从 JAR 文件的根目录中检索文件,而只能从已解压目录的根目录中检索。
Spring 框架检索类路径条目的能力源自 JDK 的
ClassLoader.getResources() 方法,该方法在传入空字符串(表示要搜索的潜在根目录)时仅返回文件系统位置。Spring 还会评估
URLClassLoader 的运行时配置以及 JAR 文件中的 java.class.path 清单,但这种方式无法保证产生可移植的行为。
|
扫描类路径(classpath)中的包需要类路径中存在相应的目录条目。当你使用 Ant 构建 JAR 文件时,请不要启用 JAR 任务的“仅文件”(files-only)选项。此外,在某些环境中,基于安全策略,类路径中的目录可能无法被暴露出来——例如,在 JDK 1.7.0_45 及更高版本中运行的独立应用程序(这要求在你的清单文件(manifest)中设置 'Trusted-Library'。参见 https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。 在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常能按预期正常工作。 在此情况下,同样强烈建议将资源放入专用目录中, 以避免前面提到的在 JAR 文件根级别进行搜索时可能出现的可移植性问题。 |
如果要搜索的根包存在于多个类路径位置中,则使用 classpath: 资源的 Ant 风格模式不能保证找到匹配的资源。
请考虑以下资源位置的示例:
com/mycompany/package1/service-context.xml
现在考虑一个有人可能用来尝试查找该文件的 Ant 风格路径:
classpath:com/mycompany/**/service-context.xml
此类资源可能仅存在于一个位置,但当使用如上例所示的路径尝试解析它时,解析器会基于getResource("com/mycompany");返回的(第一个)URL进行操作。如果该基础包节点存在于多个类加载器位置中,那么实际的目标资源可能并不在该位置。因此,在这种情况下,您应优先使用带有相同 Ant 风格模式的classpath*:前缀,它会在包含根包的所有类路径位置中进行搜索。
2.7.3. FileSystemResource注意事项
一个未附加到 FileSystemResource 的 FileSystemApplicationContext(即当 FileSystemApplicationContext 不是实际的 ResourceLoader 时),对绝对路径和相对路径的处理方式符合您的预期:相对路径相对于当前工作目录,而绝对路径则相对于文件系统的根目录。
然而,出于向后兼容性(历史原因)的考虑,当 FileSystemApplicationContext 作为 ResourceLoader 时,情况会发生变化。FileSystemApplicationContext 会强制所有关联的 FileSystemResource 实例将所有位置路径都视为相对路径,无论它们是否以斜杠开头。
实际上,这意味着以下示例是等效的:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/context.xml");
val ctx = FileSystemXmlApplicationContext("conf/context.xml")
ApplicationContext ctx =
new FileSystemXmlApplicationContext("/conf/context.xml");
val ctx = FileSystemXmlApplicationContext("/conf/context.xml")
以下示例也是等效的(尽管它们看起来应该有所不同,因为一种情况是相对路径,另一种是绝对路径):
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
val ctx: FileSystemXmlApplicationContext = ...
ctx.getResource("some/resource/path/myTemplate.txt")
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");
val ctx: FileSystemXmlApplicationContext = ...
ctx.getResource("/some/resource/path/myTemplate.txt")
在实践中,如果你需要真正的绝对文件系统路径,应避免使用 FileSystemResource 或 FileSystemXmlApplicationContext 的绝对路径,而应通过使用 UrlResource URL 前缀来强制使用 file:。以下示例展示了如何实现这一点:
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt")
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
new FileSystemXmlApplicationContext("file:///conf/context.xml");
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml")
3. 验证、数据绑定和类型转换
将验证视为业务逻辑有其优缺点,而 Spring 提供了一种验证(和数据绑定)的设计,不会排除其中任何一种观点。
具体而言,验证不应与 Web 层耦合,应易于本地化,并且应能够轻松集成任何可用的验证器。考虑到这些需求,
Spring 提供了一个 Validator 接口,该接口既基础又非常适合在应用程序的每一层中使用。
数据绑定对于将用户输入动态绑定到应用程序的领域模型(或你用于处理用户输入的任何对象)非常有用。Spring 提供了恰如其名的 DataBinder 来实现这一功能。Validator 和 DataBinder 共同组成了 validation 包,该包主要用于 Web 层,但并不限于此。
BeanWrapper 是 Spring 框架中的一个基本概念,在很多地方都有使用。然而,你很可能不需要直接使用 BeanWrapper。但由于这是参考文档,我们认为有必要做一些解释。我们在本章中介绍 BeanWrapper,因为如果你确实需要使用它,最有可能是在尝试将数据绑定到对象时。
Spring 的 DataBinder 和底层的 BeanWrapper 都使用 PropertyEditorSupport 的实现来解析和格式化属性值。PropertyEditor 和 PropertyEditorSupport 类型属于 JavaBeans 规范的一部分,也在本章中进行了解释。Spring 3 引入了 core.convert 包,提供了通用的类型转换功能,以及一个更高级别的“format”包,用于格式化 UI 字段值。你可以将这些包作为 PropertyEditorSupport 实现的更简单替代方案。它们也在本章中进行了讨论。
Spring 通过设置基础设施以及适配到 Spring 自有的 Validator 契约,支持 Java Bean 验证。应用程序可以如 Java Bean 验证 中所述,全局启用一次 Bean 验证,并专门将其用于所有验证需求。在 Web 层,应用程序还可以如 配置 DataBinder 中所述,为每个 DataBinder 注册控制器本地的 Spring Validator 实例,这对于插入自定义验证逻辑非常有用。
3.1. 使用 Spring 的 Validator 接口进行验证
Spring 提供了一个 Validator 接口,可用于验证对象。Validator 接口通过使用一个 Errors 对象来工作,这样在验证过程中,验证器可以将验证失败信息报告给该 Errors 对象。
考虑以下小型数据对象的示例:
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
class Person(val name: String, val age: Int)
下一个示例通过实现 Person 接口的以下两个方法,为 org.springframework.validation.Validator 类提供了验证行为:
-
supports(Class):此Validator能否验证所提供的Class的实例? -
validate(Object, org.springframework.validation.Errors):验证给定的对象,如果存在验证错误,则将这些错误注册到给定的Errors对象中。
实现一个 Validator 相当简单,特别是当你了解 Spring 框架还提供了 ValidationUtils 辅助类时。下面的示例为 Validator 实例实现了 Person:
public class PersonValidator implements Validator {
/**
* This Validator validates only Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
class PersonValidator : Validator {
/*
* This Validator validates only Person instances
*/
override fun supports(clazz: Class<>): Boolean {
return Person::class.java == clazz
}
override fun validate(obj: Any, e: Errors) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty")
val p = obj as Person
if (p.age < 0) {
e.rejectValue("age", "negativevalue")
} else if (p.age > 110) {
e.rejectValue("age", "too.darn.old")
}
}
}
ValidationUtils 类上的 static rejectIfEmpty(..) 方法用于在 name 属性为 null 或空字符串时将其拒绝。请查看
ValidationUtils Javadoc,
了解除了前述示例之外它所提供的功能。
虽然当然可以实现一个单一的 Validator 类来验证丰富对象中的每个嵌套对象,但将每个嵌套对象类的验证逻辑封装在其自身的 Validator 实现中可能更好。一个“丰富”对象的简单示例是一个 Customer,它由两个 String 属性(名字和姓氏)以及一个复杂的 Address 对象组成。Address 对象可以独立于 Customer 对象使用,因此已实现了独立的 AddressValidator。如果您希望您的 CustomerValidator 重用 AddressValidator 类中包含的逻辑而不采用复制粘贴的方式,您可以像以下示例所示,在您的 CustomerValidator 中进行依赖注入或实例化一个 AddressValidator:
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
class CustomerValidator(private val addressValidator: Validator) : Validator {
init {
if (addressValidator == null) {
throw IllegalArgumentException("The supplied [Validator] is required and must not be null.")
}
if (!addressValidator.supports(Address::class.java)) {
throw IllegalArgumentException("The supplied [Validator] must support the validation of [Address] instances.")
}
}
/*
* This Validator validates Customer instances, and any subclasses of Customer too
*/
override fun supports(clazz: Class<>): Boolean {
return Customer::class.java.isAssignableFrom(clazz)
}
override fun validate(target: Any, errors: Errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required")
val customer = target as Customer
try {
errors.pushNestedPath("address")
ValidationUtils.invokeValidator(this.addressValidator, customer.address, errors)
} finally {
errors.popNestedPath()
}
}
}
验证错误会被报告给传递给验证器的 Errors 对象。在 Spring Web MVC 中,您可以使用 <spring:bind/> 标签来检查错误消息,但也可以直接检查 Errors 对象本身。有关它提供的方法的更多信息,请参见 javadoc。
3.2. 将代码解析为错误消息
我们已经介绍了数据绑定和验证。本节介绍如何输出与验证错误相对应的消息。在上一节所示的示例中,我们拒绝了name和age字段。如果我们要通过使用一个
MessageSource
来输出错误信息,我们可以在拒绝字段(例如这里的'name'和'age'
)时提供相应的错误代码来实现。当您调用(直接或间接地,例如通过使用ValidationUtils类)rejectValue方法或reject接口中的其他任意方法时,底层实现不仅会注册您传递的代码,还会注册若干额外的错误码。MessageCodesResolver 决定了 Errors 接口注册哪些错误代码。默认情况下,
DefaultMessageCodesResolver 被使用,这(例如)不仅会注册带有您提供的代码的消息,还会注册包含您传递给 reject 方法的字段名的消息。如果您通过使用rejectValue("age", "too.darn.old")拒绝一个字段,除了注册too.darn.old代码外,Spring还会注册too.darn.old.age和too.darn.old.age.int(第一个包括字段名称,第二个包括字段类型)。这样做是为了方便开发人员在处理错误消息时使用。
关于 MessageCodesResolver 和默认策略的更多信息,可分别在
MessageCodesResolver 和
DefaultMessageCodesResolver 的 Javadoc 中找到。
3.3. Bean 操作与BeanWrapper
org.springframework.beans 包遵循 JavaBeans 标准。
JavaBean 是一个具有默认无参构造函数的类,并且遵循一种命名约定:例如,名为 bingoMadness 的属性
应具有 setter 方法 setBingoMadness(..) 和 getter 方法 getBingoMadness()。有关 JavaBeans 及其规范的更多信息,请参见
javabeans。
beans 包中一个非常重要的类是 BeanWrapper 接口及其对应的实现类(BeanWrapperImpl)。正如 Javadoc 所述,BeanWrapper 提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性以确定其是否可读或可写的功能。此外,BeanWrapper 还支持嵌套属性,允许对子属性进行无限深度的设置。BeanWrapper 还支持添加标准的 JavaBeans PropertyChangeListeners 和 VetoableChangeListeners,而无需在目标类中编写支持代码。最后但同样重要的是,BeanWrapper 提供了对设置索引属性的支持。BeanWrapper 通常不会被应用程序代码直接使用,而是由 DataBinder 和 BeanFactory 使用。
BeanWrapper 的工作方式部分体现在其名称中:它包装一个 bean,以便对该 bean 执行操作,例如设置和获取属性。
3.3.1. 设置和获取基本属性及嵌套属性
设置和获取属性是通过 setPropertyValue 的 getPropertyValue 和 BeanWrapper 重载方法变体完成的。有关详细信息,请参阅它们的 Javadoc。下表展示了一些这些约定的示例:
| 表达式 | 说明 |
|---|---|
|
指示与 |
|
表示属性 |
|
表示索引属性 |
|
表示 |
(如果您不打算直接使用 BeanWrapper,那么下一节对您来说并不是至关重要。如果您仅使用 DataBinder 和 BeanFactory 及其默认实现,您可以直接跳转到关于 PropertyEditors 的章节。)
以下两个示例类使用 BeanWrapper 来获取和设置属性:
public class Company {
private String name;
private Employee managingDirector;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Employee getManagingDirector() {
return this.managingDirector;
}
public void setManagingDirector(Employee managingDirector) {
this.managingDirector = managingDirector;
}
}
class Company {
var name: String? = null
var managingDirector: Employee? = null
}
public class Employee {
private String name;
private float salary;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
class Employee {
var name: String? = null
var salary: Float? = null
}
以下代码片段展示了一些如何获取和操作已实例化的Companies和Employees对象属性的示例:
BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);
// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
val company = BeanWrapperImpl(Company())
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.")
// ... can also be done like this:
val value = PropertyValue("name", "Some Company Inc.")
company.setPropertyValue(value)
// ok, let's create the director and tie it to the company:
val jim = BeanWrapperImpl(Employee())
jim.setPropertyValue("name", "Jim Stravinsky")
company.setPropertyValue("managingDirector", jim.wrappedInstance)
// retrieving the salary of the managingDirector through the company
val salary = company.getPropertyValue("managingDirector.salary") as Float?
3.3.2. 内置PropertyEditor实现
Spring 使用PropertyEditor的概念来实现Object与String之间的转换。有时以不同于对象本身的方式来表示属性会很有用。例如,Date可以以一种人类可读的方式表示(如String:'2007-14-09'),同时我们仍然可以将这种人类可读的形式转换回原始日期(或者更好的是,将以任何人类可读形式输入的日期转换回Date对象)。这种行为可以通过注册类型为java.beans.PropertyEditor的自定义编辑器来实现。在BeanWrapper上注册自定义编辑器,或者在特定的 IoC 容器中注册(如前一章所述),可使其具备如何将属性转换为所需类型的知识。有关PropertyEditor的更多信息,请参阅Oracle 提供的java.beans包的 Javadoc。
Spring 中使用属性编辑的几个示例:
-
通过使用
PropertyEditor的实现类来设置 bean 的属性。 当你在 XML 文件中声明某个 bean 的属性时,若该属性的值为String类型, Spring(如果对应属性的 setter 方法具有Class类型的参数)会使用ClassEditor尝试将该参数解析为一个Class对象。 -
在 Spring 的 MVC 框架中,解析 HTTP 请求参数是通过使用各种
PropertyEditor实现来完成的,你可以手动将这些实现绑定到所有CommandController的子类中。
Spring 提供了多个内置的 PropertyEditor 实现,以简化开发工作。
它们都位于 org.springframework.beans.propertyeditors
包中。其中大部分(但并非全部,如下表所示)默认情况下会由
BeanWrapperImpl 自动注册。如果某个属性编辑器支持某种形式的配置,
你仍然可以注册自己的变体来覆盖默认实现。下表描述了 Spring 提供的各种
PropertyEditor 实现:
| 类 | 说明 |
|---|---|
|
字节数组的编辑器。将字符串转换为其对应的字节表示形式。默认由 |
|
将表示类的字符串解析为实际的类,反之亦然。当找不到某个类时,会抛出 |
|
用于 |
|
用于集合的属性编辑器,可将任意源 |
|
用于 |
|
适用于任意 |
|
将字符串解析为 |
|
一种单向属性编辑器,可以接收一个字符串,并通过中间的 |
|
可以将字符串解析为 |
|
可以将字符串解析为 |
|
可以将字符串(使用 |
|
用于修剪字符串的属性编辑器。可选择将空字符串转换为 |
|
可以将 URL 的字符串表示形式解析为实际的 |
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 源代码将 CustomNumberEditor 与 age 类的 Something 属性关联起来:
public class SomethingBeanInfo extends SimpleBeanInfo {
public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
};
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}
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,有几种机制可供使用。最手动的方式(通常既不方便也不推荐)是使用registerCustomEditor()接口的ConfigurableBeanFactory方法,前提是你持有BeanFactory的引用。另一种(稍微更方便的)机制是使用一个名为CustomEditorConfigurer的特殊 Bean 工厂后置处理器。虽然你可以在BeanFactory实现中使用 Bean 工厂后置处理器,但CustomEditorConfigurer具有嵌套属性配置,因此我们强烈建议将其与ApplicationContext一起使用,这样你可以像部署其他任何 Bean 一样部署它,并且它可以被自动检测和应用。
请注意,所有 BeanFactory 和 ApplicationContext 都会通过使用 BeanWrapper 来处理属性转换,从而自动应用若干内置的属性编辑器。BeanWrapper 所注册的标准属性编辑器已在前一节中列出。
此外,ApplicationContexts 还会覆盖或添加额外的编辑器,以适合特定 ApplicationContext 类型的方式来处理资源查找。
标准的 JavaBeans PropertyEditor 实例用于将字符串形式表示的属性值转换为该属性的实际复杂类型。您可以使用 CustomEditorConfigurer(一种 BeanFactory 后置处理器)方便地向 PropertyEditor 中添加对额外 ApplicationContext 实例的支持。
考虑以下示例,其中定义了一个名为 ExoticType 的用户类,
以及另一个名为 DependsOnExoticType 的类,该类需要将 ExoticType 设置为一个属性:
package example;
public class ExoticType {
private String name;
public ExoticType(String name) {
this.name = name;
}
}
public class DependsOnExoticType {
private ExoticType type;
public void setType(ExoticType type) {
this.type = type;
}
}
package example
class ExoticType(val name: String)
class DependsOnExoticType {
var type: ExoticType? = null
}
当一切正确配置后,我们希望能够将 type 属性以字符串形式赋值,然后由 PropertyEditor 将其转换为实际的 ExoticType 实例。以下的 bean 定义展示了如何建立这种关系:
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>
PropertyEditor 的实现可能类似于以下内容:
// converts string representation to ExoticType object
package example;
public class ExoticTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
// converts string representation to ExoticType object
package example
import java.beans.PropertyEditorSupport
class ExoticTypeEditor : PropertyEditorSupport() {
override fun setAsText(text: String) {
value = ExoticType(text.toUpperCase())
}
}
最后,以下示例展示了如何使用 CustomEditorConfigurer 将新的 PropertyEditor 注册到
ApplicationContext 中,之后该上下文便可在需要时使用它:
<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(..) 的属性。以这种方式添加到 CustomEditorConfigurer 的 PropertyEditorRegistrar 实例可以轻松地与 DataBinder 和 Spring MVC 控制器共享。此外,它避免了对自定义编辑器进行同步的需要:PropertyEditorRegistrar 预期会为每次 Bean 创建尝试生成全新的 PropertyEditor 实例。
以下示例展示了如何创建您自己的 PropertyEditorRegistrar 实现:
package com.foo.editors.spring;
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// you could register as many custom property editors as are required here...
}
}
package com.foo.editors.spring
import org.springframework.beans.PropertyEditorRegistrar
import org.springframework.beans.PropertyEditorRegistry
class CustomPropertyEditorRegistrar : PropertyEditorRegistrar {
override fun registerCustomEditors(registry: PropertyEditorRegistry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor())
// you could register as many custom property editors as are required here...
}
}
另请参阅 org.springframework.beans.support.ResourceEditorRegistrar,这是一个
PropertyEditorRegistrar 实现的示例。注意在其对
registerCustomEditors(..) 方法的实现中,它是如何为每个属性编辑器创建新实例的。
下一个示例展示了如何配置一个 CustomEditorConfigurer,并将我们自定义的 CustomPropertyEditorRegistrar 实例注入其中:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean>
<bean id="customPropertyEditorRegistrar"
class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
最后(对于使用Spring MVC Web框架的读者来说,这稍微偏离了本章的重点),在数据绑定的PropertyEditorRegistrars(例如Controllers)中结合使用SimpleFormController会非常方便。以下示例在PropertyEditorRegistrar方法的实现中使用了一个initBinder(..):
public final class RegisterUserController extends SimpleFormController {
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}
protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder binder) throws Exception {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
// other methods to do with registering a User
}
class RegisterUserController(
private val customPropertyEditorRegistrar: PropertyEditorRegistrar) : SimpleFormController() {
protected fun initBinder(request: HttpServletRequest,
binder: ServletRequestDataBinder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder)
}
// other methods to do with registering a User
}
这种 PropertyEditor 注册方式可以生成简洁的代码(initBinder(..) 的实现仅需一行),并将通用的 PropertyEditor 注册代码封装在一个类中,然后在任意多个 Controllers 中共享使用。
3.4. Spring 类型转换
Spring 3 引入了一个 core.convert 包,提供了一套通用的类型转换系统。该系统定义了一个 SPI(服务提供者接口)用于实现类型转换逻辑,以及一个 API 用于在运行时执行类型转换。在 Spring 容器中,您可以使用该系统作为 PropertyEditor 实现的替代方案,将外部化的 Bean 属性值字符串转换为所需的属性类型。此外,在应用程序中任何需要进行类型转换的地方,您也可以使用该公共 API。
3.4.1. 转换器 SPI
要实现类型转换逻辑的SPI接口简单且强类型化,如下接口定义所示:
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
package org.springframework.core.convert.converter
interface Converter<S, T> {
fun convert(source: S): T
}
要创建您自己的转换器,请实现 Converter 接口,并将泛型参数 S 指定为您要转换的源类型,T 指定为您要转换成的目标类型。此外,如果需要将 S 类型的集合或数组转换为 T 类型的数组或集合,也可以透明地应用此类转换器,前提是已同时注册了一个委托的数组或集合转换器(DefaultConversionService 默认会自动注册该转换器)。
每次调用 convert(S) 时,都可以保证源参数不为 null。如果转换失败,您的 Converter 可以抛出任何非检查异常。具体来说,当遇到无效的源值时,应抛出 IllegalArgumentException。
请务必确保您的 Converter 实现是线程安全的。
为方便起见,core.convert.support 包中提供了多个转换器(Converter)实现。这些实现包括从字符串到数字以及其他常见类型的转换器。
以下代码清单展示了 StringToInteger 类,这是一个典型的 Converter 实现:
package org.springframework.core.convert.support;
final class StringToInteger implements Converter<String, Integer> {
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
package org.springframework.core.convert.support
import org.springframework.core.convert.converter.Converter
internal class StringToInteger : Converter<String, Int> {
override fun convert(source: String): Int? {
return Integer.valueOf(source)
}
}
3.4.2. 使用ConverterFactory
当你需要为整个类层次结构集中转换逻辑时(例如,从 String 转换为 Enum 对象),你可以实现 ConverterFactory,如下例所示:
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
package org.springframework.core.convert.converter
interface ConverterFactory<S, R> {
fun <T : R> getConverter(targetType: Class<T>): Converter<S, T>
}
将泛型参数 S 设为你所要转换的源类型,将 R 设为你所能转换到的类的范围所对应的基类型。然后实现 getConverter(Class<T>) 方法,其中 T 是 R 的子类。
以 StringToEnumConverterFactory 为例:
package org.springframework.core.convert.support;
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnumConverter(targetType);
}
private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {
private Class<T> enumType;
public StringToEnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
public T convert(String source) {
return (T) Enum.valueOf(this.enumType, source.trim());
}
}
}
3.4.3. 使用GenericConverter
当你需要一个复杂的 Converter 实现时,可以考虑使用
GenericConverter 接口。与 Converter 相比,GenericConverter 具有更灵活但类型安全性较弱的签名,
支持在多种源类型和目标类型之间进行转换。此外,GenericConverter 还提供了源字段和目标字段的上下文信息,
你可以在实现转换逻辑时加以利用。这种上下文信息使得类型转换可以由字段上的注解或字段签名中声明的泛型信息驱动。
以下代码清单展示了 GenericConverter 的接口定义:
package org.springframework.core.convert.converter;
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
package org.springframework.core.convert.converter
interface GenericConverter {
fun getConvertibleTypes(): Set<ConvertiblePair>?
fun convert(@Nullable source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor): Any?
}
要实现一个GenericConverter,请让getConvertibleTypes()方法返回所支持的源类型→目标类型对。然后实现convert(Object, TypeDescriptor,
TypeDescriptor)方法,在其中编写你的转换逻辑。源TypeDescriptor提供了对包含待转换值的源字段的访问,而目标TypeDescriptor则提供了对将要设置转换后值的目标字段的访问。
GenericConverter 的一个很好的示例是用于在 Java 数组和集合之间进行转换的转换器。这种 ArrayToCollectionConverter 会内省声明目标集合类型的字段,以解析集合的元素类型。这样,在将集合设置到目标字段之前,源数组中的每个元素都可以被转换为集合的元素类型。
由于 GenericConverter 是一个更复杂的 SPI 接口,因此仅在确实需要时才应使用它。对于基本的类型转换需求,应优先选择 Converter 或 ConverterFactory。 |
使用ConditionalGenericConverter
有时,你希望仅在特定条件成立时才运行某个 Converter。例如,你可能希望仅当目标字段上存在某个特定注解时才运行该 Converter,或者仅当目标类中定义了某个特定方法(例如 Converter 方法)时才运行该 static valueOf。ConditionalGenericConverter 是 GenericConverter 和 ConditionalConverter 接口的结合,允许你定义此类自定义匹配条件:
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
interface ConditionalConverter {
fun matches(sourceType: TypeDescriptor, targetType: TypeDescriptor): Boolean
}
interface ConditionalGenericConverter : GenericConverter, ConditionalConverter
ConditionalGenericConverter 的一个很好的示例是 IdToEntityConverter,它用于在持久化实体标识符和实体引用之间进行转换。这样的 IdToEntityConverter 可能仅在目标实体类型声明了静态查找方法(例如 findAccount(Long))时才匹配。你可以在 matches(TypeDescriptor, TypeDescriptor) 的实现中执行此类查找方法的检查。
3.4.4.ConversionServiceAPI
ConversionService 定义了一个统一的 API,用于在运行时执行类型转换逻辑。转换器通常在以下外观接口背后运行:
package org.springframework.core.convert;
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
<T> T convert(Object source, Class<T> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
package org.springframework.core.convert
interface ConversionService {
fun canConvert(sourceType: Class<*>, targetType: Class<*>): Boolean
fun <T> convert(source: Any, targetType: Class<T>): T
fun canConvert(sourceType: TypeDescriptor, targetType: TypeDescriptor): Boolean
fun convert(source: Any, sourceType: TypeDescriptor, targetType: TypeDescriptor): Any
}
大多数 ConversionService 实现也实现了 ConverterRegistry,后者提供了一个用于注册转换器的 SPI。在内部,ConversionService 的实现会委托其已注册的转换器来执行类型转换逻辑。
ConversionService 包中提供了一个功能强大的 core.convert.support 实现。GenericConversionService 是一种通用的实现,适用于大多数环境。ConversionServiceFactory 提供了一个便捷的工厂类,用于创建常用的 ConversionService 配置。
3.4.5. 配置一个ConversionService
ConversionService 是一个无状态对象,设计为在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,通常会为每个 Spring 容器(或 ConversionService)配置一个 ApplicationContext 实例。Spring 会自动获取该 ConversionService,并在框架需要执行类型转换时使用它。你也可以将此 ConversionService 注入到任意 Bean 中,并直接调用它。
如果 Spring 中未注册 ConversionService,则使用基于原始 PropertyEditor 的系统。 |
要向 Spring 注册一个默认的 ConversionService,请添加以下 bean 定义,并将其 id 设为 conversionService:
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默认的 ConversionService 可以在字符串、数字、枚举、集合、映射(map)以及其他常见类型之间进行转换。若要使用您自己的自定义转换器来补充或覆盖默认的转换器,请设置 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 章节中的转换与格式化。
在某些情况下,您可能希望在转换过程中应用格式化。请参阅
《FormatterRegistry SPI》以了解如何使用FormattingConversionServiceFactoryBean的详细信息。
3.4.6. 使用一个ConversionService以编程方式
要以编程方式使用 ConversionService 实例,您可以像注入其他任何 Bean 一样注入对其的引用。以下示例展示了如何实现这一点:
@Service
public class MyService {
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
@Service
class MyService(private val conversionService: ConversionService) {
fun doIt() {
conversionService.convert(...)
}
}
对于大多数使用场景,你可以使用指定 convert 的 targetType 方法,但它无法处理更复杂的类型,例如包含参数化元素的集合。
例如,如果你想以编程方式将一个 List 的 Integer 转换为 List 的 String,
就需要提供源类型和目标类型的正式定义。
幸运的是,TypeDescriptor 提供了多种选项,使此类操作变得简单直接,如下例所示:
DefaultConversionService cs = new DefaultConversionService();
List<Integer> input = ...
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
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 会自动注册适用于大多数环境的转换器。这包括集合转换器、标量转换器以及基本的 Object 到 String 的转换器。你可以通过 ConverterRegistry 类的静态方法 addDefaultConverters,将相同的转换器注册到任意 DefaultConversionService 中。
值类型的转换器可被复用于数组和集合,因此无需专门创建一个从 Collection 的 S 转换为 Collection 的 T 的转换器,前提是标准的集合处理方式适用。
3.5. Spring 字段格式化
正如上一节所讨论的,core.convert 是一个通用的类型转换系统。它提供了一个统一的 ConversionService API,以及一个强类型的 Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring 容器使用该系统来绑定 Bean 属性值。此外,Spring 表达式语言 (SpEL) 和 DataBinder 也使用该系统来绑定字段值。例如,当 SpEL 需要将 Short 强制转换为 Long 以完成 expression.setValue(Object bean, Object value) 尝试时,core.convert 系统会执行该强制转换。
现在考虑典型客户端环境(例如 Web 应用或桌面应用)中的类型转换需求。在这些环境中,你通常需要从 String 进行转换,以支持客户端的回传(postback)过程;同时也需要转换回 String,以支持视图渲染过程。此外,你通常还需要对 String 值进行本地化处理。更通用的 core.convert Converter SPI 并未直接解决此类格式化需求。为了直接应对这些需求,Spring 3 引入了一个便捷的 Formatter SPI,为客户端环境提供了一种简单而健壮的替代方案,取代了传统的 PropertyEditor 实现。
通常,当你需要实现通用的类型转换逻辑时(例如,在 Converter 和 java.util.Date 之间进行转换),可以使用 Long SPI。
当你在客户端环境(例如 Web 应用程序)中工作,并且需要解析和打印本地化的字段值时,可以使用 Formatter SPI。
ConversionService 为这两个 SPI 提供了统一的类型转换 API。
3.5.1.FormatterSPI
用于实现字段格式化逻辑的 Formatter SPI 简单且具有强类型。以下代码清单展示了 Formatter 接口的定义:
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
Formatter 继承自 Printer 和 Parser 这两个基础接口。以下代码清单展示了这两个接口的定义:
public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
interface Printer<T> {
fun print(fieldValue: T, locale: Locale): String
}
import java.text.ParseException;
public interface Parser<T> {
T parse(String clientValue, Locale locale) throws ParseException;
}
interface Parser<T> {
@Throws(ParseException::class)
fun parse(clientValue: String, locale: Locale): T
}
要创建您自己的Formatter,请实现前面展示的Formatter接口。
将T参数化为您希望格式化的对象类型——例如,
java.util.Date。实现print()操作以打印T的实例,
以便在客户端区域设置中显示。实现parse()操作以从客户端区域设置返回的格式化表示中解析T的实例。如果解析尝试失败,您的Formatter
应抛出ParseException或IllegalArgumentException。请务必确保您的Formatter实现是线程安全的。
format 子包为方便起见提供了多种 Formatter 实现。
number 包提供 NumberStyleFormatter、CurrencyStyleFormatter 和
PercentStyleFormatter,用于格式化使用 java.text.NumberFormat 的 Number 对象。
datetime 包提供一个 DateFormatter,用于通过
java.text.DateFormat 格式化 java.util.Date 对象。datetime.joda 包基于 Joda-Time 库 提供全面的日期时间格式化支持。
以下的 DateFormatter 是一个 Formatter 实现示例:
package org.springframework.format.datetime;
public final class DateFormatter implements Formatter<Date> {
private String pattern;
public DateFormatter(String pattern) {
this.pattern = pattern;
}
public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}
public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}
protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}
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接口的定义:
package org.springframework.format;
public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
package org.springframework.format
interface AnnotationFormatterFactory<A : Annotation> {
val fieldTypes: Set<Class<*>>
fun getPrinter(annotation: A, fieldType: Class<*>): Printer<*>
fun getParser(annotation: A, fieldType: Class<*>): Parser<*>
}
要创建一个实现:
. 将 A 参数化为希望关联格式化逻辑的字段 annotationType —— 例如 org.springframework.format.annotation.DateTimeFormat。
. 让 getFieldTypes() 返回可使用该注解的字段类型。
. 让 getPrinter() 返回一个 Printer,用于打印带注解字段的值。
. 让 getParser() 返回一个 Parser,用于解析带注解字段的 clientValue。
以下示例中的 AnnotationFormatterFactory 实现将 @NumberFormat
注解绑定到一个格式化器,以允许指定数字的样式或模式:
public final class NumberFormatAnnotationFormatterFactory
implements AnnotationFormatterFactory<NumberFormat> {
public Set<Class<?>> getFieldTypes() {
return new HashSet<Class<?>>(asList(new Class<?>[] {
Short.class, Integer.class, Long.class, Float.class,
Double.class, BigDecimal.class, BigInteger.class }));
}
public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
if (!annotation.pattern().isEmpty()) {
return new NumberStyleFormatter(annotation.pattern());
} else {
Style style = annotation.style();
if (style == Style.PERCENT) {
return new PercentStyleFormatter();
} else if (style == Style.CURRENCY) {
return new CurrencyStyleFormatter();
} else {
return new NumberStyleFormatter();
}
}
}
}
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 注解字段,如下例所示:
public class MyModel {
@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}
class MyModel(
@field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal
)
格式化注解 API
在 org.springframework.format.annotation 包中存在一个可移植的格式注解 API。您可以使用 @NumberFormat 来格式化 Number 字段,例如 Double 和 Long;并使用 @DateTimeFormat 来格式化 java.util.Date、java.util.Calendar、Long(用于毫秒时间戳),以及 JSR-310 java.time 和 Joda-Time 值类型。
以下示例使用 @DateTimeFormat 将 java.util.Date 格式化为 ISO 日期
(yyyy-MM-dd):
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
class MyModel(
@DateTimeFormat(iso= ISO.DATE) private val date: Date
)
3.5.3.FormatterRegistrySPI
FormatterRegistry 是一个用于注册格式化器(formatter)和转换器(converter)的 SPI 接口。
FormattingConversionService 是 FormatterRegistry 的一种实现,适用于大多数环境。你可以通过编程方式或声明方式将此实现配置为 Spring Bean,例如使用 FormattingConversionServiceFactoryBean。由于该实现同时也实现了 ConversionService 接口,因此你可以直接将其配置用于 Spring 的 DataBinder 和 Spring 表达式语言(SpEL)。
以下代码清单展示了 FormatterRegistry SPI:
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Formatter<?> formatter);
void addFormatterForAnnotation(AnnotationFormatterFactory<?> factory);
}
package org.springframework.format
interface FormatterRegistry : ConverterRegistry {
fun addFormatterForFieldType(fieldType: Class<*>, printer: Printer<*>, parser: Parser<*>)
fun addFormatterForFieldType(fieldType: Class<*>, formatter: Formatter<*>)
fun addFormatterForFieldType(formatter: Formatter<*>)
fun addFormatterForAnnotation(factory: AnnotationFormatterFactory<*>)
}
如上所示,您可以按字段类型或按注解注册格式化器。
FormatterRegistry SPI 允许您集中配置格式化规则,而无需在各个控制器中重复此类配置。例如,您可能希望强制所有日期字段都以某种特定方式格式化,或者带有特定注解的字段以某种特定方式格式化。通过共享的 FormatterRegistry,您只需定义一次这些规则,它们就会在需要格式化时自动应用。
3.5.4.FormatterRegistrarSPI
FormatterRegistrar 是一个 SPI(服务提供者接口),用于通过 FormatterRegistry 注册格式化器(formatter)和转换器(converter)。以下代码清单展示了其接口定义:
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
package org.springframework.format
interface FormatterRegistrar {
fun registerFormatters(registry: FormatterRegistry)
}
FormatterRegistrar 在为某一格式化类别(例如日期格式化)注册多个相关的转换器(converter)和格式化器(formatter)时非常有用。当声明式注册方式不够用时,它也很有帮助——例如,当某个格式化器需要根据与其自身 <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,或针对 Joda-Time 的org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar。
例如,以下 Java 配置注册了一个全局的 yyyyMMdd 格式:
@Configuration
public class AppConfig {
@Bean
public FormattingConversionService conversionService() {
// Use the DefaultFormattingConversionService but do not register defaults
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
// Ensure @NumberFormat is still supported
conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
// Register JSR-310 date conversion with a specific global format
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
registrar.registerFormatters(conversionService);
// Register date conversion with a specific global format
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
registrar.setFormatter(new DateFormatter("yyyyMMdd"));
registrar.registerFormatters(conversionService);
return conversionService;
}
}
@Configuration
class AppConfig {
@Bean
fun conversionService(): FormattingConversionService {
// Use the DefaultFormattingConversionService but do not register defaults
return DefaultFormattingConversionService(false).apply {
// Ensure @NumberFormat is still supported
addFormatterForFieldAnnotation(NumberFormatAnnotationFormatterFactory())
// Register JSR-310 date conversion with a specific global format
val registrar = DateTimeFormatterRegistrar()
registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"))
registrar.registerFormatters(this)
// Register date conversion with a specific global format
val registrar = DateFormatterRegistrar()
registrar.setFormatter(DateFormatter("yyyyMMdd"))
registrar.registerFormatters(this)
}
}
}
如果你更喜欢基于 XML 的配置,可以使用 FormattingConversionServiceFactoryBean。以下示例展示了如何实现这一点(这次使用的是 Joda Time):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
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.joda.JodaTimeFormatterRegistrar">
<property name="dateFormatter">
<bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
<property name="pattern" value="yyyyMMdd"/>
</bean>
</property>
</bean>
</set>
</property>
</bean>
</beans>
请注意,在 Web 应用程序中配置日期和时间格式时还有额外的注意事项。请参阅 WebMVC 转换与格式化 或 WebFlux 转换与格式化。
3.7. Java Bean 验证
Spring 框架提供了对 Java Bean Validation API 的支持。
3.7.1. Bean 验证概述
Bean Validation 为 Java 应用程序提供了一种通过约束声明和元数据进行验证的通用方式。要使用它,您可以在领域模型属性上添加声明式验证约束注解,这些约束将在运行时被强制执行。该框架提供了内置的约束,您也可以定义自己的自定义约束。
考虑以下示例,它展示了一个包含两个属性的简单 PersonForm 模型:
public class PersonForm {
private String name;
private int age;
}
class PersonForm(
private val name: String,
private val age: Int
)
Bean Validation 允许你声明如下示例所示的约束:
public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}
class PersonForm(
@get:NotNull @get:Size(max=64)
private val name: String,
@get:Min(0)
private val age: Int
)
然后,Bean Validation 验证器会根据声明的约束条件来验证该类的实例。有关该 API 的一般信息,请参阅 Bean Validation。有关特定约束的详细信息,请参阅 Hibernate Validator 文档。若要了解如何将 Bean Validation 提供程序配置为 Spring Bean,请继续阅读。
3.7.2. 配置 Bean 验证提供者
Spring 提供对 Bean Validation API 的完整支持,包括将 Bean Validation 提供程序作为 Spring Bean 进行引导。这使得你可以在应用程序中任何需要验证的地方注入 javax.validation.ValidatorFactory 或 javax.validation.Validator。
您可以使用 LocalValidatorFactoryBean 将默认的 Validator 配置为 Spring bean,如下例所示:
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class AppConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
前面示例中的基本配置会触发 Bean Validation,使用其默认的引导机制进行初始化。系统期望在类路径中存在一个 Bean Validation 提供者(例如 Hibernate Validator),并会自动检测到它。
注入验证器
LocalValidatorFactoryBean 同时实现了 javax.validation.ValidatorFactory 和
javax.validation.Validator,以及 Spring 的 org.springframework.validation.Validator。
你可以将对其中任意一个接口的引用注入到需要调用验证逻辑的 bean 中。
如果你更倾向于直接使用 Bean Validation API,可以注入一个对 javax.validation.Validator 的引用,如下例所示:
import javax.validation.Validator;
@Service
public class MyService {
@Autowired
private Validator validator;
}
import javax.validation.Validator;
@Service
class MyService(@Autowired private val validator: Validator)
如果您的 bean 需要使用 Spring 验证 API,您可以注入一个对 org.springframework.validation.Validator 的引用,如下例所示:
import org.springframework.validation.Validator;
@Service
public class MyService {
@Autowired
private Validator validator;
}
import org.springframework.validation.Validator
@Service
class MyService(@Autowired private val validator: Validator)
配置自定义约束
每个 Bean Validation 约束由两部分组成:
-
一个
@Constraint注解,用于声明约束及其可配置的属性。 -
实现
javax.validation.ConstraintValidator接口的类,用于实现约束的行为。
为了将声明与实现关联起来,每个 @Constraint 注解都会引用一个对应的 ConstraintValidator 实现类。在运行时,当在您的领域模型中遇到约束注解时,ConstraintValidatorFactory 会实例化所引用的实现类。
默认情况下,LocalValidatorFactoryBean 会配置一个 SpringConstraintValidatorFactory,
该工厂使用 Spring 来创建 ConstraintValidator 实例。这使得您自定义的
ConstraintValidators 能够像其他 Spring Bean 一样受益于依赖注入。
以下示例展示了一个自定义的 @Constraint 声明,以及一个关联的 ConstraintValidator 实现,该实现使用 Spring 进行依赖注入:
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator::class)
annotation class MyConstraint
import javax.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator {
@Autowired;
private Foo aDependency;
// ...
}
import javax.validation.ConstraintValidator
class MyConstraintValidator(private val aDependency: Foo) : ConstraintValidator {
// ...
}
如上例所示,ConstraintValidator 的实现可以像其他任何 Spring Bean 一样,通过 @Autowired 注入其依赖项。
Spring 驱动的方法验证
你可以通过一个 MethodValidationPostProcessor bean 定义,将 Bean Validation 1.1(以及作为自定义扩展,Hibernate Validator 4.3 也支持)所提供的方法验证功能集成到 Spring 上下文中:
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
@Configuration
public class AppConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
要使基于 Spring 的方法验证生效,所有目标类都需要使用 Spring 的 @Validated 注解进行标注,该注解还可以可选地声明要使用的验证组。请参阅
MethodValidationPostProcessor
以了解使用 Hibernate Validator 和 Bean Validation 1.1 提供程序的设置详情。
其他配置选项
默认的 LocalValidatorFactoryBean 配置足以满足大多数情况。针对各种 Bean Validation 构造,存在多种配置选项,范围涵盖消息插值到遍历解析。有关这些选项的更多信息,请参阅 LocalValidatorFactoryBean 的 javadoc。
3.7.3. 配置一个DataBinder
从 Spring 3 开始,你可以使用 DataBinder 来配置一个 Validator 实例。配置完成后,可以通过调用 Validator 来触发验证器。所有验证产生的 binder.validate() 会自动添加到绑定器的 Errors 中。
以下示例展示了如何以编程方式使用 DataBinder,在绑定到目标对象后调用验证逻辑:
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());
// bind to the target object
binder.bind(propertyValues);
// validate the target object
binder.validate();
// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();
val target = Foo()
val binder = DataBinder(target)
binder.validator = FooValidator()
// bind to the target object
binder.bind(propertyValues)
// validate the target object
binder.validate()
// get BindingResult that includes any validation errors
val results = binder.bindingResult
您还可以通过 DataBinder 和 Validator 为 dataBinder.addValidators 配置多个 dataBinder.replaceValidators 实例。当您需要将全局配置的 Bean 验证与在 DataBinder 实例上本地配置的 Spring Validator 结合使用时,这种方式非常有用。请参阅
Spring MVC 验证配置。
3.7.4. Spring MVC 3 验证
参见 Spring MVC 章节中的验证部分。
4. Spring 表达式语言 (SpEL)
Spring 表达式语言(简称 “SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。该语言的语法类似于 Unified EL,但提供了额外的功能,尤其是方法调用和基本的字符串模板功能。
尽管还有其他几种可用的 Java 表达式语言——例如 OGNL、MVEL 和 JBoss EL 等,但 Spring 表达式语言(SpEL)的创建旨在为 Spring 社区提供一种统一且得到良好支持的表达式语言,可在整个 Spring 产品组合中通用。其语言特性由 Spring 产品组合中各项目的需求驱动,包括在 Spring Tools for Eclipse 中对代码自动补全等工具支持的需求。 尽管如此,SpEL 基于一个与技术无关的 API,如有需要,可集成其他表达式语言的实现。
虽然 SpEL 是 Spring 产品组合中表达式求值的基础,但它并不直接依赖于 Spring,可以独立使用。为了保持自包含性,本章中的许多示例都将 SpEL 视为一种独立的表达式语言来使用。这需要创建一些引导所需的基础设施类,例如解析器。大多数 Spring 用户无需处理这些基础设施,而只需编写用于求值的表达式字符串即可。这种典型用法的一个例子是将 SpEL 集成到基于 XML 或基于注解的 bean 定义中,如 定义 Bean 时的表达式支持 所示。
本章介绍了表达式语言的特性、其 API 以及语言语法。在多处示例中,Inventor 和 Society 类被用作表达式求值的目标对象。这些类的声明以及用于填充它们的数据列在本章末尾。
表达式语言支持以下功能:
-
字面量表达式
-
布尔和关系运算符
-
正则表达式
-
类表达式
-
访问属性、数组、列表和映射
-
方法调用
-
关系运算符
-
赋值
-
调用构造函数
-
Bean 引用
-
数组构造
-
行内列表
-
内联映射
-
三元运算符
-
变量
-
用户自定义函数
-
集合投影
-
集合选择
-
模板表达式
4.1. 评估
本节介绍 SpEL 接口及其表达式语言的简单用法。 完整的语言参考请见 语言参考。
以下代码使用 SpEL API 来求值字面量字符串表达式 Hello World。
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
| 1 | message 变量的值为 'Hello World'。 |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
| 1 | message 变量的值为 'Hello World'。 |
你最有可能使用的 SpEL 类和接口位于 org.springframework.expression 包及其子包中,例如 spel.support。
ExpressionParser 接口负责解析表达式字符串。在前面的示例中,表达式字符串是由单引号包围的字符串字面量。Expression 接口负责对先前定义的表达式字符串进行求值。调用 ParseException 和 EvaluationException 时,可能会分别抛出两种异常:parser.parseExpression 和 exp.getValue。
SpEL 支持丰富的功能,例如调用方法、访问属性以及调用构造函数。
在以下方法调用的示例中,我们在字符串字面量上调用 concat 方法:
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();
| 1 | message 的值现在是 'Hello World!'。 |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'.concat('!')") (1)
val message = exp.value as String
| 1 | message 的值现在是 'Hello World!'。 |
以下调用 JavaBean 属性的示例调用了 String 类型的属性 Bytes:
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();
| 1 | 此行将字面量转换为字节数组。 |
val parser = SpelExpressionParser()
// invokes 'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") (1)
val bytes = exp.value as ByteArray
| 1 | 此行将字面量转换为字节数组。 |
SpEL 还支持使用标准的点号表示法(例如 prop1.prop2.prop3)来访问嵌套属性,并支持相应地设置属性值。
也可以访问公共字段。
以下示例展示了如何使用点号表示法来获取字面量的长度:
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1)
int length = (Integer) exp.getValue();
| 1 | 'Hello World'.bytes.length 给出该字面量的长度。 |
val parser = SpelExpressionParser()
// invokes 'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") (1)
val length = exp.value as Int
| 1 | 'Hello World'.bytes.length 给出该字面量的长度。 |
可以调用 String 的构造函数,而不使用字符串字面量,如下例所示:
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);
| 1 | 从字面量构造一个新的String,并将其转换为大写。 |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()") (1)
val message = exp.getValue(String::class.java)
| 1 | 从字面量构造一个新的String,并将其转换为大写。 |
请注意泛型方法的使用:public <T> T getValue(Class<T> desiredResultType)。
使用此方法可以避免将表达式的值强制转换为所需的结果类型。
如果该值无法转换为类型 EvaluationException,或者无法通过已注册的类型转换器进行转换,则会抛出 T 异常。
SpEL 更常见的用法是提供一个表达式字符串,该字符串针对特定的对象实例(称为根对象)进行求值。以下示例展示了如何从 name 类的实例中获取 Inventor 属性,或创建一个布尔条件:
// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // Parse name as an expression
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
// Create and set a calendar
val c = GregorianCalendar()
c.set(1856, 7, 9)
// The constructor arguments are name, birthday, and nationality.
val tesla = Inventor("Nikola Tesla", c.time, "Serbian")
val parser = SpelExpressionParser()
var exp = parser.parseExpression("name") // Parse name as an expression
val name = exp.getValue(tesla) as String
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'")
val result = exp.getValue(tesla, Boolean::class.java)
// result == true
4.1.1. 理解EvaluationContext
在对表达式进行求值时,EvaluationContext 接口用于解析属性、方法或字段,并协助执行类型转换。Spring 提供了两种实现。
-
SimpleEvaluationContext:公开 SpEL 语言核心功能和配置选项的一个子集,适用于那些不需要完整 SpEL 语言语法、且应进行有意义限制的表达式类别。示例包括但不限于数据绑定表达式和基于属性的过滤器。 -
StandardEvaluationContext:提供完整的 SpEL 语言特性和配置选项。你可以用它来指定默认的根对象,并配置所有可用的与表达式求值相关的策略。
SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。
它排除了 Java 类型引用、构造函数和 Bean 引用。同时,它还要求你显式选择表达式中对属性和方法的支持级别。
默认情况下,create() 静态工厂方法仅启用对属性的读取访问。
你也可以获取一个构建器(builder),以配置所需的确切支持级别,针对以下一项或多项组合进行设置:
-
仅自定义
PropertyAccessor(无反射) -
用于只读访问的数据绑定属性
-
用于读取和写入的数据绑定属性
类型转换
默认情况下,SpEL 使用 Spring Core 中提供的转换服务(org.springframework.core.convert.ConversionService)。该转换服务内置了许多用于常见类型转换的转换器,同时也完全可扩展,允许你添加自定义的类型间转换。此外,它还支持泛型。这意味着,当你在表达式中使用泛型类型时,SpEL 会尝试进行类型转换,以确保所遇到的任何对象都保持类型正确性。
这在实践中意味着什么?假设正在使用 setValue() 进行赋值,以设置一个 List 属性。该属性的实际类型是 List<Boolean>。SpEL 会识别出列表中的元素在放入列表之前需要转换为 Boolean 类型。以下示例展示了如何实现这一点:
class Simple {
public List<Boolean> booleanList = new ArrayList<Boolean>();
}
Simple simple = new Simple();
simple.booleanList.add(true);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
// b is false
Boolean b = simple.booleanList.get(0);
class Simple {
var booleanList: MutableList<Boolean> = ArrayList()
}
val simple = Simple()
simple.booleanList.add(true)
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false")
// b is false
val b = simple.booleanList[0]
4.1.2. 解析器配置
可以通过使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)来配置 SpEL 表达式解析器。该配置对象用于控制某些表达式组件的行为。例如,当你对数组或集合进行索引访问时,如果指定索引处的元素为 null,可以自动创建该元素。这在使用由一系列属性引用组成的表达式时非常有用。此外,如果你对数组或列表进行索引访问,并指定了超出当前数组或列表大小的索引,也可以自动扩展数组或列表以容纳该索引。以下示例演示了如何自动扩展列表:
class Demo {
public List<String> list;
}
// Turn on:
// - auto null reference initialization
// - auto collection growing
SpelParserConfiguration config = new SpelParserConfiguration(true,true);
ExpressionParser parser = new SpelExpressionParser(config);
Expression expression = parser.parseExpression("list[3]");
Demo demo = new Demo();
Object o = expression.getValue(demo);
// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String
class Demo {
var list: List<String>? = null
}
// Turn on:
// - auto null reference initialization
// - auto collection growing
val config = SpelParserConfiguration(true, true)
val parser = SpelExpressionParser(config)
val expression = parser.parseExpression("list[3]")
val demo = Demo()
val o = expression.getValue(demo)
// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String
4.1.3. SpEL 编译
Spring Framework 4.1 包含一个基本的表达式编译器。表达式通常以解释方式执行,这在求值过程中提供了很大的动态灵活性,但无法实现最佳性能。对于偶尔使用的表达式来说,这没有问题;然而,当被其他组件(例如 Spring Integration)使用时,性能就变得非常重要,而此时实际上并不需要这种动态性。
SpEL 编译器正是为满足这一需求而设计的。在表达式求值过程中,编译器会生成一个 Java 类,该类在运行时体现表达式的行为,并利用该类实现更快的表达式求值。由于表达式本身缺乏类型信息,编译器在编译时会利用解释执行阶段收集到的表达式求值信息。例如,仅从表达式本身无法得知某个属性引用的具体类型,但在首次解释执行求值时,编译器就能确定其实际类型。当然,如果表达式中各个元素的类型随时间发生变化,那么基于此类推导出的信息进行编译可能会在后续引发问题。因此,编译最适合用于那些在重复求值过程中类型信息不会发生变化的表达式。
考虑以下基本表达式:
someArray[0].someProperty.someOtherProperty < 0.1
由于上述表达式涉及数组访问、某些属性解引用以及数值运算,因此性能提升可能非常明显。在一个包含 50000 次迭代的微基准测试示例中,使用解释器求值耗时 75 毫秒,而使用编译后的表达式版本仅耗时 3 毫秒。
编译器配置
编译器默认未启用,但您可以通过以下两种不同方式之一来启用它。您可以使用解析器配置过程(如前所述)来启用,或者在 SpEL 被嵌入到另一个组件中使用时,通过系统属性来启用。本节将讨论这两种选项。
编译器可以以三种模式之一运行,这些模式在 org.springframework.expression.spel.SpelCompilerMode 枚举中定义。这些模式如下所示:
-
OFF(默认):编译器已关闭。 -
IMMEDIATE:在即时模式下,表达式会尽快被编译。这通常发生在首次解释执行之后。如果编译后的表达式执行失败(通常是由于前面所述的类型发生变化),调用该表达式求值的代码将收到一个异常。 -
MIXED:在混合模式下,表达式会随着时间的推移,在解释模式和编译模式之间自动静默切换。经过若干次解释执行后,它们会切换到编译形式;如果编译形式出现问题(例如前面提到的类型发生变化),表达式会自动切换回解释形式。稍后,它可能会再次生成另一个编译形式并切换过去。基本上,在IMMEDIATE模式下用户会收到的异常,此时会在内部被自动处理。
IMMEDIATE 模式之所以存在,是因为 MIXED 模式可能会对具有副作用的表达式造成问题。如果一个已编译的表达式在部分成功后发生异常,它可能已经执行了某些影响系统状态的操作。在这种情况下,调用者可能不希望该表达式在解释模式下静默地重新运行,因为表达式的部分内容可能会被执行两次。
选择模式后,使用 SpelParserConfiguration 来配置解析器。以下示例展示了如何进行配置:
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
this.getClass().getClassLoader());
SpelExpressionParser parser = new SpelExpressionParser(config);
Expression expr = parser.parseExpression("payload");
MyMessage message = new MyMessage();
Object payload = expr.getValue(message);
val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
this.javaClass.classLoader)
val parser = SpelExpressionParser(config)
val expr = parser.parseExpression("payload")
val message = MyMessage()
val payload = expr.getValue(message)
当你指定编译器模式时,还可以指定一个类加载器(允许传入 null)。 编译后的表达式定义在一个子类加载器中,该子类加载器是在所提供的任何类加载器之下创建的。 重要的是要确保:如果指定了类加载器,则该类加载器必须能够访问表达式求值过程中涉及的所有类型。 如果你未指定类加载器,则会使用默认的类加载器(通常是执行表达式求值的线程的上下文类加载器)。
配置编译器的第二种方式适用于 SpEL 嵌入到其他组件内部,且无法通过配置对象进行配置的情况。在这些情况下,可以使用系统属性。你可以将 spring.expression.compiler.mode 属性设置为 SpelCompilerMode 枚举值之一(off、immediate 或 mixed)。
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>)。
以下示例展示了如何将 systemProperties bean 作为 SpEL 变量进行访问:
<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 注解应用于字段、方法以及方法或构造函数的参数上。
以下示例设置了字段变量的默认值:
public class FieldValueTestBean {
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}
class FieldValueTestBean {
@Value("#{ systemProperties['user.region'] }")
var defaultLocale: String? = null
}
以下示例展示了等效的用法,但应用于属性的 setter 方法上:
public class PropertyValueTestBean {
private String defaultLocale;
@Value("#{ systemProperties['user.region'] }")
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}
class PropertyValueTestBean {
@Value("#{ systemProperties['user.region'] }")
var defaultLocale: String? = null
}
自动装配的方法和构造函数也可以使用 @Value 注解,如下例所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
private String defaultLocale;
@Autowired
public void configure(MovieFinder movieFinder,
@Value("#{ systemProperties['user.region'] }") String defaultLocale) {
this.movieFinder = movieFinder;
this.defaultLocale = defaultLocale;
}
// ...
}
class SimpleMovieLister {
private lateinit var movieFinder: MovieFinder
private lateinit var defaultLocale: String
@Autowired
fun configure(movieFinder: MovieFinder,
@Value("#{ systemProperties['user.region'] }") defaultLocale: String) {
this.movieFinder = movieFinder
this.defaultLocale = defaultLocale
}
// ...
}
public class MovieRecommender {
private String defaultLocale;
private CustomerPreferenceDao customerPreferenceDao;
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
@Value("#{systemProperties['user.country']}") String defaultLocale) {
this.customerPreferenceDao = customerPreferenceDao;
this.defaultLocale = defaultLocale;
}
// ...
}
class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao,
@Value("#{systemProperties['user.country']}") private val defaultLocale: String) {
// ...
}
4.3. 语言参考
本节介绍 Spring 表达式语言(Spring Expression Language)的工作原理。涵盖以下主题:
4.3.1. 字面量表达式
支持的字面量表达式类型包括字符串、数值(整数、实数、十六进制)、布尔值和 null。字符串用单引号括起来。若要在字符串中包含单引号本身,请使用两个连续的单引号字符。
以下示例展示了字面量的简单用法。通常,它们不会像这样单独使用,而是作为更复杂表达式的一部分——例如,在逻辑比较运算符的一侧使用字面量。
ExpressionParser parser = new SpelExpressionParser();
// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();
double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();
// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
boolean trueValue = (Boolean) parser.parseExpression("true").getValue();
Object nullValue = parser.parseExpression("null").getValue();
val parser = SpelExpressionParser()
// evals to "Hello World"
val helloWorld = parser.parseExpression("'Hello World'").value as String
val avogadrosNumber = parser.parseExpression("6.0221415E+23").value as Double
// evals to 2147483647
val maxValue = parser.parseExpression("0x7FFFFFFF").value as Int
val trueValue = parser.parseExpression("true").value as Boolean
val nullValue = parser.parseExpression("null").value
数字支持使用负号、指数表示法和小数点。 默认情况下,实数通过使用 Double.parseDouble() 进行解析。
4.3.2. 属性、数组、列表、映射和索引器
使用属性引用来进行导航非常简单。为此,使用点号(.)来表示嵌套的属性值。Inventor 类的实例 pupin 和 tesla 已根据 示例中使用的类 部分所列的数据进行了填充。
要“向下”导航并获取特斯拉的出生年份和普平的出生城市,我们使用以下表达式:
// evals to 1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);
String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);
// evals to 1856
val year = parser.parseExpression("Birthdate.Year + 1900").getValue(context) as Int
val city = parser.parseExpression("placeOfBirth.City").getValue(context) as String
属性名称的首字母允许不区分大小写。数组和列表的内容通过使用方括号表示法获取,如下例所示:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// Inventions Array
// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
context, tesla, String.class);
// Members List
// evaluates to "Nikola Tesla"
String name = parser.parseExpression("Members[0].Name").getValue(
context, ieee, String.class);
// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
context, ieee, String.class);
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// Inventions Array
// evaluates to "Induction motor"
val invention = parser.parseExpression("inventions[3]").getValue(
context, tesla, String::class.java)
// Members List
// evaluates to "Nikola Tesla"
val name = parser.parseExpression("Members[0].Name").getValue(
context, ieee, String::class.java)
// List and Array navigation
// evaluates to "Wireless communication"
val invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
context, ieee, String::class.java)
通过在方括号内指定字面量键值来获取映射(map)的内容。在下面的示例中,由于 Officers 映射的键是字符串,因此我们可以指定字符串字面量:
// Officer's Dictionary
Inventor pupin = parser.parseExpression("Officers['president']").getValue(
societyContext, Inventor.class);
// evaluates to "Idvor"
String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
societyContext, String.class);
// setting values
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
societyContext, "Croatia");
// Officer's Dictionary
val pupin = parser.parseExpression("Officers['president']").getValue(
societyContext, Inventor::class.java)
// evaluates to "Idvor"
val city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
societyContext, String::class.java)
// setting values
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
societyContext, "Croatia")
4.3.3. 内联列表
你可以通过使用 {} 符号在表达式中直接表示列表。
// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
// evaluates to a Java list containing the four numbers
val numbers = parser.parseExpression("{1,2,3,4}").getValue(context) as List<*>
val listOfLists = parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context) as List<*>
{} 本身表示一个空列表。出于性能考虑,如果该列表完全由固定字面量组成,则会创建一个常量列表来表示该表达式(而不是在每次求值时都构建一个新列表)。
4.3.4. 内联映射
你也可以通过使用 {key:value} 表示法在表达式中直接表示映射(map)。以下示例展示了如何实现这一点:
// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
// 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)。出于性能考虑,如果该映射本身由固定字面量或其他嵌套的常量结构(列表或映射)组成,则会创建一个常量映射来表示该表达式(而不是在每次求值时都新建一个映射)。映射的键是否加引号是可选的。上述示例中未使用带引号的键。
4.3.5. 数组构造
你可以使用熟悉的 Java 语法来构建数组,并可选择提供一个初始化器,以便在构造时填充数组。以下示例展示了如何实现这一点:
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
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 编程语法来调用方法。您也可以对字面量调用方法。此外,还支持可变参数。以下示例展示了如何调用方法:
// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);
// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
societyContext, Boolean.class);
// string literal, evaluates to "bc"
val bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String::class.java)
// evaluates to true
val isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
societyContext, Boolean::class.java)
4.3.7. 操作符
Spring 表达式语言支持以下类型的运算符:
关系运算符
关系运算符(等于、不等于、小于、小于等于、大于和大于等于)支持使用标准运算符符号。以下列表展示了一些运算符的示例:
// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);
// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);
// evaluates to true
val trueValue = parser.parseExpression("2 == 2").getValue(Boolean::class.java)
// evaluates to false
val falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean::class.java)
// evaluates to true
val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java)
|
与 如果你更倾向于使用数值比较,请避免基于数字的 |
除了标准的关系运算符外,SpEL 还支持 instanceof 运算符以及基于正则表达式的 matches 运算符。以下示例展示了这两种运算符的用法:
// evaluates to false
boolean falseValue = parser.parseExpression(
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression(
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
//evaluates to false
boolean falseValue = parser.parseExpression(
"'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
// 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(!)
以下示例展示了如何使用逻辑运算符
// -- AND --
// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);
// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- OR --
// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);
// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- NOT --
// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);
// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- AND --
// evaluates to false
val falseValue = parser.parseExpression("true and false").getValue(Boolean::class.java)
// evaluates to true
val expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')"
val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)
// -- OR --
// evaluates to true
val trueValue = parser.parseExpression("true or false").getValue(Boolean::class.java)
// evaluates to true
val expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')"
val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)
// -- NOT --
// evaluates to false
val falseValue = parser.parseExpression("!true").getValue(Boolean::class.java)
// -- AND and NOT --
val expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"
val falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)
数学运算符
您可以对数字和字符串使用加法运算符。减法、乘法和除法运算符仅可用于数字。您还可以使用取模(%)和指数幂(^)运算符。标准的运算符优先级规则会被强制执行。以下示例展示了数学运算符的使用:
// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2
String testString = parser.parseExpression(
"'test' + ' ' + 'string'").getValue(String.class); // 'test string'
// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4
double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000
// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6
double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0
// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2
double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0
// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3
int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1
// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21
// Addition
val two = parser.parseExpression("1 + 1").getValue(Int::class.java) // 2
val testString = parser.parseExpression(
"'test' + ' ' + 'string'").getValue(String::class.java) // 'test string'
// Subtraction
val four = parser.parseExpression("1 - -3").getValue(Int::class.java) // 4
val d = parser.parseExpression("1000.00 - 1e4").getValue(Double::class.java) // -9000
// Multiplication
val six = parser.parseExpression("-2 * -3").getValue(Int::class.java) // 6
val twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double::class.java) // 24.0
// Division
val minusTwo = parser.parseExpression("6 / -3").getValue(Int::class.java) // -2
val one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double::class.java) // 1.0
// Modulus
val three = parser.parseExpression("7 % 4").getValue(Int::class.java) // 3
val one = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java) // 1
// Operator precedence
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21
赋值运算符
要设置一个属性,请使用赋值运算符(=)。这通常在调用 setValue 时完成,但也可以在调用 getValue 时完成。以下示例展示了使用赋值运算符的两种方式:
Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
parser.parseExpression("Name").setValue(context, inventor, "Aleksandar Seovic");
// alternatively
String aleks = parser.parseExpression(
"Name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);
val inventor = Inventor()
val context = SimpleEvaluationContext.forReadWriteDataBinding().build()
parser.parseExpression("Name").setValue(context, inventor, "Aleksandar Seovic")
// alternatively
val aleks = parser.parseExpression(
"Name = 'Aleksandar Seovic'").getValue(context, inventor, String::class.java)
4.3.8. 类型
您可以使用特殊的 T 运算符来指定 java.lang.Class(该类型)的实例。静态方法也通过使用该运算符进行调用。StandardEvaluationContext 使用 TypeLocator 来查找类型,而可替换的 StandardTypeLocator 则是基于对 java.lang 包的理解构建的。这意味着,在 java.lang 内部对类型的 T() 引用无需完全限定,但所有其他类型引用则必须完全限定。以下示例展示了如何使用 T 运算符:
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
boolean trueValue = parser.parseExpression(
"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
.getValue(Boolean.class);
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 操作符来调用构造函数。除了基本类型(int、float 等)和 String 之外,其他类型都应使用完整的限定类名。以下示例展示了如何使用 new 操作符来调用构造函数:
Inventor einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor.class);
//create new inventor instance within add method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor(
'Albert Einstein', 'German'))").getValue(societyContext);
val einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor::class.java)
//create new inventor instance within add method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
.getValue(societyContext)
4.3.10. 变量
你可以使用 #variableName 语法在表达式中引用变量。变量通过在 setVariable 的实现类上调用 EvaluationContext 方法进行设置。
|
有效的变量名必须由以下支持的字符中的一个或多个组成。
|
以下示例展示了如何使用变量。
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");
parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName()) // "Mike Tesla"
val tesla = Inventor("Nikola Tesla", "Serbian")
val context = SimpleEvaluationContext.forReadWriteDataBinding().build()
context.setVariable("newName", "Mike Tesla")
parser.parseExpression("Name = #newName").getValue(context, tesla)
println(tesla.name) // "Mike Tesla"
这#this和#root变量
#this 变量始终被定义,并指向当前的求值对象(所有未限定的引用都将根据该对象进行解析)。#root 变量也始终被定义,并指向根上下文对象。尽管在表达式各部分求值过程中 #this 可能会发生变化,但 #root 始终指向根对象。以下示例展示了如何使用 #this 和 #root 变量:
// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));
// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);
// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
"#primes.?[#this>10]").getValue(context);
// create an array of integers
val primes = ArrayList<Int>()
primes.addAll(listOf(2, 3, 5, 7, 11, 13, 17))
// create parser and set variable 'primes' as the array of integers
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataAccess()
context.setVariable("primes", primes)
// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
val primesGreaterThanTen = parser.parseExpression(
"#primes.?[#this>10]").getValue(context) as List<Int>
4.3.11. 函数
你可以通过注册用户自定义函数来扩展 SpEL,这些函数可以在表达式字符串中被调用。该函数通过 EvaluationContext 进行注册。以下示例展示了如何注册一个用户自定义函数:
Method method = ...;
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);
val method: Method = ...
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
context.setVariable("myFunction", method)
例如,考虑以下用于反转字符串的工具方法:
public abstract class StringUtils {
public static String reverseString(String input) {
StringBuilder backwards = new StringBuilder(input.length());
for (int i = 0; i < input.length(); i++) {
backwards.append(input.charAt(input.length() - 1 - i));
}
return backwards.toString();
}
}
fun reverseString(input: String): String {
val backwards = StringBuilder(input.length)
for (i in 0 until input.length) {
backwards.append(input[input.length - 1 - i])
}
return backwards.toString()
}
然后,您可以注册并使用上述方法,如下例所示:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", String.class));
String helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String.class);
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
context.setVariable("reverseString", ::reverseString::javaMethod)
val helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String::class.java)
4.3.12. Bean 引用
如果评估上下文已配置了 bean 解析器,你可以通过使用 @ 符号在表达式中查找 bean。以下示例展示了如何实现这一点:
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
context.setBeanResolver(MyBeanResolver())
// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
val bean = parser.parseExpression("@something").getValue(context)
要访问工厂 Bean 本身,你应该在 Bean 名称前加上 & 符号。
以下示例展示了如何实现这一点:
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
context.setBeanResolver(MyBeanResolver())
// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
val bean = parser.parseExpression("&foo").getValue(context)
4.3.13. 三元运算符(如果 - 那么 - 否则)
您可以在表达式内部使用三元运算符来执行 if-then-else 条件逻辑。以下示例展示了一个最简用法:
String falseString = parser.parseExpression(
"false ? 'trueExp' : 'falseExp'").getValue(String.class);
val falseString = parser.parseExpression(
"false ? 'trueExp' : 'falseExp'").getValue(String::class.java)
在这种情况下,布尔值 false 会导致返回字符串值 'falseExp'。下面是一个更实际的示例:
parser.parseExpression("Name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");
expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
"+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";
String queryResultString = parser.parseExpression(expression)
.getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"
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 运算符是对三元运算符语法的一种简化,在 Groovy 语言中使用。 使用三元运算符语法时,通常需要重复写两次同一个变量,如下例所示:
String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");
相反,你可以使用 Elvis 运算符(因其形似猫王 Elvis 的发型而得名)。 以下示例展示了如何使用 Elvis 运算符:
ExpressionParser parser = new SpelExpressionParser();
String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name); // 'Unknown'
val parser = SpelExpressionParser()
val name = parser.parseExpression("name?:'Unknown'").getValue(Inventor(), String::class.java)
println(name) // 'Unknown'
以下列表展示了一个更复杂的示例:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Nikola Tesla
tesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Elvis Presley
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
val tesla = Inventor("Nikola Tesla", "Serbian")
var name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name) // Nikola Tesla
tesla.setName(null)
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name) // Elvis Presley
|
您可以使用 Elvis 运算符在表达式中应用默认值。以下示例展示了如何在
如果定义了系统属性 |
4.3.15. 安全导航运算符
安全导航操作符用于避免 NullPointerException,它源自 Groovy 语言。通常,当你持有一个对象的引用时,在访问该对象的方法或属性之前,可能需要先验证它是否为 null。为了避免这种情况,安全导航操作符在遇到 null 时会返回 null,而不是抛出异常。以下示例展示了如何使用安全导航操作符:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // Smiljan
tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // null - does not throw NullPointerException!!!
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] 的语法。它会过滤集合,并返回一个包含原集合中部分元素的新集合。例如,通过选择操作,我们可以轻松获取塞尔维亚发明者的列表,如下例所示:
List<Inventor> list = (List<Inventor>) parser.parseExpression(
"Members.?[Nationality == 'Serbian']").getValue(societyContext);
val list = parser.parseExpression(
"Members.?[Nationality == 'Serbian']").getValue(societyContext) as List<Inventor>
选择操作既可用于列表,也可用于映射。对于列表,选择条件将针对每个单独的列表元素进行求值;对于映射,选择条件则针对每个映射条目(Java 类型为 Map.Entry 的对象)进行求值。每个映射条目都将其键和值作为属性公开,以便在选择条件中使用。
以下表达式返回一个新映射,该映射由原映射中值小于27的那些元素组成:
Map newMap = parser.parseExpression("map.?[value<27]").getValue();
val newMap = parser.parseExpression("map.?[value<27]").getValue()
除了返回所有选中的元素外,你还可以仅获取第一个或最后一个值。要获取与选择条件匹配的第一个条目,语法为
.^[selectionExpression]。要获取最后一个匹配的选择项,语法为
.$[selectionExpression]。
4.3.17. 集合投影
投影(Projection)允许一个集合驱动对子表达式的求值,其结果是一个新的集合。投影的语法是 .![projectionExpression]。例如,假设我们有一个发明者列表,但希望获取他们出生城市的列表。实际上,我们希望对发明者列表中的每个条目都求值 'placeOfBirth.city'。以下示例使用投影来实现这一目的:
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");
// returns ['Smiljan', 'Idvor' ]
val placesOfBirth = parser.parseExpression("Members.![placeOfBirth.city]") as List<*>
你也可以使用一个映射(map)来驱动投影,在这种情况下,投影表达式将针对映射中的每个条目(表示为 Java Map.Entry)进行求值。对映射执行投影的结果是一个列表,其中包含针对每个映射条目求值投影表达式所得的结果。
4.3.18. 表达式模板化
表达式模板允许将字面文本与一个或多个求值块混合使用。
每个求值块由您可自定义的前缀和后缀字符分隔。
常见的选择是使用 #{ } 作为分隔符,如下例所示:
String randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
new TemplateParserContext()).getValue(String.class);
// evaluates to "random number is 0.7038186818312008"
val randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
TemplateParserContext()).getValue(String::class.java)
// evaluates to "random number is 0.7038186818312008"
该字符串通过将字面文本 'random number is ' 与 #{ } 分隔符内表达式求值的结果(在本例中,即调用 random() 方法的返回值)进行拼接来完成求值。传递给 parseExpression() 方法的第二个参数类型为 ParserContext。ParserContext 接口用于影响表达式的解析方式,以支持表达式模板功能。
TemplateParserContext 的定义如下:
public class TemplateParserContext implements ParserContext {
public String getExpressionPrefix() {
return "#{";
}
public String getExpressionSuffix() {
return "}";
}
public boolean isTemplate() {
return true;
}
}
class TemplateParserContext : ParserContext {
override fun getExpressionPrefix(): String {
return "#{"
}
override fun getExpressionSuffix(): String {
return "}"
}
override fun isTemplate(): Boolean {
return true
}
}
4.4. 示例中使用的类
本节列出了本章各示例中使用的类。
package org.spring.samples.spel.inventor;
import java.util.Date;
import java.util.GregorianCalendar;
public class Inventor {
private String name;
private String nationality;
private String[] inventions;
private Date birthdate;
private PlaceOfBirth placeOfBirth;
public Inventor(String name, String nationality) {
GregorianCalendar c= new GregorianCalendar();
this.name = name;
this.nationality = nationality;
this.birthdate = c.getTime();
}
public Inventor(String name, Date birthdate, String nationality) {
this.name = name;
this.nationality = nationality;
this.birthdate = birthdate;
}
public Inventor() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNationality() {
return nationality;
}
public void setNationality(String nationality) {
this.nationality = nationality;
}
public Date getBirthdate() {
return birthdate;
}
public void setBirthdate(Date birthdate) {
this.birthdate = birthdate;
}
public PlaceOfBirth getPlaceOfBirth() {
return placeOfBirth;
}
public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
this.placeOfBirth = placeOfBirth;
}
public void setInventions(String[] inventions) {
this.inventions = inventions;
}
public String[] getInventions() {
return inventions;
}
}
class Inventor(
var name: String,
var nationality: String,
var inventions: Array<String>? = null,
var birthdate: Date = GregorianCalendar().time,
var placeOfBirth: PlaceOfBirth? = null)
package org.spring.samples.spel.inventor;
public class PlaceOfBirth {
private String city;
private String country;
public PlaceOfBirth(String city) {
this.city=city;
}
public PlaceOfBirth(String city, String country) {
this(city);
this.country = country;
}
public String getCity() {
return city;
}
public void setCity(String s) {
this.city = s;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
class PlaceOfBirth(var city: String, var country: String? = null) {
package org.spring.samples.spel.inventor;
import java.util.*;
public class Society {
private String name;
public static String Advisors = "advisors";
public static String President = "president";
private List<Inventor> members = new ArrayList<Inventor>();
private Map officers = new HashMap();
public List getMembers() {
return members;
}
public Map getOfficers() {
return officers;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isMember(String name) {
for (Inventor inventor : members) {
if (inventor.getName().equals(name)) {
return true;
}
}
return false;
}
}
package org.spring.samples.spel.inventor
import java.util.*
class Society {
val Advisors = "advisors"
val President = "president"
var name: String? = null
val members = ArrayList<Inventor>()
val officers = mapOf<Any, Any>()
fun isMember(name: String): Boolean {
for (inventor in members) {
if (inventor.name == name) {
return true
}
}
return false
}
}
5. 使用 Spring 进行面向切面编程
面向切面编程(AOP)通过提供另一种思考程序结构的方式,对面向对象编程(OOP)进行了补充。在 OOP 中,模块化的基本单元是类,而在 AOP 中,模块化的基本单元是切面(aspect)。切面能够将横跨多个类型和对象的关注点(例如事务管理)进行模块化。(在 AOP 文献中,这类关注点通常被称为“横切”关注点。)
Spring 的关键组件之一是 AOP 框架。虽然 Spring IoC 容器并不依赖于 AOP(这意味着如果你不想使用 AOP,就无需使用),但 AOP 与 Spring IoC 相辅相成,共同提供了一个功能强大的中间件解决方案。
AOP 在 Spring 框架中用于:
-
提供声明式的企业级服务。其中最重要的服务是 声明式事务管理。
-
让用户实现自定义切面,以 AOP 补充其对 OOP 的使用。
| 如果你仅对通用的声明式服务或其他预封装的声明式中间件服务(例如池化)感兴趣,那么你无需直接使用 Spring AOP,可以跳过本章的大部分内容。 |
5.1. AOP 概念
首先,我们来定义一些核心的AOP概念和术语。这些术语并非Spring所特有。不幸的是,AOP的术语并不是特别直观。然而,如果Spring使用自己的术语,反而会造成更大的混淆。
-
切面(Aspect):对横切多个类的关注点的模块化。 事务管理是企业级 Java 应用程序中横切关注点的一个典型示例。 在 Spring AOP 中,切面通过使用普通类(基于 Schema 的方式)或使用
@Aspect注解标注的普通类(@AspectJ 风格)来实现。 -
连接点(Join point):程序执行过程中的一个点,例如方法的执行或异常的处理。在 Spring AOP 中,连接点始终代表方法的执行。
-
通知(Advice):切面在特定连接点(join point)所采取的操作。不同类型的通知包括“环绕(around)”、“前置(before)”和“后置(after)”通知。(通知类型将在后文讨论。)包括 Spring 在内的许多 AOP 框架都将通知建模为拦截器(interceptor),并在连接点周围维护一个拦截器链。
-
切入点(Pointcut):一种匹配连接点(join points)的谓词。通知(Advice)与切入点表达式相关联,并在任何被该切入点匹配的连接点处执行(例如,执行某个特定名称的方法)。通过切入点表达式来匹配连接点这一概念是面向切面编程(AOP)的核心,Spring 默认使用 AspectJ 的切入点表达式语言。
-
引言:代表某个类型声明额外的方法或字段。Spring AOP 允许你向任何被通知(advised)的对象引入新的接口(以及相应的实现)。例如,你可以使用引入(introduction)使一个 Bean 实现
IsModified接口,以简化缓存操作。(在 AspectJ 社区中,这种引入被称为类型间声明(inter-type declaration)。) -
目标对象:被一个或多个切面所通知的对象。也称为“被通知对象”。由于 Spring AOP 是通过运行时代理实现的,因此该对象始终是一个代理对象。
-
AOP代理:由AOP框架创建的对象,用于实现切面契约(例如拦截方法执行等)。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
-
织入(Weaving):将切面与其他应用程序类型或对象链接起来以创建一个被通知(advised)的对象。这可以在编译时(例如使用 AspectJ 编译器)、类加载时或运行时完成。Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行织入。
Spring AOP 包含以下几种通知类型:
-
前置通知(Before advice):在连接点之前执行的通知,但不具备阻止执行流程继续到达该连接点的能力(除非它抛出异常)。
-
返回后通知(After returning advice):在连接点正常完成后执行的通知(例如,方法返回时未抛出异常)。
-
异常抛出后通知(After throwing advice):当方法因抛出异常而退出时执行的通知。
-
后置(最终)通知:无论连接点以何种方式退出(正常返回或异常返回),都会执行的通知。
-
环绕通知(Around advice):一种包围连接点(例如方法调用)的通知。 这是功能最强大的通知类型。环绕通知可以在方法调用前后执行自定义行为。 它还负责决定是否继续执行连接点,或者通过返回自己的返回值或抛出异常来直接结束被通知方法的执行。
环绕通知(Around advice)是最通用的通知类型。由于 Spring AOP 和 AspectJ 一样,提供了完整范围的通知类型,我们建议您使用能够实现所需行为的最弱(即最具体、限制性最强)的通知类型。例如,如果您仅需使用方法的返回值来更新缓存,那么实现一个返回后通知(after returning advice)会比使用环绕通知更为合适,尽管环绕通知也能完成同样的功能。使用最具体的通知类型可以提供更简单的编程模型,并减少出错的可能性。例如,您无需在用于环绕通知的 proceed() 上调用 JoinPoint 方法,因此也就不会因忘记调用它而导致错误。
所有通知参数都是静态类型的,因此您可以使用适当类型的通知参数(例如方法执行的返回值类型),而不是 Object 数组。
切入点(pointcut)所匹配的连接点(join point)这一概念是 AOP 的核心,它使 AOP 有别于仅提供拦截功能的早期技术。切入点使得通知(advice)能够独立于面向对象的继承层次结构进行目标定位。例如,你可以将一个提供声明式事务管理的环绕通知(around advice)应用到跨越多个对象的一组方法上(例如服务层中的所有业务操作)。
5.2. Spring AOP 的功能与目标
Spring AOP 是用纯 Java 实现的。无需特殊的编译过程。Spring AOP 不需要控制类加载器层次结构,因此适用于 Servlet 容器或应用服务器。
Spring AOP 目前仅支持方法执行连接点(即对 Spring Bean 上方法的执行进行增强)。虽然字段拦截尚未实现,但即使将来添加对字段拦截的支持,也不会破坏 Spring AOP 的核心 API。如果您需要对字段访问和更新的连接点进行增强,请考虑使用 AspectJ 等语言。
Spring AOP 的 AOP 实现方式不同于大多数其他 AOP 框架。其目标并非提供最完整的 AOP 实现(尽管 Spring AOP 已经相当强大),而是旨在实现 AOP 与 Spring IoC 的紧密集成,以帮助企业应用程序解决常见问题。
因此,举例来说,Spring 框架的 AOP 功能通常与 Spring IoC 容器结合使用。切面通过普通的 bean 定义语法进行配置(尽管这种方式提供了强大的“自动代理”能力)。这是 Spring AOP 与其他 AOP 实现的一个关键区别。使用 Spring AOP,有些事情难以轻松或高效地完成,例如对非常细粒度的对象(通常是领域对象)进行通知。在这种情况下,AspectJ 是更好的选择。然而,我们的经验表明,对于企业级 Java 应用中适合采用 AOP 解决的大多数问题,Spring AOP 提供了出色的解决方案。
Spring AOP 从不试图与 AspectJ 竞争以提供全面的 AOP 解决方案。我们认为,基于代理的框架(如 Spring AOP)和功能完备的框架(如 AspectJ)都具有重要价值,并且二者是互补关系,而非竞争关系。Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,从而在一致的基于 Spring 的应用架构中支持 AOP 的所有用法。这种集成不会影响 Spring AOP API 或 AOP Alliance API。Spring AOP 保持向后兼容。有关 Spring AOP API 的讨论,请参见下一章。
|
Spring 框架的核心原则之一是非侵入性(non-invasiveness)。这一理念指的是,你不应被迫在自己的业务或领域模型中引入框架特定的类和接口。然而,在某些情况下,Spring 框架确实为你提供了在代码库中引入 Spring 特定依赖项的选项。之所以提供这些选项,是因为在某些场景下,以这种方式编写或阅读某些特定功能的代码可能会更加简单明了。不过,Spring 框架(几乎)始终会给你选择权:你可以自由地根据具体情况或场景,做出最适合自己的明智决策。 本章涉及的一个重要选择是使用哪种AOP框架(以及采用哪种AOP风格)。你可以选择AspectJ、Spring AOP,或者两者结合使用。你还可以选择使用@AspectJ注解风格的方法,或者Spring XML配置风格的方法。本章首先介绍@AspectJ风格这一事实,并不意味着Spring团队更倾向于@AspectJ注解风格而非Spring XML配置风格。 有关每种风格的“原因与适用场景”的更完整讨论,请参见选择使用哪种AOP声明风格。 |
5.3. AOP 代理
Spring AOP 默认使用标准的 JDK 动态代理来创建 AOP 代理。这使得任意接口(或一组接口)都可以被代理。
Spring AOP 也可以使用 CGLIB 代理。当需要代理类而不是接口时,就必须使用 CGLIB。默认情况下,如果业务对象未实现任何接口,Spring 将使用 CGLIB。由于良好的编程实践是面向接口而非类进行编程,因此业务类通常会实现一个或多个业务接口。在某些(希望是罕见的)情况下,如果你需要对未在接口中声明的方法进行增强,或者需要将代理对象以具体类型的形式传递给某个方法,此时可以强制使用 CGLIB。
重要的是要理解 Spring AOP 是基于代理的。有关这一实现细节的确切含义,请参阅 理解 AOP 代理 以进行深入探讨。
5.4. @AspectJ 支持
@AspectJ 是一种通过使用注解来声明切面的风格,即将切面定义为带有注解的普通 Java 类。这种 @AspectJ 风格由 AspectJ 项目 在 AspectJ 5 版本中引入。Spring 使用 AspectJ 提供的库来解析和匹配切入点(pointcut),并以与 AspectJ 5 相同的方式解释这些注解。不过,AOP 运行时仍然是纯 Spring AOP,不依赖于 AspectJ 编译器或织入器(weaver)。
| 使用 AspectJ 编译器和织入器可以启用完整的 AspectJ 语言,相关内容请参见在 Spring 应用程序中使用 AspectJ。 |
5.4.1. 启用 @AspectJ 支持
要在 Spring 配置中使用 @AspectJ 切面,您需要启用 Spring 对基于 @AspectJ 切面配置 Spring AOP 的支持,并启用基于这些切面是否对 Bean 提供通知(advice)的自动代理功能。所谓自动代理,是指如果 Spring 判定某个 Bean 被一个或多个切面所通知,它会自动为该 Bean 生成代理,以拦截方法调用,并确保在需要时执行相应的通知逻辑。
@AspectJ 支持可以通过 XML 或 Java 风格的配置来启用。无论采用哪种方式,您都需要确保 AspectJ 的 aspectjweaver.jar 库(版本 1.8 或更高)位于应用程序的 classpath 中。该库可从 AspectJ 发行版的 lib 目录中获取,也可从 Maven Central 仓库中获得。
使用 Java 配置启用 @AspectJ 支持
要通过 Java @Configuration 启用 @AspectJ 支持,请添加 @EnableAspectJAutoProxy 注解,如下例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
@Configuration
@EnableAspectJAutoProxy
class AppConfig
通过 XML 配置启用 @AspectJ 支持
要通过基于 XML 的配置启用 @AspectJ 支持,请使用 aop:aspectj-autoproxy 元素,如下例所示:
<aop:aspectj-autoproxy/>
这假设您使用了如基于 XML Schema 的配置中所述的 schema 支持。
有关如何在 #xsd-schemas-aop 命名空间中导入标签,请参见AOP schema。
5.4.2. 声明切面
启用 @AspectJ 支持后,应用程序上下文中任何类带有 @AspectJ 切面(即包含 @Aspect 注解)的 bean 都会被 Spring 自动检测,并用于配置 Spring AOP。接下来的两个示例展示了定义一个不太有用的切面所需的最简配置。
前两个示例中的第一个展示了一个在应用上下文中指向带有 @Aspect 注解的 bean 类的普通 bean 定义:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
第二个示例展示了 NotVeryUsefulAspect 类的定义,该类使用了 org.aspectj.lang.annotation.Aspect 注解;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
package org.xyz
import org.aspectj.lang.annotation.Aspect;
@Aspect
class NotVeryUsefulAspect
切面(使用 @Aspect 注解的类)可以拥有方法和字段,与任何其他类相同。它们还可以包含切入点(pointcut)、通知(advice)和引入(introduction,也称为类型间)声明。
通过组件扫描自动检测切面
您可以在 Spring XML 配置中将切面类注册为普通 Bean,
也可以通过类路径扫描自动检测它们——与其他任何 Spring 管理的 Bean 相同。
但请注意,@Aspect 注解本身不足以在类路径中实现自动检测。
为此,您需要额外添加一个 @Component 注解
(或者,根据 Spring 组件扫描器的规则,使用一个符合条件的自定义构造型注解)。 |
可以用其他切面来通知(advising)切面吗?
在 Spring AOP 中,切面本身不能成为其他切面的通知目标。类上的 @Aspect 注解将其标记为一个切面,因此会将其排除在自动代理(auto-proxying)之外。 |
5.4.3. 声明切点
切入点(Pointcuts)用于确定我们感兴趣的连接点(join points),从而让我们能够控制通知(advice)的执行时机。Spring AOP 仅支持针对 Spring Bean 的方法执行连接点,因此你可以将切入点理解为匹配 Spring Bean 上方法的执行。切入点声明包含两个部分:一个由名称和任意参数组成的签名,以及一个切入点表达式,该表达式精确地指定了我们感兴趣的方法执行。在基于 @AspectJ 注解风格的 AOP 中,切入点签名通过一个常规的方法定义来提供,而切入点表达式则通过使用 @Pointcut 注解来指定(作为切入点签名的方法必须具有 void 返回类型)。
一个示例或许有助于阐明切入点签名(pointcut signature)与切入点表达式(pointcut expression)之间的区别。以下示例定义了一个名为 anyOldTransfer 的切入点,它匹配任何名为 transfer 的方法的执行:
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
@Pointcut("execution(* transfer(..))") // the pointcut expression
private fun anyOldTransfer() {} // the pointcut signature
构成 @Pointcut 注解值的切点表达式是一个标准的 AspectJ 5 切点表达式。有关 AspectJ 切点语言的完整讨论,请参阅 AspectJ
编程指南(对于扩展内容,请参阅 AspectJ 5
开发者笔记),或参考任何关于 AspectJ 的书籍(例如 Colyer 等人编写的《Eclipse AspectJ》,或 Ramnivas Laddad 编写的《AspectJ 实战》)。
支持的切点设计器
Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示符(PCD):
-
execution:用于匹配方法执行连接点。这是在使用 Spring AOP 时主要使用的切入点指示符。 -
within:将匹配限制在特定类型内的连接点(使用 Spring AOP 时,指在匹配类型内声明的方法的执行)。 -
this:将匹配限制在连接点(使用 Spring AOP 时指方法的执行)上,其中 bean 引用(Spring AOP 代理)是给定类型的实例。 -
target:将匹配限制在连接点(使用 Spring AOP 时指方法的执行)上,其中目标对象(被代理的应用程序对象)是给定类型的实例。 -
args:限制匹配到连接点(使用 Spring AOP 时指方法的执行),其参数是给定类型的实例。 -
@target:将匹配限制在连接点(使用 Spring AOP 时指方法的执行)上,且执行对象的类具有指定类型的注解。 -
@args:将匹配限制在连接点(使用 Spring AOP 时指方法的执行)上,这些连接点所传递的实际参数的运行时类型具有指定类型的注解。 -
@within:将匹配限制在具有指定注解的类型内的连接点(当使用 Spring AOP 时,指的是在具有该注解的类型中声明的方法的执行)。 -
@annotation:将匹配限制在连接点的目标(在 Spring AOP 中即正在执行的方法)具有指定注解的连接点上。
由于 Spring AOP 将匹配限制在方法执行连接点(join points)上,因此上述关于切入点(pointcut)指示符的讨论比 AspectJ 编程指南中的定义更为狭窄。此外,AspectJ 本身具有基于类型的语义,在方法执行连接点处,this 和 target 都指向同一个对象:即正在执行该方法的对象。而 Spring AOP 是一个基于代理的系统,它区分代理对象本身(绑定到 this)和代理背后的目标对象(绑定到 target)。
|
由于 Spring AOP 框架基于代理的特性,目标对象内部的方法调用按照定义是不会被拦截的。对于 JDK 代理,只有通过代理对公共接口方法的调用才能被拦截。而使用 CGLIB 时,通过代理对公共(public)和受保护(protected)方法的调用会被拦截(如有必要,包可见(package-visible)的方法也会被拦截)。然而,通过代理进行的常规交互应始终通过公共方法签名来设计。 请注意,切入点(pointcut)定义通常会与任何被拦截的方法进行匹配。 如果某个切入点严格限定仅适用于公共(public)方法,即使在 CGLIB 代理场景下可能存在通过代理进行的非公共(non-public)交互,也需要相应地进行定义。 如果你的拦截需求包括目标类内部的方法调用,甚至是构造函数调用,请考虑使用 Spring 驱动的原生 AspectJ 编织(weaving),而不是基于代理的 Spring AOP 框架。这代表了一种具有不同特性的 AOP 使用方式,因此在做出决定之前,请务必先熟悉编织(weaving)的相关知识。 |
Spring AOP 还支持一个名为 bean 的额外切入点指示符(PCD)。该 PCD 允许你将连接点的匹配限制为特定名称的 Spring Bean,或一组使用通配符指定的 Spring Bean。bean PCD 的形式如下:
bean(idOrNameOfBean)
bean(idOrNameOfBean)
idOrNameOfBean 标记可以是任意 Spring Bean 的名称。系统提供了有限的通配符支持,使用 * 字符,因此,如果你为 Spring Bean 建立了一些命名约定,就可以编写一个 bean 切入点表达式(PCD)来选择它们。与其他切入点指示符(pointcut designator)一样,bean PCD 也可以与 &&(与)、||(或)和 !(非)操作符一起使用。
|
|
组合切点表达式
你可以使用 &&, || 和 ! 来组合切入点(pointcut)表达式。你也可以通过名称引用切入点表达式。以下示例展示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} (1)
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} (2)
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} (3)
| 1 | anyPublicOperation 在方法执行连接点表示任意公共方法的执行时匹配。 |
| 2 | inTrading 在方法执行位于交易模块中时匹配。 |
| 3 | tradingOperation 在方法执行代表交易模块中的任意公共方法时匹配。 |
@Pointcut("execution(public * *(..))")
private fun anyPublicOperation() {} (1)
@Pointcut("within(com.xyz.myapp.trading..*)")
private fun inTrading() {} (2)
@Pointcut("anyPublicOperation() && inTrading()")
private fun tradingOperation() {} (3)
| 1 | anyPublicOperation 在方法执行连接点表示任意公共方法的执行时匹配。 |
| 2 | inTrading 在方法执行位于交易模块中时匹配。 |
| 3 | tradingOperation 在方法执行代表交易模块中的任意公共方法时匹配。 |
最佳实践是像前面所示那样,使用更小的、具名的组件来构建更复杂的切入点(pointcut)表达式。在通过名称引用切入点时,正常的 Java 可见性规则适用(你可以在同一类型中看到私有切入点,在继承层次结构中看到受保护的切入点,任何地方都可以看到公共切入点,等等)。可见性不会影响切入点的匹配。
共享通用切点定义
在开发企业级应用程序时,开发者通常希望在多个切面中引用应用程序的模块以及特定的操作集合。为此,我们建议定义一个 CommonPointcuts 切面,用于封装通用的切入点表达式。此类切面通常类似于以下示例:
package com.xyz.myapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.myapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.myapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.myapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
* the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
package com.xyz.myapp
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
@Aspect
class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.myapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.web..*)")
fun inWebLayer() {
}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.myapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.service..*)")
fun inServiceLayer() {
}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.myapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.dao..*)")
fun inDataAccessLayer() {
}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
* the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
fun businessService() {
}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
fun dataAccessOperation() {
}
}
你可以在任何需要切入点表达式的地方引用在此类切面中定义的切入点。例如,要使服务层具有事务性,你可以编写如下代码:
<aop:config>
<aop:advisor
pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config> 和 <aop:advisor> 元素在基于 Schema 的 AOP 支持中有详细讨论。事务相关元素在事务管理中有详细讨论。
示例
Spring AOP 用户最常使用的是 execution 切入点指示符。
execution 表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式(前文代码片段中的 ret-type-pattern)、名称模式和参数模式之外,其余部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,才能匹配一个连接点。* 是最常用的返回类型模式。它匹配任意返回类型。只有当方法返回指定类型时,完全限定的类型名称才会匹配。名称模式匹配方法名。您可以使用通配符*作为名称模式的全部或部分。如果指定了声明类型模式,请在名称模式组件后添加一个尾随的.。参数模式稍微复杂一些:() 匹配不带参数的方法,而(..) 匹配任意数量(零个或多个)的参数。The (*) pattern 匹配一个接受任意类型参数的方法。(*,String) 匹配一个接受两个参数的方法。第一个可以是任何类型,而第二个必须是 String。咨询
语言语义 部分的 AspectJ 编程指南 以获取更多详细信息。
以下示例展示了一些常见的切入点表达式:
-
任何公共方法的执行:
execution(public * *(..))
-
任何方法名以
set开头的方法的执行:execution(* set*(..))
-
由
AccountService接口定义的任何方法的执行:execution(* com.xyz.service.AccountService.*(..))
-
service包中定义的任何方法的执行:execution(* com.xyz.service.*.*(..))
-
服务包或其任意子包中定义的任何方法的执行:
execution(* com.xyz.service..*.*(..))
-
服务包内的任意连接点(在 Spring AOP 中仅限方法执行):
within(com.xyz.service.*)
-
服务包或其任意子包内的任何连接点(在 Spring AOP 中仅限方法执行):
within(com.xyz.service..*)
-
任何连接点(在 Spring AOP 中仅限方法执行),其中代理实现了
AccountService接口:this(com.xyz.service.AccountService)
'this' 更常用于绑定形式。有关如何在通知(advice)体中使代理对象可用,请参见声明通知一节。 -
任何连接点(在 Spring AOP 中仅限方法执行),其中目标对象实现了
AccountService接口:target(com.xyz.service.AccountService)
'target' 更常用于绑定形式。有关如何在通知(advice)体中使目标对象可用,请参见声明通知部分。 -
任何连接点(在 Spring AOP 中仅限方法执行),该连接点接受单个参数,并且在运行时传入的参数是
Serializable类型:args(java.io.Serializable)
'args' 更常用于绑定形式。有关如何在通知(advice)体中使用方法参数,请参见声明通知部分。 请注意,本示例中给出的切入点与
execution(* *(java.io.Serializable))不同。args 版本在运行时传入的参数是Serializable类型时匹配,而 execution 版本则在方法签名声明了一个类型为Serializable的参数时匹配。 -
任何连接点(在 Spring AOP 中仅限方法执行),其中目标对象具有
@Transactional注解:@target(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用 '@target'。有关如何在通知体中使注解对象可用,请参见声明通知部分。 -
任何连接点(在 Spring AOP 中仅限方法执行),其目标对象的声明类型具有
@Transactional注解:@within(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用 '@within'。有关如何在通知体中使注解对象可用,请参见声明通知部分。 -
任何连接点(在 Spring AOP 中仅限方法执行),其执行的方法带有
@Transactional注解:@annotation(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用 '@annotation'。有关如何在通知(advice)体中使注解对象可用,请参见声明通知部分。 -
任何连接点(在 Spring AOP 中仅限方法执行),该连接点接受单个参数,并且所传递参数的运行时类型具有
@Classified注解:@args(com.xyz.security.Classified)
你也可以在绑定形式中使用 '@args'。请参阅声明通知部分,了解如何在通知体中使注解对象可用。 -
Spring Bean 名为
tradeService的任意连接点(在 Spring AOP 中仅限方法执行):bean(tradeService)
-
任何连接点(在 Spring AOP 中仅限方法执行)在名称匹配通配符表达式
*Service的 Spring Bean 上:bean(*Service)
编写良好的切点
在编译期间,AspectJ 会处理切入点(pointcut),以优化匹配性能。检查代码并确定每个连接点(join point)是否匹配(静态或动态地)给定的切入点是一个代价高昂的过程。(动态匹配是指无法仅通过静态分析完全确定匹配结果,而需要在代码中插入测试逻辑,在运行时判断是否存在实际匹配。)当 AspectJ 首次遇到一个切入点声明时,会将其重写为匹配过程的最优形式。这意味着什么?基本上,切入点会被重写为析取范式(DNF, Disjunctive Normal Form),并且切入点的各个组成部分会被排序,使得计算成本较低的组件优先被检查。因此,您无需担心理解各种切入点指示符(pointcut designator)的性能差异,可以在切入点声明中以任意顺序提供它们。
然而,AspectJ 只能处理它被告知的内容。为了实现最佳的匹配性能,你应该仔细考虑自己想要达成的目标,并在定义中尽可能缩小匹配的搜索范围。现有的指示符自然可以分为以下三类:类型(kinded)、作用域(scoping)和上下文(contextual):
-
种类化指示符用于选择特定类型的连接点:
execution、get、set、call和handler。 -
作用域指示符用于选择一组感兴趣的连接点(可能包含多种类型):
within和withincode -
上下文指示符根据上下文进行匹配(并可选择性地绑定):
this、target和@annotation
一个编写良好的切入点(pointcut)应至少包含前两种类型(kinded 和 scoping)。你可以加入上下文指示符(contextual designators),以便根据连接点(join point)的上下文进行匹配,或将该上下文绑定以在通知(advice)中使用。仅提供 kinded 指示符或仅提供 contextual 指示符虽然也能工作,但由于需要额外的处理和分析,可能会影响织入(weaving)性能(时间和内存消耗)。Scoping 指示符的匹配速度非常快,使用它们可以让 AspectJ 快速排除那些无需进一步处理的连接点组。因此,如果可能,一个好的切入点应始终包含一个 scoping 指示符。
5.4.4. 声明通知
通知(Advice)与切入点(pointcut)表达式相关联,并在匹配该切入点的 方法执行之前、之后或周围运行。切入点表达式可以是 对命名切入点的简单引用,也可以是直接声明的切入点表达式。
前置通知
你可以通过使用 @Before 注解在切面中声明前置通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doAccessCheck() {
// ...
}
}
如果我们使用内联的切入点表达式,可以将前面的示例重写为以下示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
fun doAccessCheck() {
// ...
}
}
返回后通知
返回后通知(After returning advice)在匹配的方法正常返回时执行。
你可以通过使用 @AfterReturning 注解来声明它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
@AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doAccessCheck() {
// ...
}
}
| 你可以在同一个切面中包含多个通知声明(以及其他成员)。 在这些示例中,我们仅展示单个通知声明,以便聚焦于每个通知的效果。 |
有时,你需要在通知体中访问实际返回的值。
你可以使用将返回值绑定到通知中的 @AfterReturning 形式来获得该访问权限,如下例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
@AfterReturning(
pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
returning = "retVal")
fun doAccessCheck(retVal: Any) {
// ...
}
}
returning 属性中使用的名称必须与通知方法中的参数名称相对应。当方法执行返回时,返回值将作为相应参数的值传递给通知方法。returning 子句还会限制匹配范围,仅匹配那些返回指定类型值的方法执行(在本例中为 Object,它可以匹配任何返回值)。
请注意,使用返回后通知(after returning advice)时,无法返回一个完全不同的引用。
抛出异常后通知
异常抛出后通知(After throwing advice)在匹配的方法执行因抛出异常而退出时运行。你可以使用 @AfterThrowing 注解来声明它,如下例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doRecoveryActions() {
// ...
}
}
通常,你希望通知(advice)仅在抛出特定类型异常时才执行,并且通常还需要在通知体中访问所抛出的异常。你可以使用 throwing 属性来同时限制匹配条件(如果需要的话——否则可使用 Throwable 作为异常类型),并将抛出的异常绑定到通知方法的参数上。
以下示例展示了如何实现这一点:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
@AfterThrowing(
pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
throwing = "ex")
fun doRecoveryActions(ex: DataAccessException) {
// ...
}
}
throwing 属性中使用的名称必须与通知方法中的某个参数名称相对应。当方法执行因抛出异常而退出时,该异常会作为对应的参数值传递给通知方法。throwing 子句还会限制匹配范围,仅匹配那些抛出指定类型异常(在本例中为 DataAccessException)的方法执行。
|
请注意, |
后置(最终)通知
后置(最终)通知(After (finally) advice)在匹配的方法执行退出时运行。它通过使用 @After 注解来声明。后置通知必须能够处理正常返回和异常返回两种情况。它通常用于释放资源等类似用途。以下示例展示了如何使用后置最终通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.After
@Aspect
class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doReleaseLock() {
// ...
}
}
|
请注意,AspectJ 中的 |
环绕通知
最后一种通知是环绕通知(around advice)。环绕通知在匹配的方法执行“周围”运行,它有机会在方法执行前后都进行操作,并且可以决定何时、如何,甚至是否真正执行该方法。 如果需要以线程安全的方式在方法执行前后共享状态(例如启动和停止计时器),通常会使用环绕通知。 始终使用满足需求的前提下功能最弱的通知形式(也就是说,如果前置通知(before advice)就能满足需求,就不要使用环绕通知)。
环绕通知(Around advice)通过使用 @Around 注解来声明。通知方法的第一个参数必须是 ProceedingJoinPoint 类型。在通知的主体中,调用 proceed() 上的 ProceedingJoinPoint 方法会触发底层目标方法的执行。proceed 方法还可以传入一个 Object[] 数组,该数组中的值将作为方法执行时的参数。
当使用 proceed 调用 Object[] 时,其行为与 AspectJ 编译器编译的环绕通知(around advice)中 proceed 的行为略有不同。对于使用传统AspectJ语言编写的around通知,传递给proceed的通知参数数量必须与around通知本身的参数数量匹配(而不是底层连接点所接受的参数数量),并且在某个参数位置上传递给proceed的值将替换连接点处绑定到该值的实体的原始值(如果现在还不明白也不要担心)。Spring 所采用的方法更为简单,并且更符合其基于代理、仅在执行时生效的语义。您只需在使用AspectJ编译器和织入器编译写给Spring的@AspectJ方面并且使用proceed带有参数时关注这个差异。存在一个方式可以编写这样的切面,使其在Spring AOP和AspectJ之间完全兼容,并且这在接下来的关于通知参数的部分有讨论。
接下来的部分将讨论通知参数.
|
以下示例展示了如何使用环绕通知(around advice):
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.ProceedingJoinPoint
@Aspect
class AroundExample {
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return retVal
}
}
环绕通知(around advice)返回的值就是方法调用者所看到的返回值。例如,一个简单的缓存切面可以在缓存中存在值时直接返回该值,否则就调用 proceed() 方法。请注意,在环绕通知的主体中,proceed 可以被调用一次、多次,或者完全不调用,这些情况都是合法的。
通知参数
Spring 提供完全类型化的通知(advice),这意味着你可以在通知的签名中声明所需的参数(正如我们之前在 returning 和 throwing 示例中所看到的那样),而无需始终使用 Object[] 数组。本节稍后我们将介绍如何将参数及其他上下文值传递给通知体。首先,我们来看看如何编写通用的通知,使其能够获知当前正在被通知的方法。
访问当前JoinPoint
任何通知方法都可以将其第一个参数声明为类型为
org.aspectj.lang.JoinPoint 的参数(注意:环绕通知必须将第一个参数声明为类型为 ProceedingJoinPoint,它是 JoinPoint 的子类)。
JoinPoint 接口提供了许多有用的方法:
-
getArgs():返回方法参数。 -
getThis():返回代理对象。 -
getTarget():返回目标对象。 -
getSignature():返回被通知方法的描述。 -
toString():打印被通知方法的有用描述。
有关更多详细信息,请参阅javadoc。
向通知传递参数
我们已经了解了如何绑定返回值或异常值(使用 after-returning 和 after-throwing 通知)。为了在通知体中使用参数值,你可以使用 args 的绑定形式。如果你在 args 表达式中使用参数名代替类型名,那么在调用通知时,对应参数的值将作为参数值传递给通知方法。一个例子可以更清楚地说明这一点。
假设你希望对以 Account 对象作为第一个参数的 DAO 操作的执行进行增强,并且你需要在通知体中访问该账户对象。
你可以编写如下代码:
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
fun validateAccount(account: Account) {
// ...
}
切入点表达式中的 args(account,..) 部分具有两个作用。首先,它将匹配限制为仅那些方法至少带有一个参数,并且传入该参数的实参是 Account 实例的方法执行。其次,它通过 Account 参数使实际的 account 对象在通知(advice)中可用。
另一种写法是声明一个切入点(pointcut),当它匹配某个连接点(join point)时“提供”Account对象的值,然后在通知(advice)中引用这个命名的切入点。其写法如下所示:
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private fun accountDataAccessOperation(account: Account) {
}
@Before("accountDataAccessOperation(account)")
fun validateAccount(account: Account) {
// ...
}
有关更多详细信息,请参阅 AspectJ 编程指南。
代理对象(this)、目标对象(target)以及注解(@within、
@target、@annotation 和 @args)都可以以类似的方式进行绑定。接下来的两个
示例展示了如何匹配带有 @Auditable 注解的方法执行,并提取审计代码:
前两个示例中的第一个展示了 @Auditable 注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Auditable(val value: AuditCode)
两个示例中的第二个展示了匹配执行 @Auditable 方法的通知(advice):
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
fun audit(auditable: Auditable) {
val code = auditable.value()
// ...
}
通知参数与泛型
Spring AOP 能够处理类声明和方法参数中使用的泛型。假设你有一个如下所示的泛型类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
interface Sample<T> {
fun sampleGenericMethod(param: T)
fun sampleGenericCollectionMethod(param: Collection<T>)
}
你可以通过将通知参数的类型指定为你想要拦截的方法所对应的参数类型,来限制对特定参数类型的方法进行拦截:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
fun beforeSampleMethod(param: MyType) {
// Advice implementation
}
这种方法不适用于泛型集合。因此,您不能像下面这样定义切入点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
fun beforeSampleMethod(param: Collection<MyType>) {
// Advice implementation
}
要实现这一点,我们必须检查集合中的每个元素,这是不合理的,因为我们通常也无法决定如何处理 null 值。若要实现类似的功能,您必须将参数类型声明为 Collection<?>,并手动检查元素的类型。
确定参数名称
通知(advice)调用中的参数绑定依赖于切点(pointcut)表达式中使用的名称与通知和切点方法签名中声明的参数名称之间的匹配。 参数名称无法通过 Java 反射获取,因此 Spring AOP 采用以下策略来确定参数名称:
-
如果参数名称已由用户显式指定,则使用所指定的参数名称。通知(advice)和切入点(pointcut)注解都具有一个可选的
argNames属性,可用于指定被注解方法的参数名称。这些参数名称在运行时可用。以下示例展示了如何使用argNames属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(bean: Any, auditable: Auditable) {
val code = auditable.value()
// ... use code and bean
}
如果第一个参数的类型是 JoinPoint、ProceedingJoinPoint 或
JoinPoint.StaticPart,你可以从 argNames 属性的值中省略该参数的名称。例如,如果你修改前面的通知以接收连接点对象,则 argNames 属性无需包含它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
@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 属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
fun audit(jp: JoinPoint) {
// ... use jp
}
-
使用
'argNames'属性有点笨拙,因此如果未指定'argNames'属性,Spring AOP 会查看类的调试信息,并尝试从局部变量表中确定参数名称。只要类在编译时包含了调试信息(至少使用'-g:vars'选项),就会存在这些信息。启用此编译标志带来的影响包括:(1) 你的代码略微更容易被理解(反编译);(2) 类文件大小会略微增大(通常微不足道);(3) 编译器不会执行删除未使用局部变量的优化。换句话说,启用此标志进行构建不会给你带来任何困难。如果一个 @AspectJ 切面已经由 AspectJ 编译器(ajc)编译过,即使没有调试信息,也无需添加 argNames属性,因为编译器会保留所需的信息。 -
如果代码在编译时未包含必要的调试信息,Spring AOP 会尝试推断绑定变量与参数之间的对应关系(例如,如果切入点表达式中仅绑定了一个变量,且通知方法只接受一个参数,那么这种对应关系是显而易见的)。如果根据可用信息无法明确确定变量的绑定关系,则会抛出
AmbiguousBindingException异常。 -
如果上述所有策略都失败,则会抛出一个
IllegalArgumentException异常。
使用参数进行过程处理
我们之前提到过,将介绍如何编写一个带有参数的 proceed 调用,使其在 Spring AOP 和 AspectJ 中都能一致地工作。解决方法是确保通知(advice)的签名按顺序绑定方法的每个参数。以下示例展示了如何实现这一点:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
accountHolderNamePattern: String): Any {
val newPattern = preProcess(accountHolderNamePattern)
return pjp.proceed(arrayOf<Any>(newPattern))
}
在许多情况下,无论如何你都会进行这种绑定(如前面的示例所示)。
通知排序
当多个通知(advice)都希望在同一个连接点(join point)执行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知的执行顺序。优先级最高的通知在“进入”时最先执行(因此,如果有两个前置通知(before advice),优先级最高的那个会先运行)。在从连接点“退出”时,优先级最高的通知最后执行(因此,如果有两个后置通知(after advice),优先级最高的那个将第二个运行)。
当定义在不同切面中的两条通知(advice)都需要在同一连接点(join point)执行时,除非另行指定,否则它们的执行顺序是未定义的。你可以通过指定优先级(precedence)来控制执行顺序。在 Spring 中,这可以通过常规方式实现:要么在切面类中实现 org.springframework.core.Ordered 接口,要么使用 @Order 注解对其进行标注。对于两个切面而言,从 Ordered.getOrder() 方法(或注解值)返回较小值的切面具有更高的优先级。
|
某个特定切面中的每种不同类型的通知(advice)在概念上都是直接应用于连接点(join point)的。因此, 自 Spring Framework 5.2.7 起,定义在同一个 当同一个 |
5.4.5. 介绍
引入(在 AspectJ 中称为类型间声明)使切面能够声明被通知对象实现某个给定的接口,并代表这些对象提供该接口的实现。
你可以使用 @DeclareParents 注解来引入(introduction)。该注解用于声明匹配的类型拥有一个新的父类(因此得名)。例如,给定一个名为 UsageTracked 的接口以及该接口的一个实现类 DefaultUsageTracked,下面的切面声明了所有服务接口的实现类同时也实现了 UsageTracked 接口(例如,通过 JMX 提供统计信息):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
@Aspect
class UsageTracking {
companion object {
@DeclareParents(value = "com.xzy.myapp.service.*+", defaultImpl = DefaultUsageTracked::class)
lateinit var mixin: UsageTracked
}
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
}
要实现的接口由被注解字段的类型决定。value 注解的 @DeclareParents 属性是一个 AspectJ 类型模式。任何匹配该类型的 bean 都会实现 UsageTracked 接口。请注意,在前面示例的前置通知(before advice)中,服务 bean 可以直接用作 UsageTracked 接口的实现。如果以编程方式访问 bean,则应编写如下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
val usageTracked = context.getBean("myService") as UsageTracked
5.4.6. 切面实例化模型
| 这是一个高级主题。如果你刚刚开始学习AOP,可以放心地暂时跳过,稍后再回来看。 |
默认情况下,应用程序上下文中每个切面(aspect)只有一个实例。AspectJ 将此称为单例(singleton)实例化模型。也可以定义具有其他生命周期的切面。Spring 支持 AspectJ 的 perthis 和 pertarget 实例化模型;目前不支持 percflow、percflowbelow 和 pertypewithin。
你可以通过在 perthis 注解中指定一个 perthis 子句来声明一个 @Aspect 切面。请看以下示例:
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
private int someState;
@Before("com.xyz.myapp.CommonPointcuts.businessService()")
public void recordServiceUsage() {
// ...
}
}
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
class MyAspect {
private val someState: Int = 0
@Before("com.xyz.myapp.CommonPointcuts.businessService()")
fun recordServiceUsage() {
// ...
}
}
在前面的示例中,perthis 子句的作用是:每当一个唯一的业务服务对象(即在切点表达式所匹配的连接点处绑定到 this 的每个唯一对象)执行业务服务时,都会创建一个切面实例。该切面实例在首次调用该服务对象的方法时被创建。当服务对象超出作用域时,该切面也随之超出作用域。在切面实例创建之前,其中的任何通知都不会执行。一旦切面实例被创建,其中声明的通知就会在匹配的连接点上执行,但仅限于与该切面关联的服务对象。
有关 per 子句的更多信息,请参阅 AspectJ 编程指南。
pertarget 实例化模型的工作方式与 perthis 完全相同,但它会在匹配的连接点处为每个唯一的目 标对象创建一个切面实例。
5.4.7. AOP 示例
现在你已经了解了各个组成部分是如何工作的,我们可以将它们组合起来,完成一些有用的任务。
业务服务的执行有时会因并发问题(例如死锁导致的事务回滚)而失败。如果重试该操作,很可能在下一次尝试时成功。对于在这些情况下适合重试的业务服务(即幂等操作,且无需返回用户进行冲突解决),我们希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException 异常。这一需求显然横切了服务层中的多个服务,因此非常适合通过切面(aspect)来实现。
由于我们希望重试该操作,因此需要使用环绕通知(around advice),以便可以多次调用 proceed 方法。以下代码清单展示了基本的切面实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
@Aspect
class ConcurrentOperationExecutor : Ordered {
private val DEFAULT_MAX_RETRIES = 2
private var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
fun setMaxRetries(maxRetries: Int) {
this.maxRetries = maxRetries
}
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException
}
}
请注意,该切面实现了 Ordered 接口,以便我们可以将该切面的优先级设置得高于事务通知(我们希望每次重试时都开启一个全新的事务)。maxRetries 和 order 属性均由 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 注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
@Retention(AnnotationRetention.RUNTIME)
annotation class Idempotent// marker annotation
然后,我们可以使用该注解来标注服务操作的实现。为了使切面仅对幂等操作进行重试,需要细化切入点表达式,使其仅匹配带有 @Idempotent 注解的操作,如下所示:
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
// ...
}
5.5. 基于 Schema 的 AOP 支持
如果你更喜欢基于 XML 的格式,Spring 也提供了使用 aop 命名空间标签来定义切面的支持。与使用 @AspectJ 风格时一样,它支持完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点介绍这种语法,并请读者参考前一节(@AspectJ 支持)中关于编写切入点表达式以及通知参数绑定的讨论。
要使用本节中描述的 aop 命名空间标签,您需要导入 spring-aop 模式,如基于 XML Schema 的配置中所述。有关如何在 #xsd-schemas-aop 命名空间中导入这些标签,请参见AOP 模式。
在您的 Spring 配置中,所有的切面(aspect)和通知器(advisor)元素都必须放置在 <aop:config> 元素内部(您可以在一个应用上下文配置中包含多个 <aop:config> 元素)。一个 <aop:config> 元素可以包含切入点(pointcut)、通知器(advisor)和切面(aspect)元素(请注意,这些元素必须按照此顺序声明)。
<aop:config> 配置方式大量使用了 Spring 的自动代理机制。如果你已经通过使用 BeanNameAutoProxyCreator 或类似方式显式地启用了自动代理,这可能会引发问题(例如通知未被织入)。推荐的使用模式是:要么仅使用 <aop:config> 配置方式,要么仅使用 AutoProxyCreator 方式,切勿将两者混合使用。 |
5.5.1. 声明切面
当你使用 schema 支持时,切面(aspect)就是一个普通的 Java 对象,并在 Spring 应用上下文中被定义为一个 bean。该对象的状态和行为由其字段和方法来体现,而切入点(pointcut)和通知(advice)信息则在 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> 元素内部声明一个命名的切入点(pointcut),使该切入点定义能够在多个切面(aspect)和通知器(advisor)之间共享。
一个切入点(pointcut),用于表示服务层中任意业务服务的执行,可定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
请注意,切入点表达式本身使用的是与@AspectJ 支持中所述相同的 AspectJ 切入点表达式语言。如果您使用基于 schema 的声明风格,则可以在切入点表达式中引用在类型(@Aspect)中定义的命名切入点。定义上述切入点的另一种方式如下所示:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.CommonPointcuts.businessService()"/>
</aop:config>
假设你有一个如共享通用切入点定义中所述的#aop-common-pointcuts切面。
然后,在切面内部声明一个切入点与声明一个顶层切入点非常相似,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与 @AspectJ 切面非常相似,使用基于 schema 的定义风格声明的切入点(pointcut)也可以收集连接点(join point)上下文。例如,以下切入点会将 this 对象作为连接点上下文进行收集,并将其传递给通知(advice):
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
通知必须声明为通过包含具有匹配名称的参数来接收所收集的连接点上下文,如下所示:
public void monitor(Object service) {
// ...
}
fun monitor(service: Any) {
// ...
}
在组合切入点子表达式时,在 XML 文档中使用 && 会显得很笨拙,因此你可以分别用 and、or 和 not 关键字来代替 &&、|| 和 !。例如,前面的切入点可以更好地改写如下:
<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. 声明通知
基于 schema 的 AOP 支持使用与 @AspectJ 风格相同的五种通知类型,并且它们具有完全相同的语义。
前置通知
前置通知(Before advice)在匹配的方法执行之前运行。它通过使用 <aop:aspect> 元素在 <aop:before> 内部进行声明,如下例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
此处,dataAccessOperation 是在顶层(id 级别)定义的切入点(pointcut)的 <aop:config>。若要改为内联定义切入点,请将 pointcut-ref 属性替换为 pointcut 属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
正如我们在讨论 @AspectJ 风格时所提到的,使用命名切入点可以显著提高代码的可读性。
method 属性标识了一个方法(doAccessCheck),该方法提供了通知(advice)的具体实现。此方法必须在包含该通知的切面(aspect)元素所引用的 bean 中定义。在执行数据访问操作之前(即在匹配切入点表达式的连接点处执行方法之前),会调用切面 bean 上的 doAccessCheck 方法。
返回后通知
后置返回通知(After returning advice)在匹配的方法正常执行完成后运行。它的声明方式与前置通知(before advice)相同,位于 <aop:aspect> 元素内部。以下示例展示了如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
与 @AspectJ 风格一样,你可以在通知(advice)体中获取返回值。
为此,请使用 returning 属性来指定接收返回值的参数名称,如下例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck 方法必须声明一个名为 retVal 的参数。该参数的类型对匹配的限制方式与 @AfterReturning 中所述相同。例如,您可以将方法签名声明如下:
public void doAccessCheck(Object retVal) {...
fun doAccessCheck(retVal: Any) {...
抛出异常后通知
异常抛出后通知(After throwing advice)在匹配的方法执行因抛出异常而退出时运行。它通过使用 <aop:aspect> 元素在 after-throwing 内部进行声明,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>
...
</aop:aspect>
与 @AspectJ 风格一样,你可以在通知(advice)体中获取抛出的异常。
为此,请使用 throwing 属性来指定参数名称,
该参数将接收所抛出的异常,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions 方法必须声明一个名为 dataAccessEx 的参数。
该参数的类型对匹配的限制方式与 @AfterThrowing 中所述相同。例如,方法签名可以如下声明:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
后置(最终)通知
无论匹配的方法执行以何种方式退出,后置(最终)通知都会运行。
你可以使用 after 元素来声明它,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
环绕通知
最后一种通知是环绕通知(around advice)。环绕通知在匹配的方法执行过程中“环绕”运行。它可以在方法运行之前和之后执行额外的工作,并且能够决定何时、如何,甚至是否真正执行该方法。 环绕通知通常用于以线程安全的方式在方法执行前后共享状态(例如启动和停止计时器)。始终使用满足需求的前提下功能最弱的通知形式。如果前置通知(before advice)可以完成任务,就不要使用环绕通知。
您可以使用 aop:around 元素声明环绕通知。通知方法的第一个参数必须是 ProceedingJoinPoint 类型。在通知主体中,对 ProceedingJoinPoint 调用 proceed() 将导致底层方法执行。proceed 方法也可以使用 Object[] 进行调用。数组中的值将在方法继续执行时用作其参数。
有关使用 Object[] 调用 proceed 的说明,请参阅 环绕通知。
以下示例展示了如何在 XML 中声明环绕通知:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling 通知的实现可以与 @AspectJ 示例中的完全相同(当然,去掉注解部分),如下例所示:
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return pjp.proceed()
}
通知参数
基于 schema 的声明方式以与 @AspectJ 支持相同的方式支持完全类型化的通知(advice)——即通过将切入点(pointcut)参数名称与通知方法参数名称进行匹配。详情请参见通知参数(Advice Parameters)。如果您希望显式指定通知方法的参数名称(而不依赖于前面描述的自动检测策略),可以通过使用通知元素的 arg-names 属性来实现,该属性的处理方式与通知注解中的 argNames 属性相同(如确定参数名称(Determining Argument Names)中所述)。
以下示例展示了如何在 XML 中指定参数名称:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>
arg-names 属性接受一个以逗号分隔的参数名称列表。
以下是一个稍微复杂一点的基于 XSD 方法的示例,展示了环绕通知(around advice)与多个强类型参数结合使用的情况:
package x.y.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
package x.y.service
interface PersonService {
fun getPerson(personName: String, age: Int): Person
}
class DefaultPersonService : PersonService {
fun getPerson(name: String, age: Int): Person {
return Person(name, age)
}
}
接下来是切面(aspect)。请注意,profile(..) 方法接受多个强类型参数,其中第一个参数恰好是用于继续执行方法调用的连接点(join point)。该参数的存在表明 profile(..) 将被用作 around 通知(advice),如下例所示:
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
import org.aspectj.lang.ProceedingJoinPoint
import org.springframework.util.StopWatch
class SimpleProfiler {
fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any {
val clock = StopWatch("Profiling for '$name' and '$age'")
try {
clock.start(call.toShortString())
return call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
}
}
最后,以下示例 XML 配置会在特定的连接点(join point)上执行前述通知(advice):
<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>
考虑以下驱动脚本:
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;
public final class Boot {
public static void main(final String[] args) throws Exception {
BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
PersonService person = (PersonService) ctx.getBean("personService");
person.getPerson("Pengo", 12);
}
}
fun main() {
val ctx = ClassPathXmlApplicationContext("x/y/plain.xml")
val person = ctx.getBean("personService") as PersonService
person.getPerson("Pengo", 12)
}
使用这样一个 Boot 类,我们将在标准输出上得到类似于以下内容的输出:
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
通知排序
当多个通知(advice)需要在同一连接点(即方法执行处)运行时,其排序规则如通知排序(Advice Ordering)中所述。切面(aspect)之间的优先级通过order元素中的<aop:aspect>属性确定,或者通过在支撑该切面的 Bean 上添加@Order注解,或让该 Bean 实现Ordered接口来确定。
|
与在同一个 例如,对于在同一个 通常情况下,如果你发现同一个 |
5.5.4. 简介
引入(在 AspectJ 中称为类型间声明)允许切面声明被通知的对象实现某个接口,并代表这些对象提供该接口的实现。
你可以通过在 aop:declare-parents 元素内部使用 aop:aspect 元素来引入新功能。
你可以使用 aop:declare-parents 元素声明匹配的类型拥有一个新的父类(因此得名)。
例如,给定一个名为 UsageTracked 的接口以及该接口的一个实现类
DefaultUsageTracked,以下切面声明所有服务接口的实现类同时也实现了 UsageTracked 接口。(例如,为了通过 JMX 暴露统计信息。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.CommonPointcuts.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
支持 usageTracking bean 的类将包含以下方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
要实现的接口由 implement-interface 属性决定。types-matching 属性的值是一个 AspectJ 类型模式。任何匹配该类型的 bean 都会实现 UsageTracked 接口。请注意,在前面示例的前置通知(before advice)中,服务 bean 可以直接用作 UsageTracked 接口的实现。若要以编程方式访问 bean,可以编写如下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
val usageTracked = context.getBean("myService") as UsageTracked
5.5.6. 顾问
“通知器”(advisors)的概念源自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接对应的等价物。通知器类似于一个小型的、自包含的切面,仅包含一条通知(advice)。该通知本身由一个 bean 表示,并且必须实现 Spring 中的通知类型 所描述的某个通知接口之一。通知器可以利用 AspectJ 的切入点表达式。
Spring 通过 <aop:advisor> 元素支持通知器(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 属性来内联定义一个切入点表达式。
要定义通知器(advisor)的优先级,以便其通知(advice)能够参与排序,请使用 order 属性来定义该通知器的 Ordered 值。
5.5.7. AOP 架构示例
本节展示了当使用 schema 支持重写时,一个 AOP 示例 中的并发锁失败重试示例如何呈现。
业务服务的执行有时会因并发问题(例如死锁导致的事务回滚)而失败。如果重试该操作,很可能在下一次尝试时成功。对于在这些情况下适合重试的业务服务(即幂等操作,且无需返回用户进行冲突解决),我们希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException 异常。这一需求显然横切了服务层中的多个服务,因此非常适合通过切面(aspect)来实现。
由于我们希望重试该操作,因此需要使用环绕通知(around advice),以便可以多次调用 proceed 方法。以下代码清单展示了基本的切面实现(这是一个使用 schema 支持的普通 Java 类):
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
class ConcurrentOperationExecutor : Ordered {
private val DEFAULT_MAX_RETRIES = 2
private var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
fun setMaxRetries(maxRetries: Int) {
this.maxRetries = maxRetries
}
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException
}
}
请注意,该切面实现了 Ordered 接口,以便我们可以将该切面的优先级设置得高于事务通知(我们希望每次重试时都开启一个全新的事务)。maxRetries 和 order 属性均由 Spring 进行配置。主要逻辑发生在 doConcurrentOperation 环绕通知方法中。我们尝试继续执行;如果因 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 注解,并使用该注解来标记服务操作的实现,从而细化切面,使其仅对真正幂等的操作进行重试,如下例所示:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
@Retention(AnnotationRetention.RUNTIME)
annotation class Idempotent {
// marker annotation
}
将切面修改为仅重试幂等操作,需要细化切入点表达式,使其仅匹配 @Idempotent 注解的操作,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>
5.6. 选择使用哪种 AOP 声明风格
一旦你确定使用切面(aspect)是实现某个需求的最佳方式,那么接下来该如何在 Spring AOP 和 AspectJ 之间进行选择,以及在 Aspect 语言(代码)风格、@AspectJ 注解风格和 Spring XML 风格之间做出决定呢?这些决策受到多种因素的影响,包括应用需求、开发工具以及团队对 AOP 的熟悉程度。
5.6.1. 使用 Spring AOP 还是完整的 AspectJ?
使用能够满足需求的最简单方案。Spring AOP 比完整的 AspectJ 更简单,因为无需在开发和构建过程中引入 AspectJ 编译器或织入器(weaver)。如果你仅需对 Spring Bean 中的操作执行进行通知(advice),那么 Spring AOP 是合适的选择。但如果你需要对非 Spring 容器管理的对象(例如典型的领域对象)进行通知,则必须使用 AspectJ。此外,如果你希望通知的连接点(join point)不仅仅是简单的方法执行(例如字段的读取或设置等连接点),你也需要使用 AspectJ。
当你使用 AspectJ 时,可以选择 AspectJ 语言语法(也称为“代码风格”)或 @AspectJ 注解风格。显然,如果你不使用 Java 5 及以上版本,那么选择已经为你确定好了:使用代码风格。如果你的设计中切面(aspects)扮演着重要角色,并且你能够使用 Eclipse 的 AspectJ 开发工具(AJDT) 插件,那么 AspectJ 语言语法是首选方案。因为它更简洁、更清晰,毕竟该语言正是专门为编写切面而设计的。如果你不使用 Eclipse,或者你的应用中只有少量切面且它们并不起主要作用,那么你可以考虑使用 @AspectJ 风格,在 IDE 中继续使用常规的 Java 编译方式,并在构建脚本中添加一个切面织入(aspect weaving)阶段。
5.6.2. 使用 @AspectJ 还是 XML 来实现 Spring AOP?
如果你选择使用 Spring AOP,你可以选择 @AspectJ 风格或 XML 风格。 需要考虑各种权衡因素。
XML 风格对现有的 Spring 用户来说可能最为熟悉,并且它基于真正的 POJO。当将 AOP 用作配置企业服务的工具时,XML 可能是一个不错的选择(一个很好的判断标准是:你是否认为切入点表达式是你配置的一部分,并且可能需要独立进行修改)。使用 XML 风格时,从配置中可以更清晰地看出系统中存在哪些切面。
XML 风格有两个缺点。首先,它无法将所处理需求的实现完全封装在单一位置。DRY(Don't Repeat Yourself,不要重复自己)原则指出,系统中任何一段知识都应当有且仅有一个明确、权威的表示。使用 XML 风格时,关于如何实现某个需求的知识被分散到了支持该功能的 bean 类声明和配置文件中的 XML 配置两处。而当你使用 @AspectJ 风格时,这些信息会被封装在一个单独的模块中:即切面(aspect)。其次,XML 风格在表达能力上略逊于 @AspectJ 风格:它仅支持“singleton”(单例)的切面实例化模型,并且无法组合在 XML 中声明的具名切入点(named pointcut)。例如,在 @AspectJ 风格中,你可以编写如下代码:
@Pointcut("execution(* get*())")
public void propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
@Pointcut("execution(* get*())")
fun propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
fun operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
fun accountPropertyAccess() {}
在 XML 风格中,你可以声明前两个切入点:
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>
XML 方法的缺点在于,你无法通过组合这些定义来定义 accountPropertyAccess 切入点。
@AspectJ 风格支持额外的实例化模型和更丰富的切入点组合。它的优势在于将切面保持为一个模块化单元。此外,@AspectJ 切面既可以被 Spring AOP 理解(并使用),也可以被 AspectJ 理解(并使用)。因此,如果你日后决定需要使用 AspectJ 的功能来实现更多需求,可以轻松迁移到经典的 AspectJ 设置。总体而言,对于超出企业服务简单配置范围的自定义切面,Spring 团队更倾向于使用 @AspectJ 风格。
5.7. 混合切面类型
完全可以将使用自动代理(auto-proxying)支持的 @AspectJ 风格切面、通过 schema 定义的 <aop:aspect> 切面、声明的 <aop:advisor> 通知器,甚至其他风格的代理和拦截器混合在同一配置中。所有这些都基于相同的底层支持机制实现,并且可以毫无困难地共存。
5.8. 代理机制
Spring AOP 使用 JDK 动态代理或 CGLIB 来为目标对象创建代理。JDK 动态代理内置于 JDK 中,而 CGLIB 是一个常用的开源类定义库(已重新打包到 spring-core 中)。
如果要被代理的目标对象实现了至少一个接口,则使用 JDK 动态代理。目标类型所实现的所有接口都会被代理。 如果目标对象未实现任何接口,则会创建一个 CGLIB 代理。
如果你想强制使用 CGLIB 代理(例如,代理目标对象定义的所有方法,而不仅仅是其接口中实现的方法),你可以这样做。但是,你应该考虑以下问题:
-
使用 CGLIB 时,
final方法无法被通知(advised),因为它们不能在运行时生成的子类中被重写。 -
从 Spring 4.0 开始,您的代理对象的构造函数不会再被调用两次, 因为 CGLIB 代理实例是通过 Objenesis 创建的。只有在您的 JVM 不允许绕过构造函数的情况下,您才可能会看到构造函数被调用两次, 并看到来自 Spring AOP 支持的相应调试日志条目。
要强制使用 CGLIB 代理,请将 proxy-target-class 元素的 <aop:config> 属性值设置为 true,如下所示:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
当你使用 @AspectJ 自动代理支持时,若要强制使用 CGLIB 代理,请将 proxy-target-class 元素的 <aop:aspectj-autoproxy> 属性设置为 true,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
|
多个 需要明确的是,在 |
5.8.1. 理解 AOP 代理
Spring AOP 基于代理。在编写自己的切面或使用 Spring 框架所提供的任何基于 Spring AOP 的切面之前,你必须充分理解上述语句的实际含义,这一点至关重要。
首先考虑这样一种场景:你拥有一个普通的、未被代理的、没有任何特殊之处的直接对象引用,如下列代码片段所示:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
class SimplePojo : Pojo {
fun foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar()
}
fun bar() {
// some logic...
}
}
如果你在一个对象引用上调用一个方法,该方法会直接在该对象引用上被调用,如下图和代码清单所示:
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
fun main() {
val pojo = SimplePojo()
// this is a direct method call on the 'pojo' reference
pojo.foo()
}
当客户端代码持有的引用是一个代理时,情况会略有不同。请看下面的图示和代码片段:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
fun main() {
val factory = ProxyFactory(SimplePojo())
factory.addInterface(Pojo::class.java)
factory.addAdvice(RetryAdvice())
val pojo = factory.proxy as Pojo
// this is a method call on the proxy!
pojo.foo()
}
这里需要理解的关键点是:main(..) 类的 Main 方法中的客户端代码持有对代理对象的引用。这意味着对该对象引用的方法调用实际上是对代理的调用。因此,代理可以将调用委托给所有与该特定方法调用相关的拦截器(通知)。
然而,一旦调用最终到达目标对象(在本例中是 SimplePojo 引用),该目标对象对其自身进行的任何方法调用(例如 this.bar() 或 this.foo())都将作用于 this 引用本身,而不是代理对象。
这一点具有重要的影响:它意味着自调用(self-invocation)不会触发与该方法调用相关联的通知逻辑执行。
那么,对此该怎么办呢?最好的方法(此处“最好”一词用得较为宽松)是重构你的代码,以避免发生自调用。这确实需要你做一些工作,但这是最佳且侵入性最小的方法。 下一种方法则完全糟糕透顶,我们甚至犹豫是否要提出来,正是因为这种方法实在太糟糕了。你可以(尽管对我们而言这很痛苦)将类中的逻辑完全与 Spring AOP 绑定在一起,如下例所示:
public class SimplePojo implements Pojo {
public void foo() {
// this works, but... gah!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
class SimplePojo : Pojo {
fun foo() {
// this works, but... gah!
(AopContext.currentProxy() as Pojo).bar()
}
fun bar() {
// some logic...
}
}
这完全将您的代码与 Spring AOP 耦合在一起,并且使得该类自身意识到它正被用于 AOP 上下文中,这违背了 AOP 的初衷。此外,如以下示例所示,在创建代理时还需要一些额外的配置:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
fun main() {
val factory = ProxyFactory(SimplePojo())
factory.addInterface(Pojo::class.java)
factory.addAdvice(RetryAdvice())
factory.isExposeProxy = true
val pojo = factory.proxy as Pojo
// this is a method call on the proxy!
pojo.foo()
}
最后,必须指出的是,AspectJ 没有这种自调用问题,因为它不是基于代理的 AOP 框架。
5.9. 以编程方式创建 @AspectJ 代理
除了通过使用 <aop:config> 或 <aop:aspectj-autoproxy> 在配置中声明切面之外,还可以以编程方式创建代理来对目标对象进行增强。有关 Spring AOP API 的完整细节,请参阅下一章。在这里,我们重点关注使用 @AspectJ 切面自动创建代理的能力。
你可以使用 org.springframework.aop.aspectj.annotation.AspectJProxyFactory 类
为一个目标对象创建代理,该目标对象由一个或多个 @AspectJ 切面进行增强。
此类的基本用法非常简单,如下例所示:
// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();
// create a factory that can generate a proxy for the given target object
val factory = AspectJProxyFactory(targetObject)
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager::class.java)
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker)
// now get the proxy object...
val proxy = factory.getProxy<Any>()
有关更多信息,请参阅javadoc。
5.10. 在 Spring 应用程序中使用 AspectJ
到目前为止,本章所涵盖的所有内容都是纯 Spring AOP。在本节中,我们将探讨如果你的需求超出了 Spring AOP 单独提供的功能范围,如何使用 AspectJ 编译器或织入器来替代或补充 Spring AOP。
Spring 自带了一个小型的 AspectJ 切面库,在您的发行包中以独立的 spring-aspects.jar 形式提供。您需要将其添加到类路径中,才能使用其中的切面。使用 AspectJ 通过 Spring 对领域对象进行依赖注入 和 适用于 AspectJ 的其他 Spring 切面 介绍了该库的内容以及如何使用它。使用 Spring IoC 配置 AspectJ 切面 讨论了如何对使用 AspectJ 编译器织入的 AspectJ 切面进行依赖注入。最后,在 Spring 框架中使用 AspectJ 进行加载时织入 为使用 AspectJ 的 Spring 应用程序提供了加载时织入(Load-time Weaving)的入门介绍。
5.10.1. 使用 AspectJ 通过 Spring 对领域对象进行依赖注入
Spring 容器会实例化并配置在您的应用程序上下文中定义的 bean。此外,也可以要求 bean 工厂根据某个包含所需配置的 bean 定义名称,来配置一个已存在的对象。spring-aspects.jar 包含一个注解驱动的切面(aspect),它利用这一功能,允许对任意对象进行依赖注入。该支持旨在用于那些在任何容器控制范围之外创建的对象。领域对象(Domain objects)通常就属于这一类,因为它们通常是通过 new 操作符以编程方式创建的,或者由 ORM 工具在执行数据库查询后创建的。
@Configurable 注解用于将一个类标记为可接受 Spring 驱动的配置。在最简单的情况下,你可以仅将其用作一个标记注解,如下例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
package com.xyz.myapp.domain
import org.springframework.beans.factory.annotation.Configurable
@Configurable
class Account {
// ...
}
当以这种方式用作标记接口时,Spring 会使用一个与该注解类型(本例中为 Account)完全限定类名(com.xyz.myapp.domain.Account)同名的 bean 定义(通常为 prototype 作用域)来配置该类型的全新实例。由于 bean 的默认名称就是其类型的完全限定名,因此声明该 prototype 定义的一种便捷方式是省略 id 属性,如下例所示:
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果你想显式指定要使用的原型(prototype)bean 定义的名称,可以直接在注解中进行指定,如下例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
package com.xyz.myapp.domain
import org.springframework.beans.factory.annotation.Configurable
@Configurable("account")
class Account {
// ...
}
Spring 现在会查找名为 account 的 bean 定义,并使用该定义来配置新的 Account 实例。
你也可以使用自动装配(autowiring)来完全避免显式指定专门的 bean 定义。要让 Spring 应用自动装配,请使用 autowire 注解的 @Configurable 属性。你可以分别通过指定 @Configurable(autowire=Autowire.BY_TYPE) 或 @Configurable(autowire=Autowire.BY_NAME 来按类型或按名称进行自动装配。不过,更推荐的方式是在字段或方法级别上,通过 @Configurable 或 @Autowired 为你的 @Inject bean 显式地指定基于注解的依赖注入(详见基于注解的容器配置以获取更多详细信息)。
最后,你可以通过使用 dependencyCheck 属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))来为新创建并已配置的对象中的对象引用启用 Spring 的依赖项检查。如果该属性设置为 true,Spring 会在配置完成后验证所有属性(非基本类型或集合类型的属性)是否都已被设置。
请注意,单独使用该注解本身不会产生任何效果。真正对注解的存在作出响应的是 AnnotationBeanConfigurerAspect 中的 spring-aspects.jar。本质上,该切面表示:“在完成一个带有 @Configurable 注解的类型的新对象初始化之后,根据该注解的属性,使用 Spring 对新创建的对象进行配置”。在此上下文中,“初始化”既指新实例化的对象(例如,通过 new 操作符创建的对象),也指正在经历反序列化的 Serializable 对象(例如,通过 readResolve() 方法)。
|
上述段落中的一个关键词是“本质上”。在大多数情况下,“在新对象初始化完成后”这一表述的确切语义是合适的。在此上下文中,“初始化之后”意味着依赖项是在对象构造完成之后才被注入的。这意味着在类的构造函数体内无法使用这些依赖项。如果你希望依赖项在构造函数体执行之前就被注入,从而可以在构造函数体内使用,那么你需要在 Java
Kotlin
您可以在《AspectJ 编程指南》的附录中找到有关 AspectJ 各种切入点类型语言语义的更多信息。 |
要使此功能正常工作,带注解的类型必须经过 AspectJ 编织器(weaver)进行编织。你可以使用构建时的 Ant 或 Maven 任务来完成此操作(例如,参见AspectJ 开发环境指南),也可以使用加载时编织(参见Spring 框架中的 AspectJ 加载时编织)。AnnotationBeanConfigurerAspect 本身需要由 Spring 进行配置(以便获取用于配置新对象的 bean 工厂引用)。如果你使用基于 Java 的配置,可以在任意 @EnableSpringConfigured 类上添加 @Configuration 注解,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
@Configuration
@EnableSpringConfigured
class AppConfig {
}
如果您更倾向于基于 XML 的配置,Spring
context 命名空间
定义了一个便捷的 context:spring-configured 元素,您可以按如下方式使用:
<context:spring-configured/>
在切面(aspect)配置完成之前创建的 @Configurable 对象实例,
会导致向调试日志中输出一条消息,并且该对象不会被配置。
一个典型的例子是 Spring 配置中的某个 Bean,在 Spring 初始化它时会创建领域对象。
在这种情况下,你可以使用 depends-on Bean 属性来手动指定该 Bean 依赖于配置切面。
以下示例展示了如何使用 depends-on 属性:
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>
除非你确实打算在运行时依赖其语义,否则不要通过 bean 配置器切面(bean configurer aspect)激活 @Configurable 处理。特别要注意的是,请确保不要在那些已作为常规 Spring bean 注册到容器中的 bean 类上使用 @Configurable。这样做会导致重复初始化:一次通过容器,另一次通过切面。 |
单元测试@Configurable对象
@Configurable 支持的目标之一是使领域对象能够进行独立的单元测试,而无需面对硬编码查找所带来的困难。
如果 @Configurable 类型尚未被 AspectJ 织入,则该注解在单元测试期间不会产生任何影响。
你可以在被测试的对象中设置模拟(mock)或桩(stub)属性引用,并像平常一样进行测试。
如果 @Configurable 类型已被 AspectJ 织入,你仍然可以像平常一样在容器外部进行单元测试,
但每次创建 @Configurable 对象时,都会看到一条警告消息,提示该对象尚未被 Spring 配置。
使用多个应用上下文
用于实现 AnnotationBeanConfigurerAspect 支持的 @Configurable 是一个 AspectJ 单例切面。单例切面的作用域与 static 成员的作用域相同:每个定义该类型的类加载器对应一个切面实例。
这意味着,如果你在同一个类加载器层次结构中定义了多个应用上下文,则需要考虑在何处定义 @EnableSpringConfigured bean,以及将 spring-aspects.jar 放置在类路径的哪个位置。
考虑一个典型的 Spring Web 应用程序配置:它包含一个共享的父应用上下文,用于定义通用的业务服务以及支持这些服务所需的一切组件,同时每个 Servlet 都拥有一个子应用上下文(其中包含该 Servlet 特有的 Bean 定义)。所有这些上下文共存于同一个类加载器层次结构中,因此 AnnotationBeanConfigurerAspect 只能持有对其中一个上下文的引用。
在这种情况下,我们建议在共享的(父)应用上下文中定义 @EnableSpringConfigured Bean。这样可以定义你可能希望注入到领域对象中的服务。其结果是,你无法通过 @Configurable 机制将领域对象配置为引用子上下文(即特定于某个 Servlet 的上下文)中定义的 Bean(这通常也不是你希望做的事情)。
在同一容器内部署多个Web应用程序时,请确保每个Web应用程序都通过其自身的类加载器加载spring-aspects.jar中的类型(例如,将spring-aspects.jar放置在'WEB-INF/lib'目录下)。如果仅将spring-aspects.jar添加到容器范围的类路径中(从而由共享的父类加载器加载),则所有Web应用程序将共享同一个切面实例(这很可能不是您期望的行为)。
5.10.2. 面向 AspectJ 的其他 Spring 方面
除了 @Configurable 切面之外,spring-aspects.jar 还包含一个 AspectJ 切面,可用于为使用 @Transactional 注解标注的类型和方法驱动 Spring 的事务管理。这主要是为那些希望在 Spring 容器之外使用 Spring 框架事务支持的用户而设计的。
解释 @Transactional 注解的切面是
AnnotationTransactionAspect。当您使用此切面时,必须在实现类(或该类中的方法,或两者)上添加注解,而不是在该类所实现的接口(如果有的话)上添加注解。AspectJ 遵循 Java 的规则,即接口上的注解不会被继承。
类上的 @Transactional 注解为该类中任何公共方法的执行指定了默认的事务语义。
类中方法上的 @Transactional 注解会覆盖类级别注解(如果存在)所指定的默认事务语义。任何可见性的方法都可以添加注解,包括私有方法。直接对非公共方法进行注解是为这类方法的执行获得事务边界控制的唯一方式。
从 Spring Framework 4.2 起,spring-aspects 提供了一个类似的切面,为标准的 javax.transaction.Transactional 注解提供了完全相同的功能。更多详情请参阅 JtaAnnotationTransactionAspect。 |
对于希望使用 Spring 的配置和事务管理支持,但不想(或不能)使用注解的 AspectJ 程序员来说,spring-aspects.jar 还包含了一些 abstract 切面,你可以通过继承这些切面来提供自己的切入点(pointcut)定义。有关更多信息,请参阅 AbstractBeanConfigurerAspect 和 AbstractTransactionAspect 切面的源代码。例如,以下代码片段展示了如何编写一个切面,通过使用与完全限定类名匹配的原型(prototype)bean 定义,来配置领域模型中定义的所有对象实例:
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
public DomainObjectConfiguration() {
setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
}
// the creation of a new bean (any object in the domain model)
protected pointcut beanCreation(Object beanInstance) :
initialization(new(..)) &&
CommonPointcuts.inDomainModel() &&
this(beanInstance);
}
5.10.3. 使用 Spring IoC 配置 AspectJ 切面
在 Spring 应用程序中使用 AspectJ 切面时,自然会希望并期望能够通过 Spring 来配置这些切面。AspectJ 运行时本身负责切面的创建,而通过 Spring 配置由 AspectJ 创建的切面的方式,则取决于该切面所使用的 AspectJ 实例化模型(即 per-xxx 子句)。
大多数 AspectJ 切面都是单例切面。这些切面的配置非常简单。您可以像平常一样创建一个引用切面类型的 bean 定义,并包含 factory-method="aspectOf" bean 属性。这样可确保 Spring 通过向 AspectJ 请求来获取切面实例,而不是尝试自行创建实例。以下示例展示了如何使用 factory-method="aspectOf" 属性:
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf"> (1)
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
| 1 | 注意 factory-method="aspectOf" 属性 |
非单例的切面更难配置。然而,可以通过创建原型(prototype)bean定义,并使用 @Configurable 中的 spring-aspects.jar 支持,在 AspectJ 运行时创建切面实例后对其进行配置。
如果你有一些 @AspectJ 切面希望使用 AspectJ 进行织入(例如,对领域模型类型使用加载时织入),同时还有其他 @AspectJ 切面希望与 Spring AOP 一起使用,并且所有这些切面都在 Spring 中进行了配置,那么你需要告知 Spring AOP 的 @AspectJ 自动代理支持机制:在配置中定义的 @AspectJ 切面中,究竟哪一部分子集应当用于自动代理。你可以在 <include/> 声明内部使用一个或多个 <aop:aspectj-autoproxy/> 元素来实现这一点。每个 <include/> 元素指定一个名称模式,只有至少匹配其中一个模式的 bean 才会被用于 Spring AOP 的自动代理配置。以下示例展示了如何使用 <include/> 元素:
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被 <aop:aspectj-autoproxy/> 元素的名称所误导。使用它会创建 Spring AOP 代理。此处采用的是 @AspectJ 风格的切面声明方式,但并未涉及 AspectJ 运行时。 |
5.10.4. 在 Spring Framework 中使用 AspectJ 进行加载时织入
加载时织入(Load-time weaving,LTW)是指在应用程序的类文件被加载到 Java 虚拟机(JVM)时,将 AspectJ 切面织入这些类文件的过程。 本节重点介绍在 Spring 框架特定上下文中配置和使用 LTW。本节并非对 LTW 的一般性介绍。有关 LTW 的详细信息,以及仅使用 AspectJ(完全不涉及 Spring)来配置 LTW 的具体方法,请参阅 AspectJ 开发环境指南中的 LTW 章节。
Spring Framework 为 AspectJ LTW(加载时织入)带来的价值在于能够对织入过程实现更细粒度的控制。“原生”的 AspectJ LTW 通过使用一个 Java(5+)代理来实现,该代理在启动 JVM 时通过指定一个虚拟机参数来启用。因此,这是一种 JVM 范围内的全局设置,在某些情况下可能适用,但通常粒度过于粗糙。而 Spring 支持的 LTW 允许你以每个 ClassLoader 为基础来启用 LTW,这种控制更加精细,在“单 JVM 多应用”环境(例如典型的应用服务器环境)中也更为合理。
此外,在某些环境中,这种支持使得无需修改应用服务器的启动脚本(该脚本通常需要添加 -javaagent:path/to/aspectjweaver.jar 或(如本节稍后所述)-javaagent:path/to/spring-instrument.jar)即可启用加载时织入(load-time weaving)。开发者通过配置应用程序上下文来启用加载时织入,而无需依赖通常负责部署配置(例如启动脚本)的管理员。
现在推销部分已经结束,让我们首先通过一个使用 Spring 的 AspectJ 加载时织入(LTW)的快速示例,然后详细介绍该示例中引入的各个元素。完整的示例,请参见Petclinic 示例应用程序。
第一个示例
假设你是一名应用程序开发人员,任务是诊断系统中某些性能问题的根本原因。我们不会立即使用专业的性能分析工具,而是先启用一个简单的性能监控切面(profiling aspect),以便快速获取一些性能指标。随后,我们可以立即在该特定区域应用更细粒度的性能分析工具。
此处所示的示例使用 XML 配置。您也可以通过Java 配置来配置和使用 @AspectJ。具体来说,您可以使用@EnableLoadTimeWeaving注解作为<context:load-time-weaver/>的替代方案(详情请参见下文)。 |
以下示例展示了该性能分析切面,它并不复杂。 这是一个基于时间的性能分析器,使用了 @AspectJ 风格的切面声明方式:
package foo;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;
@Aspect
public class ProfilingAspect {
@Around("methodsToBeProfiled()")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
StopWatch sw = new StopWatch(getClass().getSimpleName());
try {
sw.start(pjp.getSignature().getName());
return pjp.proceed();
} finally {
sw.stop();
System.out.println(sw.prettyPrint());
}
}
@Pointcut("execution(public * foo..*.*(..))")
public void methodsToBeProfiled(){}
}
package foo
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Pointcut
import org.springframework.util.StopWatch
import org.springframework.core.annotation.Order
@Aspect
class ProfilingAspect {
@Around("methodsToBeProfiled()")
fun profile(pjp: ProceedingJoinPoint): Any {
val sw = StopWatch(javaClass.simpleName)
try {
sw.start(pjp.getSignature().getName())
return pjp.proceed()
} finally {
sw.stop()
println(sw.prettyPrint())
}
}
@Pointcut("execution(public * foo..*.*(..))")
fun methodsToBeProfiled() {
}
}
我们还需要创建一个 META-INF/aop.xml 文件,以告知 AspectJ 编织器(weaver)我们要将 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(稍后会详细解释)。这个加载时织入器(load-time weaver)是核心组件,负责将一个或多个 META-INF/aop.xml 文件中的切面配置织入到应用程序的类中。好消息是,它不需要大量配置(尽管你还可以指定一些其他选项,但这些内容将在后面详细介绍),如下例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- a service object; we will be profiling its methods -->
<bean id="entitlementCalculationService"
class="foo.StubEntitlementCalculationService"/>
<!-- this switches on the load-time weaving -->
<context:load-time-weaver/>
</beans>
现在,所有必需的组件(切面、META-INF/aop.xml 文件以及 Spring 配置)都已就位,我们可以创建以下带有 main(..) 方法的驱动类,以演示运行时织入(LTW)的实际效果:
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
(EntitlementCalculationService) ctx.getBean("entitlementCalculationService");
// the profiling aspect is 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
package foo
import org.springframework.context.support.ClassPathXmlApplicationContext
fun main() {
val ctx = ClassPathXmlApplicationContext("beans.xml")
val entitlementCalculationService = ctx.getBean("entitlementCalculationService") as EntitlementCalculationService
// the profiling aspect is 'woven' around this method execution
entitlementCalculationService.calculateEntitlement()
}
我们还有一件事要做。本节的引言确实提到,可以使用 Spring 按 ClassLoader 粒度选择性地启用 LTW(加载时织入),这是正确的。
然而,在本示例中,我们使用一个 Java 代理(由 Spring 提供)来启用 LTW。
我们使用以下命令运行前面所示的 Main 类:
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main
-javaagent 是一个用于指定和启用代理(agents)以对在 JVM 上运行的程序进行字节码增强(instrumentation)的标志。Spring 框架自带了这样一个代理,即 InstrumentationSavingAgent,它被打包在 spring-instrument.jar 中,如前例所示,该 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 Bean进行通知。下面对Main程序稍作修改,即可得到相同的结果:
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
new StubEntitlementCalculationService();
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
package foo
import org.springframework.context.support.ClassPathXmlApplicationContext
fun main(args: Array<String>) {
ClassPathXmlApplicationContext("beans.xml")
val entitlementCalculationService = StubEntitlementCalculationService()
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement()
}
请注意,在前面的程序中,我们引导启动了 Spring 容器,然后完全在 Spring 上下文之外创建了一个 StubEntitlementCalculationService 的新实例。然而,性能分析通知(profiling advice)仍然被织入其中。
诚然,这个示例过于简单。然而,Spring 中 LTW(加载时织入)支持的基本要素已在前面的示例中全部介绍过,本节其余部分将详细解释每项配置和用法背后的“原因”。
本示例中使用的 ProfilingAspect 可能比较简单,但却非常实用。这是一个很好的开发期切面示例,开发者可以在开发过程中使用它,然后在构建部署到 UAT(用户验收测试)或生产环境的应用程序时轻松地将其排除。 |
切面
你在LTW(加载时织入)中使用的切面必须是AspectJ切面。你可以直接使用AspectJ语言编写这些切面,也可以采用@AspectJ风格来编写。这样编写的切面既是有效的AspectJ切面,也是有效的Spring AOP切面。 此外,编译后的切面类需要在类路径(classpath)中可用。
'META-INF/aop.xml'
AspectJ LTW 基础设施通过使用一个或多个位于 Java 类路径上的 META-INF/aop.xml 文件进行配置(这些文件可直接位于类路径中,但更常见的是打包在 JAR 文件中)。
该文件的结构和内容在AspectJ 参考文档的 LTW(加载时织入)部分中有详细说明。由于 aop.xml 文件完全是 AspectJ 的内容,因此我们在此不再赘述。
所需库(JARS)
要使用 Spring 框架对 AspectJ LTW(加载时织入)的支持,至少需要以下库:
-
spring-aop.jar -
aspectjweaver.jar
如果你使用Spring 提供的代理来启用 instrumentation,你还需要:
-
spring-instrument.jar
Spring 配置
Spring 对加载时织入(LTW)支持的关键组件是 LoadTimeWeaver 接口(位于 org.springframework.instrument.classloading 包中),以及 Spring 发行版中附带的多种该接口的实现。一个 LoadTimeWeaver 负责在运行时向 java.lang.instrument.ClassFileTransformers 添加一个或多个 ClassLoader,从而为各种有趣的应用打开大门,其中之一便是切面的加载时织入(LTW)。
如果你不熟悉运行时类文件转换的概念,请在继续阅读之前先查看 java.lang.instrument 包的 Javadoc API 文档。
尽管该文档并不全面,但至少你可以看到关键的接口和类(供你在阅读本节内容时参考)。 |
为特定的LoadTimeWeaver配置一个ApplicationContext可能只需添加一行代码即可。(请注意,您几乎肯定需要使用ApplicationContext作为您的Spring容器——通常,仅使用BeanFactory是不够的,因为LTW支持依赖于BeanFactoryPostProcessors。)
要启用 Spring 框架的加载时织入(LTW)支持,您需要配置一个 LoadTimeWeaver,
通常通过使用 @EnableLoadTimeWeaving 注解来完成,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
@Configuration
@EnableLoadTimeWeaving
class AppConfig {
}
或者,如果你更喜欢基于 XML 的配置,请使用
<context:load-time-weaver/> 元素。请注意,该元素定义在
context 命名空间中。以下示例展示了如何使用 <context:load-time-weaver/>:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:load-time-weaver/>
</beans>
上述配置会自动为您定义并注册多个与LTW(加载时织入)相关的基础设施Bean,例如 LoadTimeWeaver 和 AspectJWeavingEnabler。
默认的 LoadTimeWeaver 是 DefaultContextLoadTimeWeaver 类,它会尝试对自动检测到的 LoadTimeWeaver 进行封装。
所“自动检测”到的 LoadTimeWeaver 的具体类型取决于您的运行环境。
下表总结了各种 LoadTimeWeaver 的实现:
| 运行环境 | LoadTimeWeaver 的实现 |
|---|---|
在 Apache Tomcat 中运行 |
|
在 GlassFish 中运行(仅限 EAR 部署) |
|
|
|
在 IBM 的 WebSphere 中运行 |
|
在 Oracle 的 WebLogic 中运行 |
|
JVM 使用 Spring |
|
回退机制,期望底层的 ClassLoader 遵循通用约定
(即包含 |
|
请注意,该表格仅列出在使用 LoadTimeWeavers 时自动检测到的 DefaultContextLoadTimeWeaver。您可以明确指定要使用的 LoadTimeWeaver 实现。
要通过 Java 配置指定特定的 LoadTimeWeaver,请实现
LoadTimeWeavingConfigurer 接口并重写 getLoadTimeWeaver() 方法。
以下示例指定了一个 ReflectiveLoadTimeWeaver:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
@Configuration
@EnableLoadTimeWeaving
class AppConfig : LoadTimeWeavingConfigurer {
override fun getLoadTimeWeaver(): LoadTimeWeaver {
return ReflectiveLoadTimeWeaver()
}
}
如果你使用基于 XML 的配置,可以将完全限定类名指定为 weaver-class 元素的 <context:load-time-weaver/> 属性值。同样,以下示例指定了一个 ReflectiveLoadTimeWeaver:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>
通过配置定义并注册的 LoadTimeWeaver,之后可以通过众所周知的名称 loadTimeWeaver 从 Spring 容器中获取。
请记住,LoadTimeWeaver 仅作为 Spring 的 LTW(加载时织入)基础设施用于添加一个或多个 ClassFileTransformers 的机制而存在。实际执行 LTW 的 ClassFileTransformer 是 ClassPreProcessorAgentAdapter 类(位于 org.aspectj.weaver.loadtime 包中)。有关织入具体如何实现的更多细节,请参阅 ClassPreProcessorAgentAdapter 类的类级别 Javadoc,因为这些细节超出了本文档的范围。
还有一个配置属性尚未讨论:aspectjWeaving 属性(如果你使用 XML,则为 aspectj-weaving)。该属性用于控制是否启用 LTW(加载时织入)。它可接受三个可能的值之一,如果未指定该属性,则默认值为 autodetect。下表总结了这三个可能的值:
| 注解值 | XML 值 | 说明 |
|---|---|---|
|
|
AspectJ 编织已启用,切面会在适当的时候进行加载时编织。 |
|
|
LTW 已关闭。没有切面在加载时被织入。 |
|
|
如果 Spring LTW 基础设施能够找到至少一个 |
环境特定配置
最后一节包含在应用服务器和Web容器等环境中使用Spring的LTW(加载时织入)支持时所需的任何额外设置和配置。
Tomcat, JBoss, WebSphere, WebLogic
Tomcat、JBoss/WildFly、IBM WebSphere Application Server 和 Oracle WebLogic Server 都提供了一个通用的应用程序 ClassLoader,该 ClassLoader 能够进行本地 instrumentation。Spring 的原生 LTW(加载时织入)可以利用这些 ClassLoader 实现来提供 AspectJ 织入。
您可以按照前面所述简单地启用加载时织入。
具体来说,您无需修改 JVM 启动脚本以添加
-javaagent:path/to/spring-instrument.jar。
请注意,在 JBoss 上,您可能需要禁用应用服务器的扫描功能,以防止它在应用程序实际启动之前加载这些类。一个快速的解决方法是在您的构件中添加一个名为 WEB-INF/jboss-scanning.xml 的文件,其内容如下:
<scanning xmlns="urn:jboss:scanning:1.0"/>
通用 Java 应用程序
当在特定 LoadTimeWeaver 实现不支持的环境中需要类织入(class instrumentation)时,JVM 代理(agent)是一种通用解决方案。
针对此类情况,Spring 提供了 InstrumentationLoadTimeWeaver,它需要一个 Spring 特定(但非常通用)的 JVM 代理 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 官方网站 上找到。
Eclipse AspectJ(作者:Adrian Colyer 等,Addison-Wesley,2005 年)为 AspectJ 语言提供了全面的介绍和参考。
AspectJ实战(第二版),作者 Ramnivas Laddad(Manning 出版社,2009 年)备受推崇。本书重点介绍 AspectJ,但也深入探讨了许多通用的 AOP 主题。
6. Spring AOP API
上一章介绍了 Spring 对使用 @AspectJ 注解和基于 schema 的切面定义所提供的 AOP 支持。在本章中,我们将讨论 Spring AOP 的底层 API。对于常规应用程序,我们推荐使用上一章所述的、结合 AspectJ 切点表达式的 Spring AOP。
6.1. Spring 中的切点 API
本节介绍 Spring 如何处理关键的切入点(pointcut)概念。
6.1.1. 概念
Spring 的切入点模型支持独立于通知类型的切入点复用。你可以使用相同的切入点来应用不同类型的通知。
org.springframework.aop.Pointcut 接口是核心接口,用于将通知(advice)定向到特定的类和方法。该接口的完整定义如下:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
interface Pointcut {
fun getClassFilter(): ClassFilter
fun getMethodMatcher(): MethodMatcher
}
将 Pointcut 接口拆分为两个部分,可以复用类匹配和方法匹配的部分,并支持细粒度的组合操作(例如与另一个方法匹配器执行“并集”操作)。
ClassFilter 接口用于将切入点限制在给定的目标类集合中。如果 matches() 方法始终返回 true,则匹配所有目标类。以下代码清单展示了 ClassFilter 接口的定义:
public interface ClassFilter {
boolean matches(Class clazz);
}
interface ClassFilter {
fun matches(clazz: Class<*>): Boolean
}
MethodMatcher 接口通常更为重要。完整的接口如下:
public interface MethodMatcher {
boolean matches(Method m, Class targetClass);
boolean isRuntime();
boolean matches(Method m, Class targetClass, Object[] args);
}
interface MethodMatcher {
val isRuntime: Boolean
fun matches(m: Method, targetClass: Class<*>): Boolean
fun matches(m: Method, targetClass: Class<*>, args: Array<Any>): Boolean
}
matches(Method, Class) 方法用于测试该切入点(pointcut)是否曾匹配目标类上的某个给定方法。此评估可在创建 AOP 代理时执行,从而避免在每次方法调用时都进行测试。如果针对某个给定方法,双参数的 matches 方法返回 true,并且该 MethodMatcher 的 isRuntime() 方法也返回 true,那么每次方法调用时都会调用三参数的 matches 方法。这使得切入点可以在目标通知(advice)开始执行前,立即检查传递给方法调用的参数。
大多数 MethodMatcher 实现都是静态的,这意味着它们的 isRuntime() 方法返回 false。在这种情况下,三个参数的 matches 方法永远不会被调用。
| 如果可能,请尽量将切入点(pointcut)设为静态的,以便在创建 AOP 代理时,AOP 框架能够缓存切入点评估的结果。 |
6.1.2. 对切点的操作
Spring 支持对切入点(pointcut)进行操作(特别是并集和交集)。
联合(Union)表示匹配任一切点的方法。
交集(Intersection)表示同时匹配两个切点的方法。
通常,联合更有用。
你可以通过使用 org.springframework.aop.support.Pointcuts 类中的静态方法,或者使用同一包中的 ComposablePointcut 类来组合切点。然而,使用 AspectJ 切点表达式通常是更简单的方法。
6.1.3. AspectJ 表达式切点
从 2.0 版本开始,Spring 使用的最重要的一类切入点(pointcut)是
org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一种使用 AspectJ 提供的库来解析 AspectJ 切入点表达式字符串的切入点。
有关支持的 AspectJ 切入点原语的讨论,请参见上一章。
6.1.4. 便捷切点实现
Spring 提供了多种便捷的切入点(pointcut)实现。其中一些可直接使用;另一些则旨在被子类化,以创建特定于应用程序的切入点。
静态切点
静态切入点基于方法和目标类,无法考虑方法的参数。在大多数使用场景中,静态切入点已足够,并且是最佳选择。 Spring 仅在方法首次被调用时对静态切入点进行一次评估。 此后,在每次方法调用时就无需再次评估该切入点。
本节其余部分将介绍 Spring 自带的一些静态切入点(static pointcut)实现。
正则表达式切点
指定静态切入点的一种显而易见的方法是使用正则表达式。除了 Spring 之外,还有多个 AOP 框架也支持这种方式。
org.springframework.aop.support.JdkRegexpMethodPointcut 是一个通用的正则表达式切入点,它利用了 JDK 中提供的正则表达式支持。
使用 JdkRegexpMethodPointcut 类,您可以提供一组模式字符串。
只要其中任意一个模式匹配成功,该切入点(pointcut)就会评估为 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 同时封装了切入点(pointcut)和通知(advice),如下例所示:
<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 的控制流切入点(control flow pointcut)在概念上类似于 AspectJ 的 cflow 切入点,但功能较弱。(目前无法指定某个切入点在另一个切入点所匹配的连接点之下运行。)控制流切入点会匹配当前的调用栈。例如,如果连接点是由 com.mycompany.web 包中的某个方法或由 SomeCaller 类所调用的,该切入点就可能被触发。控制流切入点通过使用 org.springframework.aop.support.ControlFlowPointcut 类来指定。
| 控制流切入点(control flow pointcuts)在运行时的评估开销显著高于其他动态切入点,即使与其他动态切入点相比也是如此。在 Java 1.4 中,其开销大约是其他动态切入点的五倍。 |
6.1.5. 切点超类
Spring 提供了有用的切入点超类,以帮助您实现自己的切入点。
由于静态切入点最为有用,你很可能应该继承
StaticMethodMatcherPointcut 类。这仅需实现一个
抽象方法(尽管你可以重写其他方法以自定义行为)。以下示例展示了如何继承 StaticMethodMatcherPointcut:
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}
class TestStaticPointcut : StaticMethodMatcherPointcut() {
override fun matches(method: Method, targetClass: Class<*>): Boolean {
// return true if custom criteria match
}
}
此外,动态切入点也有对应的超类。 您可以将自定义切入点与任何类型的通知一起使用。
6.2. Spring 中的通知 API
现在我们可以研究 Spring AOP 是如何处理通知(advice)的。
6.2.1. 通知的生命周期
每条通知(advice)都是一个 Spring bean。一个通知实例可以被所有被通知的对象共享,也可以对每个被通知的对象保持唯一。这分别对应于按类(per-class)或按实例(per-instance)的通知。
最常用的是基于类的增强(advice)。它适用于通用型增强,例如事务增强器(transaction advisors)。这类增强不依赖于被代理对象的状态,也不会添加新的状态,而仅作用于方法及其参数。
每实例的增强(advice)适用于引入(introductions),以支持混入(mixins)。在这种情况下,增强会为被代理对象添加状态。
你可以在同一个 AOP 代理中混合使用共享的和每个实例的增强(advice)。
6.2.2. Spring 中的通知类型
Spring 提供了多种通知(advice)类型,并且可扩展以支持任意的通知类型。本节将介绍基本概念和标准的通知类型。
环绕拦截通知
Spring 中最基本的通知类型是环绕通知。
Spring 遵循 AOP Alliance 接口规范,用于实现基于方法拦截的环绕通知(around advice)。实现 MethodInterceptor 并提供环绕通知的类还应实现以下接口:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
interface MethodInterceptor : Interceptor {
fun invoke(invocation: MethodInvocation) : Any
}
传递给 MethodInvocation 方法的 invoke() 参数暴露了正在被调用的方法、目标连接点、AOP 代理以及该方法的参数。invoke() 方法应返回本次调用的结果:即连接点的返回值。
以下示例展示了一个简单的 MethodInterceptor 实现:
public class DebugInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
System.out.println("Invocation returned");
return rval;
}
}
class DebugInterceptor : MethodInterceptor {
override fun invoke(invocation: MethodInvocation): Any {
println("Before: invocation=[$invocation]")
val rval = invocation.proceed()
println("Invocation returned")
return rval
}
}
注意对 proceed() 的 MethodInvocation 方法的调用。该调用会沿着拦截器链继续向下执行,直至到达连接点(join point)。大多数拦截器都会调用此方法并返回其返回值。然而,MethodInterceptor 与任何环绕通知(around advice)一样,也可以选择返回一个不同的值,或者抛出异常,而不调用 proceed 方法。不过,除非有充分的理由,否则你不应这样做。
MethodInterceptor 的实现提供了与其他符合 AOP Alliance 规范的 AOP 实现之间的互操作性。本节其余部分讨论的其他通知类型实现了通用的 AOP 概念,但采用了 Spring 特有的方式。尽管使用最具体的通知类型具有一定优势,但如果你希望将来在其他 AOP 框架中运行该切面,则应坚持使用基于 MethodInterceptor 的环绕通知。请注意,目前各框架之间的切入点(pointcut)并不具备互操作性,而且 AOP Alliance 目前也未定义切入点接口。 |
前置通知
一种更简单的通知类型是前置通知。它不需要 MethodInvocation 对象,因为它仅在进入方法之前被调用。
前置通知(before advice)的主要优点是无需调用 proceed() 方法,因此也就不会意外地忘记继续执行拦截器链。
以下代码清单展示了 MethodBeforeAdvice 接口:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
interface MethodBeforeAdvice : BeforeAdvice {
fun before(m: Method, args: Array<Any>, target: Any)
}
(Spring 的 API 设计虽然允许在字段访问前加入通知(before advice),但通常的对象适用于字段拦截,而且 Spring 很可能永远不会实现这一功能。)
请注意,返回类型为 void。前置通知(before advice)可以在连接点执行之前插入自定义行为,但不能更改返回值。如果前置通知抛出异常,它将停止拦截器链的进一步执行。该异常会沿着拦截器链向上传播。如果该异常是未检查异常(unchecked exception),或者出现在被调用方法的签名中,则会直接传递给客户端;否则,AOP 代理会将其包装在一个未检查异常中。
以下示例展示了 Spring 中的一个前置通知(before advice),用于统计所有方法调用次数:
public class CountingBeforeAdvice implements MethodBeforeAdvice {
private int count;
public void before(Method m, Object[] args, Object target) throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
class CountingBeforeAdvice : MethodBeforeAdvice {
var count: Int = 0
override fun before(m: Method, args: Array<Any>, target: Any?) {
++count
}
}
| 前置通知(Before advice)可以与任意切入点(pointcut)一起使用。 |
抛出建议
异常通知(Throws advice)在连接点抛出异常后、连接点返回时被调用。Spring 提供了类型化的异常通知。请注意,这意味着 org.springframework.aop.ThrowsAdvice 接口不包含任何方法。它是一个标记接口,用于标识给定对象实现了一个或多个类型化的异常通知方法。这些方法应采用以下形式:
afterThrowing([Method, args, target], subclassOfThrowable)
仅最后一个参数是必需的。方法签名可以包含一个或四个参数,具体取决于通知方法是否需要访问目标方法及其参数。接下来的两个代码清单展示了作为异常通知示例的类。
如果抛出 RemoteException(包括其子类)时,将调用以下通知:
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}
class RemoteThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Do something with remote exception
}
}
与前面的
通知不同,下面的示例声明了四个参数,因此它可以访问被调用的方法、方法参数和目标对象。当下列通知在抛出 ServletException 时会被调用:
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
class ServletThrowsAdviceWithArguments : ThrowsAdvice {
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Do something with all arguments
}
}
最后一个示例说明了如何在单个类中同时使用这两种方法来处理 RemoteException 和 ServletException。任意数量的异常通知(throws advice)方法都可以组合在同一个类中。以下代码清单展示了最后一个示例:
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
class CombinedThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Do something with remote exception
}
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Do something with all arguments
}
}
| 如果一个 throws-advice 方法自身抛出异常,它会覆盖原始异常(即,将抛给用户的异常进行替换)。被覆盖后的异常通常是一个 RuntimeException,因为它与任何方法签名都兼容。然而,如果一个 throws-advice 方法抛出了一个受检异常(checked exception),则该异常必须与目标方法声明的异常相匹配,因此在某种程度上会与特定的目标方法签名耦合。切勿抛出与目标方法签名不兼容的未声明受检异常! |
| 异常通知(Throws advice)可以与任意切入点(pointcut)一起使用。 |
返回后通知
Spring 中的后置返回通知(after returning advice)必须实现
org.springframework.aop.AfterReturningAdvice 接口,如下所示:
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
interface AfterReturningAdvice : Advice {
fun afterReturning(returnValue: Any, m: Method, args: Array<Any>, target: Any)
}
后置返回通知可以访问返回值(但不能修改)、被调用的方法、方法的参数以及目标对象。
以下的后置返回通知(after returning advice)会统计所有未抛出异常的成功方法调用次数:
public class CountingAfterReturningAdvice implements AfterReturningAdvice {
private int count;
public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
class CountingAfterReturningAdvice : AfterReturningAdvice {
var count: Int = 0
private set
override fun afterReturning(returnValue: Any?, m: Method, args: Array<Any>, target: Any?) {
++count
}
}
该通知不会改变执行路径。如果它抛出异常,则会将该异常沿拦截器链向上抛出,而不是返回一个返回值。
| 后置返回通知可以与任何切入点一起使用。 |
介绍建议
Spring 将引入型通知(introduction advice)视为一种特殊的拦截通知(interception advice)。
引入(Introduction)需要一个 IntroductionAdvisor 和一个实现以下接口的 IntroductionInterceptor:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
interface IntroductionInterceptor : MethodInterceptor {
fun implementsInterface(intf: Class<*>): Boolean
}
从 AOP Alliance 的 invoke() 接口继承而来的 MethodInterceptor 方法必须实现引介(introduction)。也就是说,如果被调用的方法属于一个被引介的接口,则该引介拦截器负责处理该方法调用——它不能调用 proceed()。
引介(Introduction)通知不能与任意切入点(pointcut)一起使用,因为它仅作用于类级别,而非方法级别。你只能将引介通知与IntroductionAdvisor一起使用,该接口包含以下方法:
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class<?>[] getInterfaces();
}
interface IntroductionAdvisor : Advisor, IntroductionInfo {
val classFilter: ClassFilter
@Throws(IllegalArgumentException::class)
fun validateInterfaces()
}
interface IntroductionInfo {
val interfaces: Array<Class<*>>
}
没有与引入型通知(introduction advice)关联的 MethodMatcher,因此也没有 Pointcut。仅类过滤是合理的。
getInterfaces() 方法返回此通知器所引入的接口。
validateInterfaces() 方法在内部用于检查所引入的接口是否可以由配置的 IntroductionInterceptor 实现。
考虑一个来自 Spring 测试套件的示例,假设我们希望向一个或多个对象引入以下接口:
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
interface Lockable {
fun lock()
fun unlock()
fun locked(): Boolean
}
这展示了一个混入(mixin)示例。我们希望无论被通知对象的类型是什么,都能将其转换为 Lockable 类型,并调用 lock 和 unlock 方法。如果我们调用了 lock() 方法,那么所有 setter 方法都应抛出一个 LockedException 异常。因此,我们可以添加一个切面(aspect),在对象本身对此一无所知的情况下,赋予它们不可变的能力:这是面向切面编程(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 可以被用于引入(introduction),因此我们无需显式指定这一点。通过这种方式,我们可以引入任意数量的接口。
注意对 locked 实例变量的使用。这实际上为目标对象中已有的状态额外添加了新的状态。
以下示例展示了 LockMixin 类的示例:
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
private boolean locked;
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public boolean locked() {
return this.locked;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
throw new LockedException();
}
return super.invoke(invocation);
}
}
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 类:
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)
我们可以非常简单地应用这个通知器(advisor),因为它不需要任何配置。(然而,不使用 IntroductionInterceptor 就无法使用 IntroductionAdvisor。)与通常的引入(introduction)一样,该通知器必须是每个实例独立的,因为它是有状态的。对于每个被通知的对象,我们都需要一个不同的 LockMixinAdvisor 实例,因此也需要一个不同的 LockMixin 实例。该通知器构成了被通知对象状态的一部分。
我们可以通过编程方式使用 Advised.addAdvisor() 方法来应用此通知器,或者(推荐的方式)在 XML 配置中像配置其他通知器一样进行配置。下面讨论的所有代理创建选项,包括“自动代理创建器”,都能正确处理引入(introductions)和有状态的混入(stateful mixins)。
6.3. Spring 中的 Advisor API
在 Spring 中,Advisor 是一种切面,仅包含一个与切入点表达式关联的通知对象。
除了引入(introductions)这一特殊情况外,任何通知器(advisor)都可以与任何通知(advice)一起使用。
org.springframework.aop.support.DefaultPointcutAdvisor 是最常用的通知器类。它可以与 MethodInterceptor、BeforeAdvice 或
ThrowsAdvice 一起使用。
在 Spring 中,可以在同一个 AOP 代理中混合使用通知器(advisor)和通知(advice)类型。例如,你可以在一个代理配置中同时使用环绕通知(around advice)、异常通知(throws advice)和前置通知(before advice)。Spring 会自动创建所需的拦截器链。
6.4. 使用ProxyFactoryBean创建 AOP 代理
如果你为你的业务对象使用 Spring IoC 容器(ApplicationContext 或 BeanFactory)(而且你应该这样做!),那么你应当使用 Spring AOP 的某个 FactoryBean 实现。(请记住,工厂 bean 引入了一层间接性,使其能够创建不同类型的对象。)
| Spring AOP 支持在底层也使用了工厂 Bean。 |
在 Spring 中创建 AOP 代理的基本方法是使用
org.springframework.aop.framework.ProxyFactoryBean。这可以完全控制
切入点(pointcuts)、所应用的通知(advice)以及它们的执行顺序。然而,如果你不需要如此精细的控制,
还有更简单的选项可供选择。
6.4.1. 基础知识
ProxyFactoryBean 与其他 Spring FactoryBean 实现一样,引入了一层间接性。如果你定义了一个名为 ProxyFactoryBean 的 foo,那么引用 foo 的对象并不会看到 ProxyFactoryBean 实例本身,而是看到由 getObject() 中 ProxyFactoryBean 方法实现所创建的对象。该方法会创建一个 AOP 代理,用以包装目标对象。
使用 ProxyFactoryBean 或其他感知 IoC 的类来创建 AOP 代理,其中一个最重要的好处是通知(advice)和切入点(pointcut)也可以由 IoC 容器进行管理。这是一个强大的特性,使得某些在其他 AOP 框架中难以实现的方法成为可能。例如,一个通知本身可以引用应用程序对象(除了目标对象——这在任何 AOP 框架中都应该是可用的),从而充分利用依赖注入所提供的全部可插拔性。
6.4.2. JavaBean 属性
与 Spring 提供的大多数 FactoryBean 实现一样,ProxyFactoryBean 类本身也是一个 JavaBean。它的属性用于:
-
指定您要代理的目标。
-
指定是否使用 CGLIB(稍后将进行说明,另请参阅基于 JDK 和 CGLIB 的代理)。
某些关键属性继承自 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:一个String类型的接口名称数组。如果未提供此参数,则将为目标类使用 CGLIB 代理(另请参阅基于 JDK 和 CGLIB 的代理)。 -
interceptorNames:一个String类型的数组,包含要应用的Advisor、拦截器或其他通知(advice)的名称。顺序很重要,遵循先到先服务的原则。也就是说,列表中的第一个拦截器将最先有机会拦截方法调用。这些名称是当前工厂中的 bean 名称,包括从父级工厂继承而来的 bean 名称。此处不能引用 bean 引用,因为这样做会导致
ProxyFactoryBean忽略通知(advice)的单例(singleton)设置。您可以在拦截器名称后附加一个星号(
*)。这样会将所有名称以星号前部分开头的 Advisor Bean 应用到目标上。您可以在使用“全局”Advisor一节中找到使用此功能的示例。 -
singleton:无论
getObject()方法被调用多少次,工厂是否都应返回同一个对象。多个FactoryBean实现提供了此属性。默认值为true。如果你希望使用有状态的增强(advice)——例如,用于有状态的混入(mixins)——请将原型(prototype)增强与false的 singleton 值一起使用。
6.4.3. 基于 JDK 和 CGLIB 的代理
本节作为关于ProxyFactoryBean如何为特定目标对象(即需要被代理的对象)选择创建基于JDK的代理还是基于CGLIB的代理的权威文档。
ProxyFactoryBean 在创建基于 JDK 或 CGLIB 的代理方面的行为,在 Spring 1.2.x 版本和 2.0 版本之间发生了变化。ProxyFactoryBean 现在在自动检测接口方面的语义与 TransactionProxyFactoryBean 类的行为类似。 |
如果要被代理的目标对象的类(以下简称目标类)未实现任何接口,则会创建一个基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,而没有接口就意味着根本无法使用 JDK 进行代理。您可以通过设置 interceptorNames 属性来注入目标 bean 并指定拦截器列表。请注意,即使将 proxyTargetClass 的 ProxyFactoryBean 属性设置为 false,也会创建基于 CGLIB 的代理。(这样做毫无意义,最好从 bean 定义中移除该设置,因为它至多是多余的,最坏的情况下还会引起混淆。)
如果目标类实现了一个(或多个)接口,则所创建的代理类型取决于 ProxyFactoryBean 的配置。
如果 proxyTargetClass 的 ProxyFactoryBean 属性被设置为 true,
则会创建一个基于 CGLIB 的代理。这种行为是合理的,并且符合“最小意外原则”(principle of least surprise)。
即使 proxyInterfaces 的 ProxyFactoryBean 属性已被设置为一个或多个完全限定的接口名称,
但由于 proxyTargetClass 属性被设为 true,仍然会启用基于 CGLIB 的代理。
如果 proxyInterfaces 的 ProxyFactoryBean 属性被设置为一个或多个
完全限定的接口名称,则会创建一个基于 JDK 的代理。所创建的
代理将实现 proxyInterfaces 属性中指定的所有接口。
如果目标类恰好实现了比 proxyInterfaces 属性中指定的更多的接口,
这完全没有问题,但这些额外的接口不会被返回的代理所实现。
如果未设置 proxyInterfaces 的 ProxyFactoryBean 属性,但目标类确实实现了一个(或多个)接口,则 ProxyFactoryBean 会自动检测到目标类实际上至少实现了一个接口,并创建一个基于 JDK 的代理。实际被代理的接口是目标类所实现的所有接口。实际上,这等同于将目标类实现的每一个接口都显式提供给 proxyInterfaces 属性。然而,这种方式工作量显著减少,且不易出现拼写错误。
6.4.4. 代理接口
考虑一个正在运行的简单 ProxyFactoryBean 示例。该示例包含:
-
一个被代理的目标 bean。这是示例中的
personTargetbean 定义。 -
一个用于提供通知的
Advisor和一个Interceptor。 -
一个 AOP 代理 bean 定义,用于指定目标对象(
personTargetbean)、要代理的接口以及要应用的通知(advices)。
以下列表展示了该示例:
<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 类型的列表,其中包含当前工厂中拦截器或通知器(advisor)的 bean 名称。您可以使用通知器(advisor)、拦截器(interceptor)、前置通知(before)、后置返回通知(after returning)以及异常抛出通知(throws advice)对象。通知器的顺序非常重要。
你可能会疑惑为什么这个列表不保存 bean 的引用。原因在于,如果 ProxyFactoryBean 的 singleton 属性被设置为 false,它就必须能够返回独立的代理实例。如果其中任意一个通知器(advisor)本身是原型(prototype)作用域的,那么就需要返回该通知器的一个独立实例,因此必须能够从工厂中获取该原型的一个新实例。仅仅持有引用是不够的。 |
前面所示的 person bean 定义可以用来替代 Person 的实现,如下所示:
Person person = (Person) factory.getBean("person");
val person = factory.getBean("person") as Person;
同一 IoC 容器中的其他 bean 可以像对待普通 Java 对象一样,对其声明强类型的依赖关系。以下示例展示了如何实现这一点:
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>
本例中的 PersonUser 类暴露了一个类型为 Person 的属性。就该类而言,AOP 代理可以透明地替代“真实”的 Person 实现。然而,其实际类将是一个动态代理类,因此可以将其转换(cast)为 Advised 接口(稍后讨论)。
你可以通过使用匿名内部 bean 来隐藏目标对象与代理之间的区别。只有 ProxyFactoryBean 的定义有所不同。此处包含通知(advice)仅为了完整性。以下示例展示了如何使用匿名内部 bean:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<!-- Use inner bean, not local reference to target -->
<property name="target">
<bean class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
使用匿名内部 bean 的优势在于,Person 类型的对象只有一个。如果我们希望防止应用程序上下文的用户获取未被代理(un-advised)对象的引用,或者需要避免与 Spring IoC 自动装配产生任何歧义,这一点非常有用。此外,可以说还有一个优势,即 ProxyFactoryBean 的定义是自包含的。然而,在某些情况下,能够从工厂中获取未被代理的目标对象实际上可能是一种优势(例如,在某些测试场景中)。
6.4.5. 代理类
如果你需要代理一个类,而不是一个或多个接口,该怎么办?
假设在我们之前的示例中,并不存在 Person 接口。我们需要对一个名为 Person 的类进行增强,而该类并未实现任何业务接口。在这种情况下,您可以配置 Spring 使用 CGLIB 代理,而不是动态代理。为此,请将前面所示的 proxyTargetClass 的 ProxyFactoryBean 属性设置为 true。尽管最好面向接口而非类进行编程,但在处理遗留代码时,能够对未实现接口的类进行增强的功能仍然非常有用。(总体而言,Spring 并不强制推行某种特定方式。虽然它使应用良好实践变得容易,但不会强制要求采用某种特定方法。)
如果需要,即使存在接口,你也可以强制在任何情况下使用 CGLIB。
CGLIB 代理通过在运行时生成目标类的子类来实现。Spring 将此生成的子类配置为将方法调用委托给原始目标对象。该子类用于实现装饰器(Decorator)模式,将通知(advice)织入其中。
CGLIB 代理对用户来说通常是透明的。然而,有一些问题需要注意:
-
Final方法无法被通知(advised),因为它们不能被重写。 -
无需将 CGLIB 添加到您的 classpath 中。从 Spring 3.2 开始,CGLIB 已被重新打包并包含在 spring-core JAR 文件中。换句话说,基于 CGLIB 的 AOP 可以“开箱即用”,JDK 动态代理也是如此。
CGLIB 代理与动态代理之间的性能差异很小。 在这种情况下,性能不应该是决定性的考量因素。
6.4.6. 使用“全局”通知器(Advisors)
通过在拦截器名称后附加一个星号(*),所有 bean 名称与星号前部分匹配的增强器(advisor)都会被添加到增强器链中。当你需要添加一组标准的“全局”增强器时,这一功能非常有用。以下示例定义了两个全局增强器:
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
6.5. 简洁的代理定义
特别是在定义事务代理时,你可能会得到许多相似的代理定义。通过使用父 bean 和子 bean 定义,以及内部 bean 定义,可以使代理定义更加简洁清晰。
首先,我们为代理创建一个父级的、模板化的 bean 定义,如下所示:
<bean id="txProxyTemplate" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
该定义本身永远不会被实例化,因此实际上可以是不完整的。然后,每个需要创建的代理都是一个子 bean 定义,它将代理的目标包装为一个内部 bean 定义,因为该目标无论如何都不会单独使用。 以下示例展示了一个这样的子 bean:
<bean id="myService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MyServiceImpl">
</bean>
</property>
</bean>
您可以覆盖父模板中的属性。在以下示例中, 我们覆盖了事务传播设置:
<bean id="mySpecialService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MySpecialServiceImpl">
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="store*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
请注意,在父 bean 示例中,我们通过将 abstract 属性设置为 true,显式地将父 bean 定义标记为抽象(如前文所述),以确保它永远不会被实际实例化。应用程序上下文(但简单的 bean 工厂除外)默认会预实例化所有单例 bean。因此,如果你有一个(父)bean 定义仅打算用作模板,并且该定义指定了一个类,那么你必须确保将 abstract 属性设置为 true(至少对于单例 bean 而言这一点非常重要)。否则,应用程序上下文实际上会尝试预实例化它。
6.6. 以编程方式创建 AOP 代理,使用ProxyFactory
使用 Spring 以编程方式创建 AOP 代理非常简单。这使得你可以在不依赖 Spring IoC 的情况下使用 Spring AOP。
目标对象所实现的接口会自动被代理。以下示例展示了为一个目标对象创建代理的过程,其中包含一个拦截器和一个通知器(advisor):
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
val factory = ProxyFactory(myBusinessInterfaceImpl)
factory.addAdvice(myMethodInterceptor)
factory.addAdvisor(myAdvisor)
val tb = factory.proxy as MyBusinessInterface
第一步是构造一个类型为
org.springframework.aop.framework.ProxyFactory 的对象。您可以像前面的示例那样,使用目标对象来创建它,也可以在另一个构造函数中指定要代理的接口。
您可以添加通知(以拦截器作为通知的一种特殊形式)、通知器,或同时添加两者,并在 ProxyFactory 的整个生命周期内对其进行操作。如果您添加了一个 IntroductionInterceptionAroundAdvisor,就可以使代理实现额外的接口。
ProxyFactory(继承自AdvisedSupport)也提供了一些便捷方法,
允许你添加其他类型的通知,例如前置通知(before advice)和异常通知(throws advice)。
AdvisedSupport 是 ProxyFactory 和 ProxyFactoryBean 的共同父类。
| 在大多数应用程序中,将 AOP 代理创建与 IoC 框架集成是一种最佳实践。我们建议您像通常所做的那样,将 AOP 的配置从 Java 代码中外部化。 |
6.7. 操作被代理对象
无论你如何创建 AOP 代理,都可以通过使用
org.springframework.aop.framework.Advised 接口来操作它们。任何 AOP 代理都可以强制转换为该接口,无论它还实现了哪些其他接口。该接口包含以下方法:
Advisor[] getAdvisors();
void addAdvice(Advice advice) throws AopConfigException;
void addAdvice(int pos, Advice advice) throws AopConfigException;
void addAdvisor(Advisor advisor) throws AopConfigException;
void addAdvisor(int pos, Advisor advisor) throws AopConfigException;
int indexOf(Advisor advisor);
boolean removeAdvisor(Advisor advisor) throws AopConfigException;
void removeAdvisor(int index) throws AopConfigException;
boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;
boolean isFrozen();
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)、拦截器(interceptor)或其他类型的通知(advice)返回一个 Advisor。如果您添加的是一个 Advisor,那么在此索引位置返回的通知器就是您所添加的对象。如果您添加的是一个拦截器或其他类型的通知,Spring 会将其包装在一个通知器中,该通知器所包含的切入点(pointcut)始终返回 true。因此,如果您添加了一个 MethodInterceptor,那么在此索引位置返回的通知器就是一个 DefaultPointcutAdvisor,它会返回您提供的 MethodInterceptor 以及一个匹配所有类和方法的切入点。
addAdvisor() 方法可用于添加任意 Advisor。通常,持有切入点(pointcut)和通知(advice)的 Advisor 是通用的 DefaultPointcutAdvisor,你可以将其与任何通知或切入点一起使用(但不能用于引入(introductions))。
默认情况下,即使代理对象已经创建,仍然可以添加或移除通知器(advisor)或拦截器(interceptor)。唯一的限制是无法添加或移除引入型通知器(introduction advisor),因为工厂生成的现有代理不会体现接口的变更。(你可以从工厂获取一个新的代理来避免此问题。)
以下示例展示了如何将 AOP 代理强制转换为 Advised 接口,并检查和操作其通知(advice):
Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());
// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
val advised = myObject as Advised
val advisors = advised.advisors
val oldAdvisorCount = advisors.size
println("$oldAdvisorCount advisors")
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(DebugInterceptor())
// Add selective advice using a pointcut
advised.addAdvisor(DefaultPointcutAdvisor(mySpecialPointcut, myAdvice))
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.advisors.size)
| 在生产环境中修改业务对象的增强逻辑(advice)是否可取(此处无意双关),这一点值得商榷,尽管毫无疑问确实存在一些合理的使用场景。 然而,在开发阶段(例如在测试中),这种做法可能非常有用。我们有时发现,能够以拦截器或其他增强逻辑的形式添加测试代码,从而深入到我们想要测试的方法调用内部,是非常有用的。(例如,该增强逻辑可以进入为该方法创建的事务内部,或许执行 SQL 来验证数据库是否已被正确更新,然后再将事务标记为回滚。) |
根据创建代理的方式,通常可以设置一个 frozen 标志。在这种情况下,Advised 接口的 isFrozen() 方法将返回 true,此时任何试图通过添加或移除通知(advice)来修改代理的行为都会抛出 AopConfigException 异常。冻结被通知对象(advised object)状态的能力在某些场景下非常有用(例如,防止调用代码移除安全拦截器)。
6.8. 使用“自动代理”功能
到目前为止,我们已经讨论了通过使用 ProxyFactoryBean 或类似的工厂 bean 来显式创建 AOP 代理。
Spring 还允许我们使用“自动代理”(auto-proxy)的 bean 定义,它可以自动为选定的 bean 定义创建代理。这是基于 Spring 的“bean 后处理器”(bean post processor)基础设施构建的,该机制可在容器加载时修改任意 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 属性,而不是一个拦截器列表,以便原型(prototype)通知器(advisor)能够表现出正确的行为。所命名的“拦截器”可以是通知器(advisor),也可以是任意类型的通知(advice)。
与一般的自动代理一样,使用 BeanNameAutoProxyCreator 的主要目的是以最少的配置量,将相同的配置一致地应用于多个对象。它常被用于对多个对象应用声明式事务。
名称匹配的 Bean 定义(例如前面示例中的 jdkMyBean 和 onlyJdk)是具有目标类的普通 Bean 定义。BeanNameAutoProxyCreator 会自动为它们创建 AOP 代理。相同的增强(advice)会被应用到所有匹配的 Bean 上。请注意,如果使用的是通知器(advisors)(而不是前面示例中的拦截器),那么切入点(pointcuts)可能会对不同的 Bean 产生不同的应用效果。
DefaultAdvisorAutoProxyCreator
一种更为通用且功能强大的自动代理创建器是
DefaultAdvisorAutoProxyCreator。它会自动将当前上下文中符合条件的增强器(advisor)应用到相应 bean 上,而无需在自动代理增强器的 bean 定义中显式指定具体的 bean 名称。它与 BeanNameAutoProxyCreator 一样,具有配置一致性并能避免重复的优点。
使用此机制包括:
-
指定一个
DefaultAdvisorAutoProxyCreator的 bean 定义。 -
在相同或相关上下文中指定任意数量的顾问(Advisor)。请注意,这些必须是顾问(Advisor),而不是拦截器(Interceptor)或其他通知(Advice)。这是必要的,因为必须存在一个切入点(Pointcut)用于评估,以检查每条通知对候选 Bean 定义的适用性。
DefaultAdvisorAutoProxyCreator 会自动评估每个通知器(advisor)中包含的切入点(pointcut),以确定应将哪些(如果有的话)通知(advice)应用到每个业务对象上(例如示例中的 businessObject1 和 businessObject2)。
这意味着任意数量的通知器(advisors)都可以自动应用于每个业务对象。如果这些通知器中的任何切入点(pointcut)都不匹配该业务对象中的任何方法,则该对象不会被代理。当为新的业务对象添加 Bean 定义时,如有必要,它们会自动被代理。
自动代理(Auto-proxying)通常具有一个优势,即调用者或依赖项无法获取未经增强(un-advised)的对象。在此getBean("businessObject1")上调用ApplicationContext返回的是一个AOP代理,而不是目标业务对象。(前面所示的“内部bean”(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"/>
如果你希望将相同的增强(advice)一致地应用到许多业务对象上,DefaultAdvisorAutoProxyCreator 非常有用。一旦基础设施的定义就位,你就可以添加新的业务对象,而无需包含特定的代理配置。你也可以轻松地加入额外的切面(例如,跟踪或性能监控切面),且只需对配置进行极少的修改。
DefaultAdvisorAutoProxyCreator 提供了过滤支持(通过使用命名约定,仅对特定的通知器(advisor)进行评估,从而允许在同一工厂中使用多个配置不同的 AdvisorAutoProxyCreator)以及排序功能。
如果排序是一个问题,通知器可以实现 org.springframework.core.Ordered 接口以确保正确的顺序。
前面示例中使用的 TransactionAttributeSourceAdvisor 具有一个可配置的 order 值,默认设置为无序。
6.9. 使用TargetSource实现
Spring 提供了 TargetSource 的概念,该概念由 org.springframework.aop.TargetSource 接口表示。此接口负责返回实现连接点(join point)的“目标对象”。每当 AOP 代理处理方法调用时,都会向 TargetSource 实现请求一个目标实例。
使用 Spring AOP 的开发者通常不需要直接操作 TargetSource 的实现,但它提供了一种强大的机制,用于支持对象池、热替换以及其他复杂的代理目标。例如,一个基于对象池的 TargetSource 可以通过池来管理实例,从而在每次方法调用时返回不同的目标实例。
如果你没有指定 TargetSource,则会使用一个默认实现来包装本地对象。每次调用都会返回同一个目标对象(正如你所期望的那样)。
本节其余部分将介绍 Spring 提供的标准目标源(target sources)以及如何使用它们。
| 使用自定义目标源时,您的目标通常需要是原型(prototype)bean 定义,而不是单例(singleton)bean 定义。这使得 Spring 能够在需要时创建一个新的目标实例。 |
6.9.1. 可热切换的目标源
org.springframework.aop.target.HotSwappableTargetSource 的存在是为了允许在调用者保持对其引用的同时,切换 AOP 代理的目标对象。
更改目标源(target source)的目标会立即生效。HotSwappableTargetSource 是线程安全的。
你可以通过在 HotSwappableTargetSource 上使用 swap() 方法来更改目标,如下例所示:
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
val swapper = beanFactory.getBean("swapper") as HotSwappableTargetSource
val oldTarget = swapper.swap(newTarget)
以下示例展示了所需的 XML 定义:
<bean id="initialTarget" class="mycompany.OldTarget"/>
<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
<constructor-arg ref="initialTarget"/>
</bean>
<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="swapper"/>
</bean>
前面的 swap() 调用会更改可交换 bean 的目标。持有该 bean 引用的客户端不会察觉这一变化,但会立即开始调用新的目标。
尽管此示例未添加任何通知(使用 TargetSource 并不需要添加通知),但任何 TargetSource 都可以与任意通知结合使用。
6.9.2. 池化目标源
使用池化目标源(pooling target source)提供了一种与无状态会话EJB类似的编程模型,其中维护一个由相同实例组成的池,方法调用会被分派给池中空闲的对象。
Spring 池化与 SLSB(无状态会话 Bean)池化之间的一个关键区别在于,Spring 池化可以应用于任何 POJO。与 Spring 框架整体一样,该服务可以以一种非侵入性的方式应用。
Spring 提供了对 Commons Pool 2.2 的支持,该库提供了一种相当高效的池化实现。要在应用程序中使用此功能,您需要在类路径(classpath)中包含 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)必须是原型(prototype)。这使得PoolingTargetSource实现能够根据需要创建新的目标实例以扩展池。有关其属性的信息,请参阅AbstractPoolingTargetSource的 Javadoc以及您希望使用的具体子类。maxSize是最基本的实现,并且始终保证存在。
在这种情况下,myInterceptor 是一个拦截器的名称,该拦截器需要在同一个 IoC 容器中定义。然而,使用池化功能时并不一定需要指定拦截器。如果你只需要池化而不需要其他通知(advice),则完全不必设置 interceptorNames 属性。
你可以配置 Spring,使其能够将任何池化对象转换为
org.springframework.aop.target.PoolingConfig 接口,该接口通过一个引入(introduction)暴露有关池的配置和当前大小的信息。
你需要定义一个类似于以下的 advisor:
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="poolTargetSource"/>
<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>
该通知器是通过调用 AbstractPoolingTargetSource 类上的一个便捷方法获得的,因此使用了 MethodInvokingFactoryBean。此通知器的名称(此处为 poolConfigAdvisor)必须包含在暴露池化对象的 ProxyFactoryBean 的拦截器名称列表中。
转换定义如下:
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
val conf = beanFactory.getBean("businessObject") as PoolingConfig
println("Max pool size is " + conf.maxSize)
| 通常没有必要对无状态的服务对象进行池化。我们认为这不应当作为默认选择,因为大多数无状态对象天然就是线程安全的,而且如果缓存了资源,实例池化反而会带来问题。 |
通过使用自动代理(auto-proxying)可以实现更简单的池化。您可以设置任何自动代理创建器所使用的 TargetSource 实现。
6.9.3. 原型目标源
设置一个“原型”目标源(prototype target source)与设置池化(pooling)的TargetSource类似。在这种情况下,每次方法调用时都会创建一个新的目标实例。尽管在现代JVM中创建新对象的成本并不高,但装配新对象(满足其IoC依赖)的成本可能更高。因此,除非有非常充分的理由,否则你不应使用这种方法。
为此,您可以按如下方式修改前面所示的 poolTargetSource 定义
(为了清晰起见,我们也更改了名称):
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
唯一的属性是目标 bean 的名称。在 TargetSource 的实现中使用了继承,以确保命名的一致性。与池化目标源(pooling target source)一样,目标 bean 必须是一个原型(prototype)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.set(null) 本身(封装类内部除外)。此外,您还应始终记得正确地设置和取消设置(后者只需调用 ThreadLocal)线程本地的资源。无论何种情况都必须执行取消设置操作,因为如果不这样做,可能会引发问题行为。Spring 对 ThreadLocal 的支持会自动为您处理这些操作,因此应优先考虑使用 Spring 的支持,而不是在没有其他适当处理代码的情况下直接使用 5 实例。 |
6.10. 定义新的通知类型
Spring AOP 被设计为可扩展的。虽然目前内部使用的是拦截实现策略,但除了环绕通知(around advice)、前置通知(before advice)、异常通知(throws advice)和返回后通知(after returning advice)之外,还能够支持任意类型的通知。
org.springframework.aop.framework.adapter 包是一个 SPI(服务提供者接口)包,它允许在不修改核心框架的情况下添加对新的自定义通知(Advice)类型的支持。
自定义 Advice 类型唯一的约束是必须实现 org.aopalliance.aop.Advice 标记接口。
请参阅 org.springframework.aop.framework.adapter
javadoc 以获取更多信息。
7. 空安全
尽管 Java 无法通过其类型系统表达空值安全性,但 Spring 框架现在在 org.springframework.lang 包中提供了以下注解,用于声明 API 和字段的可空性:
-
@Nullable:用于指示特定参数、返回值或字段可以null的注解。 -
@NonNull:用于指示特定参数、返回值或字段不能为null的注解(在分别适用@NonNullApi和@NonNullFields的参数/返回值和字段上无需使用)。 -
@NonNullApi:包级别的注解,用于声明参数和返回值的默认非空语义。 -
@NonNullFields:包级别的注解,用于声明字段的默认非空语义。
Spring Framework 本身利用了这些注解,但它们也可用于任何基于 Spring 的 Java 项目中,以声明可空安全的 API 和可选的可空安全字段。 目前尚不支持泛型类型参数、可变参数(varargs)和数组元素的可空性,但预计将在即将发布的版本中提供支持,有关最新信息,请参见 SPR-15942。 可空性声明可能会在 Spring Framework 的各个版本(包括次要版本)之间进行微调。 方法体内部所使用类型的可空性不在本特性的范围之内。
| 其他常用库(例如 Reactor 和 Spring Data)提供了使用类似可空性约定的空安全 API,为 Spring 应用开发者带来一致的整体体验。 |
7.1. 使用场景
除了为 Spring Framework API 的可空性提供显式声明外,这些注解还可被 IDE(例如 IDEA 或 Eclipse)用来提供与空安全相关的有用警告,以避免在运行时出现 NullPointerException。
它们还用于在 Kotlin 项目中使 Spring API 具备空安全(null-safe)特性,因为 Kotlin 原生支持空安全。更多详细信息请参阅Kotlin 支持文档。
7.2. JSR-305 元注解
Spring 注解通过 JSR 305 注解进行了元注解(这是一项虽已停滞但广泛使用的 JSR)。JSR-305 元注解使 IntelliJ IDEA 或 Kotlin 等工具厂商能够以通用方式提供空安全(null-safety)支持,而无需为 Spring 注解硬编码特定支持。
无需也不建议将 JSR-305 依赖项添加到项目类路径中以利用 Spring 的空安全(null-safe)API。只有那些在其代码库中使用空安全注解的基于 Spring 的库等项目,才应通过 Gradle 的 com.google.code.findbugs:jsr305:3.0.2 配置或 Maven 的 compileOnly 作用域添加 provided 依赖,以避免编译警告。
8. 数据缓冲区与编解码器
Java NIO 提供了 ByteBuffer,但许多库在其之上构建了自己的字节缓冲区 API,
尤其是在网络操作中,重用缓冲区和/或使用直接缓冲区对性能有益。
例如,Netty 拥有 ByteBuf 类层次结构,Undertow 使用 XNIO,Jetty 使用带回调释放机制的池化字节缓冲区,等等。
spring-core 模块提供了一组抽象,用于与各种字节缓冲区 API 协同工作,如下所示:
-
DataBufferFactory抽象了数据缓冲区的创建过程。 -
DataBuffer表示一个字节缓冲区,该缓冲区可能 被池化。 -
DataBufferUtils提供用于数据缓冲区的实用方法。 -
编解码器(Codecs) 用于将数据缓冲流解码或编码为更高级别的对象。
8.1. DataBufferFactory
DataBufferFactory 用于通过以下两种方式之一创建数据缓冲区:
-
分配一个新的数据缓冲区,如果已知容量,可选择预先指定容量,这样效率更高,即使
DataBuffer的实现可以根据需要动态增长或缩小。 -
包装一个现有的
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、InputStream或OutputStream。 -
确定给定字节的索引或最后一个索引。
8.3. PooledDataBuffer
正如 ByteBuffer 的 Javadoc 中所解释的, 字节缓冲区可以是直接缓冲区(direct)或非直接缓冲区(non-direct)。直接缓冲区可能位于 Java 堆之外, 从而在执行本地 I/O 操作时无需进行数据复制。这使得直接缓冲区 在通过套接字接收和发送数据时特别有用,但它们的创建和释放成本也更高, 这就引出了缓冲区池化的概念。
PooledDataBuffer 是 DataBuffer 的一个扩展,用于协助引用计数,这对于字节缓冲区池化至关重要。其工作原理如下:当分配一个 PooledDataBuffer 时,其引用计数为 1。retain() 方法调用会增加该计数,而 release() 方法调用则会减少该计数。只要引用计数大于 0,就能保证缓冲区不会被释放。当引用计数减少到 0 时,该池化的缓冲区就可以被释放,在实际操作中,这意味着为该缓冲区预留的内存将被归还到内存池中。
请注意,大多数情况下,与其直接操作 PooledDataBuffer,不如使用 DataBufferUtils 中提供的便捷方法,这些方法仅在 DataBuffer 是 PooledDataBuffer 实例时才会对其执行释放(release)或保留(retain)操作。
8.4. DataBufferUtils
DataBufferUtils 提供了多种用于操作数据缓冲区的实用方法:
-
将数据缓冲区流合并为单个缓冲区,可能实现零拷贝(例如通过复合缓冲区),前提是底层字节缓冲区 API 支持该特性。
-
将
InputStream或 NIOChannel转换为Flux<DataBuffer>,反之亦可将Publisher<DataBuffer>转换为OutputStream或 NIOChannel。 -
如果缓冲区是
DataBuffer的实例,则用于释放或保留PooledDataBuffer的方法。 -
跳过或从字节流中读取,直到达到指定的字节数。
8.5. 编解码器
org.springframework.core.codec 包提供了以下策略接口:
-
Encoder用于将Publisher<T>编码为数据缓冲区流。 -
Decoder用于将Publisher<DataBuffer>解码为高级对象的流。
spring-core 模块提供了 byte[]、ByteBuffer、DataBuffer、Resource 和
String 的编码器和解码器实现。spring-web 模块增加了 Jackson JSON、
Jackson Smile、JAXB2、Protocol Buffers 等其他编码器和解码器。详见
WebFlux 章节中的 编解码器(Codecs)。
8.6. 使用DataBuffer
在处理数据缓冲区时,需要特别注意确保释放缓冲区,因为它们可能是池化的。我们将以编解码器(codecs)为例来说明其工作原理,但这些概念具有更普遍的适用性。让我们看看编解码器在内部必须如何管理数据缓冲区。
Decoder 是在创建高层对象之前读取输入数据缓冲区的最后一个组件,因此它必须按如下方式释放这些缓冲区:
-
如果一个
Decoder只是读取每个输入缓冲区并能立即释放它,那么可以通过DataBufferUtils.release(dataBuffer)来实现。 -
如果
Decoder使用了Flux或Mono的操作符(例如flatMap、reduce等会内部预取并缓存数据项的操作符),或者使用了filter、skip等会跳过某些数据项的操作符,那么必须在组合链中添加doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release),以确保这些缓冲区在被丢弃之前得到释放,即使是因为错误或取消信号导致的丢弃也不例外。 -
如果
Decoder以任何其他方式持有一个或多个数据缓冲区,则必须确保在完全读取后释放它们,或者在发生错误或取消信号(这些信号发生在缓存的数据缓冲区被读取和释放之前)时释放它们。
请注意,DataBufferUtils#join 提供了一种安全且高效的方式,可将数据缓冲区流聚合为单个数据缓冲区。同样地,skipUntilByteCount 和 takeUntilByteCount 也是解码器可使用的其他安全方法。
Encoder 负责分配数据缓冲区,而这些缓冲区必须由其他组件读取(并释放)。因此,Encoder 本身并没有太多工作要做。然而,如果在向缓冲区填充数据时发生序列化错误,Encoder 必须注意释放该数据缓冲区。例如:
DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
// serialize and populate buffer..
release = false;
}
finally {
if (release) {
DataBufferUtils.release(buffer);
}
}
return buffer;
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. 附录
9.1. XML 模式
本附录的这一部分列出了与核心容器相关的 XML Schema。
9.1.1.util架构
顾名思义,util 标签用于处理常见的工具类配置问题,例如配置集合、引用常量等。
若要在 Spring XML 配置文件中使用 util 命名空间中的标签,您需要在配置文件顶部包含以下前导声明(以下代码片段中的文本引用了正确的 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 的 isolation 属性值设置为 java.sql.Connection.TRANSACTION_SERIALIZABLE 常量的值。这样做固然可行,但代码冗长,并且(不必要地)向最终用户暴露了 Spring 的内部实现细节。
以下基于 XML Schema 的版本更加简洁,清晰地表达了开发者的意图(“注入此常量值”),并且可读性更好:
<bean id="..." class="...">
<property name="isolation">
<util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</property>
</bean>
从字段值设置 Bean 属性或构造函数参数
FieldRetrievingFactoryBean
是一个FactoryBean,用于检索static或非静态字段的值。它通常用于检索publicstaticfinal常量,随后可将这些常量用于设置另一个 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 使用时也非常方便,因为 bean 引用无需指定 id,如下例所示:
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
您还可以访问另一个 bean 的非静态(实例)字段,如
FieldRetrievingFactoryBean
类的 API 文档中所述。
在 Spring 中,将枚举值注入到 Bean 中作为属性或构造函数参数非常简单。你实际上不需要做任何额外操作,也无需了解 Spring 的内部机制(甚至不需要知道诸如 FieldRetrievingFactoryBean 这样的类)。下面的枚举示例展示了注入枚举值是多么简单:
package javax.persistence;
public enum PersistenceContextType {
TRANSACTION,
EXTENDED
}
package javax.persistence
enum class PersistenceContextType {
TRANSACTION,
EXTENDED
}
现在考虑以下类型为 PersistenceContextType 的 setter 方法及其对应的 bean 定义:
package example;
public class Client {
private PersistenceContextType persistenceContextType;
public void setPersistenceContextType(PersistenceContextType type) {
this.persistenceContextType = type;
}
}
package example
class Client {
lateinit var persistenceContextType: PersistenceContextType
}
<bean class="example.Client">
<property name="persistenceContextType" value="TRANSACTION"/>
</bean>
使用<util:property-path/>
考虑以下示例:
<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<bean id="testBean.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
上述配置使用了一个 Spring FactoryBean 实现(即 PropertyPathFactoryBean)来创建一个名为 int 的 bean(类型为 testBean.age),其值等于 age bean 的 testBean 属性。
现在考虑以下示例,它添加了一个 <util:property-path/> 元素:
<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>
path 元素的 <property-path/> 属性值遵循 beanName.beanProperty 的形式。在此例中,它获取名为 age 的 bean 的 testBean 属性。该 age 属性的值为 10。
使用<util:property-path/>设置 Bean 属性或构造函数参数
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>
在下面的示例中,一个路径将针对一个内部 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>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</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>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</util:list>
你也可以通过在 List 元素上使用 list-class 属性,显式地控制所实例化和填充的 <util:list/> 的具体类型。例如,如果我们确实需要实例化一个 java.util.LinkedList,可以使用以下配置:
<util:list id="emails" list-class="java.util.LinkedList">
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>d'[email protected]</value>
</util:list>
如果没有提供 list-class 属性,容器将选择一个 List 实现。
使用<util:map/>
考虑以下示例:
<!-- creates a java.util.Map instance with values loaded from the supplied 'sourceMap' -->
<bean id="emails" class="org.springframework.beans.factory.config.MapFactoryBean">
<property name="sourceMap">
<map>
<entry key="pechorin" value="[email protected]"/>
<entry key="raskolnikov" value="[email protected]"/>
<entry key="stavrogin" value="[email protected]"/>
<entry key="porfiry" value="[email protected]"/>
</map>
</property>
</bean>
上述配置使用了一个 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="[email protected]"/>
<entry key="raskolnikov" value="[email protected]"/>
<entry key="stavrogin" value="[email protected]"/>
<entry key="porfiry" value="[email protected]"/>
</util:map>
你也可以通过在 Map 元素上使用 'map-class' 属性,显式地控制所实例化和填充的 <util:map/> 的具体类型。例如,如果我们确实需要实例化一个 java.util.TreeMap,可以使用以下配置:
<util:map id="emails" map-class="java.util.TreeMap">
<entry key="pechorin" value="[email protected]"/>
<entry key="raskolnikov" value="[email protected]"/>
<entry key="stavrogin" value="[email protected]"/>
<entry key="porfiry" value="[email protected]"/>
</util:map>
如果没有提供 'map-class' 属性,容器将选择一个 Map 实现。
使用<util:set/>
考虑以下示例:
<!-- creates a java.util.Set instance with values loaded from the supplied 'sourceSet' -->
<bean id="emails" class="org.springframework.beans.factory.config.SetFactoryBean">
<property name="sourceSet">
<set>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</set>
</property>
</bean>
上述配置使用了一个 Spring FactoryBean 实现(即 SetFactoryBean)来创建一个 java.util.Set 实例,并使用所提供的 sourceSet 中的值进行初始化。
以下示例使用 <util:set/> 元素来实现更简洁的表示形式:
<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</util:set>
你也可以通过在 Set 元素上使用 set-class 属性,显式地控制所实例化和填充的 <util:set/> 的具体类型。例如,如果我们确实需要实例化一个 java.util.TreeSet,可以使用以下配置:
<util:set id="emails" set-class="java.util.TreeSet">
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</util:set>
如果没有提供 set-class 属性,容器将选择一个 Set 实现。
9.1.2.aop架构
aop 标签用于配置 Spring 中所有与 AOP 相关的内容,包括 Spring 自身基于代理的 AOP 框架,以及 Spring 与 AspectJ AOP 框架的集成。
这些标签在题为 使用 Spring 进行面向切面编程 的章节中有全面介绍。
为了完整起见,若要在您的 Spring XML 配置文件中使用 aop 命名空间中的标签,您需要在配置文件顶部包含以下前导声明(以下代码片段中的文本引用了正确的 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>
9.1.3.context架构
context 标签用于处理与基础设施相关的 ApplicationContext 配置——也就是说,通常不是对最终用户而言重要的 bean,而是执行 Spring 中大量“繁重”工作的 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 配置进行更精细的控制,可以将其显式定义为一个 Bean。
使用<annotation-config/>
此元素用于激活 Spring 基础设施,以检测 bean 类中的注解:
-
Spring 的
@Configuration模型 -
@Autowired/@Inject、@Value和@Lookup -
JSR-250 的
@Resource、@PostConstruct和@PreDestroy(如果可用) -
JAX-WS 的
@WebServiceRef和 EJB 3 的@EJB(如果可用) -
JPA 的
@PersistenceContext和@PersistenceUnit(如果可用) -
Spring 的
@EventListener
或者,您可以选择显式激活用于这些注解的各个 BeanPostProcessors。
此元素不会激活对 Spring 的
@Transactional 注解的处理;
您可以使用 <tx:annotation-driven/>
元素来实现该目的。同样,Spring 的
缓存注解也需要被显式地
启用。 |
使用<component-scan/>
该元素在基于注解的容器配置一节中有详细说明。
使用<load-time-weaver/>
该元素在Spring 框架中使用 AspectJ 进行加载时织入一节中有详细说明。
使用<spring-configured/>
该元素在使用 AspectJ 通过 Spring 对领域对象进行依赖注入一节中有详细说明。
使用<mbean-export/>
此元素在配置基于注解的MBean导出一节中有详细说明。
9.1.4. Beans 模式
最后但同样重要的是,我们还有 beans 命名空间中的元素。这些元素自 Spring 框架诞生之初就已存在。本文未在此展示 beans 命名空间中各种元素的示例,因为它们已在依赖项与配置详解一节中得到了非常全面的介绍(事实上,整个章节都对此进行了阐述)。
请注意,您可以在 <bean/> 的 XML 定义中添加零个或多个键值对。
如何处理这些额外的元数据完全取决于您自己的自定义逻辑(因此,通常仅在您编写自己的自定义元素时才有用,如附录《XML Schema 编写》中所述)。
以下示例展示了在周围 <meta/> 元素上下文中使用的 <bean/> 元素
(请注意,如果没有用于解释它的逻辑,这些元数据实际上是无效的)。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="foo" class="x.y.Foo">
<meta key="cacheName" value="foo"/> (1)
<property name="name" value="Rick"/>
</bean>
</beans>
| 1 | 这是示例 meta 元素 |
在前面的例子中,你可以假定存在某些逻辑会消费该 bean 定义,并设置一些缓存基础设施,以使用所提供的元数据。
9.2. XML 模式编写
从 2.0 版本开始,Spring 提供了一种机制,用于在基础的 Spring XML 格式之上添加基于 schema 的扩展,以定义和配置 bean。本节将介绍如何编写自定义的 XML bean 定义解析器,并将此类解析器集成到 Spring IoC 容器中。
为了便于使用支持 Schema 的 XML 编辑器编写配置文件,Spring 的可扩展 XML 配置机制基于 XML Schema 构建。如果您还不熟悉 Spring 标准发行版中自带的当前 XML 配置扩展功能,请先阅读前面关于XML Schemas的部分。
要创建新的 XML 配置扩展:
为了提供一个统一的示例,我们创建一个 XML 扩展(一个自定义 XML 元素),用于配置 SimpleDateFormat 类型的对象(来自 java.text 包)。完成后,我们将能够按如下方式定义 SimpleDateFormat 类型的 bean 定义:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
(我们在本附录后面会提供更详细的示例。这个简单示例的目的是引导您完成创建自定义扩展的基本步骤。)
9.2.1. 编写架构
为 Spring 的 IoC 容器创建一个 XML 配置扩展,首先需要编写一个 XML Schema 来描述该扩展。在我们的示例中,使用以下 schema 来配置 SimpleDateFormat 对象:
<!-- myns.xsd (inside package org/springframework/samples/xml) -->
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.example/schema/myns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.mycompany.example/schema/myns"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans"/>
<xsd:element name="dateformat">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType"> (1)
<xsd:attribute name="lenient" type="xsd:boolean"/>
<xsd:attribute name="pattern" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
| 1 | 所指示的行包含所有可标识标签的扩展基类
(即它们具有一个 id 属性,我们可以将其用作容器中的 bean 标识符)。我们可以使用此属性,因为我们导入了 Spring 提供的
beans 命名空间。 |
上述模式允许我们通过使用 SimpleDateFormat 元素,直接在 XML 应用上下文文件中配置 <myns:dateformat/> 对象,如下例所示:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
请注意,在我们创建了基础设施类之后,前面的 XML 片段本质上与以下 XML 片段相同:
<bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-HH-dd HH:mm"/>
<property name="lenient" value="true"/>
</bean>
前面两个代码片段中的第二个在容器中创建了一个 bean(通过名称 dateFormat 标识,类型为 SimpleDateFormat),并设置了几个属性。
| 基于 Schema 的配置格式创建方法可与支持 Schema 感知的 XML 编辑器的 IDE 紧密集成。通过使用编写得当的 Schema,您可以利用自动补全功能,让用户从枚举中定义的多个配置选项中进行选择。 |
9.2.2. 编写代码NamespaceHandler
除了该 schema 之外,我们还需要一个 NamespaceHandler,用于解析 Spring 在解析配置文件时遇到的此特定命名空间下的所有元素。在本例中,NamespaceHandler 应负责解析 myns:dateformat 元素。
NamespaceHandler 接口包含三个方法:
-
init():允许对NamespaceHandler进行初始化,并在 Spring 使用该处理器之前调用。 -
BeanDefinition parse(Element, ParserContext):当 Spring 遇到顶级元素(未嵌套在 bean 定义或其他命名空间内部)时调用此方法。该方法本身可以注册 bean 定义、返回一个 bean 定义,或者同时执行这两种操作。 -
BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext):当 Spring 遇到属于不同命名空间的属性或嵌套元素时调用。 对一个或多个 bean 定义的装饰(decoration)被用于(例如)Spring 所支持的作用域。 我们首先展示一个不使用装饰的简单示例,然后在一个稍复杂的示例中演示装饰的用法。
尽管你可以为整个命名空间编写自己的 NamespaceHandler(从而提供解析该命名空间中每个元素的代码),但在很多情况下,Spring XML 配置文件中的每个顶层 XML 元素都会对应一个单独的 Bean 定义(就像我们的例子中,单个 <myns:dateformat/> 元素会生成一个 SimpleDateFormat 的 Bean 定义)。Spring 提供了多个便捷类来支持这种场景。在下面的示例中,我们使用了 NamespaceHandlerSupport 类:
package org.springframework.samples.xml;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
}
}
package org.springframework.samples.xml
import org.springframework.beans.factory.xml.NamespaceHandlerSupport
class MyNamespaceHandler : NamespaceHandlerSupport {
override fun init() {
registerBeanDefinitionParser("dateformat", SimpleDateFormatBeanDefinitionParser())
}
}
你可能会注意到,这个类中实际上并没有太多解析逻辑。事实上,NamespaceHandlerSupport 类内置了委托机制。它支持注册任意数量的 BeanDefinitionParser 实例,并在其命名空间中需要解析某个元素时将解析任务委托给这些实例。这种清晰的关注点分离使得 NamespaceHandler 能够负责协调其命名空间内所有自定义元素的解析流程,而将 XML 解析的具体繁重工作委托给 BeanDefinitionParsers 来完成。这意味着每个 BeanDefinitionParser 仅包含解析单个自定义元素所需的逻辑,正如我们将在下一步中看到的那样。
9.2.3. 使用BeanDefinitionParser
当 BeanDefinitionParser 遇到已被映射到特定 Bean 定义解析器的 XML 元素类型时(本例中为 NamespaceHandler),就会使用 dateformat。换句话说,BeanDefinitionParser 负责解析 schema 中定义的一个特定的顶层 XML 元素。在解析器中,我们可以访问该 XML 元素(因此也能访问其子元素),从而能够解析自定义的 XML 内容,如下例所示:
package org.springframework.samples.xml;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
import java.text.SimpleDateFormat;
public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { (1)
protected Class getBeanClass(Element element) {
return SimpleDateFormat.class; (2)
}
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// this will never be null since the schema explicitly requires that a value be supplied
String pattern = element.getAttribute("pattern");
bean.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 的类型。 |
package org.springframework.samples.xml
import org.springframework.beans.factory.support.BeanDefinitionBuilder
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser
import org.springframework.util.StringUtils
import org.w3c.dom.Element
import java.text.SimpleDateFormat
class SimpleDateFormatBeanDefinitionParser : AbstractSingleBeanDefinitionParser() { (1)
override fun getBeanClass(element: Element): Class<*>? { (2)
return SimpleDateFormat::class.java
}
override fun doParse(element: Element, bean: BeanDefinitionBuilder) {
// this will never be null since the schema explicitly requires that a value be supplied
val pattern = element.getAttribute("pattern")
bean.addConstructorArgValue(pattern)
// this however is an optional property
val lenient = element.getAttribute("lenient")
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", java.lang.Boolean.valueOf(lenient))
}
}
}
| 1 | 我们使用 Spring 提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的大量基础性工作。 |
| 2 | 我们为 AbstractSingleBeanDefinitionParser 超类提供其所代表的单个 BeanDefinition 的类型。 |
在这个简单的情况下,这就是我们需要做的全部工作。我们单个 BeanDefinition 的创建由 AbstractSingleBeanDefinitionParser 超类处理,bean 定义的唯一标识符的提取和设置也同样由该超类处理。
9.2.4. 注册处理器和模式
编码工作已经完成。剩下的工作就是让 Spring 的 XML 解析基础设施能够识别我们的自定义元素。我们通过在两个专用的属性文件中注册自定义的 namespaceHandler 和自定义的 XSD 文件来实现这一点。这两个属性文件都放置在应用程序的 META-INF 目录中,例如,可以与你的二进制类文件一起打包到 JAR 文件中进行分发。Spring 的 XML 解析基础设施会自动读取这些特殊的属性文件,从而识别并加载你的新扩展,下两节将详细介绍这些属性文件的格式。
正在写入META-INF/spring.handlers
名为 spring.handlers 的属性文件包含 XML Schema URI 到命名空间处理器类的映射。在我们的示例中,需要编写如下内容:
http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler
(: 字符在 Java 属性格式中是一个有效的分隔符,因此 URI 中的 : 字符需要用反斜杠进行转义。)
键值对的第一部分(即键)是与您的自定义命名空间扩展相关联的 URI,必须与您自定义 XSD 架构中指定的 targetNamespace 属性值完全一致。
正在写入 'META-INF/spring.schemas'
名为 spring.schemas 的属性文件包含 XML Schema 位置(在使用该 Schema 的 XML 文件中,与 Schema 声明一起作为 xsi:schemaLocation 属性的一部分被引用)到类路径资源的映射。此文件的作用是避免 Spring 必须使用默认的 EntityResolver,而该默认解析器需要通过互联网访问来获取 Schema 文件。如果你在此属性文件中指定了映射关系,Spring 就会在类路径中查找该 Schema(在本例中,即 myns.xsd 包中的 org.springframework.samples.xml)。以下代码片段展示了我们需要为自定义 Schema 添加的配置行:
http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd
(请记住,: 字符必须进行转义。)
建议您将 XSD 文件(或多个文件)与 NamespaceHandler 和 BeanDefinitionParser 类一起部署在类路径中。
9.2.5. 在您的 Spring XML 配置中使用自定义扩展
使用你自己实现的自定义扩展与使用 Spring 提供的“自定义”扩展并无区别。以下示例在 Spring XML 配置文件中使用了前几步中开发的自定义 <dateformat/> 元素:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:myns="http://www.mycompany.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。 |
9.2.6. 更详细的示例
本节提供了一些更详细的自定义 XML 扩展示例。
在自定义元素中嵌套自定义元素
本节所展示的示例说明了如何编写满足以下配置目标所需的各种构件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:foo="http://www.foo.example/schema/component"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.foo.example/schema/component http://www.foo.example/schema/component/component.xsd">
<foo:component id="bionic-family" name="Bionic-1">
<foo:component name="Mother-1">
<foo:component name="Karate-1"/>
<foo:component name="Sport-1"/>
</foo:component>
<foo:component name="Rock-1"/>
</foo:component>
</beans>
上述配置将自定义扩展元素相互嵌套。实际上由 <foo:component/> 元素配置的类是 Component 类(如下例所示)。请注意,Component 类并未为 components 属性提供 setter 方法。这使得通过 setter 注入方式为 Component 类配置 bean 定义变得困难(甚至根本不可能)。以下代码清单展示了 Component 类:
package com.foo;
import java.util.ArrayList;
import java.util.List;
public class Component {
private String name;
private List<Component> components = new ArrayList<Component> ();
// mmm, there is no setter method for the 'components'
public void addComponent(Component component) {
this.components.add(component);
}
public List<Component> getComponents() {
return components;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.foo
import java.util.ArrayList
class Component {
var name: String? = null
private val components = ArrayList<Component>()
// mmm, there is no setter method for the 'components'
fun addComponent(component: Component) {
this.components.add(component)
}
fun getComponents(): List<Component> {
return components
}
}
解决此问题的典型方法是创建一个自定义的 FactoryBean,为其 components 属性暴露一个 setter 方法。以下代码清单展示了一个这样的自定义 FactoryBean:
package com.foo;
import org.springframework.beans.factory.FactoryBean;
import java.util.List;
public class ComponentFactoryBean implements FactoryBean<Component> {
private Component parent;
private List<Component> children;
public void setParent(Component parent) {
this.parent = parent;
}
public void setChildren(List<Component> children) {
this.children = children;
}
public Component getObject() throws Exception {
if (this.children != null && this.children.size() > 0) {
for (Component child : children) {
this.parent.addComponent(child);
}
}
return this.parent;
}
public Class<Component> getObjectType() {
return Component.class;
}
public boolean isSingleton() {
return true;
}
}
package com.foo
import org.springframework.beans.factory.FactoryBean
import org.springframework.stereotype.Component
class ComponentFactoryBean : FactoryBean<Component> {
private var parent: Component? = null
private var children: List<Component>? = null
fun setParent(parent: Component) {
this.parent = parent
}
fun setChildren(children: List<Component>) {
this.children = children
}
override fun getObject(): Component? {
if (this.children != null && this.children!!.isNotEmpty()) {
for (child in children!!) {
this.parent!!.addComponent(child)
}
}
return this.parent
}
override fun getObjectType(): Class<Component>? {
return Component::class.java
}
override fun isSingleton(): Boolean {
return true
}
}
这种方式效果很好,但它向最终用户暴露了大量 Spring 的底层实现细节。我们将要编写一个自定义扩展,以隐藏所有这些 Spring 的底层细节。 如果我们遵循前面描述的步骤,首先需要创建 XSD 模式来定义我们自定义标签的结构,如下列代码所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.example/schema/component"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.example/schema/component"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:element name="component">
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="component"/>
</xsd:choice>
<xsd:attribute name="id" type="xsd:ID"/>
<xsd:attribute name="name" use="required" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
再次按照前面描述的流程,
我们接着创建一个自定义的NamespaceHandler:
package com.foo;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class ComponentNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
}
}
package com.foo
import org.springframework.beans.factory.xml.NamespaceHandlerSupport
class ComponentNamespaceHandler : NamespaceHandlerSupport() {
override fun init() {
registerBeanDefinitionParser("component", ComponentBeanDefinitionParser())
}
}
接下来是自定义的 BeanDefinitionParser。请记住,我们正在创建一个描述 BeanDefinition 的 ComponentFactoryBean。以下代码清单展示了我们自定义的 BeanDefinitionParser 实现:
package com.foo;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import java.util.List;
public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser {
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
return parseComponentElement(element);
}
private static AbstractBeanDefinition parseComponentElement(Element element) {
BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class);
factory.addPropertyValue("parent", parseComponent(element));
List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
if (childElements != null && childElements.size() > 0) {
parseChildComponents(childElements, factory);
}
return factory.getBeanDefinition();
}
private static BeanDefinition parseComponent(Element element) {
BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
component.addPropertyValue("name", element.getAttribute("name"));
return component.getBeanDefinition();
}
private static void parseChildComponents(List<Element> childElements, BeanDefinitionBuilder factory) {
ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
for (Element element : childElements) {
children.add(parseComponentElement(element));
}
factory.addPropertyValue("children", children);
}
}
package com.foo
import org.springframework.beans.factory.config.BeanDefinition
import org.springframework.beans.factory.support.AbstractBeanDefinition
import org.springframework.beans.factory.support.BeanDefinitionBuilder
import org.springframework.beans.factory.support.ManagedList
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser
import org.springframework.beans.factory.xml.ParserContext
import org.springframework.util.xml.DomUtils
import org.w3c.dom.Element
import java.util.List
class ComponentBeanDefinitionParser : AbstractBeanDefinitionParser() {
override fun parseInternal(element: Element, parserContext: ParserContext): AbstractBeanDefinition? {
return parseComponentElement(element)
}
private fun parseComponentElement(element: Element): AbstractBeanDefinition {
val factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean::class.java)
factory.addPropertyValue("parent", parseComponent(element))
val childElements = DomUtils.getChildElementsByTagName(element, "component")
if (childElements != null && childElements.size > 0) {
parseChildComponents(childElements, factory)
}
return factory.getBeanDefinition()
}
private fun parseComponent(element: Element): BeanDefinition {
val component = BeanDefinitionBuilder.rootBeanDefinition(Component::class.java)
component.addPropertyValue("name", element.getAttribute("name"))
return component.beanDefinition
}
private fun parseChildComponents(childElements: List<Element>, factory: BeanDefinitionBuilder) {
val children = ManagedList<BeanDefinition>(childElements.size)
for (element in childElements) {
children.add(parseComponentElement(element))
}
factory.addPropertyValue("children", children)
}
}
最后,需要通过修改 META-INF/spring.handlers 和 META-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
在“普通”元素上使用自定义属性
编写您自己的自定义解析器及相关组件并不困难。然而,有时这样做并不是最合适的选择。请考虑这样一种场景:您需要为已存在的 bean 定义添加元数据。在这种情况下,您肯定不想去编写一整套自定义扩展,而只是希望为现有的 bean 定义元素添加一个额外的属性。
再举一个例子,假设你为一个服务对象定义了一个 bean 定义,该服务对象(自身并不知晓)会访问一个集群化的 JCache,而你希望确保所命名的 JCache 实例在所属集群中被提前启动。 以下代码清单展示了这样一个定义:
<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
jcache:cache-name="checking.account">
<!-- other dependencies here... -->
</bean>
然后,当解析 BeanDefinition 属性时,我们可以创建另一个 'jcache:cache-name'。该 BeanDefinition 将为我们初始化指定名称的 JCache。我们还可以修改现有的 BeanDefinition 的 'checkingAccountService',使其依赖于这个新创建的用于初始化 JCache 的 BeanDefinition。以下代码清单展示了我们的 JCacheInitializer:
package com.foo;
public class JCacheInitializer {
private String name;
public JCacheInitializer(String name) {
this.name = name;
}
public void initialize() {
// lots of JCache API calls to initialize the named cache...
}
}
package com.foo
class JCacheInitializer(private val name: String) {
fun initialize() {
// lots of JCache API calls to initialize the named cache...
}
}
现在我们可以继续处理自定义扩展了。首先,我们需要编写描述该自定义属性的 XSD 模式,如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.example/schema/jcache"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.example/schema/jcache"
elementFormDefault="qualified">
<xsd:attribute name="cache-name" type="xsd:string"/>
</xsd:schema>
接下来,我们需要创建相应的 NamespaceHandler,如下所示:
package com.foo;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class JCacheNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
new JCacheInitializingBeanDefinitionDecorator());
}
}
package com.foo
import org.springframework.beans.factory.xml.NamespaceHandlerSupport
class JCacheNamespaceHandler : NamespaceHandlerSupport() {
override fun init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
JCacheInitializingBeanDefinitionDecorator())
}
}
接下来,我们需要创建解析器。请注意,在这种情况下,由于我们要解析的是一个 XML 属性,因此我们编写的是 BeanDefinitionDecorator,而不是 BeanDefinitionParser。
以下代码清单展示了我们的 BeanDefinitionDecorator 实现:
package com.foo;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder,
ParserContext ctx) {
String initializerBeanName = registerJCacheInitializer(source, ctx);
createDependencyOnJCacheInitializer(holder, initializerBeanName);
return holder;
}
private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder,
String initializerBeanName) {
AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition());
String[] dependsOn = definition.getDependsOn();
if (dependsOn == null) {
dependsOn = new String[]{initializerBeanName};
} else {
List dependencies = new ArrayList(Arrays.asList(dependsOn));
dependencies.add(initializerBeanName);
dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY);
}
definition.setDependsOn(dependsOn);
}
private String registerJCacheInitializer(Node source, ParserContext ctx) {
String cacheName = ((Attr) source).getValue();
String beanName = cacheName + "-initializer";
if (!ctx.getRegistry().containsBeanDefinition(beanName)) {
BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class);
initializer.addConstructorArg(cacheName);
ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition());
}
return beanName;
}
}
package com.foo
import org.springframework.beans.factory.config.BeanDefinitionHolder
import org.springframework.beans.factory.support.AbstractBeanDefinition
import org.springframework.beans.factory.support.BeanDefinitionBuilder
import org.springframework.beans.factory.xml.BeanDefinitionDecorator
import org.springframework.beans.factory.xml.ParserContext
import org.w3c.dom.Attr
import org.w3c.dom.Node
import java.util.ArrayList
class JCacheInitializingBeanDefinitionDecorator : BeanDefinitionDecorator {
override fun decorate(source: Node, holder: BeanDefinitionHolder,
ctx: ParserContext): BeanDefinitionHolder {
val initializerBeanName = registerJCacheInitializer(source, ctx)
createDependencyOnJCacheInitializer(holder, initializerBeanName)
return holder
}
private fun createDependencyOnJCacheInitializer(holder: BeanDefinitionHolder,
initializerBeanName: String) {
val definition = holder.beanDefinition as AbstractBeanDefinition
var dependsOn = definition.dependsOn
dependsOn = if (dependsOn == null) {
arrayOf(initializerBeanName)
} else {
val dependencies = ArrayList(listOf(*dependsOn))
dependencies.add(initializerBeanName)
dependencies.toTypedArray()
}
definition.setDependsOn(*dependsOn)
}
private fun registerJCacheInitializer(source: Node, ctx: ParserContext): String {
val cacheName = (source as Attr).value
val beanName = "$cacheName-initializer"
if (!ctx.registry.containsBeanDefinition(beanName)) {
val initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer::class.java)
initializer.addConstructorArg(cacheName)
ctx.registry.registerBeanDefinition(beanName, initializer.getBeanDefinition())
}
return beanName
}
}
最后,我们需要通过修改 META-INF/spring.handlers 和 META-INF/spring.schemas 文件,将各种构件注册到 Spring XML 基础设施中,如下所示:
# 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