核心
1. IoC 容器
本章介绍Spring的控制反转(IoC)容器。
1.1. Spring IoC 容器和 Bean 简介
本章介绍控制反转(IoC)原则的Spring框架实现。IoC也被称为依赖注入(DI)。这是一个过程,其中对象仅通过构造函数参数、工厂方法的参数,或者在对象实例被构造或从工厂方法返回后设置的属性来定义它们的依赖项(即它们所使用的其他对象)。然后,容器在创建bean时注入这些依赖项。这个过程本质上是bean自身通过使用直接的类构造或类似服务定位器模式的机制来控制其依赖项的实例化或位置的逆过程。
org.springframework.beans 和 org.springframework.context 包是 Spring Framework 的 IoC 容器的基础。该
BeanFactory
接口提供了一种高级的配置机制,能够管理任何类型的对象。
ApplicationContext
是 BeanFactory 的子接口。它添加了:
-
与 Spring 的 AOP 功能更容易地集成
-
消息资源处理(用于国际化)
-
事件发布
-
应用层特定的上下文,例如
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实例化)。如果您使用Eclipse的Spring工具(一个基于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框架的一部分。 因此,您可以使用Java而不是XML文件来定义应用程序类外部的bean。要使用这些新功能,请参阅
@Configuration,@Bean,@Import, 和@DependsOn注解。
Spring配置至少包含一个且通常包含多个由容器管理的bean定义。基于XML的配置元数据将这些bean配置为顶级<beans/>元素内的<bean/>元素。Java配置通常在@Configuration类中使用@Bean注解的方法。
这些bean定义对应于构成您的应用程序的实际对象。
通常,您会定义服务层对象、数据访问对象(DAO)、表示对象(如Struts Action实例)、基础设施对象(如Hibernate SessionFactories、JMS Queues)等。通常,不会在容器中配置细粒度的域对象,因为创建和加载域对象通常是DAO和业务逻辑的责任。但是,您可以使用Spring与AspectJ的集成来配置由IoC容器以外的代码创建的对象。请参阅 使用AspectJ通过Spring对域对象进行依赖注入。
以下示例显示了基于 XML 的配置元数据的基本结构:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
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 定义。
|
使用相对 "../" 路径引用父目录中的文件是可能的,但不推荐。这样做会创建对当前应用程序外部文件的依赖。特别是,对于 您可以始终使用完全限定的资源位置而不是相对路径:例如, |
命名空间本身提供了导入指令功能。Spring 提供的其他 XML 命名空间中还包含超出普通 bean 定义的进一步配置功能——例如,context 和 util 命名空间。
Groovy Bean 定义 DSL
作为外部化配置元数据的另一个例子,bean定义也可以使用Spring的Groovy Bean Definition DSL来表达,这与Grails框架中的用法相同。通常,这样的配置位于一个".groovy"文件中,其结构如下面的例子所示:
beans {
dataSource(BasicDataSource) {
driverClassName = "org.hsqldb.jdbcDriver"
url = "jdbc:hsqldb:mem:grailsDB"
username = "sa"
password = ""
settings = [mynew:"setting"]
}
sessionFactory(SessionFactory) {
dataSource = dataSource
}
myService(MyService) {
nestedBean = { AnotherBean bean ->
dataSource = dataSource
}
}
}
这种配置方式在很大程度上等同于XML bean定义,甚至支持Spring的XML配置命名空间。它还允许通过importBeans指令导入XML bean定义文件。
1.2.3. 使用容器
ApplicationContext 是一个高级工厂接口,能够维护不同 bean 及其依赖项的注册表。通过使用方法 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")
最灵活的变体是与reader委托结合使用的GenericApplicationContext,例如,对于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完成其工作所需的。这些引用也称为协作对象或依赖项。
-
在新创建的对象中设置的其他配置选项——例如,池的大小限制或管理连接池的 bean 使用的连接数量。
此元数据转换为构成每个 bean 定义的一组属性。 下表描述了这些属性:
| 属性 | 解释在…… |
|---|---|
类 |
|
名称 |
|
作用域 |
|
构造函数参数 |
|
属性 |
|
自动装配模式 |
|
延迟初始化模式 |
|
初始化方法 |
|
销毁方法 |
除了包含如何创建特定bean的信息的bean定义外,ApplicationContext实现还允许注册在容器外部(由用户创建)的现有对象。这是通过通过getBeanFactory()方法访问ApplicationContext的BeanFactory来完成的,该方法返回BeanFactoryDefaultListableBeanFactory实现。DefaultListableBeanFactory通过registerSingleton(..)和registerBeanDefinition(..)方法支持此注册。然而,典型的应用程序仅使用通过常规bean定义元数据定义的bean。
|
Bean 元数据和手动提供的单例实例需要尽可能早地进行注册,以便容器在自动连线和其他内省步骤中能够正确地处理它们。虽然在一定程度上支持覆盖现有的元数据和现有的单例实例,但运行时注册新 Bean(同时访问工厂)并不被正式支持,可能会导致并发访问异常、Bean 容器中的状态不一致,或两者皆有。 |
1.3.1. Bean命名
每个bean有一个或多个标识符。这些标识符必须在托管该bean的容器内唯一。通常,一个bean只有一个标识符。然而,如果需要多个标识符,额外的标识符可以视为别名。
在基于XML的配置元数据中,您使用id属性、name属性或两者来指定Bean标识符。 id属性允许您指定一个唯一的id。通常,这些名称是字母数字的('myBean','someService'等),但也可以包含特殊字符。如果您想为Bean引入其他别名,还可以在name属性中指定它们,用逗号(,)、分号(;)或空格分隔。作为历史说明,在Spring 3.1之前的版本中,id属性被定义为xsd:ID类型,这限制了可能的字符。从3.1开始,它被定义为xsd:string类型。请注意,Bean id的唯一性仍然由容器强制执行,尽管不再由XML解析器强制执行。
您不需要为bean提供name或id。如果您没有显式提供name或id,容器会为该bean生成一个唯一名称。但是,如果您希望通过使用ref元素或Service Locator风格的查找来通过名称引用该bean,则必须提供一个名称。
不提供名称的动机与使用内部bean和自动连线协作器有关。
通过类路径中的组件扫描,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 定义所封装的配置元数据来创建(或获取)实际的对象。
如果您使用基于XML的配置元数据,则在class属性中指定要实例化的对象的类型(或类)<bean/>元素。此class属性(内部上,是Class属性在BeanDefinition实例上)通常是必需的。(例外情况,请参见使用实例工厂方法进行实例化和Bean定义继承。)您可以以两种方式使用Class属性:
-
通常,在容器本身通过反射调用其构造函数直接创建 bean 的情况下,指定要构造的 bean 类,有点类似于带有
new运算符的 Java 代码。 -
要指定包含实际调用以创建对象的
static工厂方法的类,在容器对类调用static工厂方法来创建 bean 的较少见情况下。从static工厂方法的调用中返回的对象类型可以是同一类,也可以是另一个类。
通过构造函数实例化
当您通过构造函数方法创建 bean 时,所有普通类都可以被 Spring 使用和兼容。也就是说,所开发的类不需要实现任何特定的接口,也不需要以特定的方式进行编码。只需指定 bean 类即可。然而,根据您为该特定 bean 使用的 IoC 类型,可能需要一个默认(空)构造函数。
Spring IoC 容器可以管理你希望它管理的几乎任何类。它不限于管理真正的 JavaBean。大多数 Spring 用户更倾向于使用实际的 JavaBean,这些 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。该定义没有指定返回对象的类型(类),只指定了包含工厂方法的类。在本例中,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)进行管理和配置。参见 详细信息中的依赖关系和配置。
在Spring文档中,“工厂bean”指的是在Spring容器中配置的bean,它通过一个
实例或
静态工厂方法创建对象。相比之下,
FactoryBean(请注意大小写)指的是Spring特定的
FactoryBean实现类。 |
确定 Bean 的运行时类型
确定特定bean的运行时类型并不总是显而易见的。在bean元数据定义中指定的类只是一个初始类引用,可能与声明的工厂方法结合使用,或者是FactoryBean类,这可能导致bean的不同运行时类型,或者在实例级工厂方法的情况下根本未设置(此时通过指定的factory-bean名称来解决)。
此外,AOP代理可能会使用基于接口的代理包装bean实例,仅暴露目标bean的实际类型(即其实现的接口)。
推荐的方法是通过指定的bean名称调用BeanFactory.getType来了解特定bean的实际运行时类型。这会考虑所有上述情况,并返回调用BeanFactory.getBean时对于同一bean名称将要返回的对象类型。
1.4. 依赖项
一个典型的企业应用程序并不由单个对象(或在Spring术语中的Bean)组成。即使是最简单的应用程序,也有几个对象共同工作,以呈现最终用户看到的连贯的应用程序。下一节将解释如何从定义多个独立的Bean定义转变为一个完整的应用程序,其中对象协作以实现目标。
1.4.1. 依赖注入
依赖注入(DI)是一种过程,其中对象通过构造函数参数、工厂方法的参数,或者在对象实例被构造或从工厂方法返回后设置的属性来定义它们的依赖项(即,它们所协作的其他对象)。当创建bean时,容器会注入这些依赖项。这个过程本质上是相反的(因此得名“控制反转”),与bean自身通过使用类的直接构造或服务定位器模式来控制其依赖项的实例化或定位完全不同。
使用DI原则,代码会更加整洁,当对象被提供依赖项时,解耦会更有效。对象不会查找其依赖项,也不会知道依赖项的位置或类。结果是,你的类更容易测试,特别是当依赖项是接口或抽象基类时,这允许在单元测试中使用模拟或桩实现。
DI 存在两种主要形式: 基于构造函数的依赖注入 和 基于 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...
}
请注意,这个类并没有什么特别之处。它是一个没有依赖于容器特定接口、基类或注解的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>
请注意,为了让此功能开箱即用,您的代码必须使用调试标志进行编译,以便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)
基于设置的依赖注入
基于设置方法的DI是通过容器在调用无参数构造函数或无参数static工厂方法实例化你的bean之后调用你的bean上的设置方法来完成的。
以下示例显示了一个只能通过使用纯setter注入进行依赖注入的类。这个类是常规的Java类。它是一个没有依赖于容器特定接口、基类或注解的POJO。
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 支持基于构造函数和基于setter的DI,用于管理它所管理的beans。在通过构造函数方法注入一些依赖项之后,它还支持基于setter的DI。您以 BeanDefinition 的形式配置依赖项,这与 PropertyEditor 实例一起使用,以将属性从一种格式转换为另一种格式。然而,大多数Spring用户并不直接(即编程方式)使用这些类,而是使用XML bean 定义、带注解的组件(即用 @Component、@Controller 等注解的类),或Java-based @Configuration 类中的 @Bean 方法。这些源随后被内部转换为 BeanDefinition 的实例,并用于加载整个Spring IoC容器实例。
依赖项解析过程
容器按照以下方式执行 bean 依赖项解析:
-
ApplicationContext是通过描述所有 bean 的配置元数据创建和初始化的。配置元数据可以通过 XML、Java 代码或注释来指定。 -
对于每个 bean,它的依赖项以属性、构造函数参数或静态工厂方法的参数形式表示(如果你使用的是静态工厂方法而不是普通构造函数)。这些依赖项在 bean 实际被创建时提供给该 bean。
-
每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个bean的引用。
-
每个属性或构造函数参数,如果是值类型,都会从指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将字符串格式提供的值转换为所有内置类型,例如
int、long、String、boolean等。
Spring容器在容器创建时验证每个bean的配置。 但是,直到bean实际创建之前,不会设置bean属性。 作用域为单例且设置为预实例化(默认)的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 被实例化(如果它不是预实例化的单例),其依赖项被设置,并且相关的生命周期方法(例如 配置的 init 方法 或 InitializingBean 回调方法)会被调用。
依赖注入示例
以下示例使用基于 XML 的配置元数据进行基于 setter 的 DI。Spring XML 配置文件的一小部分如下所示,指定了某些 bean 定义:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
下面的示例显示了相应的 ExampleBean 类:
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
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-namespace 以实现更简洁的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="root"
p:password="misterkaoli"/>
</beans>
前面的XML更加简洁。但是,除非您使用支持在创建 bean 定义时自动完成属性的 IDE(例如 IntelliJ IDEA 或 Eclipse 的 Spring 工具),否则拼写错误会在运行时而不是设计时被发现。这种 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 PropertyEditor机制,将<value/>元素中的文本转换为java.util.Properties实例。这是一项便捷的快捷方式,也是Spring团队倾向于使用嵌套的<value/>元素而不是value属性风格的几个地方之一。
元素 idref
idref 元素是一种简单且无错误的方法,用于将容器中另一个 bean 的 id(字符串值 - 不是引用)传递给 <constructor-arg/> 或 <property/>
元素。下面的示例显示了如何使用它:
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
前面的 bean 定义片段在运行时与以下片段完全等价:
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>
第一种形式比第二种更可取,因为使用<code>0</code>标签可以让容器在部署时验证所引用的命名Bean是否确实存在。在第二种变体中,不会对传递给<code>1</code>属性的值进行验证,该属性属于<code>2</code> Bean。拼写错误只有在实际实例化<code>3</code> Bean时才会被发现(很可能造成严重后果)。如果<code>4</code> Bean是一个<a t="C5">原型</a> Bean,那么此拼写错误和由此引发的异常可能在容器部署很久之后才会被发现。
local 属性在 idref 元素上在 4.0 beans
XSD 中已不再支持,因为其不再提供比常规 bean 引用更多的价值。在升级到 4.0 模式时,请将现有的 idref local 引用更改为 idref bean。 |
一个常见的情况(至少在Spring 2.0之前的版本中)是<idref/>元素带来的价值,是在配置ProxyFactoryBean bean定义中的AOP拦截器时。在指定拦截器名称时使用<idref/>元素可以防止拼写错误的拦截器ID。
对其他Bean的引用(协作对象)
ref 元素是 <constructor-arg/> 或 <property/> 定义元素内的最后一个元素。在这里,您将指定的 bean 属性的值设置为容器管理的另一个 bean(协作对象)的引用。被引用的 bean 是要设置其属性的 bean 的依赖项,并且在属性设置之前按需进行初始化。(如果协作对象是一个单例 bean,它可能已经被容器初始化了。)所有的引用最终都是对另一个对象的引用。作用域和验证取决于您是否通过 bean 或 parent 属性指定了其他对象的 ID 或名称。
通过 bean 属性指定目标 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>
local 属性在 ref 元素上在 4.0 beans
XSD 中已不再支持,因为其不再提供比常规 bean 引用更多的价值。在升级到 4.0 模式时,请将现有的 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 或名称。如果已指定,容器不会将该值用作标识符。由于内部 bean 始终是匿名的,并且始终与外部 bean 一起创建,因此容器在创建时会忽略 scope 标志。无法独立访问内部 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>
映射键或值,或者集合的值,也可以是以下元素中的任意一个:
bean | ref | idref | list | set | map | props | value | null
集合合并
Spring容器还支持合并集合。应用程序开发人员可以定义一个父级<code>0</code>、<code>1</code>、<code>2</code>或<code>3</code>元素,并让子级<code>4</code>、<code>5</code>、<code>6</code>或<code>7</code>元素从父级集合继承并覆盖值。也就是说,子集合的值是父集合和子集合元素的合并结果,其中子集合元素会覆盖父集合中指定的值。
本节讨论了父-子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>
请注意在 merge=true 元素的 <props/> 属性中使用了 adminEmails bean 定义的 child 属性。当容器解析并实例化 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属性注入到something bean时,通过反射可以获得强类型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-namespace 的 XML 快捷方式
p-命名空间允许你使用 bean 元素的属性(而不是嵌套的 <property/> 元素)来描述你的属性值协作的 bean,或者两者都有。
Spring 支持可扩展的配置格式 使用命名空间,
这些格式基于 XML 模式定义。本章讨论的 beans 配置格式是在 XML 模式文档中定义的。然而,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>
此示例显示了p命名空间中的一个属性,名为email在bean定义中。
这告诉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-namespace 的属性值,还使用了一种特殊格式来声明属性引用。第一个 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 快捷方式,c-命名空间在 Spring 3.1 中引入,允许内联属性来配置构造函数参数,而不是嵌套的 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: 相同的约定(通过名称设置构造函数参数时在末尾添加 -ref)。同样,它需要在 XML 文件中声明,尽管它不在 XSD 模式中定义(它存在于 Spring 核心中)。
在构造函数参数名称不可用的极少数情况下(通常是在编译时未包含调试信息的字节码),你可以使用参数索引作为替代,如下所示:
<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
c:_2="[email protected]"/>
由于XML语法,索引表示法需要以_开头,
因为XML属性名称不能以数字开头(尽管某些IDE允许这样做)。
对于<constructor-arg>元素也提供了相应的索引表示法,但由于声明的顺序通常已经足够,因此不常用。 |
在实际应用中,构造函数解析 机制 在匹配 参数方面非常高效,因此除非您确实需要,否则我们建议在整个配置中使用名称符号。
复合属性名称
在设置 bean 属性时,可以使用复合或嵌套的属性名称,只要路径中的所有组件(最后一个属性名称除外)都不是 null。考虑以下 bean 定义:
<bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>
该 something Bean 具有 fred 属性,该属性具有 bob 属性,该属性具有 sammy
属性,而最后一个 sammy 属性被设置为 123 的值。为了使这正常工作,
something 的 fred 属性和 fred 的 bob 属性在 Bean 构造后不能是
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" />
The depends-on attribute can specify both an initialization-time dependency and,
in the case of singleton beans only, a corresponding
destruction-time dependency. Dependent beans that define a depends-on relationship
with a given bean are destroyed first, prior to the given bean itself being destroyed.
Thus, depends-on can also control shutdown order. |
1.4.4. 懒加载的 Bean
默认情况下,ApplicationContext 实现会在初始化过程中急切地创建和配置所有
单例 bean。通常,这种预实例化是可取的,因为可以在配置或周围环境中的错误被立即发现,而不是在数小时甚至数天之后。当这种行为不可取时,可以通过将 bean 定义标记为延迟初始化来防止单例 bean 的预实例化。延迟初始化的 bean 会告诉 IoC 容器在首次请求时创建 bean 实例,而不是在启动时。
在 XML 中,此行为由 lazy-init 属性控制,该属性位于 <bean/> 元素上,如下例所示:
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>
当前面的配置被ApplicationContext消耗时,lazy bean 在ApplicationContext启动时不会被急切地预先实例化,
而not.lazy bean 会被急切地预先实例化。
然而,当一个延迟初始化的bean是另一个非延迟初始化的单例bean的依赖项时,ApplicationContext会在启动时创建这个延迟初始化的bean,因为它必须满足单例bean的依赖项。这个延迟初始化的bean会被注入到其他非延迟初始化的单例bean中。
您还可以通过在 <beans/> 元素上使用 default-lazy-init 属性,在容器级别控制延迟初始化,如下例所示:
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
1.4.5. 自动连线协作对象
Spring 容器可以自动连接协作的 bean。您可以通过检查 ApplicationContext 的内容,让 Spring 自动为您解析(其他 bean)。
自动装配具有以下优点:
-
自动装配可以显著减少指定属性或构造函数参数的需要。(其他机制,如本章其他地方讨论的bean模板 在这一点上也很有价值。)
-
自动装配可以随着对象的演变更新配置。例如,如果你需要向一个类添加依赖项,该依赖项可以自动满足,而无需你修改配置。因此,在开发过程中,自动装配尤其有用,而不会排除在代码库变得稳定后切换到显式装配的可能性。
在使用基于XML的配置元数据(参见 依赖注入)时,您可以使用 autowire 属性指定 bean 定义的自动装配模式。自动装配功能有四种模式。您可以在每个 bean 上指定自动装配,并因此选择要自动装配的那些。下表描述了四种自动装配模式:
| 模式 | 说明 |
|---|---|
|
(默认)不进行自动装配。Bean引用必须通过<code>0</code>元素定义。对于较大的部署,更改默认设置不建议,因为显式指定协作对象可以提供更大的控制和清晰度。在某种程度上,它能说明系统的结构。 |
|
按属性名称自动连线。Spring 会查找与需要自动连线的属性名称相同的 Bean。例如,如果一个 Bean 定义设置为按名称自动连线,并且它包含一个 |
|
如果容器中恰好存在一个与属性类型匹配的bean,则允许该属性进行自动注入。如果存在多个,则会抛出致命异常,这表明您可能不能对该bean使用 |
|
类似于 |
使用 byType 或 constructor 自动接线模式,可以接线数组和类型集合。在这种情况下,容器中所有符合预期类型的自动接线候选都会被提供以满足依赖关系。如果预期的键类型是 String,则可以自动接线强类型 Map 实例。自动接线的 Map 实例的值由所有符合预期类型的 bean 实例组成,而 Map 实例的键包含相应的 bean 名称。
自动装配的限制和缺点
自动注入在项目中一致使用时效果最佳。如果通常不使用自动注入,那么仅对一个或两个 bean 定义使用自动注入可能会让开发人员感到困惑。
考虑自动连线的局限性和缺点:
-
在
property和constructor-arg设置中的显式依赖项始终会覆盖自动连线。您无法自动连线简单属性,例如基本类型、Strings和Classes(以及这些简单属性的数组)。此限制是故意设计的。 -
自动装配不如显式连接精确。尽管如前所述的表格中所提到的,Spring在存在歧义的情况下会小心避免猜测,以免产生意外结果。你由Spring管理的对象之间的关系不再被显式地记录。
-
如果工具无法从Spring容器中生成文档,则可能无法获取布线信息。
-
容器中可能有多个Bean定义与要自动装配的setter方法或构造函数参数指定的类型匹配。对于数组、集合或
Map个实例来说,这不一定是个问题。然而,对于期望单个值的依赖项,这种歧义不会被随意解决。如果没有唯一的Bean定义可用,将抛出异常。
在后一种情况下,你有几个选项:
-
放弃自动装配,改用显式装配。
-
通过将bean定义的
autowire-candidate属性设置为false来避免自动连线,如< a t="C2">下一节所述。 -
通过将它的
<bean/>元素的primary属性设置为true,可以将单个bean定义指定为主要候选。 -
使用基于注解的配置提供的更细粒度的控制, 如 基于注解的容器配置 中所述。
排除自动装配的Bean
按每个Bean的设置,您可以将一个Bean排除在自动连线之外。在Spring的XML格式中,将autowire-candidate元素的<bean/>属性设置为false。容器会使得该特定的Bean定义无法被自动连线基础设施使用(包括诸如@Autowired之类的注解风格配置)。
The autowire-candidate attribute 仅用于影响基于类型的自动连线。
它不会影响按名称的显式引用,即使指定了的bean未被标记为自动连线候选,这些引用仍然会被解析。因此,如果名称匹配,按名称的自动连线仍然会注入一个bean。 |
您也可以根据基于对 bean 名称的模式匹配来限制自动连线候选。顶级 <beans/> 元素在其 default-autowire-candidates 属性中接受一个或多个模式。例如,要将自动连线候选状态限制为任何名称以 Repository 结尾的 bean,请提供值 *Repository。要提供多个模式,请在逗号分隔的列表中定义它们。bean 定义的 autowire-candidate 属性的显式值 true 或 false 总是具有优先权。对于这些 bean,模式匹配规则不适用。
这些技术适用于你永远不希望被自动注入到其他 bean 中的 bean。这并不意味着被排除的 bean 本身不能通过使用自动注入进行配置。而是说,该 bean 本身不是其他 bean 自动注入的候选对象。
1.4.6. 方法注入
在大多数应用场景中,容器中的大多数bean都是 单例。当一个单例bean需要与另一个单例bean协作,或者一个非单例bean需要与另一个非单例bean协作时,通常通过将其中一个bean定义为另一个的属性来处理依赖关系。当bean的生命周期不同时,就会出现问题。假设单例bean A需要使用非单例(原型)bean B,可能在每次调用A的方法时都需要。容器只创建单例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框架。方法注入是Spring IoC容器的一个较为高级的功能,它可以让您以更清晰的方式处理这种情况。
查找方法注入
查找方法注入是容器覆盖容器管理的bean上的方法并返回容器中另一个命名bean的查找结果的能力。查找通常涉及一个原型bean,如< a t="C0">上一节所述的情况。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 在需要 myCommand Bean 的新实例时会调用其自身的 createCommand() 方法。您必须小心,如果确实需要的话,应将 myCommand Bean 部署为原型。如果是 单例,每次都会返回 myCommand Bean 的同一实例。
或者,在基于注解的组件模型中,你可以通过 @Lookup 注解声明一个查找方法,如下例所示:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
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
}
请注意,通常应使用具体的存根实现来声明此类带注解的查找方法,以便它们与 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类级别上固定对象的作用域。可以定义beans以在多种作用域中部署。 Spring框架支持六种作用域,其中四种只有在使用web感知的<code>0</code>时才可用。您还可以创建 自定义作用域。
下表描述了支持的作用域:
| 作用域 | 描述 |
|---|---|
(默认)将单个bean定义作用于每个Spring IoC容器的单个对象实例。 |
|
将单个 bean 定义作用于任意数量的对象实例。 |
|
将单个Bean定义的作用域限定为单个HTTP请求的生命周期。也就是说,
每个HTTP请求都会根据一个单一的Bean定义创建一个Bean的实例。
仅在具有Web感知能力的Spring |
|
将单个Bean定义的作用域限定为HTTP |
|
将单个bean定义的作用域限定为 |
|
将单个bean定义的作用域限定为 |
从 Spring 3.0 开始,提供了一个线程作用域,但默认情况下未注册。有关更多信息,请参阅
SimpleThreadScope。
有关如何注册此作用域或其他自定义作用域的说明,请参阅
使用自定义作用域。 |
1.5.1. 单例作用域
只有一个共享的单例 bean 实例被管理,所有请求 ID 或 IDs 与该 bean 定义匹配的 bean 的请求,都会导致 Spring 容器返回该特定的 bean 实例。
换句话说,当你定义一个 Bean 定义,并且它的作用域设置为 singleton 时,Spring IoC 容器会为该 Bean 定义所定义的对象创建一个实例。这个单一实例被存储在一个 singleton Bean 的缓存中,所有后续对该命名 Bean 的请求和引用都会返回缓存中的对象。下图展示了 singleton 作用域的工作方式:

Spring的单例bean概念与Gang of Four(GoF)模式书中定义的单例模式不同。GoF单例硬编码了对象的作用域,使得每个类加载器只创建一个特定类的实例。Spring单例的作用域最好描述为按容器和按bean来确定。这意味着,如果你在单个Spring容器中为某个类定义了一个bean,Spring容器将只创建该bean定义所定义的类的一个实例。单例作用域是Spring的默认作用域。要在XML中将一个bean定义为单例,可以按照以下示例定义一个bean:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
1.5.2. 原型作用域
Bean部署的非单例原型作用域会导致每次请求该特定Bean时都创建一个新的Bean实例。也就是说,该Bean被注入到另一个Bean中,或者您通过容器上的getBean()方法调用请求它。通常情况下,您应该将原型作用域用于所有有状态的Bean,将单例作用域用于无状态的Bean。
下图展示了 Spring 原型作用域:

数据访问对象(DAO)通常不会被配置为原型,因为典型的DAO不保存任何会话状态。对我们来说,复用单例模式的核心更容易。
以下示例在 XML 中将 bean 定义为原型:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他作用域不同,Spring 不管理原型(prototype)Bean 的完整生命周期。容器会实例化、配置并组装原型对象,然后将其交给客户端,而不再记录该原型实例。因此,尽管无论作用域如何都会调用初始化生命周期回调方法,但在原型作用域的情况下,配置的销毁生命周期回调方法不会被调用。客户端代码必须清理原型作用域的对象,并释放原型 Bean 所持有的昂贵资源。要让 Spring 容器释放原型作用域 Bean 所持有的资源,请尝试使用自定义的 bean 后处理器,该处理器保存需要清理的 Bean 的引用。
在某些方面,Spring容器对于原型作用域的bean的作用是Java new运算符的替代。在此之后的所有生命周期管理都必须由客户端处理。(有关Spring容器中bean的生命周期的详细信息,请参阅生命周期回调。)
1.5.3. 具有原型 bean 依赖项的单例 bean
当您使用依赖于原型作用域Bean的单例作用域Bean时,请注意依赖关系是在实例化时解析的。因此,如果您将一个原型作用域的Bean注入到一个单例作用域的Bean中,会实例化一个新的原型Bean,然后将其依赖注入到单例Bean中。该原型实例是唯一提供给单例作用域Bean的实例。
然而,假设你希望单例作用域的bean在运行时反复获取原型作用域bean的新实例。你不能将原型作用域的bean依赖注入到你的单例bean中,因为这种注入只会在Spring容器实例化单例bean并解析和注入其依赖项时发生一次。如果你需要在运行时多次获取原型bean的新实例,请参阅方法注入。
1.5.4. 请求、会话、应用程序和 WebSocket 范围
request、session、application 和 websocket 范围仅在您使用基于网络的 Spring ApplicationContext 实现时可用(例如 XmlWebApplicationContext)。如果您将这些范围与常规 Spring IoC 容器一起使用,例如 ClassPathXmlApplicationContext,则会抛出一个指出未知 bean 范围的 IllegalStateException。
初始 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 上。这使得请求作用域和会话作用域的 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 容器通过使用单个 HTTP UserPreferences 的 userPreferences bean 定义来创建 Session bean 的新实例。换句话说,userPreferences bean 在 HTTP Session 层面实际上是作用域限定的。与请求作用域的 bean 一样,您可以随意更改由该 Session bean 定义创建的实例的内部状态,知道其他也使用从同一 userPreferences bean 定义创建的实例的 HTTP Session 实例不会看到这些状态更改,因为它们是特定于单个 HTTP Session 的。当 HTTP Session 最终被丢弃时,作用域限定于该特定 HTTP 10 的 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容器通过使用AppPreferences bean定义为整个Web应用程序创建一个新的appPreferences bean实例。也就是说,appPreferences bean的作用域在ServletContext级别,并作为常规的ServletContext属性存储。这在某种程度上类似于Spring单例bean,但在两个重要方面有所不同:它是每个ServletContext的单例,而不是每个SpringApplicationContext的单例(在任何给定的Web应用程序中可能有多个),并且它实际上是公开的,因此作为ServletContext属性可见。
在使用注解驱动的组件或Java配置时,可以使用
@ApplicationScope 注解将组件分配到 application 范围。下面的示例显示了如何操作:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
作用域Bean作为依赖项
Spring IoC 容器不仅管理你的对象(Bean)的实例化, 还负责协作对象(或依赖项)的连接。如果你希望将一个 HTTP 请求作用域的 Bean 注入到另一个作用域更长的 Bean 中,你可以选择注入一个 AOP 代理来代替该作用域的 Bean。也就是说,你需要注入一个代理对象,该对象暴露与作用域对象相同的公共接口,但也可以从相关作用域(如 HTTP 请求)中检索真正的目标对象,并将方法调用委托给真实对象。
|
您还可以在作用域为 在将 此外,作用域代理并不是以生命周期安全的方式从较短作用域中访问Bean的唯一方法。您还可以将您的注入点(即构造函数或设置器参数或自动连线字段)声明为 作为扩展变体,您可以声明 此JSR-330变体称为 |
以下示例中的配置只有一行,但理解其背后的“为什么”以及“如何做”是很重要的:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/> (1)
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.something.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
| 1 | 定义代理的行。 |
要创建这样的代理,您需要将子<aop:scoped-proxy/>元素插入到作用域的bean定义中(参见选择要创建的代理类型和基于XML Schema的配置)。
为什么作用域为request、session和自定义作用域级别的bean定义需要<aop:scoped-proxy/>元素?
考虑以下单例bean定义,并与上述作用域所需的定义进行对比(请注意,以下userPreferences bean定义目前是不完整的):
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
在前面的示例中,单例Bean(userManager)被注入了一个对HTTP Session作用域Bean(userPreferences)的引用。这里的关键点是,userManager Bean是一个单例:它在每个容器中仅实例化一次,其依赖项(在这种情况下只有一个,即userPreferences Bean)也仅注入一次。这意味着userManager Bean仅操作相同的userPreferences对象(即它最初被注入的那个对象)。
这不是在将生命周期较短的作用域Bean注入到生命周期较长的作用域Bean时所期望的行为(例如,将HTTP Session作用域的协作Bean作为依赖项注入到单例Bean中)。相反,你需要一个userManager对象,并且在HTTP Session的生命周期内,你需要一个特定于HTTP Session的userPreferences对象。因此,容器会创建一个暴露与UserPreferences类完全相同公共接口的对象(最好是UserPreferences实例),该对象可以从作用域机制(HTTP请求,Session等)中获取真正的UserPreferences对象。容器将这个代理对象注入到userManager Bean中,而该Bean并不知道这个UserPreferences引用是一个代理。在这个例子中,当UserManager实例调用依赖注入的UserPreferences对象上的方法时,实际上是在调用代理上的方法。然后代理会从(在此情况下)HTTP Session中获取真正的UserPreferences对象,并将方法调用委托给获取到的真正的UserPreferences对象。
因此,当将request-和session-scoped beans 注入到协作对象中时,你需要以下(正确且完整的)配置,如下例所示:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
选择要创建的代理类型
默认情况下,当Spring容器为一个使用<aop:scoped-proxy/>元素标记的Bean创建代理时,会创建一个基于CGLIB的类代理。
|
CGLIB 代理仅拦截公共方法调用!不要在这样的代理上调用非公共方法。它们不会被委托给实际的范围目标对象。 |
或者,您可以配置Spring容器,为这些作用域的bean创建标准JDK接口代理,通过将false指定为proxy-target-class属性的值,该属性属于<aop:scoped-proxy/>元素。使用基于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接口,本
Scope
实现以及
Scope javadoc,
其中更详细地解释了您需要实现的方法。
Scope 接口有四个方法可以从作用域中获取对象、从作用域中移除它们,并让它们被销毁。
会话范围的实现,例如,返回会话作用域的 bean(如果它不存在,该方法将返回该 bean 的新实例,并将其绑定到会话中以供以后引用)。以下方法从底层作用域中返回对象:
Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any
会话范围的实现,例如,会从底层会话中删除会话范围的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 的具体 ApplicationContext 实现的 BeanFactory 属性获得。
方法 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)
然后,您可以创建符合您自定义作用域规则的bean定义,
Scope,如下所示:
<bean id="..." class="..." scope="thread">
使用自定义的 Scope 实现,您不限于通过编程方式注册作用域。您还可以通过使用 Scope 类,以声明的方式进行 CustomScopeConfigurer 注册,如下例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="thing2" class="x.y.Thing2" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="thing1" class="x.y.Thing1">
<property name="thing2" ref="thing2"/>
</bean>
</beans>
当您将 <aop:scoped-proxy/> 放入 <bean> 声明中以用于 FactoryBean 实现时,作用域是工厂 Bean 本身,而不是从 getObject() 返回的对象。 |
1.6. 自定义Bean的性质
Spring Framework 提供了许多接口,您可以使用它们来定制 bean 的性质。本节将其按如下方式分组:
1.6.1. 生命周期回调
要与容器对bean生命周期的管理进行交互,可以实现Spring InitializingBean和DisposableBean接口。容器会调用afterPropertiesSet()用于前者,destroy()用于后者,以便在您的beans初始化和销毁时让bean执行某些操作。
|
JSR-250 如果您不希望使用 JSR-250 注释但仍希望解耦,请考虑 |
内部,Spring 框架使用 BeanPostProcessor 个实现来处理它能找到的任何回调接口并调用相应的方法。如果您需要 Spring 默认不提供的自定义功能或其他生命周期行为,您可以自己实现一个 BeanPostProcessor。有关更多信息,请参阅
容器扩展点。
除了初始化和销毁回调之外,Spring管理的对象还可以实现 Lifecycle 接口,以便这些对象可以参与由容器自身生命周期驱动的启动和关闭过程。
生命周期回调接口在此部分中进行描述。
初始化回调
org.springframework.beans.factory.InitializingBean 接口允许在容器对 bean 设置所有必要属性之后,让 bean 执行初始化工作。InitializingBean 接口指定了一个方法:
void afterPropertiesSet() throws Exception;
我们建议不要使用 InitializingBean 接口,因为它不必要地将代码与 Spring 耦合。或者,我们建议使用 @PostConstruct 注解或指定一个 POJO 初始化方法。在基于 XML 的配置元数据情况下,可以使用 init-method 属性来指定一个具有 void 无参数签名的方法名称。对于 Java 配置,可以使用 initMethod 属性的 @Bean。参见 接收生命周期回调。考虑以下示例:
<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耦合。
您可以将 destroy-method 元素的 <bean> 属性分配一个特殊的
(inferred) 值,这会指示 Spring 自动检测特定 bean 类上的公共 close 或
shutdown 方法。(任何实现 java.lang.AutoCloseable 或 java.io.Closeable 的类都会匹配。)您也可以在
default-destroy-method 属性上设置此特殊 (inferred) 值,以将此行为应用于一组 beans(参见
默认初始化和销毁方法)。请注意,这是使用 Java 配置时的默认行为。 |
默认初始化和销毁方法
当你编写不使用Spring特定的InitializingBean和DisposableBean回调接口的初始化和销毁方法回调时,通常会编写诸如init()、initialize()、dispose()等名称的方法。理想情况下,这些生命周期回调方法的名称应在整个项目中标准化,以便所有开发人员使用相同的 方法名称并确保一致性。
您可以配置Spring容器以“查找”每个bean上的命名初始化和销毁回调方法名称。这意味着作为应用程序开发人员,您可以编写应用程序类并使用名为init()的初始化回调,而无需为每个bean定义配置init-method="init"属性。Spring IoC容器在创建bean时(并根据前面所述的标准生命周期回调约定描述)会调用该方法。此功能还对初始化和销毁方法回调强制执行一致的命名约定。
假设你的初始化回调方法命名为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>
在顶级 <beans/> 元素上存在 default-init-method 属性会导致 Spring IoC 容器将 bean 类上的方法 init 识别为初始化方法回调。当创建和组装 bean 时,如果 bean 类具有此类方法,它会在适当的时候被调用。
您可以类似地配置销毁方法回调(在XML中,即)通过在顶级<beans/>元素上使用default-destroy-method属性。
如果现有的 bean 类已经具有与约定不同的命名的回调方法,可以通过使用 init-method 和 destroy-method 属性在 XML 中指定方法名称来覆盖默认值。
Spring容器保证在bean获得所有依赖项之后立即调用配置的初始化回调。因此,初始化回调是在原始bean引用上调用的,这意味着AOP拦截器等尚未应用于该bean。首先完全创建目标bean,然后应用AOP代理(例如)及其拦截器链。如果目标bean和代理是单独定义的,您的代码甚至可以与原始目标bean交互,绕过代理。因此,对init方法应用拦截器将是不一致的,因为这样做会将目标bean的生命周期与其代理或拦截器耦合,并且当您的代码直接与原始目标bean交互时,会导致奇怪的语义。
结合生命周期机制
从 Spring 2.5 开始,您有三种选项可以控制 bean 的生命周期行为:
-
InitializingBean和DisposableBean回调接口 -
自定义
init()和destroy()方法 -
关于
@PostConstruct和@PreDestroy注解。您可以将这些机制结合起来以控制给定的 bean。
如果为一个 bean 配置了多种生命周期机制,并且每种机制都配置了不同的方法名称,那么在本注释之后列出的顺序中运行每个配置的方法。但是,如果同一方法名称被配置为这些生命周期机制中的多个机制 — 例如,初始化方法配置为init() — 则该方法只运行一次,如前面的章节所述。 |
对于同一个 bean 配置了多种生命周期机制,使用不同的初始化方法时,调用方式如下:
-
使用
@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 接口定义了另一种选项,即在其超接口 Phased 上定义的 getPhase() 方法。下面的列表显示了 Phased 接口的定义:
public interface Phased {
int getPhase();
}
以下列表显示了 SmartLifecycle 接口的定义:
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}
在启动时,相位最低的对象会先启动。在停止时,会遵循相反的顺序。因此,一个实现了 SmartLifecycle 并且其 getPhase() 方法返回 Integer.MIN_VALUE 的对象会是最早启动的,并且最后停止。在另一方面,相位值为 Integer.MAX_VALUE 表示该对象应该最后启动,最先停止(可能因为它依赖于其他进程正在运行)。在考虑相位值时,还有一点很重要,即任何未实现 SmartLifecycle 的“正常”Lifecycle 对象的默认相位是 0。因此,任何负数相位值表示该对象应在标准组件之前启动(并在之后停止)。正数相位值则相反。
由SmartLifecycle定义的stop方法接受一个回调。任何实现都必须在其关闭过程完成后调用该回调的run()方法。这使得在需要时可以进行异步关闭,因为LifecycleProcessor接口的默认实现DefaultLifecycleProcessor会在每个阶段中等待其对象组调用该回调,最多等待其超时值。每个阶段的默认超时时间为30秒。您可以通过在上下文中定义一个名为lifecycleProcessor的bean来覆盖默认的生命周期处理器实例。如果您只想修改超时时间,定义以下内容即可:
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
<!-- timeout value in milliseconds -->
<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>
如前所述,LifecycleProcessor 接口为上下文的刷新和关闭定义了回调方法。后者会触发关闭过程,就像显式调用了 stop() 一样,但发生于上下文关闭时。另一方面,'refresh' 回调启用了 SmartLifecycle beans 的另一个功能。当上下文被刷新(在所有对象都被实例化和初始化之后)时,会调用该回调。此时,默认的生命周期处理器会检查每个 SmartLifecycle 对象的 isAutoStartup() 方法返回的布尔值。如果为 true,则会在该点启动该对象,而不是等待显式调用上下文或其自身的 start() 方法(与上下文刷新不同,标准上下文实现不会自动进行上下文启动)。phase 值和任何“depends-on”关系决定了如前所述的启动顺序。
在非Web应用程序中优雅地关闭Spring IoC容器
|
本节仅适用于非Web应用程序。Spring的基于Web的
|
如果你在非Web应用环境中使用Spring的IoC容器(例如,在富客户端桌面环境中),请向JVM注册一个关闭钩子。这样可以确保优雅地关闭,并在单例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耦合,并且不符合控制反转风格,其中协作对象作为属性提供给bean。ApplicationContext的其他方法提供了对文件资源的访问、发布应用事件和访问MessageSource的功能。这些附加功能在 ApplicationContext的其他功能中进行了描述。
自动注入是获取对ApplicationContext的引用的另一种方法。传统的constructor和byType自动注入模式(如< a t="C8">自动注入协作对象中所述)可以分别为构造函数参数或setter方法参数提供类型为36注解时,ApplicationContext将被自动注入到期望ApplicationContext类型的字段、构造函数参数或方法参数中。有关更多信息,请参见< a t="C9">使用@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 回调接口,让 beans 可以向容器表明它们需要某种基础架构依赖。一般来说,名称表示依赖类型。下表总结了最重要的 Aware 接口:
| 名称 | 注入的依赖项 | 解释在…… |
|---|---|---|
|
声明 |
|
|
封闭的 |
|
|
用于加载 bean 类的类加载器。 |
|
|
声明 |
|
|
声明该 bean 的名称。 |
|
|
资源适配器 |
|
|
定义用于在加载时处理类定义的织入器。 |
|
|
用于解析消息的配置策略(支持参数化和国际化)。 |
|
|
Spring JMX 通知发布器。 |
|
|
用于低级别访问资源的配置加载器。 |
|
|
当前 |
|
|
当前 |
再次提醒,使用这些接口会将你的代码与 Spring API 绑定,并不遵循控制反转风格。因此,我们建议它们用于需要程序化访问容器的基础架构 Bean。
1.7. Bean定义继承
一个 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定义从父Bean继承作用域、构造函数参数值、属性值和方法覆盖,同时可以选择添加新值。您指定的任何作用域、初始化方法、销毁方法或<code>0</code>工厂方法设置都会覆盖相应的父设置。
其余的设置始终来自子定义:depends on、自动装配模式、依赖检查、单例和延迟初始化。
前面的示例通过使用abstract属性显式地将父Bean定义标记为抽象。如果父定义没有指定类,则需要显式地将父Bean定义标记为abstract,如下例所示:
<bean id="inheritedTestBeanWithoutClass" abstract="true">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBeanWithoutClass" init-method="initialize">
<property name="name" value="override"/>
<!-- age will inherit the value of 1 from the parent bean definition-->
</bean>
父级Bean不能单独实例化,因为它不完整,并且也被显式标记为abstract。当一个定义是abstract时,它只能用作纯模板Bean定义,作为子定义的父定义。尝试通过将它作为另一个Bean的ref属性引用,或使用父级Bean ID进行显式的getBean()调用,来单独使用这样的abstract父级Bean会返回错误。同样,容器的内部preInstantiateSingletons()方法会忽略被定义为抽象的Bean。
ApplicationContext 默认预先实例化所有单例。因此,对于单例 bean 来说,这一点很重要。如果你有一个(父)bean 定义,你只打算作为模板使用,并且此定义指定了一个类,那么你必须确保将 abstract 属性设置为 true,否则应用程序上下文实际上(会尝试)预先实例化 abstract 个 bean。 |
1.8. 容器扩展点
通常,应用程序开发人员不需要继承 ApplicationContext
实现类。相反,可以通过插入特殊集成接口的实现来扩展Spring IoC容器。接下来的几个部分将介绍这些集成接口。
1.8.1. 使用 BeanPostProcessor 自定义 Bean
BeanPostProcessor 接口定义了您可以实现的回调方法,以提供您自己的(或覆盖容器的默认)实例化逻辑、依赖项解析逻辑等。如果您希望在 Spring 容器完成 bean 的实例化、配置和初始化之后实现一些自定义逻辑,您可以插入一个或多个自定义的 BeanPostProcessor 实现。
您可以配置多个 BeanPostProcessor 实例,并可以通过设置 order 属性来控制这些 BeanPostProcessor 实例的运行顺序。
只有当 BeanPostProcessor 实现了 Ordered 接口时,才可以设置此属性。如果您编写了自己的 BeanPostProcessor,也应该考虑实现 Ordered 接口。有关更多详细信息,请参阅
BeanPostProcessor
和 Ordered 接口的 javadoc。另请参阅关于
编程方式注册 BeanPostProcessor 实例 的说明。
|
要更改实际的bean定义(即定义bean的蓝图),
您需要使用 |
The org.springframework.beans.factory.config.BeanPostProcessor接口包含两个回调方法。当此类作为后处理器注册到容器中时,对于容器创建的每个bean实例,后处理器会在容器初始化方法(如InitializingBean.afterPropertiesSet()或任何声明的init方法)被调用之前和之后,从容器获得回调。后处理器可以对bean实例执行任何操作,包括完全忽略回调。一个bean后处理器通常会检查回调接口,或者它可能会用代理包装一个bean。一些Spring AOP基础设施类是作为bean后处理器实现的,以提供代理包装逻辑。
一个 ApplicationContext 会自动检测配置元数据中定义的任何实现 BeanPostProcessor 接口的 beans。该 ApplicationContext 会将这些 beans 注册为后处理器,以便在 bean 创建时调用。bean 后处理器可以以与其他 beans 相同的方式部署到容器中。
请注意,当通过在配置类上使用BeanPostProcessor工厂方法声明一个@Bean时,工厂方法的返回类型应该是实现类本身,或者是至少org.springframework.beans.factory.config.BeanPostProcessor接口,以明确表明该bean的后处理器性质。否则,ApplicationContext在完全创建之前无法通过类型自动检测到它。
由于BeanPostProcessor需要在上下文中的其他bean初始化之前早期实例化,因此这种早期类型检测至关重要。
|
以编程方式注册 虽然推荐的方法是BeanPostProcessor 个实例BeanPostProcessor注册是通过ApplicationContext自动检测(如前所述),您可以将它们编程方式注册到一个ConfigurableBeanFactory通过使用addBeanPostProcessor方法。这在你需要注册前评估条件逻辑时很有用,甚至可以在层次结构中的不同上下文之间复制 bean 后处理器。但是请注意,BeanPostProcessor通过编程添加的实例不遵守
该Ordered接口。在这里,注册的顺序决定了执行的顺序。还要注意的是BeanPostProcessor通过编程注册的实例总是优先于通过自动检测注册的实例进行处理,无论是否有显式排序。 |
BeanPostProcessor 个实例和AOP自动代理实现 对于此类bean,您应该会看到一条信息日志消息: 如果您通过使用自动装配或 |
以下示例显示了如何在 ApplicationContext 中编写、注册和使用 BeanPostProcessor 实例
示例:Hello World,BeanPostProcessor样式
第一个示例说明了基本用法。该示例显示了一个自定义
BeanPostProcessor 实现,在容器创建每个 bean 时调用其
toString() 方法,并将生成的字符串打印到系统控制台。
以下列表显示了自定义 BeanPostProcessor 实现类的定义:
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// simply return the instantiated bean as-is
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean; // we could potentially return any object reference here...
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
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. 使用 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工厂后处理器,以对定义容器的配置元数据进行更改。Spring包含多个预定义的bean工厂后处理器,例如PropertyOverrideConfigurer和PropertySourcesPlaceholderConfigurer。您还可以使用自定义的BeanFactoryPostProcessor——例如,用于注册自定义属性编辑器。
一个 ApplicationContext 会自动检测部署到其中的任何实现 BeanFactoryPostProcessor 接口的 bean。它会在适当的时候将这些 bean 用作 bean 工厂后处理器。您可以像部署其他 bean 一样部署这些后处理器 bean。
与 BeanPostProcessor 一样,你通常不希望为延迟初始化配置 BeanFactoryPostProcessor。如果没有其他 bean 引用 Bean(Factory)PostProcessor,那么该后处理器根本不会被实例化。
因此,将其标记为延迟初始化将被忽略,即使你在声明 <beans /> 元素时将 default-lazy-init 属性设置为 true,Bean(Factory)PostProcessor 也会被立即实例化。 |
示例:类名替换 PropertySourcesPlaceholderConfigurer
您可以使用 PropertySourcesPlaceholderConfigurer 通过使用标准 Java Properties 格式,将 bean 定义中的属性值外部化到单独的文件中。
这样做的好处是,部署应用程序的人可以在不修改容器的主要 XML 定义文件或文件的情况下,自定义特定于环境的属性,例如数据库 URL 和密码,从而避免复杂性或风险。
考虑以下基于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时(即对于非延迟初始化的bean,在 |
示例: PropertyOverrideConfigurer
PropertyOverrideConfigurer,另一个 bean 工厂后处理器,类似于 PropertySourcesPlaceholderConfigurer,但与后者不同的是,原始定义可以为 bean 属性提供默认值或根本没有值。如果覆盖的 Properties 文件中没有某个 bean 属性的条目,则使用默认上下文定义。
请注意,bean定义并不知道它已被覆盖,因此从XML定义文件中无法立即看出正在使用覆盖配置器。如果多个<code>0</code>实例为同一bean属性定义了不同的值,则由于覆盖机制,最后一个会生效。
属性文件配置行的格式如下:
beanName.property=value
以下列表显示了该格式的示例:
dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql:mydb
此示例文件可以与包含名为
dataSource 的 bean 的容器定义一起使用,该 bean 具有 driver 和 url 属性。
也支持复合属性名称,只要路径中除了要覆盖的最后一个属性外,其他每个组件都已经不是 null(可能是由构造函数初始化的)。在下面的例子中,tom bean 的 fred 属性的 bob 属性的 sammy 属性被设置为标量值 123:
tom.fred.bob.sammy=123
| 指定的覆盖值始终是字面值。它们不会被转换为bean引用。当XML bean定义中的原始值指定一个bean引用时,此惯例同样适用。 |
随着Spring 2.5中引入的<code>0</code>命名空间,可以通过专用的配置元素配置属性覆盖,如下例所示:
<context:property-override location="classpath:override.properties"/>
1.8.3. 使用 FactoryBean 自定义实例化逻辑
您可以为本身是工厂的对象实现 org.springframework.beans.factory.FactoryBean 接口。
FactoryBean 接口是 Spring IoC 容器实例化逻辑的可扩展点。如果您有复杂的初始化代码,与使用可能相当冗长的 XML 相比,更适合用 Java 表达,那么您可以创建自己的 FactoryBean,在该类中编写复杂的初始化代码,然后将自定义的 FactoryBean 插入到容器中。
FactoryBean<T> 接口提供三个方法:
-
T getObject(): 返回此工厂创建的对象的实例。根据此工厂返回单例或原型,该实例可能是共享的。 -
boolean isSingleton(): 如果此FactoryBean返回单例,则返回true,否则返回false。此方法的默认实现返回true。 -
Class<?> getObjectType(): 返回getObject()方法返回的对象类型,或在事先不知道类型时返回null。
FactoryBean 概念和接口在 Spring 框架的许多地方都有使用。Spring 本身提供了超过 50 个 FactoryBean 接口的实现。
当您需要让容器返回实际的 FactoryBean 实例本身,而不是它生成的 bean 时,在调用 getBean() 方法时,应将 bean 的 id 前缀加上与号符号(&)。因此,对于一个具有 FactoryBean 的 id,其 myBean 为 getBean("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增加了对JSR-330(Java依赖注入)注解的支持,这些注解包含在javax.inject包中,如@Inject和@Named。有关这些注解的详细信息可以在相关部分找到。
|
注解注入在XML注入之前执行。因此,XML配置会覆盖通过这两种方法连接的属性的注解。 |
像往常一样,您可以将后处理器作为单独的bean定义进行注册,但也可以通过在基于XML的Spring配置中包含以下标记来隐式注册(注意包含context命名空间):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
</beans>
<context:annotation-config/> 元素会隐式注册以下后处理器:
|
|
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或组件类,容器通常会提前知道具体的类型。但是,对于 |
您还可以通过将 @Autowired 注解添加到期望该类型数组的字段或方法上来指示 Spring 从 ApplicationContext 提供该类型的全部 beans,如下例所示:
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。
映射值包含所有预期类型的 beans,而键包含相应的 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
// ...
}
|
Spring 的 |
1.9.3. 使用 @Primary 对基于注解的自动连线进行微调
由于按类型自动连线可能导致多个候选项,因此通常需要对选择过程有更多控制。一种实现此目的的方法是使用Spring的
@Primary注解。@Primary表示当多个bean是单值依赖项的自动连线候选项时,应优先考虑特定的bean。如果候选项中恰好有一个主要bean,它将成为自动连线的值。
考虑以下配置,其中 firstMovieCatalog 被定义为
主 MovieCatalog:
@Configuration
public class MovieConfiguration {
@Bean
@Primary
public MovieCatalog firstMovieCatalog() { ... }
@Bean
public MovieCatalog secondMovieCatalog() { ... }
// ...
}
@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 定义的情况下(如前面示例中的情况)可能会被自动生成。
限定符也适用于类型化集合,如前所述——例如,对Set<MovieCatalog>。在这种情况下,根据声明的限定符,所有匹配的 bean 会被注入为一个集合。这意味着限定符不必是唯一的。相反,它们构成过滤条件。例如,您可以定义多个带有相同限定符值“action”的MovieCatalog bean,所有这些 bean 都会注入到用@Qualifier("action")注解的Set<MovieCatalog>中。
|
允许限定符值在类型匹配的候选者中根据目标Bean名称进行选择,不需要在注入点使用<code>0</code>注解。 如果没有其他解析指示器(如限定符或主标记),在非唯一依赖情况下,Spring会将注入点名称(即字段名或参数名)与目标Bean名称进行匹配,并选择同名的候选者,如果有的话。 |
这样说的话,如果你打算通过名称来表达注解驱动的注入,请不要主要使用@Autowired,即使它能够根据bean名称在类型匹配的候选者中进行选择。相反,应使用JSR-250 @Resource注解,该注解语义上被定义为通过其唯一名称来识别特定的目标组件,声明的类型对于匹配过程是无关的。@Autowired具有不同的语义:在根据类型选择候选bean之后,指定的String限定符值仅在这些类型选择的候选者中考虑(例如,将account限定符与带有相同限定符标签的bean进行匹配)。
对于本身定义为集合、Map 或数组类型的 Bean,@Resource 是一个很好的解决方案,可以通过唯一名称引用特定的集合或数组 Bean。
不过,从 4.3 版本开始,只要元素类型信息在 @Bean 的返回类型签名或集合继承层次结构中得到保留,你也可以通过 Spring 的 @Autowired 类型匹配算法来匹配集合、Map 和数组类型。
在这种情况下,你可以使用限定符值来在相同类型的集合之间进行选择,如上一段所述。
从 4.3 版本开始,@Autowired 也考虑自引用的注入(即对当前注入的 bean 的引用)。请注意,自引用是一种回退机制。对其他组件的常规依赖始终具有优先权。从这个意义上说,自引用不参与常规候选选择,因此它们绝不会是主要的。相反,它们总是具有最低的优先级。在实际应用中,您应仅在最后不得已的情况下使用自引用(例如,通过 bean 的事务代理调用同一实例上的其他方法)。在这种情况下,可以将受影响的方法提取到一个单独的委托 bean 中。或者,您可以使用 @Resource,这可以通过其唯一名称获取对当前 bean 的代理。
|
尝试从同一配置类上的 |
@Autowired 适用于字段、构造函数和多参数方法,允许通过限定符注解在参数级别进行缩小。相比之下,@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以匹配您的自定义限定符注解。类型将与注解的全限定类名进行匹配。或者,如果不存在名称冲突的风险,可以使用简短的类名。下面的例子演示了这两种方法:
<?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定义应包含匹配的限定符值。此示例还表明,您可以使用bean元属性而不是<qualifier/>元素。如果存在,<qualifier/>元素及其属性具有优先权,但如果未提供此类限定符,自动连线机制将回退到<meta/>标签中提供的值,如下面示例中的最后两个bean定义所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean class="example.SimpleMovieCatalog">
<qualifier type="MovieQualifier">
<attribute key="format" value="VHS"/>
<attribute key="genre" value="Action"/>
</qualifier>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier type="MovieQualifier">
<attribute key="format" value="VHS"/>
<attribute key="genre" value="Comedy"/>
</qualifier>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<meta key="format" value="DVD"/>
<meta key="genre" value="Action"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<meta key="format" value="BLURAY"/>
<meta key="genre" value="Comedy"/>
<!-- inject any dependencies required by this bean -->
</bean>
</beans>
1.9.5. 使用泛型作为自动连线限定符
除了 @Qualifier 注解之外,您还可以使用 Java 泛型类型作为隐式的资格说明。例如,假设您有以下配置:
@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()
}
假设前面的 beans 实现了一个通用接口(即,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符合自动连线候选条件时,确定“主要”bean的规则如下:如果候选条件中的bean定义恰好有一个的<code>0</code>属性设置为<code>1</code>,则会选择该bean。
1.9.7. 使用 @Resource 注入
Spring 还支持通过使用 JSR-250 @Resource 注解
(javax.annotation.Resource) 在字段或 bean 属性的 setter 方法上进行注入。
这是 Java EE 中的一种常见模式:例如,在 JSF 管理的 bean 和 JAX-WS
端点中。Spring 也支持此模式用于 Spring 管理的对象。
@Resource 需要一个 name 属性。默认情况下,Spring 会将该值解释为要注入的 bean 名称。换句话说,它遵循按名称语义,如下面的示例所示:
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属性名。下面的示例将在它的setter方法中注入名为movieFinder的bean:
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 了解该名称。
如果配置 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,它将从 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 引入这些注解的支持,为在 初始化回调 和 销毁回调 中描述的生命周期回调机制提供了一种替代方案。只要 CommonAnnotationBeanPostProcessor 在 Spring ApplicationContext 中注册,带有其中任何一个注解的方法就会在生命周期中的相同点被调用,与相应的 Spring 生命周期接口方法或显式声明的回调方法相同。在下面的例子中,缓存在初始化时被预填充,在销毁时被清除:
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
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 注解它们,你的类更适合被工具处理或与方面相关联。例如,这些类型注解是切入点的理想目标。 @Repository、@Service 和 @Controller 在 Spring 框架的未来版本中也可能包含额外的语义。因此,如果你在选择在服务层使用 @Component 或 @Service,@Service 显然是更好的选择。同样,如前所述,@Repository 已经被支持作为持久化层自动异常转换的标记。
1.10.2. 使用元注解和组合注解
Spring提供的许多注解都可以在您自己的代码中作为元注解使用。元注解是指可以应用于另一个注解的注解。例如,前面提到的@Service注解被@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 注解编程模型 wiki 页面。
1.10.3. 自动检测类并注册Bean定义
Spring 可以自动检测带有 stereotype 的类,并将相应的
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:component-scan> 时不需要包含 <context:annotation-config> 元素。 |
|
扫描类路径包需要类路径中存在相应的目录条目。当您使用 Ant 构建 JAR 文件时,请确保不要激活 JAR 任务的仅文件开关。此外,在某些环境中,由于安全策略,类路径目录可能无法被暴露 — 例如,在 JDK 1.7.0_45 及更高版本上的独立应用程序(这需要在您的清单中设置“Trusted-Library” — 参见 https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。 在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常按预期工作。
但是,请确保您的组件类在您的 |
此外,当你使用 component-scan 元素时,AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 都会被隐式包含。这意味着这两个组件会被自动检测并连接在一起 — 而无需在 XML 中提供任何 bean 配置元数据。
您可以通过包含值为 false 的 annotation-config 属性来禁用 AutowiredAnnotationBeanPostProcessor 和
CommonAnnotationBeanPostProcessor 的注册。 |
1.10.4. 使用过滤器自定义扫描
默认情况下,使用 @Component、@Repository、@Service、@Controller、
@Configuration 或自定义注解标注的类是唯一被检测到的候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。将它们作为 includeFilters 或 excludeFilters 属性添加到 @ComponentScan 注解中(或作为 <context:include-filter /> 或
<context:exclude-filter /> 子元素添加到 XML 配置中的 <context:component-scan> 元素中)。每个过滤器元素都需要 type 和 expression 属性。
下表描述了过滤选项:
| 过滤器类型 | 示例表达式 | 描述 |
|---|---|---|
注解(默认) |
|
一个注解,应在目标组件的类型级别上存在或元存在。 |
可分配的 |
|
一个类(或接口),目标组件可以分配给它(继承或实现)。 |
面向方面编程 |
|
一个 AspectJ 类型表达式,用于匹配目标组件。 |
正则表达式 |
|
用于匹配目标组件类名的正则表达式。 |
自定义 |
|
对 |
以下示例显示了忽略所有 @Repository 注解并改用“占位”仓库的配置:
@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 或者在 <component-scan/> 元素上提供 use-default-filters="false" 作为属性来禁用默认过滤器。这会有效禁用对使用 @Component、@Repository、@Service、@Controller、@RestController 或 @Configuration 注解或元注解的类的自动检测。 |
1.10.5. 在组件中定义Bean元数据
Spring组件也可以向容器贡献bean定义元数据。您可以使用与在@Configuration注解类中定义bean元数据相同的@Bean注解来实现此操作。下面的示例显示了如何做到这一点:
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// Component method implementation omitted
}
}
@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>。 |
@Autowired 字段和方法得到支持,如前所述,还增加了对 @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 自动绑定到名为 privateInstance 的另一个 bean 的 age 属性的值。Spring 表达式语言元素通过符号 #{ <expression> } 定义属性的值。对于 @Value 注解,表达式解析器已预先配置,以便在解析表达式文本时查找 bean 名称。
从Spring框架4.3版本开始,您还可以声明一个类型为
InjectionPoint(或其更具体的子类:DependencyDescriptor)的工厂方法参数,以
访问触发当前bean创建的请求注入点。
请注意,这仅适用于bean实例的实际创建,而不是现有实例的注入。因此,此功能对于原型作用域的bean最有意义。对于其他作用域,工厂方法只会看到在给定作用域中触发新bean实例创建的注入点
(例如,触发延迟单例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 方法中调用方法或字段来创建协作对象的 bean 元数据引用的方式。
这些方法不是通过正常的 Java 语义调用的,而是经过容器以提供 Spring beans 的常规生命周期管理和代理功能,即使通过对 @Bean 方法的编程调用来引用其他 beans 也是如此。
相比之下,在普通 @Component 类中的 @Bean 方法中调用方法或字段具有标准的 Java 语义,没有任何特殊的 CGLIB 处理或其他约束适用。
|
您可以将 对静态 Java语言中
最后,一个类可能包含多个 |
1.10.6. 自动检测组件的命名
当组件在扫描过程中被自动检测到时,其bean名称由该扫描器所知的BeanNameGenerator策略生成。默认情况下,任何包含名称value的Spring构造型注解(@Component、@Repository、@Service和@Controller)都会将该名称提供给相应的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 开始,可以使用位于包 org.springframework.context.annotation 中的 FullyQualifiedAnnotationBeanNameGenerator 以达到此目的。 |
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"], nameGenerator = MyNameGenerator::class)
class AppConfig {
// ...
}
<beans>
<context:component-scan base-package="org.example"
name-generator="org.example.MyNameGenerator" />
</beans>
一般来说,当其他组件可能显式引用该组件时,建议使用注解指定名称。另一方面,当容器负责自动连线时,自动生成的名称是足够的。
1.10.7. 为自动检测的组件提供作用域
与Spring管理的组件一样,自动检测的组件的默认和最常见作用域是singleton。但是,有时你需要一个不同的作用域,可以通过@Scope注解来指定。你可以在注解中提供作用域的名称,如下例所示:
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
// ...
}
@Scope 注解仅在具体的 bean 类(对于注解组件)或工厂方法(对于 @Bean 方法)上进行内省。与 XML bean 定义不同,没有 bean 定义继承的概念,类级别的继承层次结构对于元数据而言无关紧要。 |
有关在Spring上下文中“request”或“session”等特定于Web的作用域的详细信息,
请参阅 Request, Session, Application, and WebSocket Scopes。与这些作用域的预定义注解一样,
您也可以通过使用Spring的元注解方法来创建自己的作用域注解:例如,一个用 @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作为依赖项。为此,component-scan元素上提供了scoped-proxy属性。三个可能的值是:no、interfaces和targetClass。例如,以下配置会产生标准的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 注解和自定义限定符注解,在解析自动连线候选时提供更精细的控制。
由于这些示例是基于 XML bean 定义的,因此通过使用 XML 中 qualifier 或 meta 元素作为 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 提供其限定符元数据的差异,因为该元数据是按实例提供的,而不是按类提供的。 |
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,从而按需访问作用域较短的 bean 或通过 Provider.get() 调用延迟访问其他 bean。下面的示例提供了前面示例的一种变体:
import javax.inject.Inject;
import javax.inject.Provider;
public class SimpleMovieLister {
private Provider<MovieFinder> movieFinder;
@Inject
public void setMovieFinder(Provider<MovieFinder> movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.get().findMovies(...);
// ...
}
}
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/> 元素相同。您可以将 @Bean 注解的方法与任何 Spring @Component 一起使用。但是,它们最常与 @Configuration Bean 一起使用。
使用 @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. 通过使用 AnnotationConfigApplicationContext 实例化 Spring 容器
以下各节记录了Spring的AnnotationConfigApplicationContext,该功能在Spring 3.0中引入。这种灵活的ApplicationContext实现不仅可以接受@Configuration类作为输入,还可以接受普通的@Component类以及带有JSR-330元数据注解的类。
当提供@Configuration个类作为输入时,@Configuration类本身会被注册为一个Bean定义,该类中所有声明的@Bean方法也会被注册为Bean定义。
当提供@Component和JSR-330类时,它们会被注册为bean定义,并假设在这些类中必要时会使用DI元数据,如@Autowired或@Inject。
简单构造
在很大程度上,Spring XML文件在实例化一个
ClassPathXmlApplicationContext 时被用作输入,你也可以在实例化一个
AnnotationConfigApplicationContext 时使用 @Configuration 类作为输入。这使得可以完全不用 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的<code>0</code>命名空间中的XML声明,如下例所示:
|
在前面的示例中,扫描了 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 类使用 @Component 进行了元注解,因此它们是组件扫描的候选类。在前面的例子中,假设 AppConfig 在 com.acme 包(或其子包)中声明,则会在调用 scan() 时被检测到。在 refresh() 时,其所有 @Bean 方法都会被处理并作为 bean 定义注册到容器中。 |
对使用 AnnotationConfigWebApplicationContext 的 Web 应用程序的支持
一个 WebApplicationContext 变体的 AnnotationConfigApplicationContext 已经可用
通过 AnnotationConfigWebApplicationContext。当
配置 Spring ContextLoaderListener servlet 监听器、Spring MVC
DispatcherServlet 等时,可以使用此实现。以下 web.xml 代码片段配置了一个典型的
Spring MVC Web 应用程序(注意使用了 contextClass 的 context-param 和
init-param):
<web-app>
<!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
instead of the default XmlWebApplicationContext -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<!-- Configuration locations must consist of one or more comma- or space-delimited
fully-qualified @Configuration classes. Fully-qualified packages may also be
specified for component-scanning -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.AppConfig</param-value>
</context-param>
<!-- Bootstrap the root application context as usual using ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Declare a Spring MVC DispatcherServlet as usual -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
instead of the default XmlWebApplicationContext -->
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<!-- Again, config locations must consist of one or more comma- or space-delimited
and fully-qualified @Configuration classes -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.web.MvcConfig</param-value>
</init-param>
</servlet>
<!-- map all requests for /app/* to the dispatcher servlet -->
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
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>
两种声明方式都会在ApplicationContext中提供一个名为transferService的bean,该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)进行匹配的时间(例如,@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配置定义的具有公共 您可能希望默认情况下对通过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 通过 作用域代理 提供了一种方便处理作用域依赖的方式。使用 XML 配置时,创建这种代理最简单的方法是 <aop:scoped-proxy/> 元素。使用 @Scope 注解在 Java 中配置您的 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 的作用域不同而有所差异。我们这里讨论的是单例模式。 |
|
从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的类路径中,因为 CGLIB 类已被重新打包到 |
|
由于CGLIB在启动时动态添加功能,因此有一些限制。特别是,配置类不能是最终的。然而,从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 定义上注入依赖项
前面的示例有效,但过于简单。在大多数实际情况下,配置类中的 beans 之间会有依赖关系。使用 XML 时,这不是问题,因为没有编译器参与,您可以声明
ref="someBean" 并信任 Spring 在容器初始化期间处理它。
使用 @Configuration 类时,Java 编译器会对配置模型施加约束,即对其他 beans 的引用必须是有效的 Java 语法。
幸运的是,解决这个问题很简单。正如我们已经讨论过的,一个@Bean方法可以拥有任意数量的参数来描述bean依赖关系。考虑以下更现实的场景,其中有几个@Configuration类,每个类都依赖于其他类中声明的beans:
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// 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 框架 4.3 版本开始受支持。另外,如果目标 bean 只定义了一个构造函数,则无需指定 @Autowired。 |
在前面的场景中,使用@Autowired可以很好地提供所需的模块化,但确定自动注入的bean定义具体在哪里声明仍然有些模糊。例如,作为查看ServiceConfig的开发人员,您如何知道@Autowired AccountRepository bean是在哪里声明的?这在代码中并不明确,这可能没问题。请记住,Eclipse的Spring工具提供了可以显示所有连接关系的图表的工具,这可能正是您需要的。此外,您的Java IDE可以轻松查找AccountRepository类型的全部声明和用法,并快速显示返回该类型的@Bean方法的位置。
在某些情况下,这种歧义是不可接受的,而您希望从IDE内部直接从一个<code>0</code>类导航到另一个类时,可以考虑将配置类本身进行自动连线。下面的示例显示了如何操作:
@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直接依赖所暗示的范围。 |
按条件包含 @Configuration 个类或 @Bean 个方法
通常根据某些任意系统状态,有条件地启用或禁用整个 @Configuration 类或甚至单独的 @Bean 方法会很有用。一个常见的例子是使用 @Profile 注解,仅在 Spring Environment 中启用了特定配置文件时激活 beans(有关详细信息,请参阅 Bean Definition Profiles)。
@Profile 注解实际上是通过使用一个更灵活的注解 @Conditional 来实现的。
@Conditional 注解表示在注册 @Bean 之前应查阅的特定 org.springframework.context.annotation.Condition 实现。
Condition 接口的实现提供了一个返回 matches(…) 的 true 方法,或者返回 false。例如,以下列表显示了用于 @Profile 的实际 Condition 实现:
@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 定义。在这些示例中,我们创建了一个名为 @Configuration 的 AppConfig 类,并将其作为 <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
在以<code>0</code>类为主要配置容器机制的应用程序中,可能仍然需要使用一些XML。在这些情况下,您可以使用<code>1</code>并仅定义您需要的XML部分。这样可以实现一种“以Java为中心”的容器配置方法,并将XML保持在最低限度。下面的示例(包括一个配置类、一个定义bean的XML文件、一个属性文件以及<code>2</code>类)展示了如何使用<code>3</code>注解来实现需要时使用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. 环境抽象
配置文件是一个命名的、逻辑上的bean定义组,仅在给定的配置文件处于激活状态时才会被注册到容器中。无论在XML中定义还是使用注释定义,都可以将beans分配到配置文件中。与配置文件相关的Environment对象的作用是确定当前处于激活状态的配置文件(如果有),以及默认应处于激活状态的配置文件(如果有)。
属性在几乎所有应用程序中都起着重要作用,可能来自多种来源:属性文件、JVM系统属性、系统环境变量、JNDI、servlet上下文参数、临时的Properties对象、Map对象等。与属性相关的Environment对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。
1.13.1. Bean定义配置文件
Bean定义配置文件在核心容器中提供了一种机制,允许在不同的环境中注册不同的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定义概要文件是核心容器功能,为这个问题提供了解决方案。
如果我们推广前面示例中展示的特定环境的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 方法时,通常选择通过使用 Spring 的 JndiTemplate/JndiLocatorDelegate 辅助类或者如前所示的直接 JNDI InitialContext 使用方式来进行编程式 JNDI 查找,而不是 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' 时,该类不会被注册或处理。如果给定的配置文件前缀带有
NOT 运算符(!),则只有在该配置文件未激活时才会注册带注解的元素。例如,给定 @Profile({"p1", "!p2"}),当配置文件
'p1' 激活或配置文件 'p2' 未激活时,将进行注册。 |
@Profile 也可以在方法级别声明,以仅包含配置类中的一个特定 bean(例如,某个特定 bean 的替代变体),如下例所示:
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") (1)
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Bean("dataSource")
@Profile("production") (2)
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
| 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 配置文件中可用。 |
|
在 如果您想定义具有不同配置文件条件的替代 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 对应项不支持前面所述的配置文件表达式。但是,可以通过使用
在前面的示例中,如果同时激活了 |
激活配置文件
在更新配置之后,我们仍然需要告诉Spring哪个配置文件是激活的。如果我们现在启动示例应用程序,将会看到一个NoSuchBeanDefinitionException被抛出,因为容器找不到名为dataSource的Spring Bean。
通过几种方式可以激活配置文件,但最直接的方法是针对 Environment API 进行编程,该 API 可通过 ApplicationContext 获得。下面的示例显示了如何操作:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
val ctx = AnnotationConfigApplicationContext().apply {
environment.setActiveProfiles("development")
register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
refresh()
}
此外,您还可以通过spring.profiles.active属性以声明方式激活配置文件,该属性可以通过系统环境变量、JVM系统属性、web.xml中的Servlet上下文参数,甚至作为JNDI中的条目来指定(参见PropertySource 抽象)。在集成测试中,可以通过在spring-test模块中使用@ActiveProfiles注解来声明活动的配置文件(参见使用环境配置文件进行上下文配置)。
请注意,配置文件并不是一个“非此即彼”的选择。您可以同时激活多个配置文件。编程方式上,您可以向setActiveProfiles()方法提供多个配置文件名称,该方法接受String…个可变参数。下面的示例将激活多个配置文件:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")
以声明方式,spring.profiles.active 可能接受一个以逗号分隔的配置文件名称列表,
如下面的示例所示:
-Dspring.profiles.active="profile1,profile2"
默认配置文件
默认配置文件表示默认启用的配置文件。考虑以下示例:
@Configuration
@Profile("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build();
}
}
@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 时,如果在运行时存在 my-property 系统属性或 my-property 环境变量,调用 env.containsProperty("my-property") 将返回 true。
|
搜索是分层进行的。默认情况下,系统属性优先于环境变量。因此,如果在调用 对于常见的
|
最重要的是,整个机制是可配置的。也许你有一个自定义的属性来源,想要将其集成到此搜索中。为此,实现并实例化你自己的 PropertySource,并将其添加到当前 Environment 的 PropertySources 集合中。下面的示例展示了如何操作:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())
在前面的代码中,MyPropertySource 在搜索中具有最高优先级。如果它包含一个 my-property 属性,则会检测并返回该属性,优于任何其他 my-property 属性在任何其他 PropertySource 中的属性。 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
Spring 使用 LoadTimeWeaver 在类加载到 Java 虚拟机 (JVM) 时动态转换类。
要启用加载时编织,可以将 @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 框架中的 AspectJ 加载时织入。
1.15. ApplicationContext 的附加功能
如在 章节介绍 中所述,org.springframework.beans.factory
包提供了管理及操作 beans 的基本功能,包括以编程方式的方式。 org.springframework.context 包添加了
ApplicationContext
接口,该接口扩展了 BeanFactory 接口,并且还扩展了其他接口,以更符合应用程序框架风格的方式提供额外的功能。许多人完全以声明式的方式使用 ApplicationContext,甚至不以编程方式创建它,而是依靠支持类如 ContextLoader 在 Java EE Web 应用程序的正常启动过程中自动实例化一个
ApplicationContext。
为了以更面向框架的方式增强 BeanFactory 的功能,上下文
包还提供了以下功能:
-
通过
MessageSource接口以国际化风格访问消息。 -
通过
ResourceLoader接口访问资源,例如 URLs 和文件。 -
事件发布,即通过使用
ApplicationEventPublisher接口,将事件发布给实现ApplicationListener接口的 Bean。 -
通过
HierarchicalBeanFactory接口加载多个(分层)上下文,使每个上下文专注于应用程序的某一层,例如网络层。
1.15.1. 使用 MessageSource 进行国际化
The ApplicationContext interface extends an interface called MessageSource and,
therefore, provides internationalization (“i18n”) functionality. Spring also provides the
HierarchicalMessageSource interface, which can resolve messages hierarchically.
Together, these interfaces provide the foundation upon which Spring effects message
resolution. The methods defined on these interfaces include:
-
String getMessage(String code, Object[] args, String default, Locale loc): 从MessageSource中检索消息的基本方法。当没有找到指定区域的消息时,将使用默认消息。传递的任何参数将成为替换值,使用标准库提供的MessageFormat功能。 -
String getMessage(String code, Object[] args, Locale loc): 与前一种方法基本相同,但有一个区别:不能指定默认消息。如果找不到消息,将抛出NoSuchMessageException。 -
String getMessage(MessageSourceResolvable resolvable, Locale locale): 所有在前面方法中使用的属性 也包含在一个名为MessageSourceResolvable的类中,您可以使用此方法。
当加载 ApplicationContext 时,它会自动在上下文中查找一个名为 MessageSource 的 bean。该 bean 必须具有名称 messageSource。如果找到这样的 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。任何用于解析消息的请求都会通过 ResourceBundle 对象按照 JDK 标准方式处理。为了示例的目的,假设上述两个资源绑定文件的内容如下:
# 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实现遵循与标准JDKResourceBundle相同的区域设置解析和回退规则。简而言之,继续使用之前定义的示例messageSource,如果您希望根据英国(en-GB)区域设置解析消息,您需要分别创建名为format_en_GB.properties、exceptions_en_GB.properties和windows_en_GB.properties的文件。
通常,区域设置的解析由应用程序的周围环境管理。在下面的例子中,指定用于解析(英国)消息的区域设置是手动进行的:
# 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 的引用。
任何在实现 MessageSourceAware 接口的 ApplicationContext 中定义的 bean 在创建和配置时都会被注入应用程序上下文的 MessageSource。
由于 Spring 的 MessageSource 基于 Java 的 ResourceBundle,因此不会合并具有相同基本名称的资源包,只会使用找到的第一个资源包。
后续具有相同基本名称的消息资源包将被忽略。 |
作为 ResourceBundleMessageSource 的替代方案,Spring 提供了一个
ReloadableResourceBundleMessageSource 类。此变体支持相同的资源包文件格式,但比基于标准 JDK 的
ResourceBundleMessageSource 实现更加灵活。特别是,它允许从任何 Spring 资源位置读取文件(而不仅仅是类路径),并且支持资源包属性文件的热重新加载(在两者之间高效缓存)。
请参阅 ReloadableResourceBundleMessageSource
javadoc 了解详细信息。 |
1.15.2. 标准和自定义事件
通过 ApplicationContext 类和 ApplicationEvent 接口提供 ApplicationContext 中的事件处理功能。如果将实现了 ApplicationListener 接口的 Bean 部署到上下文中,每当有 ApplicationEvent 发布到 ApplicationContext 时,该 Bean 会被通知。这本质上是标准的观察者设计模式。
从 Spring 4.2 开始,事件基础设施得到了显著改进,并提供了
一个 基于注解的模型 以及
发布任何任意事件(即,一个不一定从 ApplicationEvent 继承的对象)的能力。当发布此类对象时,我们会为您将其包装成一个事件。 |
下表描述了Spring提供的标准事件:
| 事件 | 说明 |
|---|---|
|
在 |
|
在使用 |
|
在使用 |
|
在使用 |
|
一个与网络相关的事件,用于通知所有Bean HTTP请求已经得到处理。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 |
|
一个添加了与 Servlet 特定的上下文信息的 |
您也可以创建并发布自己的自定义事件。下面的示例显示了一个继承 Spring 的 ApplicationEvent 基类的简单类:
public class BlockedListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlockedListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
class BlockedListEvent(source: Any,
val address: String,
val content: String) : ApplicationEvent(source)
要发布自定义 ApplicationEvent,请调用 publishEvent() 方法 on 一个 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 接口的 javadoc
和 SimpleApplicationEventMulticaster
实现以了解配置选项。
以下示例显示了用于注册和配置上述每个类的bean定义:
<bean id="emailService" class="example.EmailService">
<property name="blockedList">
<list>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</list>
</property>
</bean>
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
<property name="notificationAddress" value="[email protected]"/>
</bean>
将所有内容综合起来,当调用sendEmail() bean的emailService方法时,如果有任何应该被阻止的电子邮件消息,会发布类型为BlockedListEvent的自定义事件。 blockedListNotifier bean被注册为ApplicationListener,并接收BlockedListEvent,此时它可以通知相关方。
| Spring 的事件机制旨在实现同一应用上下文中的 Spring beans 之间的简单通信。然而,对于更复杂的企业集成需求,单独维护的 Spring Integration 项目提供了完整的支持,用于构建基于已知的 Spring 编程模型的轻量级、 模式导向 的事件驱动架构。 |
基于注解的事件监听器
您可以使用@EventListener注解在托管Bean的任何方法上注册事件监听器。BlockedListNotifier可以重写为以下形式:
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// 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
}
使用异步事件时请注意以下限制:
-
如果异步事件监听器抛出一个
Exception,它不会传播到调用者。请参阅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,其中 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 抽象,如 资源 中所述。
应用上下文是一个 ResourceLoader,可以用来加载 Resource 对象。
Resource 实质上是 JDK java.net.URL 类的更功能丰富的版本。
实际上,Resource 的实现会包装一个 java.net.URL 的实例,具体情况具体分析。Resource 可以以透明的方式从类路径、文件系统位置、任何可以用标准 URL 描述的位置以及其他一些变体中获取底层资源。如果资源位置字符串是一个没有特殊前缀的简单路径,这些资源的来源将取决于实际的应用上下文类型。
您可以配置部署到应用上下文中的bean以实现特殊的回调接口,ResourceLoaderAware,以便在初始化时自动调用,并将应用上下文本身作为ResourceLoader传入。
您还可以公开类型为Resource的属性,用于访问静态资源。
它们像其他属性一样被注入到其中。您可以将这些Resource属性指定为简单的String路径,并在bean部署时依赖从这些文本字符串到实际Resource对象的自动转换。
提供给 ApplicationContext 构造函数的位置路径或路径实际上是资源字符串,并根据特定的上下文实现以简单形式进行适当处理。例如 ClassPathXmlApplicationContext 将简单的位置路径视为类路径位置。您也可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或 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. 将 Spring ApplicationContext 部署为 Java EE RAR 文件
可以将Spring ApplicationContext 部署为RAR文件,将上下文及其所有所需的bean类和库JAR包封装在Java EE RAR部署单元中。这相当于引导一个独立的ApplicationContext(仅在Java EE环境中托管),能够访问Java EE服务器的资源。RAR部署是对部署无头WAR文件场景的一种更自然的替代方案——实际上,这是一个没有任何HTTP入口点的WAR文件,仅用于在Java EE环境中引导SpringApplicationContext。
RAR部署适用于不需要HTTP入口点的应用程序上下文,而是仅由消息端点和计划任务组成。此类上下文中的Bean可以使用应用服务器资源,例如JTA事务管理器和通过JNDI绑定的JDBC DataSource实例和JMS ConnectionFactory实例,并且还可以注册到平台的JMX服务器——这一切都通过Spring的标准事务管理和JNDI及JMX支持功能实现。应用程序组件还可以通过Spring的TaskExecutor抽象与应用服务器的JCA WorkManager进行交互。
查看RAR部署中涉及的配置细节的
SpringContextResourceAdapter
类的javadoc。
作为 Java EE RAR 文件的 Spring ApplicationContext 的简单部署:
-
Package 所有应用程序类打包到一个 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
The BeanFactory API 提供了 Spring 的 IoC 功能的基础。
其具体的契约主要在与其他 Spring 部分和相关第三方框架的集成中使用,
其 DefaultListableBeanFactory 实现是更高层次 GenericApplicationContext 容器中的关键委托。
BeanFactory 和相关接口(如 BeanFactoryAware、InitializingBean、
DisposableBean)是其他框架组件的重要集成点。
通过不需要任何注解甚至反射,它们允许容器与其组件之间进行高效的交互。应用层的 bean 可以使用相同的回调接口,但通常更倾向于通过注解或编程配置进行声明式依赖注入。
请注意,核心 BeanFactory API 层级及其 DefaultListableBeanFactory 实现不会对配置格式或任何要使用的组件注解做出假设。所有这些风格都是通过扩展(如 XmlBeanDefinitionReader 和 AutowiredAnnotationBeanPostProcessor)引入的,并且作为核心元数据表示在共享 BeanDefinition 对象上操作。这就是使 Spring 容器如此灵活和可扩展的本质。
1.16.1. BeanFactory 或 ApplicationContext?
本节解释了 BeanFactory 和 ApplicationContext 容器级别之间的区别以及对引导过程的影响。
你应该使用 ApplicationContext,除非你有充分的理由不这样做,GenericApplicationContext 和其子类 AnnotationConfigApplicationContext 是用于自定义引导的常见实现。这些是 Spring 核心容器的所有常见用途的主要入口点:加载配置文件、触发类路径扫描、以编程方式注册 bean 定义和带注解的类,以及(从 5.0 版本开始)注册功能 bean 定义。
由于ApplicationContext包含了BeanFactory的所有功能,因此通常推荐使用它,而不是普通的BeanFactory,除非需要对bean处理有完全的控制。在ApplicationContext(例如GenericApplicationContext实现)中,通过约定(即通过bean名称或bean类型——特别是后处理器)检测到几种类型的beans,而普通的DefaultListableBeanFactory则不关心任何特殊的beans。
对于许多扩展的容器功能,如注解处理和AOP代理,
BeanPostProcessor 扩展点 是必不可少的。
如果你只使用普通的 DefaultListableBeanFactory,那么这些后处理器默认情况下不会
被检测和激活。这种情况可能会让人感到困惑,因为你的 bean 配置实际上并没有问题。相反,在这种情况下,
容器需要通过额外的设置才能完全启动。
以下表格列出了BeanFactory和ApplicationContext接口及实现提供的功能。
| 特性 | BeanFactory |
ApplicationContext |
|---|---|---|
Bean实例化/装配 |
是 |
是 |
集成生命周期管理 |
No |
是 |
自动 |
No |
是 |
自动 |
No |
是 |
便捷的 |
No |
是 |
内置 |
No |
是 |
要显式地将一个 bean 后置处理器注册为 DefaultListableBeanFactory,
你需要调用 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 实现可用于从类路径或相对于 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 的其他部分。虽然这会使您的代码与 Spring 耦合,但它实际上只会使它与这一小组工具类耦合,这些工具类可以作为 URL 的更强大替代,并可以视为用于此目的的任何其他库的等价物。
The 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
这个类表示应从类路径获取的资源。它使用线程上下文类加载器、给定的类加载器或给定的类来加载资源。
这个 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 实例。
当您在特定的应用程序上下文上调用 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。它会为每个上下文返回适当的对象。
因此,你可以以适合特定应用程序上下文的方式加载资源。
另一方面,您还可以通过指定特殊的 classpath: 前缀,强制使用 ClassPathResource,而不管应用程序上下文类型如何,如下例所示:
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: |
|
从类路径加载。 |
file: |
作为 |
|
http: |
作为 |
|
无 |
|
取决于基础 |
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属性,并期望它们被注入到其中。
然后将这些属性注入变得简单的原因是,所有应用上下文都会注册并使用一个特殊的JavaBeans PropertyEditor,它可以将 String 路径转换为 Resource 对象。因此,如果 myBean 具有类型为 Resource 的模板属性,可以为该资源使用一个简单的字符串进行配置,如下例所示:
<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定义是从类路径加载的,因为使用了ClassPathResource。但是,请考虑以下示例,它创建了一个FileSystemXmlApplicationContext:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/appContext.xml");
val ctx = FileSystemXmlApplicationContext("conf/appContext.xml")
现在,bean定义是从文件系统位置加载的(在本例中,相对于当前工作目录)。
请注意,对位置路径使用特殊的类路径前缀或标准URL前缀会覆盖默认类型Resource,该类型用于加载定义。考虑以下示例:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml")
使用 FileSystemXmlApplicationContext 会从类路径加载 bean 定义。但是,它仍然是一个 FileSystemXmlApplicationContext。如果之后作为 ResourceLoader 使用,任何未加前缀的路径仍然被视为文件系统路径。
构造 ClassPathXmlApplicationContext 实例 — 快捷方式
The ClassPathXmlApplicationContext 提供了许多构造函数以实现方便的实例化。基本思路是,您只需提供一个仅包含 XML 文件名(不带前导路径信息)的字符串数组,并且还提供一个 Class。然后 ClassPathXmlApplicationContext 会从提供的类中推断出路径信息。
考虑以下目录结构:
com/
foo/
services.xml
daos.xml
MessengerService.class
以下示例显示了如何实例化一个由文件名为services.xml和daos.xml(位于类路径上)中定义的 beans 组成的ClassPathXmlApplicationContext实例:
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是文件系统路径还是显式指定),通配符操作将保证以完全可移植的方式工作。
如果指定的路径是类路径位置,解析器必须通过调用 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(…)来完成的),然后合并以形成最终的应用程序上下文定义。
通配符类路径依赖于底层类加载器的 getResources() 方法。由于现在大多数应用服务器都提供自己的类加载器实现,行为可能会有所不同,尤其是在处理 JAR 文件时。一个简单的测试方法是使用类加载器从类路径中的 JAR 文件中加载一个文件:
getClass().getClassLoader().getResources("<someFileInsideTheJar>")。尝试使用在不同位置但名称相同的文件进行此测试。如果返回了不正确的结果,请查看应用服务器文档中可能影响类加载器行为的设置。 |
您还可以将 classpath*: 前缀与位置路径其余部分中的 PathMatcher 模式结合使用(例如,classpath*:META-INF/*-beans.xml)。在这种情况下,解析策略相当简单:对最后一个非通配符路径段调用 ClassLoader.getResources() 以获取类加载器层次结构中的所有匹配资源,然后,对每个资源,使用之前描述的相同的 PathMatcher 解析策略来处理通配符子路径。
与通配符相关的其他注意事项
请注意,当与Ant风格的模式结合使用时,classpath*:仅在模式开始前至少有一个根目录时才能可靠工作,除非实际的目标文件位于文件系统中。这意味着像classpath*:*.xml这样的模式可能无法从jar文件的根目录检索文件,而只能从展开的目录的根目录检索文件。
Spring 从 JDK 的
ClassLoader.getResources() 方法中获取类路径条目,该方法仅对空字符串(表示可能的搜索根目录)返回文件系统位置。Spring 还会评估
URLClassLoader 运行时配置和 jar 文件中的 java.class.path 清单,但这不一定能保证可移植的行为。
|
扫描类路径包需要类路径中存在相应的目录条目。当您使用 Ant 构建 JAR 文件时,请不要激活 JAR 任务的仅文件开关。此外,在某些环境中,由于安全策略的原因,类路径目录可能不会被暴露 — 例如,在 JDK 1.7.0_45 及更高版本上的独立应用程序(这需要在您的清单中设置“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 注意事项
一个未连接到 FileSystemApplicationContext 的 FileSystemResource(即当 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 的绝对路径,并通过使用 file: URL 前缀强制使用 UrlResource。以下示例显示了如何操作:
// 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 包,提供了通用的类型转换功能,以及一个用于格式化 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 辅助类时。下面的示例为 Person 实例实现了 Validator:
public class PersonValidator implements Validator {
/**
* This Validator validates only Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
class PersonValidator : Validator {
/*
* This Validator validates only Person instances
*/
override fun supports(clazz: Class<>): Boolean {
return Person::class.java == clazz
}
override fun validate(obj: Any, e: Errors) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty")
val p = obj as Person
if (p.age < 0) {
e.rejectValue("age", "negativevalue")
} else if (p.age > 110) {
e.rejectValue("age", "too.darn.old")
}
}
}
static rejectIfEmpty(..) 方法在 ValidationUtils 类上用于在 name 属性为 null 或空字符串时拒绝该属性。查看
ValidationUtils javadoc
以了解它除了之前显示的示例之外还提供了哪些功能。
虽然可以实现一个 Validator 类来验证丰富对象中的每个嵌套对象,但将每个嵌套类的验证逻辑封装在其自身的 Validator 实现中可能更好。一个简单的“丰富”对象示例是一个由两个 String 属性(名和姓)以及一个复杂的 Address 对象组成的 Customer。 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接口中的其他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
The org.springframework.beans package adheres to the JavaBeans standard.
A JavaBean is a class with a default no-argument constructor and that follows
a naming convention where (for example) a property named bingoMadness would
have a setter method setBingoMadness(..) and a getter method getBingoMadness(). For
more information about JavaBeans and the specification, see
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。下表显示了这些约定的一些示例:
| 表达式 | 说明 |
|---|---|
|
表示对应 |
|
指示属性 |
|
表示索引属性 |
|
指示由 |
(如果不需要直接使用<code>0</code>,那么下一节对你来说并不至关重要。如果你只使用<code>1</code>和<code>2</code>及其默认实现,可以跳到<a t="C4">关于<code>3</code>的章节</a>。)
以下两个示例类使用 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框架中,通过使用各种
PropertyEditor实现来解析HTTP请求参数,您可以手动绑定所有CommandController的子类。
Spring 有多个内置的 PropertyEditor 实现,以使生活更轻松。
它们都位于 org.springframework.beans.propertyeditors
包中。大多数(但不是全部,如下面的表格所示)默认由
BeanWrapperImpl 注册。当属性编辑器以某种方式可配置时,您仍然可以注册自己的变体来覆盖默认值。下表描述了 Spring 提供的各种 PropertyEditor 实现:
| 类 | 说明 |
|---|---|
|
字节数组的编辑器。将字符串转换为其对应的字节表示形式。默认由 |
|
将表示类的字符串解析为实际类和反之。当找不到类时,会抛出 |
|
可自定义的属性编辑器,用于 |
|
用于集合的属性编辑器,将任何源 |
|
可自定义的属性编辑器,用于 |
|
适用于任何 |
|
将字符串解析为 |
|
单向属性编辑器,可以接受一个字符串并通过中间的 |
|
可以将字符串解析为 |
|
可以将字符串转换为 |
|
可以将字符串(使用 javadoc 中定义的格式)转换为 |
|
用于修剪字符串的属性编辑器。可选择将空字符串转换为 |
|
可以将URL的字符串表示转换为实际的 |
Spring 使用 java.beans.PropertyEditorManager 来设置可能需要的属性编辑器的搜索路径。搜索路径还包括 sun.bean.editors,它包括诸如 Font、Color 和大部分原始类型在内的 PropertyEditor 实现。请注意,标准的 JavaBeans 基础设施如果在处理类所在的同一包中,并且名称与该类相同(加上 Editor),则会自动发现 PropertyEditor 类(而无需您显式注册它们)。例如,可以有以下类和包结构,这将足以让 SomethingEditor 类被识别并用作 PropertyEditor 用于 Something 类型的属性。
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() 接口的 registerCustomEditor() 方法,假设你有一个 BeanFactory 的引用。另一种(稍微更方便)机制是使用一个特殊的 Bean 工厂后处理器,称为 CustomEditorConfigurer。虽然你可以使用 Bean 工厂后处理器与 BeanFactory 实现,但 CustomEditorConfigurer 有一个嵌套属性设置,因此我们强烈建议你与 ApplicationContext 一起使用,你可以在类似其他 Bean 的方式中部署它,并且它可以被自动检测和应用。
请注意,所有bean工厂和应用上下文通过使用BeanWrapper自动使用许多内置的属性编辑器,以处理属性转换。BeanWrapper注册的标准属性编辑器列在上一节中。
此外,ApplicationContexts还会覆盖或添加其他编辑器,以处理与特定应用上下文类型相适应的资源查找。
标准的 JavaBeans PropertyEditor 实例用于将作为字符串表达的属性值转换为属性的实际复杂类型。您可以使用 CustomEditorConfigurer,一个 bean 工厂后处理器,方便地向 ApplicationContext 添加对额外 PropertyEditor 实例的支持。
考虑以下示例,该示例定义了一个名为 ExoticType 的用户类和另一个需要将 ExoticType 设置为属性的类 DependsOnExoticType:
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
注册属性编辑器的另一种机制是创建并使用一个PropertyEditorRegistrar。此接口在需要在几种不同情况下使用相同的一组属性编辑器时特别有用。您可以编写相应的注册器并在每种情况下重复使用它。
PropertyEditorRegistrar实例与称为PropertyEditorRegistry的接口协同工作,该接口由Spring的BeanWrapper(和DataBinder)实现。PropertyEditorRegistrar实例在与CustomEditorConfigurer(在这里描述)结合使用时特别方便,它公开了一个名为setPropertyEditorRegistrars(..)的属性。PropertyEditorRegistrar实例以这种方式添加到CustomEditorConfigurer中,可以轻松与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 网络框架 的读者来说,这有点偏离本章的重点),将 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包中提供了几种转换器实现,作为便利。这些包括从字符串到数字和其他常见类型的转换器。
以下列表显示了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,或者你可能希望仅当目标类上定义了特定方法(如 static valueOf 方法)时才运行一个 Converter。
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. ConversionService API
ConversionService 为在运行时执行类型转换逻辑定义了一个统一的 API。转换器通常在以下外观接口后面运行:
package org.springframework.core.convert;
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
<T> T convert(Object source, Class<T> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
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 容器(或 ApplicationContext)配置一个 ConversionService 实例。
Spring 会检测到这个 ConversionService,并在框架需要执行类型转换时使用它。你也可以将这个 ConversionService 注入到你的任何 bean 中并直接调用它。
如果没有使用Spring注册ConversionService,则使用基于PropertyEditor的系统。 |
要使用 Spring 注册一个默认的 ConversionService,请添加以下 bean 定义,
其 id 为 conversionService:
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默认的 ConversionService 可以在字符串、数字、枚举、集合、映射和其他常见类型之间进行转换。要使用自己的自定义转换器补充或覆盖默认转换器,请设置 converters 属性。属性值可以实现任何 Converter、ConverterFactory 或 GenericConverter 接口。
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="example.MyCustomConverter"/>
</set>
</property>
</bean>
在Spring MVC应用程序中也经常使用ConversionService。请参阅Spring MVC章节中的
转换与格式化。
在某些情况下,您可能希望在转换过程中应用格式。有关使用 FormattingConversionServiceFactoryBean 的详细信息,请参阅
The FormatterRegistry SPI。
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(...)
}
}
对于大多数用例,您可以使用指定targetType的convert方法,但它无法处理更复杂的类型,例如参数化元素的集合。
例如,如果您想将Integer的List程序化地转换为String的List,则需要提供源类型和目标类型的正式定义。
幸运的是,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转换器。您可以通过在DefaultConversionService类上使用静态addDefaultConverters方法,将相同的转换器注册到任何ConverterRegistry中。
用于值类型的转换器会用于数组和集合,因此不需要创建特定的转换器将 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
系统会执行强制转换。
现在考虑典型客户端环境(如网页或桌面应用程序)中的类型转换需求。在这样的环境中,你通常需要将 String 转换以支持客户端回发过程,并将 String 转换以支持视图渲染过程。此外,你通常还需要对 String 值进行本地化。更通用的 core.convert Converter SPI 并不直接处理此类格式化需求。为了直接解决这些问题,Spring 3 引入了一个方便的 Formatter SPI,它为客户端环境提供了简单且可靠的 PropertyEditor 实现的替代方案。
通常,当需要实现通用的类型转换逻辑时,可以使用 Converter SPI — 例如,将 java.util.Date 转换为 Long。
当在客户端环境(如 Web 应用程序)中工作并需要解析和打印本地化的字段值时,可以使用 Formatter SPI。 ConversionService 为这两个 SPI 提供了统一的类型转换 API。
3.5.1. Formatter SPI
实现字段格式化逻辑的 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问题以进行贡献。
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
存在一个可移植的格式注解API,位于org.springframework.format.annotation包中。您可以使用@NumberFormat来格式化Number字段,例如Double和Long,以及@DateTimeFormat来格式化java.util.Date、java.util.Calendar、Long(毫秒时间戳)以及JSR-310java.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. FormatterRegistry SPI
FormatterRegistry 是用于注册格式化程序和转换器的 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. FormatterRegistrar SPI
FormatterRegistrar 是通过 FormatterRegistry 注册格式化程序和转换器的 SPI。以下列表显示了它的接口定义:
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
package org.springframework.format
interface FormatterRegistrar {
fun registerFormatters(registry: FormatterRegistry)
}
一个 FormatterRegistrar 在注册多个相关的转换器和格式化程序时很有用,这些转换器和格式化程序属于给定的格式类别,例如日期格式。当声明式注册不足时,它也可能有用 — 例如,当格式化程序需要根据不同于其自身 <T> 的特定字段类型进行索引,或者当注册一个 Printer/Parser 对时。下一节将提供更多关于转换器和格式化程序注册的信息。
3.5.5. 在Spring MVC中配置格式化
查看Spring MVC章节中的转换和格式化。
3.6. 配置全局日期和时间格式
默认情况下,未使用 @DateTimeFormat 注解的日期和时间字段会通过使用 DateFormat.SHORT 样式从字符串转换。如果您更喜欢,可以通过定义自己的全局格式来更改此设置。
要做到这一点,请确保Spring不注册默认的格式化程序。相反,借助以下方式手动注册格式化程序:
-
org.springframework.format.datetime.standard.DateTimeFormatterRegistrar -
org.springframework.format.datetime.DateFormatterRegistrar,或org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar用于 Joda-Time。
例如,以下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验证 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 验证提供程序设置为 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 验证的初始化。类路径中应存在一个 Bean 验证提供程序,例如 Hibernate Validator,并且会自动检测到它。
注入验证器
LocalValidatorFactoryBean 同时实现 javax.validation.ValidatorFactory 和
javax.validation.Validator,以及 Spring 的 org.springframework.validation.Validator。
您可以将对这两个接口之一的引用注入到需要调用验证逻辑的 bean 中。
您可以注入对 javax.validation.Validator 的引用,如果您希望直接使用 Bean 验证 API,如下例所示:
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 验证约束由两部分组成:
-
一个
@Constraint注解,用于声明约束及其可配置的属性。 -
对实现
javax.validation.ConstraintValidator接口的实现,该实现实现了约束的行为。
要将声明与实现相关联,每个 @Constraint 注解都引用一个相应的 ConstraintValidator 实现类。在运行时,当在您的领域模型中遇到约束注解时,ConstraintValidatorFactory 会实例化所引用的实现类。
默认情况下,LocalValidatorFactoryBean 配置了一个使用 Spring 创建 SpringConstraintValidatorFactory 实例的 ConstraintValidator。这使得您的自定义 ConstraintValidators 能够像其他 Spring bean 一样受益于依赖注入。
以下示例显示了一个自定义 @Constraint 声明,后跟一个使用 Spring 进行依赖注入的关联 ConstraintValidator 实现:
@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 实现可以将其依赖项
@Autowired 作为任何其他 Spring Bean。
基于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注解进行标注,该注解还可以选择性地声明要使用的验证组。有关与Hibernate Validator和Bean Validation 1.1提供程序的设置详情,请参见
MethodValidationPostProcessor
。
其他配置选项
默认的 LocalValidatorFactoryBean 配置对于大多数情况来说已经足够。有许多配置选项可用于各种 Bean Validation 构造,从消息插值到遍历解析。有关这些选项的更多信息,请参阅
LocalValidatorFactoryBean
javadoc。
3.7.3. 配置 DataBinder
自 Spring 3 以来,您可以使用 DataBinder 实例配置一个 Validator。配置完成后,可以通过调用 binder.validate() 来调用 Validator。任何验证 Errors 都会自动添加到绑定器的 BindingResult 中。
以下示例显示了如何将 DataBinder 用于编程方式,在绑定到目标对象后调用验证逻辑:
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());
// bind to the target object
binder.bind(propertyValues);
// validate the target object
binder.validate();
// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();
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实例。这在将全局配置的bean验证与Spring dataBinder.replaceValidators在DataBinder实例上本地配置相结合时非常有用。参见
Spring MVC验证配置。
3.7.4. Spring MVC 3 验证
查看 验证 部分在Spring MVC章节中。
4. Spring 表达式语言(SpEL)
Spring 表达式语言(“SpEL” 简称)是一种强大的表达式语言,它在运行时支持对对象图进行查询和操作。该语言的语法类似于统一 EL,但提供了额外的功能,尤其是方法调用和基本的字符串模板功能。
虽然有几种其他可用的Java表达式语言——例如 OGNL、MVEL 和 JBoss EL——Spring 表达式语言(SpEL)被创建出来,以向 Spring 社区提供一种统一且得到良好支持的表达式语言,该语言可以用于 Spring 产品组合中的所有产品。其语言特性由 Spring 产品组合中的项目需求驱动,包括在 Eclipse 的 Spring 工具 中的代码完成支持的工具要求。 当然,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 | 消息变量的值是 'Hello World'。 |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
| 1 | 消息变量的值是 'Hello World'。 |
您最可能使用的SpEL类和接口位于
org.springframework.expression包及其子包中,例如spel.support。
ExpressionParser 接口负责解析表达式字符串。在前面的示例中,表达式字符串是一个由周围单引号表示的字符串字面量。Expression 接口负责评估之前定义的表达式字符串。在调用parser.parseExpression和exp.getValue时可能抛出的两个异常,分别是ParseException和EvaluationException。
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 并将其转换为大写。 |
注意通用方法的使用:<code>0</code>。 使用此方法可以避免将表达式的值强制转换为所需的结果类型。如果无法将值转换为类型<code>2</code>或通过注册的类型转换器进行转换,则会抛出<code>1</code>。
SpEL更常见的用法是提供一个表达式字符串,该字符串针对特定对象实例(称为根对象)进行求值。下面的示例显示了如何从Inventor类的实例中检索name属性或创建一个布尔条件:
// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // 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
The EvaluationContext interface 用于在评估表达式时解析属性、方法或字段,并帮助执行类型转换。Spring 提供了两种实现。
-
SimpleEvaluationContext: 暴露了部分基本的SpEL语言功能和配置选项,适用于不需要完整SpEL语言语法的表达式类别,并且应该有意义地进行限制。例如包括但不限于数据绑定表达式和基于属性的过滤器。 -
StandardEvaluationContext: 暴露所有SpEL语言功能和配置选项。您可以使用它来指定默认的根对象,并配置所有可用的评估相关策略。
SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。
它不包括 Java 类型引用、构造函数和 bean 引用。它还要求您显式选择表达式中属性和方法的支持级别。
默认情况下,create() 静态工厂方法仅启用对属性的只读访问。
您还可以获取一个构建器来配置所需的确切支持级别,针对以下一项或多项组合:
-
仅自定义
PropertyAccessor(无反射) -
只读访问的数据绑定属性
-
读取和写入的数据绑定属性
类型转换
默认情况下,SpEL 使用 Spring 核心中的转换服务(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或注解的配置元数据来定义<code>0</code>实例。在两种情况下,定义表达式的语法形式为<code>1</code>。
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
}
以下示例显示了在属性设置方法上的等效情况:
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
}
@Autowired 方法和构造函数也可以使用 @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表达式语言的工作方式。它涵盖了以下主题:
4.3.1. 字面量表达式
支持的字面量表达式类型包括字符串、数值(int、real、hex)、布尔值和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)
映射的内容是通过在方括号内指定字面量键值来获取的。在下面的例子中,因为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} 符号直接在表达式中表示映射。以下示例显示了如何操作:
// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
// evaluates to a Java map containing the two entries
val inventorInfo = parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context) as Map<*, >
val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<, *>
{:} 本身表示一个空的映射。出于性能考虑,如果映射本身由固定字面量或其他嵌套常量结构(列表或映射)组成,则会创建一个常量映射来表示该表达式(而不是每次求值时都构建一个新的映射)。对映射键的引用是可选的。上面的示例没有使用引号括起的键。
4.3.5. 数组构造
你可以使用熟悉的 Java 语法构建数组,可选择提供一个初始化器,以便在构造时填充数组。以下示例展示了如何操作:
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 运算符:
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 而不是抛出异常。下面的例子展示了如何使用安全导航运算符:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // Smiljan
tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // null - does not throw NullPointerException!!!
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
val tesla = Inventor("Nikola Tesla", "Serbian")
tesla.setPlaceOfBirth(PlaceOfBirth("Smiljan"))
var city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String::class.java)
println(city) // Smiljan
tesla.setPlaceOfBirth(null)
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String::class.java)
println(city) // null - does not throw NullPointerException!!!
4.3.16. 集合选择
选择是一项强大的表达式语言功能,它允许您通过从其条目中进行选择,将源集合转换为另一个集合。
选择使用 .?[selectionExpression] 的语法。它会过滤集合并返回一个包含原始元素子集的新集合。例如,选择使我们能够轻松获取塞尔维亚发明家的列表,如下例所示:
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. 集合投影
投影使一个集合驱动子表达式的求值,结果是一个新的集合。投影的语法是.![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<*>
您也可以使用映射来驱动投影,在这种情况下,投影表达式将针对映射中的每个条目进行计算(表示为 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中,模块化的单元是方面。方面使能够对跨越多个类型和对象的关注点(如事务管理)进行模块化。(这类关注点在AOP文献中通常被称为“横切”关注点。)
Spring 的一个重要组成部分是 AOP 框架。虽然 Spring IoC 容器并不依赖 AOP(也就是说,如果你不想使用 AOP,就不需要使用它),但 AOP 与 Spring IoC 相辅相成,提供了一个非常强大的中间件解决方案。
AOP 在 Spring 框架中用于:
-
提供声明式企业服务。最重要的此类服务是 声明式事务管理。
-
让用户实现自定义方面,用AOP补充他们的OOP使用。
| 如果你只对通用的声明式服务或其他预包装的声明式中间件服务(如连接池)感兴趣,那么你不需要直接使用Spring AOP,可以跳过本章的大部分内容。 |
5.1. AOP 概念
让我们首先定义一些核心的AOP概念和术语。这些术语并不是Spring特有的。不幸的是,AOP的术语并不是特别直观。然而,如果Spring使用自己的术语,会更加令人困惑。
-
方面:一种跨越多个类的关注点的模块化。 事务管理是企业Java应用程序中一个良好的横切关注点示例。 在Spring AOP中,方面是通过使用普通类(基于模式的方法)或带有
@Aspect注解的普通类(@AspectJ风格)来实现的。 -
切点:程序执行过程中的一个点,例如方法的执行或异常的处理。在Spring AOP中,切点总是表示方法的执行。
-
建议:在特定的连接点上,由方面采取的操作。不同的建议类型包括“围绕”、“之前”和“之后”建议。(建议类型将在后面讨论。)许多AOP框架,包括Spring,将建议建模为拦截器,并在连接点周围维护一个拦截器链。
-
切点:一个匹配连接点的谓词。通知与切点表达式相关,并在切点匹配的任何连接点处运行(例如,具有特定名称的方法的执行)。由切点表达式匹配的连接点的概念是面向切面编程的核心,Spring 默认使用 AspectJ 切点表达式语言。
-
简介:代表一个类型声明额外的方法或字段。Spring AOP 允许您向任何建议的对象引入新的接口(以及相应的实现)。例如,您可以使用引入功能让一个 Bean 实现一个
IsModified接口,以简化缓存。(在 AspectJ 社区中,这种引入被称为交叉类型声明。) -
目标对象:由一个或多个方面进行通知的对象。也称为“被通知对象”。由于 Spring AOP 是通过使用运行时代理实现的,因此此对象始终是一个代理对象。
-
AOP代理:由AOP框架创建的对象,用于实现切面契约(例如通知方法的执行等)。在Spring框架中,AOP代理可以是JDK动态代理或CGLIB代理。
-
织入:将切面与其他应用类型或对象进行链接,以创建一个受 advised 的对象。这可以在编译时(例如使用 AspectJ 编译器)、加载时或运行时完成。Spring AOP 与其他纯 Java AOP 框架一样,在运行时进行织入。
Spring AOP 包含以下类型的增强:
-
前置通知:在连接点之前执行的通知,但无法阻止执行流程继续到连接点(除非它抛出异常)。
-
在返回建议后:在连接点正常完成之后执行的建议(例如,如果方法在不抛出异常的情况下返回)。
-
在抛出通知之后:如果方法通过抛出异常退出时要运行的通知。
-
在(最终)通知之后:无论连接点通过何种方式退出(正常或异常返回),都要执行的通知。
-
围绕通知:围绕连接点(如方法调用)的通知。 这是最强大类型的通知。围绕通知可以在方法调用前后执行自定义行为。它还负责选择是否继续执行连接点,或者通过返回自己的返回值或抛出异常来缩短被通知的方法执行。
环绕通知是类型最通用的通知。由于Spring AOP,就像AspectJ一样,提供了完整范围的通知类型,我们建议您使用能够实现所需行为的最弱通知类型。例如,如果您只需要在方法返回值上更新缓存,那么与使用环绕通知相比,实现返回后通知会更合适,尽管环绕通知也可以完成相同的事情。使用最具体的通知类型可以提供一个更简单的编程模型,减少出错的可能性。例如,您不需要在用于环绕通知的proceed()上调用JoinPoint方法,因此您不会忘记调用它。
所有通知参数都是静态类型的,这样您可以使用适当类型的通知参数(例如,方法执行的返回值类型),而不是 Object 数组。
通过切点匹配的连接点概念是面向方面编程(AOP)的关键,这使其区别于仅提供拦截功能的旧技术。切点使建议可以独立于面向对象的层次结构进行定位。例如,你可以将一个提供声明式事务管理的环绕建议应用于跨越多个对象的一组方法(比如服务层中的所有业务操作)。
5.2. Spring AOP 功能和目标
Spring AOP 是用纯 Java 实现的。不需要特殊的编译过程。Spring AOP 不需要控制类加载器层次结构,因此适合在 servlet 容器或应用服务器中使用。
Spring AOP 当前仅支持方法执行连接点(对 Spring Bean 上的方法执行进行增强)。字段拦截尚未实现,尽管可以无需破坏核心 Spring AOP API 的情况下添加对字段拦截的支持。如果您需要增强字段访问和更新连接点,请考虑使用 AspectJ 等语言。
Spring AOP 的 AOP 方法与其他大多数 AOP 框架不同。其目标不是提供最完整的 AOP 实现(尽管 Spring AOP 非常强大)。而是提供 AOP 实现与 Spring IoC 的紧密集成,以帮助解决企业应用程序中的常见问题。
因此,例如,Spring Framework 的 AOP 功能通常与 Spring IoC 容器一起使用。通过使用普通的 bean 定义语法来配置方面(尽管这允许强大的“自动代理”功能)。这与其他 AOP 实现有关键的区别。你无法轻松或高效地使用 Spring AOP 来通知非常细粒度的对象(通常是领域对象)。在这种情况下,AspectJ 是最佳选择。然而,我们的经验表明,Spring AOP 为大多数适合使用 AOP 的企业 Java 应用程序问题提供了极好的解决方案。
Spring AOP 从不试图与 AspectJ 竞争以提供全面的 AOP 解决方案。我们认为基于代理的框架如 Spring AOP 和功能完整的框架如 AspectJ 都是有价值的,它们是互补的,而不是相互竞争的。Spring 与 AspectJ 无缝集成,以在一致的基于 Spring 的应用程序架构中启用所有 AOP 的使用。此集成不会影响 Spring AOP API 或 AOP Alliance API。Spring AOP 保持向后兼容。有关 Spring AOP API 的讨论,请参阅 下一章。
|
Spring框架的核心理念之一就是非侵入性。这个理念是指你不应该被迫将框架特定的类和接口引入到你的业务或领域模型中。然而,在某些地方,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。由于编程到接口而不是类是一种良好的实践,业务类通常实现一个或多个业务接口。在那些(希望很少)需要对未在接口上声明的方法进行通知,或者需要将代理对象作为具体类型传递给方法的情况下,可以强制使用 CGLIB。
了解 Spring AOP 是基于代理的事实非常重要。有关此实现细节的实际含义的详细分析,请参阅 理解 AOP 代理。
5.4. @AspectJ 支持
@AspectJ 指的是一种将方面声明为常规 Java 类的方法,这些类使用注解进行标注。@AspectJ 风格由 AspectJ 项目 在 AspectJ 5 发布版本中引入。Spring 会解释与 AspectJ 5 相同的注解,使用由 AspectJ 提供的库来进行切入点解析和匹配。不过,AOP 运行时仍然是纯 Spring AOP,并且不依赖 AspectJ 编译器或织入器。
| 使用 AspectJ 编译器和织入器可以使用完整的 AspectJ 语言,并在 使用 AspectJ 与 Spring 应用程序 中进行了讨论。 |
5.4.1. 启用 @AspectJ 支持
要在 Spring 配置中使用 @AspectJ 切面,你需要启用 Spring 对基于 @AspectJ 切面的 Spring AOP 配置支持,并根据这些切面是否通知(advised)了 Bean 来自动代理(auto-proxy)Bean。所谓自动代理,是指如果 Spring 确定一个 Bean 被一个或多个切面通知,则会自动为该 Bean 生成一个代理,以拦截方法调用并确保在需要时运行通知。
可以通过 XML 或 Java 样式的配置启用 @AspectJ 支持。无论哪种情况,您还需要确保 AspectJ 的 aspectjweaver.jar 库在应用程序的类路径中(版本 1.8 或更高)。此库位于 AspectJ 发行版的 lib 目录中,或可以从 Maven 中央仓库获得。
使用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的配置
所述的模式支持。有关如何在
aop 命名空间中导入标记的信息,请参阅
AOP模式。
5.4.2. 声明一个切面
启用了 @AspectJ 支持后,任何在应用程序上下文中定义的、其类为 @AspectJ 切面(具有 @Aspect 注解)的 bean 都会被 Spring 自动检测,并用于配置 Spring AOP。接下来的两个示例展示了对于一个不太实用的切面所需的最小定义。
第一个示例显示了应用程序上下文中一个常规的bean定义,该定义指向一个具有@Aspect注释的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
第二个示例展示了 NotVeryUsefulAspect 类定义,
该定义使用了 org.aspectj.lang.annotation.Aspect 注解;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
package org.xyz
import org.aspectj.lang.annotation.Aspect;
@Aspect
class NotVeryUsefulAspect
切面(用@Aspect注解的类)可以有方法和字段,与其他类相同。它们还可以包含切入点、通知和引入(类型间)声明。
|
通过组件扫描自动检测切面 你可以将切面类作为普通 bean 注册到 Spring 的 XML 配置中,或者通过类路径扫描自动检测它们 — 与其他任何 Spring 管理的 bean 一样。
然而,请注意,这的@Aspect注解在类路径中不足以进行自动检测。为此,你需要添加一个单独的@Component注解
(或者,作为Spring组件扫描器规则的替代,一个自定义的构造型注解) |
|
与其他方面进行建议? 在 Spring AOP 中,切面本身不能成为其他切面的建议目标。
The@Aspect类上的注解将其标记为切面,因此会将其排除在自动代理之外。 |
5.4.3. 声明切点
切点确定了感兴趣的连接点,从而允许我们控制建议何时运行。Spring AOP仅支持Spring Bean的方法执行连接点,因此您可以将切点视为匹配Spring Bean上的方法执行。切点声明有两个部分:一个由名称和任何参数组成的签名,以及一个切点表达式,该表达式确定我们感兴趣的具体方法执行。在@AspectJ注解样式中,切点签名由常规方法定义提供,而切点表达式通过使用@Pointcut注解来指示(作为切点签名的方法必须具有void返回类型)。
一个例子可能有助于澄清切入点签名和切入点表达式之间的区别。下面的例子定义了一个名为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的书籍(例如
支持的切点指示器
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仅限于方法执行连接点的匹配,前面关于切入点指示器的讨论的定义比你在AspectJ编程指南中找到的定义要狭窄。此外,AspectJ本身具有基于类型的语义,并且在执行连接点处,this和target指的是同一个对象:正在执行方法的对象。Spring AOP是一个基于代理的系统,并区分代理对象本身(绑定到this)和代理背后的目标对象(绑定到target)。
|
由于Spring的AOP框架基于代理,目标对象内部的调用,从定义上讲是无法被拦截的。对于JDK代理而言,只有代理上的公共接口方法调用可以被拦截。使用CGLIB时,代理上的公共和受保护方法调用会被拦截(在需要的情况下,甚至包可见的方法也会被拦截)。然而,通过代理的常见交互应始终通过公共签名来设计。 请注意,切点定义通常会与任何被拦截的方法匹配。 如果一个切点严格仅适用于公共方法,在使用CGLIB代理的情况下,即使通过代理可能存在非公共的交互,也需要相应地进行定义。 如果您需要拦截目标类中的方法调用甚至构造函数,请考虑使用由Spring驱动的原生AspectJ编织,而不是Spring的基于代理的AOP框架。这是一种不同的AOP使用模式,具有不同的特性,因此在做决定之前,请务必了解编织的相关内容。 |
Spring AOP 还支持另一个名为 bean 的 PCD。此 PCD 允许您将连接点的匹配限制为特定的命名 Spring Bean,或者在使用通配符时,限制为一组命名的 Spring Bean。bean PCD 的形式如下:
bean(idOrNameOfBean)
bean(idOrNameOfBean)
idOrNameOfBean Tokens可以是任何 Spring Bean 的名称。提供有限的通配符支持,使用 * 字符,因此,如果您为 Spring Bean 建立了一些命名约定,就可以编写一个 bean PCD 表达式来选择它们。与其他切入点指示器一样,bean PCD 也可以与 &&(且)、||(或)和 !(否定)运算符一起使用。
|
The |
组合切入点表达式
您可以组合切入点表达式,使用 &&, || 和 !。您也可以通过名称引用切入点表达式。以下示例展示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} (1)
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} (2)
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} (3)
| 1 | anyPublicOperation 个匹配项,如果方法执行连接点代表任何公共方法的执行。 |
| 2 | inTrading 个匹配项,如果方法执行在交易模块中。 |
| 3 | tradingOperation 个匹配项,如果方法执行代表交易模块中的任何公共方法。 |
@Pointcut("execution(public * *(..))")
private fun anyPublicOperation() {} (1)
@Pointcut("within(com.xyz.myapp.trading..*)")
private fun inTrading() {} (2)
@Pointcut("anyPublicOperation() && inTrading()")
private fun tradingOperation() {} (3)
| 1 | anyPublicOperation 个匹配项,如果方法执行连接点代表任何公共方法的执行。 |
| 2 | inTrading 个匹配项,如果方法执行在交易模块中。 |
| 3 | tradingOperation 个匹配项,如果方法执行代表交易模块中的任何公共方法。 |
将更复杂的切入点表达式构建为较小的命名组件是一种最佳实践,如前所示。当通过名称引用切入点时,正常的Java可见性规则适用(你可以在同一类型中看到私有切入点,在层次结构中看到受保护的切入点,在任何地方看到公共切入点,等等)。可见性不影响切入点匹配。
共享通用切入点定义
在开发企业应用程序时,开发人员经常希望从多个方面引用应用程序的模块和特定的操作集。我们建议定义一个CommonPointcuts切面,用于捕获为此目的的通用切入点表达式。这样的切面通常类似于以下示例:
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>
The <aop:config> 和 <aop:advisor> 元素在基于模式的AOP支持中讨论。The
事务元素在事务管理中讨论。
示例
Spring AOP 用户最常使用 execution 切点指定符。
执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式(例如前面代码片段中的ret-type-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' 在绑定形式中更常用。请参阅 声明通知 部分,了解如何在通知体中提供代理对象。 -
任何连接点(仅限Spring AOP中的方法执行)目标对象实现
AccountService接口的地方:target(com.xyz.service.AccountService)
'target' 在绑定形式中更常用。有关如何在通知体中提供目标对象的信息,请参阅 声明通知 部分。 -
任何连接点(仅限Spring AOP中的方法执行)如果接受一个参数,并且运行时传递的参数是
Serializable:args(java.io.Serializable)
'args' 在绑定形式中更常用。有关如何在建议主体中提供方法参数的信息,请参阅 声明建议 部分。 请注意,此示例中给定的切入点与
execution(* *(java.io.Serializable))不同。如果运行时传递的参数是Serializable,则 args 版本匹配;如果方法签名声明了一个类型为Serializable的单个参数,则 execution 版本匹配。 -
任何连接点(仅限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”。请参阅声明通知部分,了解如何在通知体中提供注解对象。 -
任何连接点(仅限Spring AOP中的方法执行),如果它接受一个参数,并且传递的参数的运行时类型具有
@Classified注解:@args(com.xyz.security.Classified)
你也可以在绑定形式中使用 '@args'。请参阅声明建议部分,了解如何在建议主体中提供注解对象。 -
任何连接点(仅限Spring AOP中的方法执行)在一个名为
tradeService的Spring bean上:bean(tradeService)
-
任何连接点(仅限Spring AOP中的方法执行)在Spring bean上,这些bean的名称与通配符表达式
*Service匹配:bean(*Service)
编写良好的切入点
在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否与给定的切入点匹配(静态或动态)是一个代价高昂的过程。(动态匹配意味着无法通过静态分析完全确定匹配,并且在代码中放置了一个测试来确定代码运行时是否存在实际匹配)。在首次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这意味着什么?基本上,切入点被重写为 DNF(析取范式),并且切入点的组件按评估成本较低的顺序进行排序,以便首先检查那些评估成本较低的组件。这意味着你不必担心理解各种切入点指定符的性能,并且可以在切入点声明中以任意顺序提供它们。
然而,AspectJ 只能根据它被告知的内容工作。为了达到最佳的匹配性能,你应该考虑他们试图实现的目标,并尽可能地在定义中缩小匹配的搜索空间。现有的设计符自然分为三类:种类、范围和上下文:
-
种类选择符选择一种特定的连接点类型:
execution,get,set,call, 和handler. -
作用域设计符选择一组感兴趣的连接点 (可能是多种类型的):
within和withincode -
上下文标识符基于上下文匹配(并可选地绑定):
this,target, 和@annotation
编写良好的切入点至少应包括前两种类型(种类和作用域)。你可以包含上下文指定符,以便基于连接点上下文进行匹配,或绑定该上下文以在通知中使用。仅提供种类指定符或仅提供上下文指定符可以工作,但这可能会影响编织性能(时间和内存使用),因为需要额外的处理和分析。作用域指定符非常快速地进行匹配,并且使用它们意味着AspectJ可以非常快速地排除不应进一步处理的连接点组。如果可能,一个好的切入点应该始终包含一个作用域指定符。
5.4.4. 声明通知
建议与切入点表达式相关联,并在匹配该切入点的方法执行之前、之后或周围运行。切入点表达式可以是简单地引用命名的切入点,也可以是在此处声明的切入点表达式。
前置通知
您可以使用@注解在切面中声明前置通知:
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() {
// ...
}
}
返回后通知
在返回后通知会在匹配的方法正常执行返回后运行。
你可以通过使用@注解来声明它:
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,这匹配任何返回值)。
请注意,在使用返回后通知时,不可能返回一个完全不同的引用。
在抛出建议后
在抛出通知会在匹配的方法执行退出并抛出异常时运行。你可以通过使用@注解来声明它,如下例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doRecoveryActions() {
// ...
}
}
通常,你希望建议仅在抛出特定类型的异常时运行,并且你也经常需要在建议体中访问抛出的异常。你可以使用throwing属性来限制匹配(如果需要的话——否则使用Throwable作为异常类型)并将抛出的异常绑定到建议参数。以下示例展示了如何做到这一点:
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)的方法执行。
|
请注意, |
最后通知
在匹配的方法执行退出时,后置(finally)通知会运行。它通过使用@注解来声明。后置通知必须准备好处理正常和异常返回条件。它通常用于释放资源等类似目的。以下示例展示了如何使用后置finally通知:
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注解声明的。通知方法的第一个参数必须是类型ProceedingJoinPoint。在通知的方法体中,调用proceed()上的ProceedingJoinPoint会导致底层方法运行。proceed方法还可以传递一个Object[]。数组中的值用于方法执行时的参数。
The behavior of proceed when called with an Object[] is a little different than
the behavior of proceed for around advice compiled by the AspectJ compiler. For around
advice written using the traditional AspectJ language, the number of arguments passed to
proceed must match the number of arguments passed to the around advice (not the number
of arguments taken by the underlying join point), and the value passed to proceed in a
given argument position supplants the original value at the join point for the entity
the value was bound to (do not worry if this does not make sense right now). The approach
taken by Spring is simpler and a better match to its proxy-based, execution-only
semantics. You only need to be aware of this difference if you compile @AspectJ
aspects written for Spring and use proceed with arguments with the AspectJ compiler
and weaver. There is a way to write such aspects that is 100% compatible across both
Spring AOP and AspectJ, and this is discussed in the
following section on advice parameters. |
以下示例展示了如何使用环绕通知:
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
}
}
围绕通知返回的值是调用方法时看到的返回值。例如,一个简单的缓存切面可以在缓存中找到值时返回该值,如果找不到则调用proceed()。请注意,proceed在围绕通知的主体内可能被调用一次、多次或根本不被调用。所有这些都是合法的。
参数建议
Spring 提供了完全类型化的通知,这意味着你在通知签名中声明你需要的参数(正如我们在前面返回和抛出示例中看到的那样),而不是一直使用 Object[] 数组。在本节稍后部分,我们将介绍如何使参数和其他上下文值对通知体可用。首先,我们来看看如何编写通用的通知,以了解当前正在通知的方法。
访问当前的JoinPoint
任何建议方法都可以声明一个类型为org.aspectj.lang.JoinPoint的第一个参数(请注意,环绕建议需要声明一个类型为ProceedingJoinPoint的第一个参数,ProceedingJoinPoint是JoinPoint的子类。JoinPoint接口提供了一些有用的方法:
-
getArgs(): 返回方法参数。 -
getThis(): 返回代理对象。 -
getTarget(): 返回目标对象。 -
getSignature(): 返回正在被建议的方法的描述。 -
toString(): 打印被建议方法的有用描述。
请参阅javadoc以获取更多详细信息。
将参数传递给通知
我们已经看到了如何绑定返回值或异常值(使用after returning和after throwing通知)。要使参数值在通知体中可用,可以使用args的绑定形式。如果你在args表达式中用参数名代替类型名,当调用通知时,对应参数的值将作为参数值传递。一个示例应该能使这一点更清楚。假设你想对执行DAO操作提供建议,这些操作将Account对象作为第一个参数,并且你需要在通知体中访问该账户。你可以编写如下内容:
@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) {
// ...
}
The args(account,..) 部分的切入点表达式有两个目的。首先,它限制匹配仅限于那些方法执行,这些方法至少接受一个参数,并且传递给该参数的参数是一个 Account 的实例。其次,它通过 account 参数使实际的 Account 对象对通知可用。
另一种写法是声明一个切入点,当它匹配一个连接点时“提供”Account
对象值,然后在通知中引用命名的切入点。这将如下所示:
@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 方法的建议:
@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<?>,并手动检查元素的类型。
确定参数名称
参数绑定在通知调用中依赖于将切点表达式中使用的名称与通知和切点方法签名中声明的参数名称进行匹配。参数名称无法通过Java反射获取,因此Spring AOP采用以下策略来确定参数名称:
-
如果参数名称已被用户明确指定,则使用指定的参数名称。通知和切点注解都有一个可选的
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中都能一致工作。解决方案是确保建议签名按顺序绑定每个方法参数。以下示例展示了如何做到这一点:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
accountHolderNamePattern: String): Any {
val newPattern = preProcess(accountHolderNamePattern)
return pjp.proceed(arrayOf<Any>(newPattern))
}
在许多情况下,你无论如何都会进行这种绑定(如前面的例子所示)。
建议排序
当多个切面建议都希望在同一个连接点运行时会发生什么? Spring AOP遵循与AspectJ相同的优先级规则来确定建议执行的顺序。最高优先级的建议首先在“进入”时运行(因此,如果有两个前置建议,最高优先级的建议将首先运行)。在从一个连接点“退出”时,最高优先级的建议最后运行(因此,如果有两个后置建议,最高优先级的建议将第二个运行)。
当两个在不同切面中定义的建议都需要在同一连接点运行时,除非另有指定,否则执行顺序是未定义的。你可以通过指定优先级来控制执行顺序。这可以通过在切面类中实现org.springframework.core.Ordered接口或使用@Order注解来完成。给定两个切面,从Ordered.getOrder()(或注解值)返回较低值的切面具有较高的优先级。
|
每个特定切面的不同通知类型在概念上都旨在直接应用于连接点。因此,一个 从Spring Framework 5.2.7开始,定义在同一个 当同一类型的两个通知(例如,两个 |
5.4.5. 介绍
介绍(在AspectJ中称为类型间声明)使切面能够声明被通知对象实现给定的接口,并为这些对象提供该接口的实现。
你可以通过使用@DeclareParents注解来创建一个介绍。这个注解用于声明匹配的类型有一个新的父类(因此得名)。例如,给定一个名为UsageTracked的接口和该接口的一个实现类DefaultUsageTracked,以下切面声明所有服务接口的实现者也实现了UsageTracked接口(例如,通过JMX进行统计):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
@Aspect
class UsageTracking {
companion object {
@DeclareParents(value = "com.xzy.myapp.service.*+", defaultImpl = DefaultUsageTracked::class)
lateinit var mixin: UsageTracked
}
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
}
要实现的接口由注解字段的类型决定。value 属性的 @DeclareParents 注解是一个 AspectJ 类型模式。任何匹配类型的 bean 都会实现 UsageTracked 接口。请注意,在前面示例的前置通知中,可以直接将服务 bean 用作 UsageTracked 接口的实现。如果需要程序化地访问 bean,你可以这样写:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
val usageTracked = context.getBean("myService") as UsageTracked
5.4.6. 切面实例化模型
| 这是一个高级主题。如果你刚刚开始学习AOP,可以放心跳过它,直到以后再看。 |
默认情况下,应用程序上下文中每个切面只有一个实例。AspectJ 称之为单例实例化模型。可以定义具有替代生命周期的切面。Spring 支持 AspectJ 的 perthis 和 pertarget 实例化模型;percflow、percflowbelow 和 pertypewithin 目前不被支持。
你可以通过在@Aspect注解中指定一个perthis子句来声明一个perthis切面。考虑以下示例:
@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编程指南。
The pertarget 实例化模型的工作方式与 perthis 完全相同,但它为每个匹配连接点的唯一目标对象创建一个切面实例。
5.4.7. 一个AOP示例
现在你已经看到了所有组成部分的工作原理,我们可以将它们组合起来做一些有用的事情。
业务服务的执行有时会因并发问题(例如,死锁失败者)而失败。如果重试该操作,它很可能在下一次尝试中成功。对于在这种情况下适合重试的业务服务(幂等操作,不需要返回用户进行冲突解决),我们希望透明地重试该操作以避免客户端看到PessimisticLockingFailureException。这是一个明显跨越服务层多个服务的需求,因此通过切面实现是理想的。
因为我们想要重试操作,所以我们需要使用环绕通知,以便我们可以多次调用proceed。以下代码示例展示了基本的切面实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.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. 基于模式的AOP支持
如果你更喜欢基于XML的格式,Spring还提供了使用aop命名空间标签定义切面的支持。与使用@AspectJ风格时相同的支持点切点表达式和通知类型。因此,在本节中,我们将重点介绍该语法,并请读者参考前一节(@AspectJ支持)以了解编写点切点表达式和绑定通知参数的相关内容。
要使用本节中描述的aop命名空间标签,您需要导入spring-aop模式,如基于XML模式的配置中所述。有关如何在aop命名空间中导入标签的信息,请参见AOP模式。
在您的Spring配置中,所有切面和通知元素必须放置在<aop:config>元素内(您可以在应用程序上下文配置中拥有多个<aop:config>元素)。一个<aop:config>元素可以包含切入点、通知和切面元素(请注意,这些元素必须按此顺序声明)。
The <aop:config> 风格的配置大量使用了Spring的
自动代理 机制。这可能会导致问题(例如,建议未被编织)如果你已经通过使用
BeanNameAutoProxyCreator 或类似的方式显式地使用自动代理。推荐的使用模式是只使用
<aop:config> 风格或只使用 AutoProxyCreator 风格,不要混合使用它们。 |
5.5.1. 声明一个切面
当你使用模式支持时,切面是一个在你的Spring应用程序上下文中定义为bean的常规Java对象。对象的状态和行为通过其字段和方法来捕捉,而切入点和通知信息则通过XML来捕捉。
你可以通过使用<aop:aspect>元素来声明一个切面,并通过使用ref属性来引用支持的bean,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
支持该切面的bean(在这种情况下为aBean)当然可以像其他任何Spring bean一样进行配置和依赖注入。
5.5.2. 声明一个切入点
您可以在<aop:config>元素内声明一个命名的切入点,让切入点定义在多个切面和通知中共享。
一个表示服务层中任何业务服务执行的切入点可以如下定义:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
请注意,切点表达式本身使用的是与@AspectJ支持中描述的相同的AspectJ切点表达式语言。如果您使用基于模式的声明样式,您可以在切点表达式中引用类型(@Aspects)中定义的命名切点。定义上述切点的另一种方式如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.CommonPointcuts.businessService()"/>
</aop:config>
假设你有一个如共享通用切入点定义中所述的CommonPointcuts个切面。
然后在切面中声明一个切入点与声明顶级切入点非常相似,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
以类似于@AspectJ切面的方式,使用基于模式的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点将this对象作为连接点上下文收集并传递给通知:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && 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 引用,并且不能作为命名的切入点来形成复合切入点。因此,基于模式定义风格中的命名切入点支持比@AspectJ风格提供的支持更为有限。
5.5.3. 声明通知
基于模式的AOP支持使用与@AspectJ风格相同的五种通知类型,并且它们具有完全相同的语义。
前置通知
在匹配的方法执行之前运行的前置通知。它通过使用<aop:before>元素在<aop:aspect>中声明,如下例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
这里,dataAccessOperation 是在顶部 (<aop:config>) 级别定义的切入点的 id。要内联定义切入点,请将 pointcut-ref 属性替换为 pointcut 属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
正如我们在讨论@AspectJ风格时所指出的,使用命名切入点可以显著提高代码的可读性。
The method 属性标识了一个方法 (doAccessCheck),该方法提供了通知的主体。此方法必须为包含通知的 aspect 元素所引用的 bean 定义。在执行数据访问操作之前(通过切入点表达式匹配的方法执行连接点),将调用 aspect bean 上的 doAccessCheck 方法。
返回后通知
在返回后通知会在匹配的方法正常执行完毕后运行。它与前置通知一样,声明在一个<aop:aspect>中。以下示例展示了如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
正如@AspectJ风格中那样,你可以在通知体中获取返回值。
为此,请使用returning属性指定应将返回值传递给的参数名称,如下例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
The 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风格中那样,你可以在通知体中获取抛出的异常。
为此,请使用throwing属性来指定应将异常传递给的参数名称,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
The doRecoveryActions 方法必须声明一个名为 dataAccessEx 的参数。
该参数的类型以与描述 @AfterThrowing 相同的方式约束匹配。
例如,方法签名可以如下声明:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
最后通知
在匹配的方法执行退出后,无论以何种方式,都会运行 (finally) advice。
你可以通过使用 after 元素来声明它,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
环绕通知
最后一种建议是环绕通知。环绕通知在匹配方法执行的“周围”运行。它有机会在方法运行之前和之后进行操作,并确定方法何时、如何甚至是否实际运行。环绕通知通常用于在方法执行前后以线程安全的方式共享状态(例如,启动和停止计时器)。始终使用满足需求的最弱形式的通知。如果前置通知可以完成任务,就不要使用环绕通知。
您可以使用aop:around元素声明环绕通知。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知的主体中,调用proceed()上的ProceedingJoinPoint会导致底层方法运行。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()
}
参数建议
基于模式的声明风格以与@AspectJ支持相同的方式支持完全类型化的通知——通过按名称匹配切入点参数和通知方法参数。有关详细信息,请参阅通知参数。如果您希望显式指定通知方法的参数名称(不依赖于之前描述的检测策略),可以通过使用通知元素的arg-names属性来实现,该属性的处理方式与通知注解中的argNames属性相同(如确定参数名称中所述)。以下示例展示了如何在XML中指定参数名称:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>
The arg-names 属性接受以逗号分隔的参数名称列表。
以下是一个稍微复杂一些的基于XSD的方法示例,展示了在多个强类型参数结合使用时的一些环绕通知:
package x.y.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
package x.y.service
interface PersonService {
fun getPerson(personName: String, age: Int): Person
}
class DefaultPersonService : PersonService {
fun getPerson(name: String, age: Int): Person {
return Person(name, age)
}
}
接下来是切面。注意profile(..)方法接受多个强类型参数,其中第一个恰好是用于继续进行方法调用的连接点。这个参数的存在表明profile(..)将被用作around通知,如下例所示:
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
import org.aspectj.lang.ProceedingJoinPoint
import org.springframework.util.StopWatch
class SimpleProfiler {
fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any {
val clock = StopWatch("Profiling for '$name' and '$age'")
try {
clock.start(call.toShortString())
return call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
}
}
最后,下面的示例XML配置实现了对特定连接点执行前面所述的建议:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="x.y.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="x.y.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* x.y.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
考虑以下驱动脚本:
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)
建议排序
当多个切面需要在同一连接点(执行方法)运行时,排序规则如切面排序中所述。切面之间的优先级通过order属性在<aop:aspect>元素中确定,或者通过在支持切面的bean上添加@Order注解,或者通过让bean实现Ordered接口。
|
与在同一个 例如,给定一个 作为一个通用的经验法则,如果你发现同一个 |
5.5.4. 介绍
介绍(在AspectJ中称为类型间声明)允许一个切面声明被通知的对象实现给定的接口,并为这些对象提供该接口的实现。
你可以通过在aop:aspect内使用aop:declare-parents元素来制作介绍。
你可以使用aop:declare-parents元素声明匹配的类型有一个新的父类(因此得名)。
例如,给定一个名为UsageTracked的接口和该接口的一个实现名为DefaultUsageTracked,以下切面声明所有服务接口的实现者也实现了UsageTracked接口。(例如,为了通过JMX暴露统计信息。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.CommonPointcuts.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
支持usageTracking bean的类将包含以下方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
要实现的接口由implement-interface属性确定。types-matching属性的值是一个AspectJ类型模式。任何匹配类型的bean都实现了UsageTracked接口。请注意,在前面示例的前置通知中,服务bean可以直接用作UsageTracked接口的实现。要程序化地访问一个bean,你可以编写如下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
val usageTracked = context.getBean("myService") as UsageTracked
5.5.6. 通知者
“顾问”的概念来源于Spring中定义的AOP支持,并且在AspectJ中没有直接的等价物。顾问就像一个小型的自包含切面,具有单一的切面建议。建议本身由一个bean表示,并且必须实现Spring中的建议类型中描述的建议接口之一。顾问可以利用AspectJ的切入点表达式。
Spring 支持带有 <aop:advisor> 元素的顾问概念。你最常见的是在与事务建议结合使用时看到它,事务建议在 Spring 中也有自己的命名空间支持。以下示例展示了一个顾问:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
除了在前面示例中使用的 pointcut-ref 属性外,还可以使用 pointcut 属性来内联定义切点表达式。
要定义顾问的优先级,以便建议可以参与排序,请使用order属性来定义顾问的Ordered值。
5.5.7. 一个AOP模式示例
本节展示了如何将并发锁定失败重试示例(来自一个AOP示例)使用模式支持重写。
业务服务的执行有时会因并发问题(例如,死锁失败者)而失败。如果重试该操作,它很可能在下一次尝试中成功。对于在这种情况下适合重试的业务服务(幂等操作,不需要返回用户进行冲突解决),我们希望透明地重试该操作以避免客户端看到PessimisticLockingFailureException。这是一个明显跨越服务层多个服务的需求,因此通过切面实现是理想的。
因为我们想要重试操作,所以我们需要使用环绕通知,以便我们可以多次调用proceed。以下示例展示了基本的切面实现(这是一个使用模式支持的常规Java类):
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
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声明风格
一旦你决定使用切面是实现某个需求的最佳方法,那么如何在Spring AOP和AspectJ之间进行选择,以及在切面语言(代码)风格、@AspectJ注解风格和Spring XML风格之间进行选择呢?这些决策受到多个因素的影响,包括应用程序需求、开发工具和团队对AOP的熟悉程度。
5.6.1. Spring AOP 还是完整的AspectJ?
使用最简单可行的方法。Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/织入器。如果你只需要对 Spring bean 的操作执行进行通知,那么 Spring AOP 是正确的选择。如果你需要对不由 Spring 容器管理的对象(例如,域对象)进行通知,或者你需要通知除简单方法执行之外的连接点(例如,字段获取或设置连接点等),则需要使用 AspectJ。
当你使用AspectJ时,你可以选择AspectJ语言语法(也称为“代码风格”)或@AspectJ注解风格。显然,如果你不使用Java 5+,那么你的选择已经被决定了:使用代码风格。如果切面在你的设计中扮演重要角色,并且你能够使用AspectJ开发工具(AJDT)插件为Eclipse,AspectJ语言语法是首选选项。它更简洁和简单,因为该语言是专门为编写切面而设计的。如果你不使用Eclipse或只有少量切面并且它们在你的应用中不扮演主要角色,你可能想要考虑使用@AspectJ注解风格,坚持使用常规的Java编译器在IDE中,并在构建脚本中添加一个切面编织阶段。
5.6.2. @AspectJ 或 XML 用于 Spring AOP?
如果您选择使用 Spring AOP,您可以选择 @AspectJ 或 XML 风格。 有许多权衡需要考虑。
XML风格可能对现有的Spring用户来说最为熟悉,并且它由真正的POJO支持。在使用AOP作为配置企业服务的工具时,XML可能是一个不错的选择(一个很好的测试是你是否认为切点表达式是你可能希望独立更改的配置的一部分)。使用XML风格时,可以说从你的配置中更清楚地看出系统中存在哪些方面。
XML风格有两个缺点。首先,它没有将需求的实现完全封装在一个地方。DRY原则指出,系统中的任何知识都应该有一个单一、明确、权威的表示形式。使用XML风格时,需求的实现知识分散在后端bean类的声明和配置文件中的XML之间。当你使用@AspectJ风格时,这些信息被封装在一个模块中:切面。其次,XML风格在表达能力上略逊于@AspectJ风格:仅支持“singleton”切面实例化模型,并且不能组合在XML中声明的命名切入点。例如,在@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. 混合切面类型
通过使用自动代理支持,完全可以在同一配置中混合使用@AspectJ风格的切面、模式定义的<aop:aspect>切面、<aop:advisor>声明的顾问,甚至其他风格的代理和拦截器。所有这些都是通过相同的底层支持机制实现的,并且可以毫无困难地共存。
5.8. 代理机制
Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的目标对象创建代理。JDK 动态代理是 JDK 内置的,而 CGLIB 是一个常见的开源类定义库(重新打包为 spring-core)。
如果要代理的目标对象实现了至少一个接口,则使用JDK动态代理。目标类型实现的所有接口都将被代理。如果目标对象没有实现任何接口,则创建CGLIB代理。
如果你想强制使用CGLIB代理(例如,代理目标对象定义的每个方法,而不仅仅是它通过接口实现的方法),你可以这样做。但是,你应该考虑以下问题:
-
使用CGLIB,
final个方法无法被代理,因为它们无法在运行时生成的子类中被覆盖。 -
从 Spring 4.0 开始,你的代理对象的构造函数不会再被调用两次, 因为 CGLIB 代理实例是通过 Objenesis 创建的。只有在你的 JVM 不允许绕过构造函数的情况下, 你可能会看到两次调用以及来自 Spring 的 AOP 支持对应的调试日志条目。
要强制使用CGLIB代理,请将<aop:config>元素的proxy-target-class属性值设置为true,如下所示:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
要强制使用CGLIB代理当你使用@AspectJ自动代理支持时,请将<aop:aspectj-autoproxy>元素的proxy-target-class属性设置为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 引用进行调用,而不是代理。这有重要的含义。这意味着自我调用不会导致与方法调用相关的通知有机会运行。
好的,那么对此该怎么办?最好的方法(这里“最好”一词用得比较宽松)是重构你的代码,使得自调用不会发生。这确实需要你做一些工作,但这是最不侵入性的方法。接下来的方法绝对糟糕透顶,我们犹豫是否要指出这一点,正是因为它是如此糟糕。你可以(尽管对我们来说很痛苦)完全将类中的逻辑与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>()
请参阅Java文档以获取更多信息。
5.10. 在Spring应用程序中使用AspectJ
到目前为止,本章中我们所介绍的都是纯Spring AOP。在本节中,我们将探讨如何使用AspectJ编译器或织入器代替或补充Spring AOP,如果你的需求超出了Spring AOP本身提供的功能。
Spring 随带了一个小型的 AspectJ 方面库,该库在你的发行版中作为 spring-aspects.jar 单独提供。你需要将其添加到类路径中才能使用其中的方面。使用 AspectJ 与 Spring 进行依赖注入领域对象 和 其他 Spring 方面用于 AspectJ 讨论了此库的内容以及如何使用它。通过 Spring IoC 配置 AspectJ 方面 讨论了如何使用 AspectJ 编译器编织的 AspectJ 方面进行依赖注入。在 Spring 框架中使用 AspectJ 进行加载时编织 提供了对使用 AspectJ 的 Spring 应用程序进行加载时编织的介绍。
5.10.1. 使用AspectJ与Spring进行依赖注入域对象
Spring 容器会实例化和配置在应用程序上下文中定义的 bean。还可以让 bean 工厂根据包含要应用的配置的 bean 定义名称来配置一个预存在的对象。
spring-aspects.jar 包含一个注解驱动的切面,该切面利用此功能允许对任何对象进行依赖注入。该支持旨在用于容器控制之外创建的对象。领域对象通常属于此类,因为它们通常通过 new 操作符或由 ORM 工具作为数据库查询的结果来编程创建。
The @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 会通过使用与完全限定类型名称相同的 bean 定义(通常是原型作用域)来配置该注解类型的实例(在这种情况下为 Account)。由于 bean 的默认名称是其类型的完全限定名称,因此声明原型定义的一种方便方法是省略 id 属性,如下例所示:
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果你想显式指定要使用的原型bean定义的名称,你可以在注解中直接指定,如下例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
package com.xyz.myapp.domain
import org.springframework.beans.factory.annotation.Configurable
@Configurable("account")
class Account {
// ...
}
Spring 现在查找名为 account 的 bean 定义,并使用该定义来配置新的 Account 实例。
您还可以使用自动装配来避免必须指定专用的bean定义。要让Spring应用自动装配,请使用@Configurable注解的autowire属性。您可以指定@Configurable(autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME,分别按类型或按名称进行自动装配。作为替代,更倾向于通过@Autowired或@Inject在字段或方法级别显式地指定依赖注入,为您的@Configurablebeans指定注解驱动的依赖注入(有关详细信息,请参阅基于注解的容器配置)。您可以指定@Configurable(autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME,分别按类型或按名称进行自动装配。对于您的@Configurablebeans,建议通过@Autowired或@Inject指定明确的、注解驱动的依赖注入(请参阅基于注解的容器配置以获取更多详细信息)。
最后,你可以通过使用dependencyCheck属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))启用Spring对新创建和配置的对象中的对象引用的依赖检查。如果将此属性设置为true,Spring将在配置后验证所有属性(非原始类型或集合)是否已设置。
请注意,单独使用该注解并不会产生任何效果。是spring-aspects.jar中的AnnotationBeanConfigurerAspect作用于注解的存在。本质上,切面表示,“在返回带有@Configurable注解的新对象的初始化之后,根据注解的属性使用Spring配置新创建的对象”。在此上下文中,“初始化”指的是新实例化的对象(例如,使用new运算符实例化的对象)以及正在反序列化(例如,通过readResolve())的Serializable对象。
|
上述段落中的一个关键短语是“本质上”。在大多数情况下,“在新对象初始化返回后”的确切语义是没问题的。在这个上下文中,“初始化后”意味着在对象构造之后注入依赖项。这意味着依赖项在类的构造函数体中不可用。如果你想在构造函数体运行之前注入依赖项,从而使它们可以在构造函数体中使用,你需要在 Java
Kotlin
关于各种切入点类型的语言语义的更多信息可以在AspectJ 在这个附录中找到,这是AspectJ编程指南的一部分。 |
要使这有效,注解的类型必须与AspectJ编织器进行编织。您可以使用构建时的Ant或Maven任务来完成此操作(例如,请参阅AspectJ开发环境指南),或者使用运行时编织(请参阅Spring框架中的AspectJ运行时编织)。AnnotationBeanConfigurerAspect本身需要由Spring进行配置(以便获取将用于配置新对象的bean工厂的引用)。如果您使用基于Java的配置,可以在任何@Configuration类中添加@EnableSpringConfigured,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
@Configuration
@EnableSpringConfigured
class AppConfig {
}
如果您更喜欢基于XML的配置,Spring
context 命名空间
定义了一个方便的 context:spring-configured 元素,您可以如下使用:
<context:spring-configured/>
在切面配置之前创建的@Configurable对象的实例会导致向调试日志发出消息,并且不会对对象进行配置。一个例子可能是Spring配置中的bean在被Spring初始化时创建领域对象。在这种情况下,可以使用depends-on bean属性手动指定bean依赖于配置切面。以下示例展示了如何使用depends-on属性:
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>
请勿通过bean配置器切面激活@Configurable处理,除非你确实打算在运行时依赖其语义。特别是,请确保不要在作为常规Spring bean向容器注册的bean类上使用@Configurable。这样做会导致双重初始化,一次通过容器,一次通过切面。 |
单元测试 @Configurable 对象
One of the goals of the @Configurable support is to enable independent unit testing
of domain objects without the difficulties associated with hard-coded lookups.
If @Configurable types have not been woven by AspectJ, the annotation has no affect
during unit testing. You can set mock or stub property references in the object under
test and proceed as normal. If @Configurable types have been woven by AspectJ,
you can still unit test outside of the container as normal, but you see a warning
message each time that you construct a @Configurable object indicating that it has
not been configured by Spring.
与多个应用程序上下文一起工作
The AnnotationBeanConfigurerAspect 用于实现 @Configurable 支持的是一个AspectJ单例切面。单例切面的作用域与 static 成员的作用域相同:每个定义类型的类加载器都有一个切面实例。
这意味着,如果您在同一类加载器层次结构中定义了多个应用程序上下文,则需要考虑在哪里定义 @EnableSpringConfigured bean 以及在哪里将 spring-aspects.jar 放置在类路径上。
考虑一个典型的Spring Web应用程序配置,该配置具有共享的父应用程序上下文,用于定义通用业务服务、支持这些服务所需的一切,以及每个servlet的一个子应用程序上下文(其中包含特定于该servlet的定义)。所有这些上下文都在同一个类加载器层次结构中共存,因此AnnotationBeanConfigurerAspect只能持有它们中的一个的引用。在这种情况下,我们建议在共享(父级)应用程序上下文中定义@EnableSpringConfigured bean。这定义了你可能希望注入到域对象中的服务。其结果是你不能使用@Configurable机制将域对象配置为引用在子(servlet特定)上下文中定义的bean(这可能也不是你想要做的)。
在同一个容器中部署多个Web应用程序时,确保每个Web应用程序通过其自己的类加载器加载spring-aspects.jar中的类型(例如,将spring-aspects.jar放置在'WEB-INF/lib'中)。如果spring-aspects.jar仅添加到容器范围的类路径(并因此由共享的父类加载器加载),则所有Web应用程序将共享同一个切面实例(这可能不是你想要的)。
5.10.2. 其他Spring面向AspectJ的切面
除了@Configurable方面,spring-aspects.jar包含一个AspectJ切面,你可以使用它来驱动Spring的事务管理,适用于带有@Transactional注解的类型和方法。这主要是为那些希望在Spring容器之外使用Spring框架的事务支持的用户设计的。
解释@Transactional注解的方面是AnnotationTransactionAspect。当你使用这个方面时,你必须注解实现类(或该类中的方法或两者),而不是该类实现的接口(如果有的话)。AspectJ遵循Java的规则,即接口上的注解不会被继承。
一个 @Transactional 注解在类上指定了该类中任何公共操作执行的默认事务语义。
一个 @Transactional 注解在类中的方法上会覆盖由类注解(如果存在)给出的默认事务语义。任何可见性的方法都可以被注解,包括私有方法。直接注解非公共方法是为执行此类方法获取事务边界划分的唯一方式。
自 Spring Framework 4.2 起,spring-aspects 提供了一个类似的功能,为标准的 javax.transaction.Transactional 注解提供了完全相同的功能。有关更多详细信息,请参阅 JtaAnnotationTransactionAspect。 |
对于AspectJ程序员,如果想要使用Spring配置和事务管理支持但不想(或不能)使用注解,spring-aspects.jar还包含abstract个你可以扩展的切面来提供自己的切入点定义。有关AbstractBeanConfigurerAspect和AbstractTransactionAspect切面的更多信息,请参阅源代码。作为一个示例,以下摘录展示了如何编写一个切面来配置所有通过匹配完全限定类名的原型bean定义在领域模型中定义的对象实例:
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
public DomainObjectConfiguration() {
setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
}
// the creation of a new bean (any object in the domain model)
protected pointcut beanCreation(Object beanInstance) :
initialization(new(..)) &&
CommonPointcuts.inDomainModel() &&
this(beanInstance);
}
5.10.3. 使用Spring IoC配置AspectJ切面
当你在Spring应用程序中使用AspectJ切面时,很自然地希望并且期望能够通过Spring配置这些切面。AspectJ运行时本身负责切面的创建,而通过Spring配置AspectJ创建的切面的方式取决于AspectJ实例化模型(切面使用的per-xxx子句)。
大多数AspectJ切面是单例切面。配置这些切面很容易。你可以创建一个引用切面类型的bean定义,并包含factory-method="aspectOf" bean属性。这确保了Spring通过询问AspectJ来获取切面实例,而不是尝试自己创建实例。以下示例展示了如何使用factory-method="aspectOf"属性:
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf"> (1)
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
| 1 | 注意 factory-method="aspectOf" 属性 |
非单例切面更难配置。但是,可以通过创建原型bean定义并使用@Configurable从spring-aspects.jar提供的支持来配置切面实例,一旦它们被AspectJ运行时创建。
如果你有一些@AspectJ切面,你希望使用AspectJ进行编织(例如,使用加载时编织对领域模型类型),以及其他@AspectJ切面,你希望与Spring AOP一起使用,并且这些切面都在Spring中配置,你需要告诉Spring AOP @AspectJ自动代理支持应该使用配置中定义的哪些确切子集的@AspectJ切面用于自动代理。你可以通过在<aop:aspectj-autoproxy/>声明中使用一个或多个<include/>元素来实现这一点。每个<include/>元素指定一个名称模式,只有名称匹配至少一个模式的bean才会被用于Spring AOP自动代理配置。以下示例展示了如何使用<include/>元素:
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被<aop:aspectj-autoproxy/>元素的名称误导。使用它会导致创建Spring AOP代理。这里使用的是@AspectJ风格的切面声明,但AspectJ运行时并未参与。 |
5.10.4. 使用AspectJ在Spring框架中的加载时织入
Load-time weaving (LTW) 是指在将应用程序的类文件加载到 Java 虚拟机 (JVM) 时,将 AspectJ 的切面编织到这些类文件中的过程。本节的重点是在 Spring 框架的具体上下文中配置和使用 LTW。本节不是 LTW 的一般介绍。有关 LTW 的详细信息以及仅使用 AspectJ 配置 LTW(与 Spring 无关)的详细信息,请参阅 AspectJ 开发环境指南中关于 LTW 的部分。
Spring框架为AspectJ LTW带来的价值在于能够更精细地控制编织过程。'Vanilla' AspectJ LTW是通过使用Java(5+)代理实现的,该代理在启动JVM时通过指定一个虚拟机参数来启用。因此,它是一个全局设置,这在某些情况下可能没问题,但在很多情况下有点过于粗犷。Spring启用的LTW允许你以每个ClassLoader的基础上切换LTW,这是更精细粒度的,并且在“单个JVM-多个应用程序”的环境中更有意义(例如在典型的服务器环境中)。
Further, 在某些环境中, 这种支持可以在不修改应用程序服务器启动脚本的情况下启用加载时编织,而该启动脚本通常需要添加-javaagent:path/to/aspectjweaver.jar或(如我们在本节后面描述的)-javaagent:path/to/spring-instrument.jar。开发人员配置应用程序上下文以启用加载时编织,而不是依赖于通常负责部署配置(例如启动脚本)的管理员。
现在销售介绍已经结束,让我们首先快速浏览一个使用Spring的AspectJ LTW示例,然后详细介绍示例中引入的元素。有关完整示例,请参阅 宠物诊所示例应用。
第一个示例
假设你是一名应用程序开发人员,被要求诊断系统中某些性能问题的原因。我们不打算使用性能分析工具,而是要启用一个简单的性能分析切面,这样我们可以快速获得一些性能指标。然后,我们可以立即对该特定区域应用更精细的性能分析工具。
这里提供的示例使用了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编织器我们希望将我们的 ProfilingAspect 编织到类中。这个文件约定,即在Java类路径上存在一个名为 META-INF/aop.xml 的文件(或文件),是标准的AspectJ做法。下面的例子展示了 aop.xml 文件:
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver>
<!-- only weave classes in our application-specific packages -->
<include within="foo.*"/>
</weaver>
<aspects>
<!-- weave in just this aspect -->
<aspect name="foo.ProfilingAspect"/>
</aspects>
</aspectj>
现在我们可以继续进行Spring特定的配置部分。我们需要配置一个LoadTimeWeaver(稍后解释)。这个加载时织入器是负责将一个或多个META-INF/aop.xml文件中的切面配置织入到应用程序类中的关键组件。好处是它不需要很多配置(有一些更多的选项可以指定,但这些将在稍后详细介绍),如以下示例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- a service object; we will be profiling its methods -->
<bean id="entitlementCalculationService"
class="foo.StubEntitlementCalculationService"/>
<!-- this switches on the load-time weaving -->
<context:load-time-weaver/>
</beans>
现在所有必需的构件(切面、META-INF/aop.xml文件和Spring配置)都已就位,我们可以创建以下驱动类,并使用main(..)方法来演示LTW的实际效果:
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
The -javaagent 是一个标志,用于指定和启用
代理程序
以对在 JVM 上运行的程序进行插桩。Spring 框架附带了一个这样的
代理程序,即 InstrumentationSavingAgent,它被包含在
spring-instrument.jar 中,该文件是在前面的例子中作为 -javaagent 参数的值提供的。
执行Main程序的输出看起来类似于下一个示例。
(我在calculateEntitlement()实现中引入了一个Thread.sleep(..)语句,以便配置文件实际上捕获到除0毫秒以外的内容
(01234毫秒并不是由AOP引入的开销)。
以下列表显示了我们运行配置文件时得到的输出:
Calculating entitlement StopWatch 'ProfilingAspect': running time (millis) = 1234 ------ ----- ---------------------------- ms % Task name ------ ----- ---------------------------- 01234 100% calculateEntitlement
由于这个LTW是通过使用完整的AspectJ实现的,我们不仅限于建议Spring bean。以下对Main程序的轻微修改会得到相同的结果:
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
new StubEntitlementCalculationService();
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
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实例。配置文件建议仍然会被编织进去。
诚然,这个例子很简单。然而,在前面的例子中已经介绍了Spring中LTW支持的基本内容,本节的其余部分将详细解释每个配置和使用背后的“为什么”。
The ProfilingAspect 在这个示例中使用的基本,但它非常有用。这是一个很好的开发时特性的例子,开发人员可以在开发过程中使用它,然后在将应用程序部署到UAT或生产环境时轻松排除。 |
面向切面
你在LTW中使用的切面必须是AspectJ切面。你可以用AspectJ语言本身来编写它们,也可以用@AspectJ风格来编写你的切面。这样你的切面既是有效的AspectJ切面,也是Spring AOP切面。此外,编译后的切面类需要在类路径上可用。
'META-INF/aop.xml'
AspectJ LTW基础设施通过使用一个或多个META-INF/aop.xml
文件进行配置,这些文件位于Java类路径上(直接或更典型地在jar文件中)。
此文件的结构和内容在AspectJ参考文档的LTW部分有详细说明。因为aop.xml文件是100%的AspectJ,我们在这里不再进一步描述它。
所需库(JAR)
至少,你需要以下库来使用Spring框架对AspectJ LTW的支持:
-
spring-aop.jar -
aspectjweaver.jar
如果您使用Spring提供的代理来启用instrumentation,您还需要:
-
spring-instrument.jar
Spring 配置
Spring 的 LTW 支持中的关键组件是 LoadTimeWeaver 接口(在 org.springframework.instrument.classloading 包中),以及 Spring 发行版中附带的该接口的众多实现。LoadTimeWeaver 负责在运行时向 ClassLoader 添加一个或多个 java.lang.instrument.ClassFileTransformers,这为各种有趣的应用打开了大门,其中之一就是 LTW 方面。
如果你对运行时类文件转换的概念不熟悉,请在继续之前查看java.lang.instrument包的javadoc API文档。虽然该文档并不全面,但至少你可以看到关键接口和类(供你在阅读本节时参考)。 |
为特定的 ApplicationContext 配置一个 LoadTimeWeaver 可以像添加一行代码一样简单。(请注意,您几乎肯定需要使用一个 ApplicationContext 作为您的 Spring 容器——通常,BeanFactory 是不够的,因为 LTW 支持使用 BeanFactoryPostProcessors。)
要启用Spring框架的LTW支持,您需要配置一个LoadTimeWeaver,
这通常是通过使用@EnableLoadTimeWeaving注解来完成的,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
@Configuration
@EnableLoadTimeWeaving
class AppConfig {
}
Alternatively, if you prefer XML-based configuration, use the
<context:load-time-weaver/> element. Note that the element is defined in the
context namespace. The following example shows how to use <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 |
|
Fallback,期望底层的ClassLoader遵循常见约定
(即 |
|
请注意,该表仅列出在您使用DefaultContextLoadTimeWeaver时自动检测到的LoadTimeWeavers。您可以指定要使用的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的配置,您可以将完全限定的类名指定为<context:load-time-weaver/>元素上的weaver-class属性的值。同样,以下示例指定了一个ReflectiveLoadTimeWeaver:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>
The LoadTimeWeaver 由配置定义并注册,可以在稍后使用众所周知的名称 loadTimeWeaver 从 Spring 容器中检索。
请记住,LoadTimeWeaver 仅作为 Spring 的 LTW 基础设施添加一个或多个 ClassFileTransformers 的机制存在。实际执行 LTW 的是 ClassFileTransformer(来自 org.aspectj.weaver.loadtime 包)类。ClassPreProcessorAgentAdapter 类。有关详细信息,请参阅 ClassPreProcessorAgentAdapter 类的类级 javadoc,因为具体的编织方式超出了本文档的范围。
有一个配置属性尚未讨论:aspectjWeaving
属性(或aspectj-weaving 如果你使用 XML)。这个属性控制 LTW
是否启用。它接受三个可能的值之一,默认值为
autodetect 如果属性不存在。下表总结了这三个
可能的值:
| 注解值 | XML 值 | 说明 |
|---|---|---|
|
|
AspectJ 织入已开启,并且在加载时根据需要织入切面。 |
|
|
LTW 已关闭。加载时没有编织任何切面。 |
|
|
如果Spring LTW基础设施可以找到至少一个 |
特定于环境的配置
这一部分包含了在使用Spring的LTW支持时所需的任何额外设置和配置,特别是在应用服务器和Web容器等环境中。
Tomcat, JBoss, WebSphere, WebLogic
Tomcat、JBoss/WildFly、IBM WebSphere Application Server 和 Oracle WebLogic Server 都提供了一个通用的应用程序 ClassLoader,该应用程序能够进行本地仪表化。Spring 的原生 LTW 可以利用这些 ClassLoader 实现来提供 AspectJ 编织。你可以简单地启用加载时编织,如前面所述。具体来说,你不需要修改 JVM 启动脚本来添加-javaagent:path/to/spring-instrument.jar。
请注意,在JBoss上,您可能需要禁用应用服务器扫描以防止它在应用程序实际启动之前加载类。一个快速的解决方法是在您的构件中添加一个名为WEB-INF/jboss-scanning.xml的文件,内容如下:
<scanning xmlns="urn:jboss:scanning:1.0"/>
通用Java应用程序
当需要在不受特定LoadTimeWeaver实现支持的环境中进行类级仪器化时,JVM代理是一般解决方案。对于此类情况,Spring提供了InstrumentationLoadTimeWeaver,它需要一个Spring特有的(但非常通用的)JVM代理spring-instrument.jar,该代理可被常见的@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 APIs
上一章描述了Spring对AOP的支持,包括@AspectJ和基于模式的切面定义。在本章中,我们将讨论较低级别的Spring AOP API。对于常见的应用程序,我们建议使用Spring AOP与AspectJ切入点,如前一章所述。
6.1. Spring中的切点API
本节描述了Spring如何处理关键的切入点概念。
6.1.1. 概念
Spring 的切入点模型使独立于通知类型的切入点重用成为可能。您可以使用相同的切入点针对不同的通知。
The org.springframework.aop.Pointcut 接口是核心接口,用于将通知目标定位到特定的类和方法。完整的接口如下:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
interface Pointcut {
fun getClassFilter(): ClassFilter
fun getMethodMatcher(): MethodMatcher
}
将 Pointcut 接口拆分为两部分允许重用类和方法匹配部分,并进行细粒度的组合操作(例如与另一个方法匹配器执行“并集”)。
The ClassFilter 接口用于将切点限制为给定的目标类集合。如果 matches() 方法始终返回 true,则匹配所有目标类。以下列出显示了 ClassFilter 接口定义:
public interface ClassFilter {
boolean matches(Class clazz);
}
interface ClassFilter {
fun matches(clazz: Class<*>): Boolean
}
The 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
}
The matches(Method, Class) 方法用于测试此切入点是否曾经匹配目标类上的给定方法。此评估可以在创建 AOP 代理时执行,以避免每次方法调用都需要进行测试。如果两参数的 matches 方法返回 true 对于给定的方法,并且 isRuntime() 方法对于 MethodMatcher 返回 true,则在每次方法调用时都会调用三参数的 matches 方法。这使得切入点可以在目标通知开始之前立即查看传递给方法调用的参数。
大多数 MethodMatcher 实现是静态的,这意味着它们的 isRuntime() 方法
返回 false。在这种情况下,三参数的 matches 方法从未被调用。
| 如果可能,尽量使切入点静态化,这样可以让 AOP 框架在创建 AOP 代理时缓存切入点评估的结果。 |
6.1.2. 切面操作
Spring 支持对切点的操作(特别是并集和交集)。
Union means the methods that either pointcut matches.
Intersection means the methods that both pointcuts match.
Union is usually more useful.
You can compose pointcuts by using the static methods in the
org.springframework.aop.support.Pointcuts class or by using the
ComposablePointcut class in the same package. However, using AspectJ pointcut
expressions is usually a simpler approach。
6.1.3. AspectJ表达式切入点
自2.0版本以来,Spring使用的最重要的切入点类型是
org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一种使用AspectJ提供的库来解析AspectJ切入点表达式字符串的切入点。
请参阅上一章以了解受支持的AspectJ切入点原语的讨论。
6.1.4. 便捷切入点实现
Spring 提供了多种方便的切入点实现。你可以直接使用其中一些;其他则是为了在应用程序特定的切入点中被子类化而设计的。
静态切面
静态切点基于方法和目标类,不能考虑方法的参数。静态切点足以满足大多数使用场景,并且是最好的选择。Spring 只能在方法第一次被调用时评估一次静态切点。之后,每次方法调用时都不需要再次评估切点。
本节的其余部分描述了Spring中包含的一些静态切入点实现。
正则表达式切入点
一种明显的方式来指定静态切入点是使用正则表达式。除了Spring之外,有几个AOP框架也提供了这种可能性。
org.springframework.aop.support.JdkRegexpMethodPointcut 是一个通用的正则表达式切入点,它使用了JDK中的正则表达式支持。
使用JdkRegexpMethodPointcut类,你可以提供一个模式字符串列表。
如果其中任何一个匹配,则切点评估为true。(因此,
结果切点实际上是指定模式的并集。)
以下示例展示了如何使用JdkRegexpMethodPointcut:
<bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
Spring 提供了一个名为 RegexpMethodPointcutAdvisor 的便捷类,它让我们还可以引用一个 Advice(请记住,Advice 可以是拦截器、前置通知、抛出通知等)。在幕后,Spring 使用了一个 JdkRegexpMethodPointcut。使用 RegexpMethodPointcutAdvisor 简化了连线,因为一个 bean 封装了切入点和通知,如下例所示:
<bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref bean="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
你可以使用 RegexpMethodPointcutAdvisor 与任何 Advice 类型。
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是如何处理通知的。
6.2.1. 通知生命周期
每个通知都是一个Spring bean。一个通知实例可以在所有被通知的对象之间共享,也可以对每个被通知的对象唯一。这对应于类级别的通知或实例级别的通知。
每类建议最常使用。它适用于通用建议,例如事务顾问。这些建议不依赖于代理对象的状态,也不添加新的状态。它们只是对方法和参数进行操作。
实例级别的建议适用于引入,以支持混入。在这种情况下,建议会向代理对象添加状态。
你可以在同一个AOP代理中使用共享和每个实例的建议的组合。
6.2.2. Spring中的通知类型
Spring 提供了几种通知类型,并且可以扩展以支持任意的通知类型。本节描述了基本概念和标准通知类型。
拦截器周围的通知
在Spring中最基本的建议类型是环绕通知。
Spring 符合使用方法拦截的 AOP Alliance 接口的环绕通知。实现 MethodInterceptor 并且实现环绕通知的类还应该实现以下接口:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
interface MethodInterceptor : Interceptor {
fun invoke(invocation: MethodInvocation) : Any
}
The MethodInvocation argument to the invoke() method exposes the method being
invoked, the target join point, the AOP proxy, and the arguments to the method. The
invoke() method should return the invocation’s result: the return value of the join
point。
以下示例展示了一个简单的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
}
}
请注意对MethodInvocation的proceed()方法的调用。这会沿着拦截器链向下进行,直到连接点。大多数拦截器调用此方法并返回其返回值。但是,一个MethodInterceptor,像任何环绕通知一样,可以返回不同的值或抛出异常,而不是调用proceed方法。但是,除非有充分的理由,否则您不想这样做。
MethodInterceptor 个实现提供了与其他符合 AOP Alliance 标准的 AOP 实现之间的互操作性。本节后面讨论的其他通知类型实现了常见的 AOP 概念,但以 Spring 特定的方式。虽然使用最具体的通知类型有优势,但如果你可能希望在另一个 AOP 框架中运行切面,请坚持使用 MethodInterceptor 类型的通知。请注意,切点目前在不同框架之间是不可互操作的,并且 AOP Alliance 目前没有定义切点接口。 |
前置通知
一个更简单的通知类型是前置通知。这不需要一个MethodInvocation
对象,因为它只在进入方法之前被调用。
在使用前置通知的主要优势是不需要调用proceed()
方法,因此没有意外地未能继续执行拦截器链的可能性。
以下列表显示了MethodBeforeAdvice接口:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
interface MethodBeforeAdvice : BeforeAdvice {
fun before(m: Method, args: Array<Any>, target: Any)
}
(Spring 的 API 设计允许在字段之前使用通知,尽管通常的对象适用于字段拦截,并且 Spring 很可能永远不会实现它。)
请注意返回类型是void。前置通知可以在连接点运行之前插入自定义行为,但不能更改返回值。如果前置通知抛出异常,它会停止拦截器链的进一步执行。异常会沿着拦截器链向上传播。如果它是未检查的或在被调用方法的签名中,它会直接传递给客户端。否则,它会被AOP代理包装成一个未检查异常。
以下示例展示了 Spring 中的前置通知,它统计所有方法调用:
public class CountingBeforeAdvice implements MethodBeforeAdvice {
private int count;
public void before(Method m, Object[] args, Object target) throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
class CountingBeforeAdvice : MethodBeforeAdvice {
var count: Int = 0
override fun before(m: Method, args: Array<Any>, target: Any?) {
++count
}
}
| 在任何切点使用前置通知之前。 |
Throws Advice
Throws advice 在连接点抛出异常后被调用。Spring 提供了类型化的 throws advice。请注意,这意味着 org.springframework.aop.ThrowsAdvice 接口不包含任何方法。它是一个标记接口,用于标识给定对象实现了一个或多个类型化的 throws advice 方法。这些方法应具有以下形式:
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。可以在一个类中组合任意数量的抛出建议方法。以下代码段显示了最终示例:
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
class CombinedThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Do something with remote exception
}
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Do something with all arguments
}
}
| 如果一个throws-advice方法自身抛出异常,它会覆盖原始异常(也就是说,它改变了传递给用户的异常)。覆盖的异常通常是RuntimeException,这与任何方法签名兼容。然而,如果一个throws-advice方法抛出一个已检查异常,它必须与目标方法声明的异常匹配,并因此在某种程度上与特定的目标方法签名耦合。不要抛出与目标方法签名不兼容的未声明的已检查异常! |
| 抛出通知可以与任何切入点一起使用。 |
返回后通知
Spring 中的后置通知必须实现
org.springframework.aop.AfterReturningAdvice 接口,如下所示:
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
interface AfterReturningAdvice : Advice {
fun afterReturning(returnValue: Any, m: Method, args: Array<Any>, target: Any)
}
在方法返回后,后置通知可以访问返回值(但不能修改)、被调用的方法、方法的参数以及目标对象。
以下返回后通知统计了所有成功的方法调用,这些方法调用没有抛出异常:
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 将引入通知视为一种特殊的拦截通知。
介绍需要一个IntroductionAdvisor和一个IntroductionInterceptor,它们实现了以下接口:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
interface IntroductionInterceptor : MethodInterceptor {
fun implementsInterface(intf: Class<*>): Boolean
}
The invoke() 方法继承自 AOP Alliance MethodInterceptor 接口必须实现引入。也就是说,如果调用的方法是在一个引入的接口上,那么引入拦截器负责处理方法调用——它不能调用 proceed()。
介绍性通知不能与任何切入点一起使用,因为它仅适用于类级别,而不是方法级别。您只能将介绍性通知与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<*>>
}
没有 MethodMatcher 和,因此,没有 Pointcut 与介绍建议相关联。只有类过滤是合理的。
The getInterfaces() 方法返回由这个顾问引入的接口。
The validateInterfaces() 方法用于内部检查引入的接口是否可以由配置的 IntroductionInterceptor 实现。
考虑Spring测试套件中的一个示例,并假设我们想要向一个或多个对象引入以下接口:
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
interface Lockable {
fun lock()
fun unlock()
fun locked(): Boolean
}
这说明了一个混入。我们希望能够将被建议的对象转换为Lockable,
无论它们的类型是什么,并调用锁定和解锁方法。如果我们调用lock()方法,我们
希望所有设置器方法都抛出一个LockedException。因此,我们可以添加一个切面,
提供使对象不可变的能力,而无需对象对此有任何了解:
这是AOP的一个很好的例子。
首先,我们需要一个 IntroductionInterceptor 来完成主要工作。在这种情况下,我们扩展了 org.springframework.aop.support.DelegatingIntroductionInterceptor
辅助类。我们可以直接实现 IntroductionInterceptor,但使用
DelegatingIntroductionInterceptor 对于大多数情况是最好的。
The DelegatingIntroductionInterceptor 是为了解决一个介绍到实际实现的引入接口的问题,隐藏了使用拦截来实现这一点。你可以通过构造函数参数设置委托对象。默认的委托(当使用无参构造函数时)是 this。因此,在下一个示例中,委托是 LockMixin 类的子类 DelegatingIntroductionInterceptor。
给定一个委托(默认情况下,它本身),一个 DelegatingIntroductionInterceptor 实例会查找委托实现的所有接口(除了 IntroductionInterceptor 之外)并支持针对其中任何一个接口的引入。像 LockMixin 这样的子类可以调用 suppressInterface(Class intf) 方法来抑制不应该暴露的接口。但是,无论一个 IntroductionInterceptor 准备支持多少个接口,IntroductionAdvisor 的使用控制了哪些接口实际上被暴露。引入的接口隐藏了目标对相同接口的任何实现。
因此,LockMixin 扩展了 DelegatingIntroductionInterceptor 并实现了 Lockable 本身。超类会自动接收 Lockable 可以被支持用于介绍,所以我们不需要指定这一点。我们可以通过这种方式引入任意数量的接口。
请注意locked实例变量的使用。这有效地为目标对象持有的状态添加了额外的状态。
以下示例显示了示例LockMixin类:
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
private boolean locked;
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public boolean locked() {
return this.locked;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
throw new LockedException();
}
return super.invoke(invocation);
}
}
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() 方法。The
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)
我们可以非常简单地应用这个通知者,因为它不需要任何配置。(然而,没有IntroductionAdvisor就不可能使用IntroductionInterceptor)。像往常一样,在介绍中,通知者必须是每个实例的,因为它是有状态的。我们需要为每个被通知的对象提供一个不同的LockMixinAdvisor实例,因此也需要不同的LockMixin。通知器构成了被通知对象状态的一部分。
我们可以通过编程方式应用这个通知器,方法是使用Advised.addAdvisor()方法,或者(推荐的方式)在XML配置中,就像任何其他通知器一样。下面讨论的所有代理创建选项,包括“自动代理创建器”,都能正确处理引介和状态化混入。
6.3. Spring中的Advisor API
在Spring中,Advisor是一个切面,其中只包含一个与切入点表达式关联的advice对象。
除了介绍的特殊情况外,任何顾问都可以与任何建议一起使用。
org.springframework.aop.support.DefaultPointcutAdvisor 是最常用的
顾问类。它可以与 MethodInterceptor、BeforeAdvice 或
ThrowsAdvice 一起使用。
在Spring中,可以在同一个AOP代理中混合使用通知和建议类型。例如,你可以在一个代理配置中使用拦截通知、抛出通知和前置通知。Spring会自动创建必要的拦截器链。
6.4. 使用ProxyFactoryBean创建AOP代理
如果你使用Spring IoC容器(一个ApplicationContext或BeanFactory)来管理你的业务对象(你应该这样做!),你希望使用Spring的AOP FactoryBean实现之一。(记住,工厂bean引入了一层间接性,让它可以创建不同类型的对象。)
| Spring AOP 支持也在幕后使用工厂bean。 |
在Spring中创建AOP代理的基本方法是使用
org.springframework.aop.framework.ProxyFactoryBean。这可以完全控制切点、适用的任何通知及其顺序。但是,如果你不需要这样的控制,还有更简单的选项可供选择。
6.4.1. 基础
The ProxyFactoryBean, like other Spring FactoryBean implementations, introduces a
level of indirection. If you define a ProxyFactoryBean named foo, objects that
reference foo do not see the ProxyFactoryBean instance itself but an object
created by the implementation of the getObject() method in the ProxyFactoryBean . This
method creates an AOP proxy that wraps a target object.
使用ProxyFactoryBean或其他IoC感知类来创建AOP代理的一个最重要的好处是,切面和切入点也可以由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: AStringarray ofAdvisor, interceptor, or other advice names to apply. Ordering is significant, on a first come-first served basis. That is to say that the first interceptor in the list is the first to be able to intercept the invocation.这些名称是当前工厂中的bean名称,包括来自祖先工厂的bean名称。你不能在这里提及bean引用,因为这样做会导致
ProxyFactoryBean忽略建议的单例设置。您可以在拦截器名称后添加一个星号 (
*)。这样做会导致所有名称以星号前部分开头的advisor bean被应用。您可以在使用“全局”顾问中找到使用此功能的示例。 -
singleton: 无论工厂是否应该返回一个单一的对象,无论
getObject()方法被调用多少次。几个FactoryBean实现提供了这样的方法。默认值是true。如果你想使用有状态的建议 - 例如,对于有状态的混入 - 使用原型建议并设置singleton值为false。
6.4.3. 基于JDK和CGLIB的代理
本节作为关于ProxyFactoryBean
如何选择为特定目标对象(将被代理)创建基于JDK的代理或基于CGLIB的代理的权威文档。
The behavior of the ProxyFactoryBean with regard to creating JDK- or CGLIB-based
proxies changed between versions 1.2.x and 2.0 of Spring. The ProxyFactoryBean now
exhibits similar semantics with regard to auto-detecting interfaces as those of the
TransactionProxyFactoryBean class. |
如果要代理的目标对象的类(以下简称目标类)没有实现任何接口,则会创建一个基于CGLIB的代理。这是最简单的情况,因为JDK代理是基于接口的,如果没有接口,JDK代理甚至不可能实现。你可以插入目标bean并设置interceptorNames属性来指定拦截器列表。请注意,即使ProxyFactoryBean的proxyTargetClass属性被设置为false,也会创建一个基于CGLIB的代理。(这样做没有意义,最好从bean定义中删除,因为它最多是冗余的,最坏情况下是令人困惑的。)
如果目标类实现了一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean的配置。
如果ProxyFactoryBean的proxyTargetClass属性被设置为true,将创建一个基于CGLIB的代理。这符合预期,并遵循最小惊讶原则。即使ProxyFactoryBean的proxyInterfaces属性被设置为一个或多个完全限定的接口名称,由于proxyTargetClass属性被设置为true,基于CGLIB的代理仍然生效。
如果proxyInterfaces属性的ProxyFactoryBean被设置为一个或多个完全限定的接口名称,将创建一个基于JDK的代理。创建的代理实现了在proxyInterfaces属性中指定的所有接口。如果目标类恰好实现了比在proxyInterfaces属性中指定的更多的接口,这是很好的,但是这些额外的接口不会由返回的代理实现。
如果 proxyInterfaces 属性的 ProxyFactoryBean 尚未设置,但目标类确实实现了一个(或多个)接口,则 ProxyFactoryBean 会自动检测到目标类实际上实现了至少一个接口,并创建基于JDK的代理。实际被代理的接口是目标类实现的所有接口。实际上,这相当于将目标类实现的每个接口都提供给 proxyInterfaces 属性。然而,这种方法的工作量显著减少,且不易出错。
6.4.4. 代理接口
考虑一个简单的 ProxyFactoryBean 实际应用的例子。这个例子涉及:
-
一个被代理的目标bean。这是示例中的
personTargetbean定义。 -
一个
Advisor和一个Interceptor用于提供建议。 -
一个AOP代理bean定义,用于指定目标对象(
personTargetbean)、要代理的接口以及要应用的切面。
以下示例显示了:
<bean id="personTarget" class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<property name="target" ref="personTarget"/>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
请注意,interceptorNames 属性接受一个 String 列表,其中包含当前工厂中拦截器或顾问的 bean 名称。您可以使用顾问、拦截器、前置、后置返回和抛出建议对象。顾问的顺序很重要。
你可能会想知道为什么列表不持有bean引用。原因在于,如果ProxyFactoryBean的单例属性设置为false,它必须能够返回独立的代理实例。如果任何顾问本身是一个原型,一个独立的实例需要被返回,因此有必要能够从工厂获取原型的实例。持有引用是不够的。 |
The person bean definition shown earlier can be used in place of a Person implementation, as
follows:
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>
The PersonUser 类在这个示例中暴露了一个类型为 Person 的属性。就它而言,AOP 代理可以透明地用于替代一个“真实”的人实现。然而,它的类将是一个动态代理类。可以将其转换为 Advised 接口(稍后讨论)。
你可以通过使用匿名内部bean来隐藏目标和代理之间的区别。只有ProxyFactoryBean定义是不同的。包含通知只是为了完整性。以下示例展示了如何使用匿名内部bean:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<!-- Use inner bean, not local reference to target -->
<property name="target">
<bean class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
使用匿名内部bean的优点是只有一个类型为Person的对象。这在我们希望防止应用程序上下文的用户获得未被代理对象的引用或需要避免与Spring IoC自动装配的任何歧义时非常有用。此外,可以说ProxyFactoryBean的定义是自包含的。然而,有时能够从工厂获取未被代理的目标实际上可能是一个优势(例如,在某些测试场景中)。
6.4.5. 代理类
如果你需要代理一个类,而不是一个或多个接口,该怎么办?
想象在我们之前的示例中,没有Person接口。我们需要通知一个名为Person的类,该类没有实现任何业务接口。在这种情况下,您可以配置Spring使用CGLIB代理而不是动态代理。为此,请将proxyTargetClass属性设置为ProxyFactoryBean(如前所示)中的true。虽然最好是对接口进行编程而不是对类进行编程,但在处理遗留代码时,能够通知未实现接口的类可能会很有用。(通常,Spring并不强制规定特定的方法。尽管它使应用良好实践变得容易,但它避免强制采用特定方法。)
如果你想,你可以强制使用 CGLIB,即使你有接口也是如此。
CGLIB代理通过在运行时生成目标类的子类来工作。Spring配置这个生成的子类,使其将方法调用委托给原始目标。该子类用于实现装饰器模式,编织进通知。
CGLIB代理通常对用户来说是透明的。然而,有一些问题需要考虑:
-
Final方法不能被通知,因为它们不能被覆盖。 -
无需将 CGLIB 添加到您的类路径中。从 Spring 3.2 开始,CGLIB 已被打包并包含在 spring-core JAR 中。换句话说,基于 CGLIB 的 AOP 和 JDK 动态代理都“开箱即用”。
CGLIB代理和动态代理之间的性能差异很小。在这种情况下,性能不应成为决定性因素。
6.4.6. 使用“全局”顾问
通过在拦截器名称后附加一个星号,所有与星号前部分匹配的bean名称的顾问将被添加到顾问链中。这在需要添加一组标准的“全局”顾问时会非常有用。以下示例定义了两个全局顾问:
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
6.5. 简洁的代理定义
特别是在定义事务代理时,你可能会遇到许多类似的代理定义。使用父级和子级bean定义,以及内部bean定义,可以导致更干净和更简洁的代理定义。
首先,我们为代理创建一个父级、模板、bean定义,如下:
<bean 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示例中,我们明确将父bean定义标记为抽象的,方法是将abstract属性设置为true,如先前所述,以便它实际上可能永远不会被实例化。应用程序上下文(但不是简单的bean工厂)默认情况下会预实例化所有单例。因此,重要的是(至少对于单例bean),如果你有一个仅打算用作模板的(父)bean定义,并且此定义指定了一个类,你必须确保将abstract属性设置为true。否则,应用程序上下文实际上会尝试预实例化它。
6.6. 使用ProxyFactory编程创建AOP代理
使用Spring可以轻松地通过编程方式创建AOP代理。这使您可以在不依赖Spring IoC的情况下使用Spring AOP。
目标对象实现的接口会自动被代理。以下示例展示了为一个目标对象创建代理的过程,其中包含一个拦截器和一个通知:
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,您可以使代理实现额外的接口。
There are also convenience methods on ProxyFactory (inherited from AdvisedSupport)
that let you add other advice types, such as before and throws advice.
AdvisedSupport is the superclass of both ProxyFactory and 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
The getAdvisors() 方法返回一个 Advisor,对于添加到工厂中的每个顾问、拦截器或其他建议类型。如果你添加了一个 Advisor,则返回的顾问在这个索引处是你添加的对象。如果你添加了拦截器或其他建议类型,Spring 将其包装在一个具有始终返回 true 的切入点的顾问中。因此,如果你添加了一个 MethodInterceptor,则返回的顾问是返回你的 MethodInterceptor 和匹配所有类和方法的切入点的 DefaultPointcutAdvisor。
The addAdvisor() 方法可以用于添加任何 Advisor。通常,持有切点和通知的顾问是通用的 DefaultPointcutAdvisor,你可以将其与任何通知或切点一起使用(但不适用于引介)。
默认情况下,即使在创建代理之后,也可以添加或删除顾问或拦截器。唯一的限制是无法添加或删除引入顾问,因为工厂现有的代理不会显示接口更改。(你可以从工厂获取一个新的代理来避免这个问题。)
以下示例展示了将AOP代理转换为Advised接口并检查和操作其通知:
Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());
// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
val advised = myObject as Advised
val advisors = advised.advisors
val oldAdvisorCount = advisors.size
println("$oldAdvisorCount advisors")
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(DebugInterceptor())
// Add selective advice using a pointcut
advised.addAdvisor(DefaultPointcutAdvisor(mySpecialPointcut, myAdvice))
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.advisors.size)
| 修改业务对象上的通知是否明智值得商榷(无意双关),尽管毫无疑问存在合理的使用场景。然而,在开发中(例如在测试中)这可能非常有用。我们有时发现,能够以拦截器或其他通知的形式添加测试代码非常有用,这样可以深入到我们想要测试的方法调用中。例如,通知可以进入为该方法创建的事务中,也许是为了运行SQL来检查数据库是否正确更新,然后再标记事务回滚。 |
取决于您创建代理的方式,通常可以设置一个frozen标志。在这种情况下,Advised isFrozen()方法返回true,并且任何通过添加或删除来修改建议的尝试都会导致AopConfigException。在某些情况下(例如,为了防止调用代码移除安全拦截器),冻结被建议对象的状态的能力是有用的。
6.8. 使用“auto-proxy”功能
到目前为止,我们已经考虑了通过使用ProxyFactoryBean或类似的工厂bean显式创建AOP代理。
Spring 还允许我们使用“自动代理”bean定义,这可以自动代理选定的bean定义。这是基于 Spring 的“bean 后处理器”基础设施,该基础设施使容器加载时能够修改任何 bean 定义。
在此模型中,您在XML bean定义文件中设置了一些特殊的bean定义来配置自动代理基础设施。这使您可以声明符合条件的自动代理目标。您不必使用ProxyFactoryBean。
有两种方法可以做到这一点:
-
通过使用自动代理创建器,该创建器引用当前上下文中的特定bean。
-
自动代理创建的一个特殊情况值得单独考虑: 由源级元数据属性驱动的自动代理创建。
6.8.1. 自动代理 Bean 定义
本节涵盖了由org.springframework.aop.framework.autoproxy包提供的自动代理创建器。
BeanNameAutoProxyCreator
The BeanNameAutoProxyCreator 类是一个 BeanPostProcessor,它会自动为名称匹配字面值或通配符的 bean 创建 AOP 代理。以下示例展示了如何创建一个 BeanNameAutoProxyCreator bean:
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="jdk*,onlyJdk"/>
<property name="interceptorNames">
<list>
<value>myInterceptor</value>
</list>
</property>
</bean>
与ProxyFactoryBean一样,有一个interceptorNames属性而不是拦截器列表,以允许原型顾问的正确行为。名为“interceptors”的可以是顾问或任何建议类型。
与自动代理化一样,使用BeanNameAutoProxyCreator的主要目的是将相同的配置一致地应用于多个对象,并且配置量最小。它是将声明式事务应用于多个对象的流行选择。
Bean definitions whose names match, such as jdkMyBean and onlyJdk in the preceding
example, are plain old bean definitions with the target class. An AOP proxy is
automatically created by the BeanNameAutoProxyCreator. The same advice is applied
to all matching beans. Note that, if advisors are used (rather than the interceptor in
the preceding example), the pointcuts may apply differently to different beans。
DefaultAdvisorAutoProxyCreator
一个更通用且极其强大的自动代理创建器是
DefaultAdvisorAutoProxyCreator。这会自动在当前上下文中应用符合条件的顾问,而无需在自动代理顾问的bean定义中包含特定的bean名称。它提供了与BeanNameAutoProxyCreator相同的优点,即一致的配置和避免重复。
使用这种机制涉及:
-
指定一个
DefaultAdvisorAutoProxyCreatorbean定义。 -
在相同或相关上下文中指定任意数量的顾问。请注意,这些必须是顾问,而不是拦截器或其他通知。这是必要的,因为必须有一个切点来评估,以检查每个通知是否适用于候选的bean定义。
The DefaultAdvisorAutoProxyCreator 自动评估每个顾问中包含的切入点,以确定它应该将什么(如果有)建议应用到每个业务对象(例如示例中的 businessObject1 和 businessObject2)。
这意味着可以自动将任意数量的顾问应用到每个业务对象上。如果任何顾问中的切点都不匹配业务对象中的任何方法,则该对象不会被代理。随着为新的业务对象添加 bean 定义,如果需要,它们会自动被代理。
自动代理在一般情况下具有一个优势,即使得调用者或依赖项无法获得未被代理的对象。在这个ApplicationContext上调用getBean("businessObject1")返回的是一个AOP代理,而不是目标业务对象。(前面展示的“内部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"/>
The DefaultAdvisorAutoProxyCreator 非常有用,如果你想一致地将相同的建议应用到许多业务对象上。一旦基础设施定义就位,你可以在不包含特定代理配置的情况下添加新的业务对象。你还可以轻松地引入额外的切面(例如,跟踪或性能监控切面),而对配置的更改最小。
The DefaultAdvisorAutoProxyCreator 提供了过滤支持(通过使用命名约定,使得只有特定的顾问被评估,这允许在同一个工厂中使用多个配置不同的 AdvisorAutoProxyCreators)。顾问可以实现 org.springframework.core.Ordered 接口以确保正确的排序。如果这是一个问题,TransactionAttributeSourceAdvisor 在前面的例子中有可配置的顺序值。默认设置是无序的。
6.9. 使用TargetSource实现
Spring 提供了 TargetSource 的概念,该概念在
org.springframework.aop.TargetSource 接口中表达。此接口负责返回实现连接点的“目标对象”。每次 AOP 代理处理方法调用时,都会向 TargetSource
实现询问目标实例。
使用Spring AOP的开发人员通常不需要直接与TargetSource实现打交道,但这提供了一种强大的手段来支持池化、热插拔和其他复杂的目标。例如,一个池化的TargetSource可以在每次调用时返回不同的目标实例,通过使用池来管理实例。
如果您没有指定 TargetSource,将使用默认实现来包装本地对象。每次调用都会返回相同的target(正如您所期望的)。
本节的其余部分描述了Spring提供的标准目标源以及如何使用它们。
| 使用自定义目标源时,你的目标通常需要是一个原型而不是单例 bean 定义。这允许 Spring 在需要时创建一个新的目标实例。 |
6.9.1. 热插拔目标源
The org.springframework.aop.target.HotSwappableTargetSource 存在是为了在让调用者保留对它的引用的同时,允许切换AOP代理的目标。
更改目标源的目标会立即生效。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. 连接池目标数据源
使用池化目标源提供了一个类似于无状态会话 EJB 的编程模型,在这种模型中,维护了一组相同的实例池,方法调用会发送到池中的空闲对象。
Spring 的池化和 SLSB 的池化之间的一个关键区别是,Spring 的池化可以应用于任何 POJO。与 Spring 一般情况一样,这项服务可以以非侵入性的方式应用。
Spring 提供了对 Commons Pool 2.2 的支持,它提供了一个相当高效的池实现。你需要将 commons-pool Jar 添加到应用程序的类路径中才能使用此功能。你还可以通过继承 org.springframework.aop.target.AbstractPoolingTargetSource 来支持任何其他池 API。
| Commons Pool 1.5+ 也受到支持,但自 Spring 框架 4.2 起已废弃。 |
以下列表展示了一个示例配置:
<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
scope="prototype">
... properties omitted
</bean>
<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
<property name="maxSize" value="25"/>
</bean>
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="poolTargetSource"/>
<property name="interceptorNames" value="myInterceptor"/>
</bean>
请注意目标对象(在前面的例子中为businessObjectTarget)必须是一个原型。这使得PoolingTargetSource实现能够创建目标的新实例以根据需要扩展池。有关其属性的信息,请参阅AbstractPoolingTargetSource的javadoc和您希望使用的具体子类。maxSize是最基本的,并且始终保证存在。
在这种情况下,myInterceptor 是拦截器的名称,该拦截器需要在同一个IoC上下文中定义。但是,您不需要指定拦截器来使用池化。如果您只需要池化而不使用其他通知,则根本不需要设置 interceptorNames 属性。
您可以配置Spring,使其能够将任何池对象转换为org.springframework.aop.target.PoolingConfig接口,该接口通过引入提供了有关池的配置和当前大小的信息。您需要定义一个类似于以下的顾问:
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="poolTargetSource"/>
<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>
这个advisor是通过调用AbstractPoolingTargetSource类上的一个便捷方法获得的,因此使用了MethodInvokingFactoryBean。这个advisor的名称(这里的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)
| 池化无状态服务对象通常不是必要的。我们认为这不应该作为默认选择,因为大多数无状态对象天生就是线程安全的,并且如果资源被缓存,实例池化会带来问题。 |
更简单的池化可以通过使用自动代理来实现。你可以设置任何自动代理创建器使用的TargetSource实现。
6.9.3. 原型目标源
设置一个“原型”目标源类似于设置一个池化TargetSource。在这种情况下,每次方法调用时都会创建目标的新实例。尽管在现代JVM中创建新对象的成本不高,但满足新对象的IoC依赖关系的成本可能会更高。因此,除非有充分的理由,否则不应使用这种方法。
要实现这一点,你可以按照以下方式修改前面展示的 poolTargetSource 定义
(为了清晰起见,我们也更改了名称):
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
唯一属性是目标bean的名称。在TargetSource实现中使用继承以确保命名一致。与池化目标源类似,目标bean必须是一个原型bean定义。
6.9.4. ThreadLocal 目标源
ThreadLocal 目标源在需要为每个传入请求(即每个线程)创建一个对象时非常有用。ThreadLocal 的概念提供了在整个JDK中透明地将资源与线程一起存储的设施。设置 ThreadLocalTargetSource 基本上与前面解释的其他类型的目标源相同,如下例所示:
<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
</bean>
ThreadLocal 实例会带来严重的问题(可能会导致内存泄漏)当在多线程和多类加载器环境中错误使用它们时。你应该始终考虑将线程局部变量包装在其他类中,而不是直接使用 ThreadLocal 本身(除非是在包装类中)。此外,你应始终记住正确设置和取消设置(后者仅涉及调用 ThreadLocal.set(null))资源的线程本地实例。取消设置应在任何情况下都进行,因为不这样做可能会导致问题行为。Spring 的 ThreadLocal 支持为你完成这些操作,并且应该始终优先考虑使用 ThreadLocal 实例而不使用其他适当的处理代码。 |
6.10. 定义新的通知类型
Spring AOP 是设计为可扩展的。虽然目前内部使用的是拦截实现策略,但除了拦截式通知、前置通知、抛出通知和后置返回通知之外,还支持任意类型的通知。
The org.springframework.aop.framework.adapter 包是一个SPI包,它允许在不修改核心框架的情况下添加对新自定义建议类型的支持。
自定义 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框架本身利用了这些注解,但它们也可以在任何基于Spring的Java项目中使用,以声明非空安全的API,并可选地声明非空安全的字段。泛型类型参数、可变参数和数组元素的非空性目前还不支持,但预计在即将发布的版本中会支持,详情请参见SPR-15942获取最新信息。非空声明预计会在Spring框架的版本之间进行微调,包括小版本。方法体内部使用的类型的非空性不在该功能的范围内。
| 其他常用库(如 Reactor 和 Spring Data)提供了防空 API,这些 API 使用类似的空值安排,为 Spring 应用程序开发人员提供了一致的整体体验。 |
7.1. 使用场景
除了为Spring Framework API提供显式声明外,这些注解还可以被IDE(如IntelliJ IDEA或Eclipse)使用,以提供有关null安全性的有用警告,从而避免在运行时出现null的情况。
它们还用于在Kotlin项目中使Spring API支持非空安全,因为Kotlin原生支持非空安全性。更多详细信息可以在Kotlin支持文档中找到。
7.2. JSR-305 元注解
Spring annotations are meta-annotated with JSR 305 annotations (a dormant but wide-spread JSR). JSR-305 meta-annotations let tooling vendors like IDEA or Kotlin provide null-safety support in a generic way, without having to hard-code support for Spring annotations.
It is not necessary nor recommended to add a JSR-305 dependency to the project classpath to
take advantage of Spring null-safe API. Only projects such as Spring-based libraries that use
null-safety annotations in their codebase should add com.google.code.findbugs:jsr305:3.0.2
with compileOnly Gradle configuration or Maven provided scope to avoid compile warnings.
8. 数据缓冲区和编解码器
Java NIO provides ByteBuffer but many libraries build their own byte buffer API on top,
especially for network operations where reusing buffers and/or using direct buffers is
beneficial for performance. For example Netty has the ByteBuf hierarchy, Undertow uses
XNIO, Jetty uses pooled byte buffers with a callback to be released, and so on.
The spring-core module provides a set of abstractions to work with various byte buffer
APIs as follows:
-
DataBufferFactory抽象化了数据缓冲区的创建。 -
DataBuffer表示一个字节缓冲区,它可能是 可复用的。 -
DataBufferUtils提供用于数据缓冲区的工具方法。 -
Codec 解码或编码流数据缓冲区流为更高层次的对象。
8.1. DataBufferFactory
DataBufferFactory 用于以两种方式之一创建数据缓冲区:
-
分配一个新的数据缓冲区,可选地提前指定容量,如果已知的话,这样做更高效,即使
DataBuffer的实现可以在需要时增长和缩小。 -
Wrap an existing
byte[]orjava.nio.ByteBuffer, which decorates the given data with aDataBufferimplementation and that does not involve allocation.
请注意,WebFlux应用程序不会直接创建DataBufferFactory,而是通过客户端的ServerHttpResponse或ClientHttpRequest来访问它。
工厂的类型取决于底层客户端或服务器,例如NettyDataBufferFactory用于Reactor Netty,DefaultDataBufferFactory用于其他情况。
8.2. DataBuffer
The DataBuffer interface offers similar operations as java.nio.ByteBuffer but also
brings a few additional benefits some of which are inspired by the Netty ByteBuf.
Below is a partial list of benefits:
-
读取和写入具有独立的位置,即不需要调用
flip()来在读取和写入之间切换。 -
容量按需扩展,就像
java.lang.StringBuilder一样。 -
通过
PooledDataBuffer进行缓冲池和引用计数。 -
将缓冲区视为
java.nio.ByteBuffer、InputStream或OutputStream。 -
确定给定字节的索引,或最后一个索引。
8.3. PooledDataBuffer
如Javadoc中所述, ByteBuffer, 字节缓冲区可以是直接的或非直接的。直接缓冲区可能位于Java堆外, 这消除了本地I/O操作时复制的需要。这使得直接缓冲区特别适用于通过套接字接收和发送数据,但它们创建和释放的成本也更高,因此引入了缓冲池的概念。
PooledDataBuffer 是 DataBuffer 的扩展,它有助于引用计数,这对于字节缓冲池非常重要。它是如何工作的?当分配一个 PooledDataBuffer 时,引用计数为 1。对 retain() 的调用会增加计数,而对 release() 的调用会减少计数。只要计数大于 0,缓冲区就保证不会被释放。当计数减少到 0 时,池中的缓冲区可以被释放,实际上这意味着缓冲区占用的内存可以返回到内存池。
请注意,通常情况下,最好使用 DataBufferUtils 中的方法,这些方法仅在 DataBuffer 是 PooledDataBuffer 的实例时才应用释放或保留操作,而不是直接操作 PooledDataBuffer。
8.4. DataBufferUtils
DataBufferUtils 提供了许多实用方法来操作数据缓冲区:
-
将数据缓冲区流合并为一个缓冲区,可能采用零拷贝方式,例如通过复合缓冲区,如果底层字节缓冲区API支持的话。
-
将
InputStream或 NIOChannel转换为Flux<DataBuffer>,反之将Publisher<DataBuffer>转换为OutputStream或 NIOChannel。 -
Methods to release or retain a
DataBufferif the buffer is an instance ofPooledDataBuffer. -
跳过或从字节流中读取直到达到特定的字节数。
8.5. 编码解码器
The org.springframework.core.codec 包提供了以下策略接口:
-
Encoder用于将Publisher<T>编码为数据缓冲区流。 -
Decoder解码Publisher<DataBuffer>为更高层次对象的流。
The spring-core 模块提供了 byte[]、ByteBuffer、DataBuffer、Resource、以及
String 编码器和解码器实现。The spring-web 模块增加了 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 以及其他编码器和解码器实现。请参见 WebFlux 部分的Codecs。
8.6. 使用DataBuffer
在处理数据缓冲区时,必须特别注意确保释放缓冲区,因为它们可能是可复用的。我们将使用编解码器来说明这一点,但这些概念更广泛地适用。让我们看看编解码器内部是如何管理数据缓冲区的。
一个 Decoder 是读取输入数据缓冲区的最后一个,然后创建更高级别的对象,因此它必须按如下方式释放它们:
-
如果一个
Decoder简单地读取每个输入缓冲区并立即准备释放它,它可以通过DataBufferUtils.release(dataBuffer)来实现这一点。 -
如果一个
Decoder正在使用Flux或Mono操作符,例如flatMap、reduce等,这些操作符会预先获取并缓存数据项,或者正在使用filter、skip等操作符,这些操作符会跳过某些项,那么必须在组合链中添加doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)以确保在丢弃之前释放此类缓冲区,可能也是由于错误或取消信号的结果。 -
如果一个
Decoder以任何其他方式持有一个或多个数据缓冲区,它必须确保在完全读取后释放这些缓冲区,或者在缓存的数据缓冲区尚未被读取和释放之前发生错误或取消时进行释放。
请注意,DataBufferUtils#join 提供了一种安全且高效的方式来聚合数据缓冲流到单一的数据缓冲区。同样,skipUntilByteCount 和 takeUntilByteCount 是解码器可以使用的其他安全方法。
An Encoder allocates data buffers that others must read (and release). So an Encoder
doesn’t have much to do. However an Encoder must take care to release a data buffer if
a serialization error occurs while populating the buffer with data. For example:
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
The consumer of an Encoder is responsible for releasing the data buffers it receives.
In a WebFlux application, the output of the Encoder is used to write to the HTTP server
response, or to the client HTTP request, in which case releasing the data buffers is the
responsibility of the code writing to the server response, or to the client request.
请注意,在Netty上运行时,有用于排查缓冲区泄漏问题的调试选项。
9. 附录
9.1. XML 模式
这部分附录列出了与核心容器相关的XML模式。
9.1.1. The util Schema
如名称所示,util 标签处理常见的实用配置问题,例如配置集合、引用常量等。要使用 util 模式中的标签,你需要在 Spring XML 配置文件的顶部包含以下前缀(片段中的文本引用了正确的模式,以便 util 命名空间中的标签可供你使用):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<!-- bean definitions here -->
</beans>
使用 <util:constant/>
考虑以下的bean定义:
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
上述配置使用了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
is a FactoryBean that retrieves a static or non-static field value. It is typically
used for retrieving public static final constants, which may then be used to set a
property value or constructor argument for another bean.
以下示例展示了如何通过使用
staticField
属性来暴露static字段:
<bean id="myField"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
<property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>
还有另一种便捷的使用形式,其中static字段被指定为bean名称,如下例所示:
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
这意味着不再有任何选择来决定bean id 是什么(因此任何其他引用它的bean也必须使用这个较长的名称),但这种形式定义非常简洁,并且作为内部bean使用非常方便,因为不需要为bean引用指定 id,如下例所示:
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
您还可以访问另一个bean的非静态(实例)字段,如
在FieldRetrievingFactoryBean
类的API文档中所述。
将枚举值注入到bean中作为属性或构造函数参数在Spring中很容易实现。实际上,你不需要做任何事情或了解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)来创建一个名为testBean.age的bean(类型为int),该bean的值等于testBean bean的age属性。
现在考虑以下示例,它添加了一个<util:property-path/>元素:
<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>
path 属性的值遵循 <property-path/> 元素的 beanName.beanProperty 形式。在这种情况下,它获取名为 testBean 的 bean 的 age 属性。该 age 属性的值是 10。
使用 <util:property-path/> 设置 Bean 属性或构造函数参数
PropertyPathFactoryBean 是一个 FactoryBean,它在给定的目标对象上评估属性路径。目标对象可以直接指定或通过 bean 名称指定。然后你可以将这个值用在另一个 bean 定义中作为属性值或构造函数参数。
以下示例展示了一个路径被用于另一个按名称引用的bean:
<!-- target bean to be referenced by name -->
<bean id="person" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 11, which is the value of property 'spouse.age' of bean 'person' -->
<bean id="theAge"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
<property name="targetBeanName" value="person"/>
<property name="propertyPath" value="spouse.age"/>
</bean>
在以下示例中,一个路径会针对内部 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>
您还可以通过在<util:list/>元素上使用list-class属性来显式控制实例化和填充的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>
您还可以通过在<util:map/>元素上使用'map-class'属性来显式控制实例化和填充的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)来创建一个用提供的sourceSet中的值初始化的java.util.Set实例。
以下示例使用一个<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>
您还可以通过在<util:set/>元素上使用set-class属性来显式控制实例化和填充的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. 0 架构
The aop tags deal with configuring all things AOP in Spring, including Spring’s
own proxy-based AOP framework and Spring’s integration with the AspectJ AOP framework.
These tags are comprehensively covered in the chapter entitled Spring中的面向切面编程.
为了完整性,要使用aop模式中的标签,你需要在Spring XML配置文件的顶部添加以下前缀(片段中的文本引用了正确的模式,以便aop命名空间中的标签可供你使用):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- bean definitions here -->
</beans>
9.1.3. 0 架构
The context 标签处理与管道相关的 ApplicationContext 配置 —— 也就是说,通常不是对最终用户重要的 bean,而是 Spring 中执行大量“基础”工作的 bean,例如 BeanfactoryPostProcessors。以下片段引用了正确的模式,因此 context 命名空间中的元素可供您使用:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans 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, and@Lookup -
JSR-250的
@Resource,@PostConstruct,和@PreDestroy(如果可用) -
JAX-WS 的
@WebServiceRef和 EJB 3 的@EJB(如果可用) -
JPA 的
@PersistenceContext和@PersistenceUnit(如果可用) -
Spring的
@EventListener
Alternatively, you can choose to explicitly activate the individual BeanPostProcessors
for those annotations.
此元素不会激活Spring的
@Transactional 注解的处理;
你可以使用 <tx:annotation-driven/>
元素来实现这一目的。同样,Spring的
缓存注解 也需要显式地
启用。 |
使用 <component-scan/>
此元素在基于注解的容器配置部分有详细说明。
使用 <load-time-weaver/>
此元素在Spring框架中使用AspectJ进行加载时织入一节中有详细介绍。
使用 <spring-configured/>
此元素在使用AspectJ与Spring进行依赖注入域对象一节中有详细介绍。
使用 <mbean-export/>
此元素在配置基于注解的MBean导出一节中有详细介绍。
9.1.4. Bean模式
最后但同样重要的是,我们有beans模式中的元素。这些元素自Spring框架诞生以来就一直存在。由于beans模式中的各种元素在依赖和配置的详细信息(确实,在整个章节中)已经非常全面地涵盖了。
请注意,您可以向 <bean/> XML 定义中添加零个或多个键值对。
如何处理这些额外的元数据完全取决于您自己的自定义逻辑(通常只有在您编写了自己的自定义元素时才有用,如附录 XML 模式编写 中所述)。
以下示例展示了在周围<bean/>元素中的<meta/>元素
(请注意,如果没有任何逻辑来解释它,那么元数据本身实际上是没有用的)。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans 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 Schema 编写
从2.0版本开始,Spring提供了一种机制,可以在基本的Spring XML格式中添加基于模式的扩展来定义和配置bean。本节介绍了如何编写自己的自定义XML bean定义解析器,并将这些解析器集成到Spring IoC容器中。
为了方便使用具有模式感知功能的XML编辑器编写配置文件,Spring的可扩展XML配置机制基于XML Schema。如果您不熟悉Spring标准发行版中附带的当前XML配置扩展,您应该首先阅读关于XML Schema的前一节。
要创建新的XML配置扩展:
为了一个统一的示例,我们创建一个XML扩展(一个自定义的XML元素),它允许我们配置SimpleDateFormat类型(来自java.text包)的对象。完成后,我们将能够如下定义SimpleDateFormat类型的bean定义:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
我们在本附录后面提供了更详细的示例。这个第一个简单示例的目的是引导您完成制作自定义扩展的基本步骤。
9.2.1. 编写模式
创建用于与Spring的IoC容器一起使用的XML配置扩展首先需要编写一个XML Schema来描述该扩展。在我们的示例中,我们使用以下模式来配置SimpleDateFormat对象:
<!-- myns.xsd (inside package org/springframework/samples/xml) -->
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.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命名空间。 |
上述模式允许我们在XML应用程序上下文文件中直接配置SimpleDateFormat对象,使用<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),并设置了一些属性。
| 基于模式的方法来创建配置格式允许与具有模式感知XML编辑器的IDE紧密集成。通过使用适当编写的模式,您可以使用自动完成功能让用户在枚举中定义的多个配置选项之间进行选择。 |
9.2.2. 编码 NamespaceHandler
除了模式,我们还需要一个 NamespaceHandler 来解析 Spring 在解析配置文件时遇到的此命名空间中的所有元素。在这个示例中,NamespaceHandler 应该处理 myns:dateformat 元素的解析。
The NamespaceHandler 接口具有三个方法:
-
init(): 允许初始化NamespaceHandler,并在 Spring 使用处理器之前调用。 -
BeanDefinition parse(Element, ParserContext): 当Spring遇到顶级元素时调用(不嵌套在bean定义或不同命名空间内)。此方法本身可以注册bean定义,返回一个bean定义,或两者兼而有之。 -
BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext): 当Spring遇到不同命名空间的属性或嵌套元素时调用。 装饰一个或多个bean定义(例如)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 用于当 NamespaceHandler 遇到已映射到特定 bean 定义解析器的 XML 元素类型(在这种情况下为 dateformat)。换句话说,BeanDefinitionParser 负责解析模式中定义的一个特定顶级 XML 元素。在解析器中,我们可以访问 XML 元素(以及其子元素)以便解析我们的自定义 XML 内容,如以下示例所示:
package org.springframework.samples.xml;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
import java.text.SimpleDateFormat;
public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { (1)
protected Class getBeanClass(Element element) {
return SimpleDateFormat.class; (2)
}
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// this will never be null since the schema explicitly requires that a value be supplied
String pattern = element.getAttribute("pattern");
bean.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,必须与targetNamespace属性的值完全匹配,如在您的自定义XSD模式中指定的。
编写 'META-INF/spring.schemas'
名为spring.schemas的属性文件包含XML Schema位置的映射(在使用该模式的XML文件中,这些位置与模式声明一起被引用,并作为xsi:schemaLocation属性的一部分)。此文件用于防止Spring必须使用默认的EntityResolver,这需要通过互联网访问来检索模式文件。如果你在此属性文件中指定了映射,Spring会在类路径资源中查找模式文件(在这种情况下,myns.xsd在org.springframework.samples.xml包中)。以下是我们需要添加的自定义模式的行:
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,该自定义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)
}
}
最后,需要将各种构件注册到Spring XML基础设施中,
通过修改META-INF/spring.handlers和META-INF/spring.schemas文件,如下所示:
# 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>
我们可以在解析'jcache:cache-name'属性时创建另一个BeanDefinition。这个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