0%

SpringFramework官方文档翻译-核心

核心技术

Version 5.2.8.RELEASE

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

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

还介绍了Spring与AspectJ的集成(就特性而言,这是目前最丰富的,也是Java企业领域中最成熟的AOP实现)。

IOC容器

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

介绍springioc容器和Beans

本章介绍了控制反转(IoC)原理的Spring框架实现。IoC也称为依赖注入(DI)。它是一个过程,对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后在其上设置的属性来定义它们的依赖项(即它们使用的其他对象)。然后,容器在创建bean时注入这些依赖项。这个过程基本上是bean本身c的反向(因此得名,控制反转)。

org.springframework.beansorg.springframework.context包是 Spring Framework 的 IoC 容器的基础。 BeanFactory接口提供了一种高级 configuration 机制,能够管理任何类型的 object。 ApplicationContextBeanFactory的 sub-interface。它补充说:

  • 更容易与Spring的AOP特性集成
  • 消息资源处理(用于国际化)
  • 事件发布
  • 应用程序层特定的上下文,如web应用程序中使用的WebApplicationContext。

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

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

容器概述

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

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

在大多数 application 场景中,不需要用户显示的实例化 Spring IoC 容器的一个或多个实例。例如,在 web application 场景中,application 的web.xml文件中的简单八行(或左右)的样板代码通常就足够了(参见 Convenient ApplicationContext Instantiation for Web Applications)。如果使用Spring 工具套件(STS, Eclipse-powered 开发环境),只需单击几下鼠标或按键即可轻松创建此样板配置文件。

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

容器魔术

图 1. Spring IoC 容器

配置元数据

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

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

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

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

  • 基于注解的配置:Spring 2.5引入了对基于注解的配置元数据的支持。
  • 基于Java的配置:从Spring 3.0开始,Spring JavaConfig项目提供的许多特性成为了核心Spring框架的一部分。因此,您可以使用Java而不是XML文件来定义应用程序类的外部bean。要使用这些新特性,请参阅@Configuration@Bean@Import和[@DependsOn注解。

Spring 配置方式至少包含一种并且通常容器必须管理多个 bean 定义。 基于 XML 配置的元数据将 beans 配置为 top-level <beans/>元素内的<bean/>元素。 Java 配置通常在@Configuration 标记的类中使用@Bean 注解的方法。

这些 bean 定义对应于构成应用的实际对象。通常,您定义服务层对象,数据访问对象(DAO),表示对象(如 Struts Action实例),基础结构对象(如 Hibernate SessionFactories,JMS Queues等)。通常,不会在容器中配置 fine-grained domain objects,因为它通常是 DAO 和业务逻辑负责创建和加载 domain objects。但是,您可以通过 Spring 与 AspectJ 整合来配置在 IoC 容器控制之外创建的 objects。见使用 AspectJ 与 Spring 注入 domain objects

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

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

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

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

</beans>
1 id属性是一个 string,用于标识单个 bean 定义。
2 class属性定义 bean 的类型并需要使用完全限定的类名。

id属性的 value 指的是协作 objects。用于引用协作 objects 的 XML 未在此示例中显示。有关更多信息,请参见依赖

实例化容器

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

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

在了解 Spring 的 IoC 容器之后,您可能想要了解更多关于 Spring 的Resource抽象(如资源中所述),它提供了一种从 URI 语法中定义的位置读取 InputStream 的便捷机制。特别是,Resource 路径用于构造 applications 上下文,如Application Contexts 和 Resource Paths中所述。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- services -->

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

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

</beans>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

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

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

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

</beans>

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

撰写基于XML配置的元数据

使 bean 定义在多个 XML 文件中非常有用。通常,每个单独的 XML 配置文件代表应用架构中的逻辑层或模块。

您可以使用 application context 构造函数从所有这些 XML 片段加载 bean 定义。此构造函数传递多个Resource路径,如上一节中所示。或者,使用一个或多个<import/>元素来从一个文件或多个文件加载 bean 定义。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>

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

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

使用相对“../”路径在 parent 目录中 reference files 是可行的,但不建议这样做。这样做会对当前 application 之外的文件创建依赖关系。特别是,不建议对classpath: URL(对于 example,classpath:../services.xml)使用该方式,其中运行时解析 process 选择“最近的”classpath 根,然后查看其 parent 目录。 Classpath configuration 更改可能导致选择不同的,不正确的目录。

您始终可以使用完全限定的资源位置而不是相对路径:例如,file:C:/config/services.xmlclasspath:/config/services.xml。但是,请注意您的 application 的配置文件将与特定的绝对位置耦合。通常最好为这样的绝对位置保持间接 - 例如,通过在运行时针对 JVM 系统 properties 解析的“$ {}”占位符。

命名空间本身提供 import 指令功能。 Spring 提供的一系列 XML 命名空间(例如,context 和 util 名称空间)中提供了普通 bean 定义之外的其他配置功能。

GroovyBean定义DSL

作为外部化配置元数据的进一步示例,bean 定义也可以在 Spring 的 Groovy Bean Definition DSL 中表示,如 Grails framework 中所知。通常,此类 configuration 存在于“.groovy”文件中,其结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}
}
}

这种 configuration 样式在很大程度上等同于 XML bean 定义,甚至支持 Spring 的 XML configuration 命名空间。它还允许通过importBeans指令 importing XML bean definition files。

使用容器

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

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

1
2
3
4
5
6
7
8
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

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

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

使用 Groovy configuration,bootstrapping 看起来非常相似。它有一个不同的 context implementation class,它是 Groovy-aware(但也理解 XML bean 定义)。以下 example 显示了 Groovy configuration:

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

最灵活的变体是GenericApplicationContext与 reader 委托相结合 - 例如,表示 XML files,如下面的 example 所示:

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

您还可以使用GroovyBeanDefinitionReader for Groovy files,如下面的 example 所示:

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

您可以在同一个ApplicationContext上混合和 match 这样的 reader 委托,从不同的 configuration 源读取 bean 定义。

然后,您可以使用getBean来检索 beans 的实例。 ApplicationContext接口还有一些其他方法可以检索 beans,但理想情况下,application code 应该永远不会使用它们。实际上,你的 application code 根本不应该对getBean()方法有 calls,因此根本不依赖于 Spring API。例如,Spring 与 web 框架的 integration 为各种 web framework 组件(如控制器和 JSF-managed beans)提供依赖注入,让您通过元数据(例如自动装配 annotation)声明对特定 bean 的依赖。

Bean概述

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

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

  • package-qualified class name:通常是bean 被定义的实际实现类。
  • Bean 行为 configuration 元素,它们 state bean 应该如何在容器中运行(范围,生命周期回调等)。
  • 引用 bean 执行其工作所需的其他 beans。这些 references 也称为协作者或依赖项。
  • 要在新创建的 object 中设置的其他 configuration 设置 - 用于 example,池的大小限制或在管理连接池的 bean 中使用的连接数。

此元数据转换为构成每个 bean 定义的一组 properties。以下 table 描述了这些 properties:

属性 解释在……
Class 实例化 Beans
Name 命名 Beans
Scope Bean的作用域
Constructor arguments 依赖注入
Properties 依赖注入
Autowiring mode 自动化协作者
Lazy initialization mode Lazy-initialized Beans
Initialization method 初始化回调
Destruction method 毁灭回调

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

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

命名Beans

每个 bean 都有一个或多个标识符。这些标识符在承载 bean 的容器中必须是唯一的。 bean 通常只有一个标识符。但是,如果它需要多个,则额外的可以被视为别名。

在 XML-based configuration 元数据中,使用id属性,name属性或两者来指定 bean 标识符。 id属性允许您指定一个 id。通常,这些名称是字母数字(‘myBean’,’someService’,etc.),但它们也可以包含特殊字符。如果要为 bean 引入其他别名,您还可以在name属性中指定它们,由 a 分隔逗号(,),分号(;)或空格。作为历史记录,在 Spring 3.1 之前的版本中,id属性被定义为xsd:ID类型,它约束了可能的字符。从 3.1 开始,它被定义为xsd:string type。请注意,bean id uniqueness 仍由容器强制执行,但不再由 XML 解析器强制执行。

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

Bean 命名约定

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

命名 beans 始终使您的 configuration 更容易阅读和理解。此外,如果您使用 Spring AOP,则在将建议应用于 name 相关的一组 beans 时会有很大帮助。

使用 classpath 中的 component 扫描,Spring 按照前面描述的规则为未命名的组件生成 bean 名称:基本上,使用简单的 class name 并将其初始字符转换为 lower-case。但是,在(不常见的)特殊情况下,当有多个字符且第一个和第二个字符都是大写字母时,原始外壳将被保留。这些规则与java.beans.Introspector.decapitalize(Spring 在此处使用)中定义的规则相同。

在 Bean 定义之外别名 Bean

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

但是,指定实际定义 bean 的所有别名并不总是足够的。有时需要为其他地方定义的 bean 引入别名。在大型系统中通常就是这种情况,其中 configuration 在每个子系统之间分配,每个子系统都有自己的 object 定义集。在 XML-based configuration 元数据中,您可以使用<alias/>元素来完成此操作。以下 example 显示了如何执行此操作:

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

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

例如,子系统 A 的 configuration 元数据可以通过subsystemA-dataSource的 name 引用 DataSource。子系统 B 的 configuration 元数据可以通过subsystemB-dataSource的 name 引用 DataSource。在编写使用这两个子系统的主 application 时,main application 通过myApp-dataSource的 name 引用 DataSource。要使所有三个名称引用相同的 object,可以将以下别名定义添加到 configuration 元数据中:

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

现在每个 component 和 main application 都可以通过 name 来引用 dataSource,该 name 是唯一的,并且保证不与任何其他定义冲突(有效地创建命名空间),但它们引用相同的 bean。

Java-configuration

如果使用 Javaconfiguration,则@Bean annotation 可用于提供别名。有关详细信息,请参阅使用 @Bean Annotation

实例化Beans

bean 定义本质上是 creating 一个或多个 objects 的配方。容器在询问时查看命名 bean 的配方,并使用由 bean 定义封装的 configuration 元数据来创建(或获取)实际的 object。

如果使用 XML-based configuration 元数据,则指定要在<bean/>元素的class属性中实例化的 object 的类型(或 class)。这个class属性(内部是BeanDefinition实例上的Class property)通常是强制性的。 (对于 exceptions,请参阅使用实例工厂方法实例化Bean定义继承 .)您可以通过以下两种方式之一使用Class property:

  • 通常,要指定在容器本身通过反射调用其构造函数直接创建 bean 的情况下构造的 bean class,有点等同于运算符的 Java code。
  • 要指定包含工厂方法的实际 class,该方法被调用以创建 object,在较少 common 的情况下,容器在 class 上调用static工厂方法来创建 bean。从static工厂方法的调用返回的 object 类型可能完全是相同的 class 或另一个 class。

内部 class 名称

如果要为static嵌套 class 配置 bean 定义,则必须使用嵌套 class 的二进制 name。

例如,如果在com.example包中有一个名为SomeThing的 class,并且SomeThing class 有一个static嵌套 class,名为OtherThing,则 bean

注意在 name 中使用$字符将嵌套的 class name 与外部 class name 分开。

使用构造函数实例化

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

Spring IoC 容器几乎可以管理您希望它管理的任何 class。它不仅限于管理 true JavaBeans。大多数 Spring 用户更喜欢实际的 JavaBeans,只有一个默认的(no-argument)构造函数,并且在容器中的 properties 之后建模了适当的 setter 和 getter。您还可以在容器中拥有更多异域 non-bean-style classes。例如,对于 example,您需要使用绝对不符合 JavaBean 规范的 legacy 连接池,Spring 也可以对其进行管理。

使用 XML-based configuration 元数据,您可以指定 bean class,如下所示:

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

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

有关在构造 object 之后向构造函数提供 arguments(如果需要)和设置 object instance properties 的机制的详细信息,请参阅注入依赖关系

使用静态工厂方法实例化

定义使用静态工厂方法创建的 bean 时,请使用class属性指定包含static工厂方法的 class 和名为factory-method的属性,以指定工厂方法本身的 name。您应该能够调用此方法(使用可选的 arguments,如稍后所述)并 return live object,后来被视为通过构造函数创建。这种 bean 定义的一个用途是在 legacy code 中调用static工厂。

以下 bean 定义指定通过调用工厂方法来创建 bean。该定义未指定返回的 object 的类型(class),仅指定包含工厂方法的 class。在此 example 中,createInstance()方法必须是静态方法。以下 example 显示了如何指定工厂方法:

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

以下 example 显示了一个可以使用前面的 bean 定义的 class:

1
2
3
4
5
6
7
8
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}

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

有关在工厂返回 object 之后向工厂方法提供(可选)arguments 并设置 object instance properties 的机制的详细信息,请参阅详细信息中的依赖关系和 Configuration

使用实例工厂方法实例化

与通过静态工厂方法进行实例化类似,使用实例工厂方法进行实例化会从容器中调用现有 bean 的 non-static 方法来创建新的 bean。要使用此机制,请将class属性保留为空,并在factory-bean属性中,在当前(或 parent 或 ancestor)容器中指定 bean 的 name,该容器包含要调用以创建 object 的实例方法。使用factory-method属性设置工厂方法本身的 name。以下 example 显示了如何配置这样的 bean:

1
2
3
4
5
6
7
8
9
<!-- 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"/>

以下 example 显示了相应的 Java class:

1
2
3
4
5
6
7
8
public class DefaultServiceLocator {

private static ClientService clientService = new ClientServiceImpl();

public ClientService createClientServiceInstance() {
return clientService;
}
}

一个工厂 class 也可以包含多个工厂方法,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
<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"/>

以下 example 显示了相应的 Java class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DefaultServiceLocator {

private static ClientService clientService = new ClientServiceImpl();

private static AccountService accountService = new AccountServiceImpl();

public ClientService createClientServiceInstance() {
return clientService;
}

public AccountService createAccountServiceInstance() {
return accountService;
}
}

这种方法表明工厂 bean 本身可以通过依赖注入(DI)进行管理和配置。见详细信息中的依赖关系和 Configuration

在 Spring 文档中,“factory bean”指的是 Spring 容器中配置的 bean,它通过静态工厂方法创建 objects。相比之下,FactoryBean(注意大写)指的是 Spring-specific FactoryBean

依赖

典型的企业应用程序不包含单个 object(或 Spring 用语中的 bean)。即使是最简单的 application 也有一些 objects 可以协同工作来呈现 end-user 所看到的连贯应用程序。下一节将介绍如何定义一些独立的 bean 定义到完全实现的 application,其中 objects 协作实现目标。

依赖注入

依赖注入(DI)是一个过程,其中 objects 仅通过构造函数 arguments,工厂方法的 arguments 或 object 实例在构造之后设置的 properties 定义它们的依赖项(即,它们工作的其他 objects)或者从工厂方法返回。然后容器在创建 bean 时注入这些依赖项。这个 process 基本上是 bean 本身的逆(因此 name,控制反转),它通过使用 classes 或 Service Locator pattern 的直接构造来控制其依赖项的实例化或位置。

使用 DI 原理,Code 更干净,当 objects 具有依赖关系时,解耦更有效。 object 不查找其依赖项,也不知道依赖项的位置或 class。因此,您的 classes 变得更容易测试,特别是当依赖关系在接口或 abstract base classes 上时,它们允许在单元测试中使用 stub 或 mock implementations。

DI 存在两种主要变体:Constructor-based 依赖注入Setter-based 依赖注入

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

Constructor-based DI 由容器调用具有多个 arguments 的构造函数完成,每个 arguments 表示一个依赖项。调用具有特定 arguments 的static工厂方法来构造 bean 几乎是等效的,本讨论同样将 arguments 视为构造函数和static工厂方法。以下 example 显示了一个只能使用构造函数注入 dependency-injected 的 class:

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {

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

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

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

请注意,这个 class 没有什么特别之处。它是一个 POJO,它不依赖于容器特定的接口,base classes 或 annotations。

构造函数参数解析

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

1
2
3
4
5
6
7
8
package x.y;

public class ThingOne {

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

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

1
2
3
4
5
6
7
8
9
10
<beans>
<bean id="thingOne" class="x.y.ThingOne">
<constructor-arg ref="thingTwo"/>
<constructor-arg ref="thingThree"/>
</bean>

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package examples;

public class ExampleBean {

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

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

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

构造函数参数类型匹配

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

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>

构造函数参数索引

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

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

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

索引是 0-based。

构造函数参数 name

您还可以使用构造函数参数 name 进行 value 消歧,如下面的 example 所示:

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

请记住,要使这项工作开箱即用,必须在启用 debug flag 的情况下编译 code,以便 Spring 可以从构造函数中查找参数 name。如果您不能或不想使用 debug flag 编译 code,则可以使用@ConstructorProperties JDK annotation 显式 name 构造函数 arguments。然后 sample class 必须如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
package examples;

public class ExampleBean {

// Fields omitted

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

基于Setter方法的依赖注入

Setter-based DI 是在调用 no-argument 构造函数或 no-argument static工厂方法来实例化 bean 之后,在 beans 上调用 setter 方法的容器来完成的。

以下 example 显示了一个 class,它只能通过使用纯 setter 注入来 dependency-injected。这个 class 是传统的 Java。它是一个 POJO,它不依赖于容器特定的接口,base classes 或 annotations。

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {

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

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

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

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

Constructor-based 或 setter-based DI?

由于您可以混合使用 constructor-based 和 setter-based DI,因此将构造函数用于强制依赖项和 setter 方法或使用 configuration 方法作为可选依赖项是一个很好的经验法则。请注意,在 setter 方法上使用@Required annotation 可用于使 property 成为必需的依赖项。

Spring 团队通常提倡构造函数注入,因为它允许您将 application 组件实现为不可变 objects 并确保所需的依赖项不是null。此外,constructor-injected 组件始终返回到完全初始化的 state 中的 client(调用)code。作为旁注,大量的构造函数 arguments 是一个糟糕的 code 气味,暗示 class 可能有太多的责任,应该重构以更好地解决关注点的正确分离。

Setter 注入应主要仅用于可在 class 中指定合理默认值的可选依赖项。否则,必须在 code 使用依赖项的任何位置执行 not-null 检查。 setter 注入的一个好处是 setter 方法使 class 的 objects 适合重新配置或 re-injection 以后。 因此,通过JMX MBeans管理是一个引人注目的用于二传手注射的用例。

使用对特定 class 最有意义的 DI 样式。有时,在处理没有源的 third-party classes 时,会选择您。例如,如果 third-party class 没有公开任何 setter 方法,那么构造函数注入可能是唯一可用的 DI 形式。

依赖性解决流程

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

  • 使用描述所有 beans 的 configuration 元数据创建和初始化ApplicationContext。 Configuration 元数据可以由 XML,Java code 或 annotations 指定。
  • 对于每个 bean,其依赖关系以 properties,constructor arguments 或_ar_ments 的形式表示(如果使用它而不是普通的构造函数)。当实际创建 bean 时,这些依赖项将提供给 bean。
  • 每个 property 或构造函数参数都是要设置的 value 的实际定义,或者是容器中另一个 bean 的 reference。
  • 作为 value 的每个 property 或构造函数参数都从其指定的格式转换为该 property 或 constructor 参数的实际类型。默认情况下,Spring 可以将 string 格式提供的 value 转换为所有 built-in 类型,例如intlongStringboolean等。

Spring 容器在创建容器时验证每个 bean 的 configuration。但是,在实际创建 bean 之前,不会设置 bean properties 本身。创建容器时会创建 singleton-scoped 并设置为 pre-instantiated(默认值)的 Beans。范围在Bean 范围中定义。否则,只有在请求时才会创建 bean。创建 bean 可能会导致创建 beans 图,因为 bean 的依赖项及其依赖项的依赖项(依此类推)会被创建和分配。请注意,这些依赖项之间的解决方案不匹配可能会显示较晚 - 也就是说,首次创建受影响的 bean 时。

循环依赖

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

对于 example:Class A 需要通过构造函数注入实现 class B,而 class B 需要通过构造函数注入实现 class A.如果为 classes A 和 B 配置 beans 以相互注入,则 Spring IoC 容器会在运行时检测到此循环 reference,并抛出BeanCurrentlyInCreationException

一种可能的解决方案是编辑某些 classes 的 source code,以便由 setter 而不是构造函数配置。或者,避免构造函数注入并仅使用 setter 注入。换句话说,尽管不推荐使用,但您可以使用 setter 注入配置循环依赖项。

与典型情况(没有循环依赖)不同,bean A 和 bean B 之间的循环依赖强制其中一个 beans 在完全初始化之前被注入另一个(经典的 chicken-and-egg 场景)。

您通常可以信任 Spring 做正确的事情。它在容器 load-time 处检测 configuration 问题,例如 reference to non-existent beans 和循环依赖项。 Spring sets properties 并在实际创建 bean 时尽可能晚地解析依赖项。这意味着正确加载的 Spring 容器以后可以在请求 object 时生成 exception,如果有问题创建 object 或其中一个依赖项 - 对于 example,bean 因缺少或无效而抛出 exception 属性。这可能会延迟一些 configuration 问题的可见性,这就是为什么ApplicationContext implementations 默认为 pre-instantiate singleton beans。以某些前期 time 和 memory 为代价,在实际需要之前创建这些 beans,您会在创建ApplicationContext时发现 configuration 问题,而不是以后。您仍然可以覆盖此默认行为,以便 singleton beans 懒惰地初始化,而不是 pre-instantiated。

如果不存在循环依赖关系,则当一个或多个协作 beans 被注入依赖 bean 时,每个协作 bean 在被注入依赖 bean 之前完全配置。这意味着,如果 bean A 依赖于 bean B,Spring IoC 容器在 bean A 上调用 setter 方法之前完全配置 bean B.换句话说,bean 被实例化(如果它不是 pre-instantiated singleton),设置其依赖项,并调用相关的生命周期方法(例如配置的 init 方法InitializingBean 回调方法)。

依赖注入的示例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
<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"/>

以下 example 显示了相应的ExampleBean class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
}

在前面的 example 中,setter 被声明为与 XML 文件中指定的 properties 进行 match。以下示例使用 constructor-based DI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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"/>

以下 example 显示了相应的ExampleBean class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExampleBean {

private AnotherBean beanOne;

private YetAnotherBean beanTwo;

private int i;

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

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

现在考虑这个 example 的变体,其中,不是使用构造函数,而是告诉 Spring 调用static工厂方法来 return object 的实例:

1
2
3
4
5
6
7
8
<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"/>

以下 example 显示了相应的ExampleBean class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
}

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

详细信息中的依赖关系和Configuration

上一节中所述,您可以将 bean properties 和 constructor arguments 定义为对其他托管 beans(协作者)的 references 或作为内联定义的值。为此,Spring 的 XML-based configuration 元数据在其<property/><constructor-arg/>元素中支持 sub-element 类型。

直线值(Primitives,Strings等)

<property/>元素的value属性将 property 或构造函数参数指定为 human-readable string 表示。 Spring 的转换服务用于将这些值从String转换为 property 或参数的实际类型。以下 example 显示了设置的各种值:

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

以下 example 使用p-namespace进行更简洁的 XML configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

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

</beans>

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

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

1
2
3
4
5
6
7
8
9
10
11
<bean id="mappings"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

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

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

idref元素

idref元素只是一种 error-proof 方式,用于将容器中另一个 bean 的id(string value - 而不是 reference)传递给<constructor-arg/><property/>元素。以下 example 显示了如何使用它:

1
2
3
4
5
6
7
<bean id="theTargetBean" class="..."/>

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

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

1
2
3
4
5
<bean id="theTargetBean" class="..." />

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

第一种形式优于第二种形式,因为使用idref标记允许容器在部署 time 时验证引用的名为 bean 的实际存在。在第二个变体中,不会对传递给client bean 的targetName property 的 value 执行验证。当client bean 实际被实例化时,才会发现错别字(最有可能致命的结果)。如果client bean 是原型 bean,则只能在部署容器后 long 发现此错误和结果 exception。

4.0 beans XSD 不再支持idref元素上的local属性,因为它不再提供常规bean reference 的 value。升级到 4.0 schema 时,将现有的idref local references 更改为idref bean

<idref/>元素带来 value 的 common 位置(至少在 Spring 2.0 之前的版本中)位于ProxyFactoryBean bean 定义的AOP 拦截器的 configuration 中。指定拦截器名称时使用<idref/>元素可防止拼写错误的拦截器 ID。

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

ref元素是<constructor-arg/><property/>定义元素中的最后一个元素。在这里,您将 bean 的指定 property 的 value 设置为由容器管理的另一个 bean(协作者)的 reference。引用的 bean 是要设置 property 的 bean 的依赖项,并且在设置 property 之前根据需要初始化它。 (如果协作者是 singleton bean,它可能已经被 container.)初始化所有 references 最终都是 reference 给另一个 object。作用域和验证取决于你是否通过beanlocal,parent指定另一个 object 的 ID 或 name 属性。

通过<ref/>标记的bean属性指定目标 bean 是最常用的形式,并允许在同一容器或 parent 容器中创建 reference 给任何 bean,无论它是否在同一 XML 文件中。 bean属性的 value 可以与 target bean 的id属性相同,也可以与 target bean 的name属性中的一个值相同。以下 example 显示了如何使用ref元素:

1
<ref bean="someBean"/>

通过parent属性指定目标 bean 会创建一个 reference 给位于当前容器的 parent 容器中的 bean。 parent属性的 value 可以与 target bean 的id属性相同,也可以与 target bean 的name属性中的一个值相同。目标 bean 必须位于当前容器的 parent 容器中。您应该使用此 bean reference 变体,主要是当您有容器层次结构并且希望将现有 bean 包装在 parent 容器中时,该容器具有与 parent bean 具有相同 name 的代理。以下一对列表显示了如何使用parent属性:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
</property>
<!-- insert other configuration and dependencies as required here -->
</bean>

4.0 beans XSD 不再支持ref元素上的local属性,因为它不再提供常规bean reference 的 value。升级到 4.0 schema 时,将现有的ref local references 更改为ref bean

内部Beans

<property/><constructor-arg/>元素中的<bean/>元素定义内部 bean,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
<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 或 name。如果指定,则容器不使用这样的 value 作为标识符。容器也会在创建时忽略scope flag,因为内部 beans 始终是匿名的,并且始终使用外部 bean 创建。不可能独立访问内部 beans 或者将它们注入到协作 beans 而不是封闭的 bean 中。

作为一个极端情况,可以从自定义范围接收销毁回调 - 对于 example,包含在 singleton bean 中的 request-scoped inner bean。内部 bean 实例的创建与其包含 bean 相关联,但是销毁回调允许它参与请求范围的生命周期。这不是一个常见的场景。内部 beans 通常只是共享其包含 bean 的范围。

集合

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<bean id="moreComplexObject" class="example.ComplexObject">
<!-- results in a setAdminEmails(java.util.Properties) call -->
<property name="adminEmails">
<props>
<prop key="administrator">[emailprotected]</prop>
<prop key="support">[emailprotected]</prop>
<prop key="development">[emailprotected]</prop>
</props>
</property>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key ="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>

map key 或 value 或 set value 的 value 也可以是以下任何元素:

1
bean | ref | idref | list | set | map | props | value | null

集合合并

Spring 容器还支持合并集合。 application 开发人员可以定义 parent \,\,\或\元素,并使 child \,\,\或\元素继承并覆盖 parent 集合中的值。也就是说,child 集合的值是合并 parent 和 child 集合的元素的结果,其中 child 的集合元素覆盖 parent 集合中指定的值。

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

以下 example 演示了集合合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">[emailprotected]</prop>
<prop key="support">[emailprotected]</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- the merge is specified on the child collection definition -->
<props merge="true">
<prop key="sales">[emailprotected]</prop>
<prop key="support">[emailprotected]</prop>
</props>
</property>
</bean>
<beans>

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

1
2
3
[emailprotected]
[emailprotected]
[emailprotected]

child Properties集合的 value 集合从 parent <props/>继承所有 property 元素,child _ value 的 child 的 value 覆盖 parent 集合中的 value。

此合并行为同样适用于<list/><map/><set/>集合类型。在<list/>元素的特定情况下,保持与List集合类型相关联的语义(即,ordered值集合的概念)。 parent 的值在所有 child 列表的值之前。对于MapSetProperties集合类型,不存在 ordering。因此,没有 ordering 语义对集合类型有效,这些集合类型是容器在内部使用的关联MapSetProperties implementation 类型的基础。

集合合并的限制

您无法合并不同的集合类型(例如MapList)。如果您尝试这样做,则会抛出适当的Exception。必须在较低的,继承的 child 定义上指定merge属性。在 parent 集合定义上指定merge属性是多余的,并且不会导致所需的合并。

Strongly-typed集合

通过在 Java 5 中引入泛型类型,您可以使用强类型集合。也就是说,可以声明Collection类型,使其只能包含(对于 example)String元素。如果使用 Spring dependency-inject strongly-typed Collection进入 bean,则可以利用 Spring 的 type-conversion 支持,以便在添加到Collection之前将 strongly-typed Collection实例的元素转换为适当的类型。以下 Java class 和 bean 定义显示了如何执行此操作:

1
2
3
4
5
6
7
8
public class SomeClass {

private Map<String, Float> accounts;

public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
1
2
3
4
5
6
7
8
9
10
11
<beans>
<bean id="something" class="x.y.SomeClass">
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
</bean>
</beans>

something bean 的accounts property 准备好进行注入时,可以通过反射获得有关 strongly-typed Map<String, Float>的元素类型的泛型信息。因此,Spring 的类型转换基础结构将各种 value 元素识别为Float类型,并将 string 值(9.99, 2.753.99)转换为实际的Float类型。

Null和EmptyString值

Spring 将 properties 等的空 arguments 视为空Strings。以下 XML-based configuration 元数据片段_set email property 为空String value(“”)。

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

前面的 example 等效于以下 Java code:

1
exampleBean.setEmail("");

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

1
2
3
4
5
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>

前面的 configuration 相当于以下 Java code:

1
exampleBean.setEmail(null);

带p-namespace的XML快捷方式

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

Spring 支持可扩展的 configuration 格式使用命名空间,它们基于 XML Schema 定义。本章中讨论的beans configuration 格式在 XML Schema 文档中定义。但是,p-namespace 未在 XSD 文件中定义,仅存在于 Spring 的核心中。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

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

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

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

下一个 example 包含两个 bean 定义,它们都有一个 reference 给另一个 bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

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

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

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

此 example 不仅包含使用 p-namespace 的 property value,还使用特殊格式声明 property references。第一个 bean 定义使用<property name="spouse" ref="jane"/>从 bean john到 bean jane创建 reference,而第二个 bean 定义使用p:spouse-ref="jane"作为属性来完成同样的事情。在这种情况下,spouse是 property name,而-ref部分表示这不是一个直的 value 而是 reference 给另一个 bean。

p-namespace 不如标准 XML 格式灵活。例如,声明 property references 的格式与以Ref结尾的 properties 冲突,而标准 XML 格式则不然。我们建议您仔细选择您的方法并将其传达给您的团队成员,以避免产生在同一时间使用所有三种方法的 XML 文档。

带c-namespace的XML快捷方式

p-namespace 的 XML 快捷方式类似,Spring 3.1 中引入的 c-namespace 允许内联属性来配置构造函数 arguments 而不是嵌套的constructor-arg元素。

以下 example 使用c:名称空间来执行与Constructor-based 依赖注入相同的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

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

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

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

</beans>

c:名称空间使用与p:一样的约定(bean references 的尾随-ref),用于按名称设置构造函数 arguments。同样,它需要声明,即使它没有在 XSD schema 中定义(它存在于 Spring 核心内)。

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

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

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

实际上,构造函数分辨率机制在匹配 arguments 方面非常有效,所以除非你真的需要,否则我们建议使用 name 表示法 through-out your configuration。

复合属性名称

将 bean properties 设置为 long 时,可以使用复合或嵌套 property 名称,因为除最终 property name 之外的路径的所有组件都不是null。考虑以下 bean 定义:

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

something bean 有一个fred property,它有一个bob property,它有sammy property,最后sammy property 被设置为123的 value。为了使这个工作,的fred property 和fredbob property 在构造 bean 之后不能是null。否则,抛出NullPointerException

使用依赖

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

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

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

1
2
3
4
5
6
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>

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

depends-on属性既可以指定 initialization-time 依赖项,也可以指定singleton) beans,相应的 destruction-time 依赖项。在给定 bean 本身被销毁之前,首先销毁与给定 bean 定义depends-on关系的依赖 beans。因此,depends-on也可以控制 shutdown order。

懒初始化bean

默认情况下,ApplicationContext implementations 急切地创建和配置所有singleton beans 作为初始化 process 的一部分。通常,这个 pre-instantiation 是可取的,因为 configuration 或周围环境中的错误会立即被发现,而不是几小时甚至几天。如果不希望出现这种情况,可以通过将 bean 定义标记为 lazy-initialized 来阻止 singleton bean 的 pre-instantiation。 lazy-initialized bean 告诉 IoC 容器在第一次请求时创建 bean 实例,而不是在启动时创建 bean 实例。

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

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

当前面的 configuration 被ApplicationContext消耗时,ApplicationContext bean 在ApplicationContext启动时并不急切 pre-instantiated,而not.lazy bean 则急切地 pre-instantiated。

但是,当 lazy-initialized bean 是 singleton bean 的依赖关系而不是 lazy-initialized 时,ApplicationContext会在启动时创建 lazy-initialized bean,因为它必须满足 singleton 的依赖关系。 lazy-initialized bean 被注入 singleton bean,而不是 lazy-initialized。

您还可以使用<beans/>元素上的default-lazy-init属性控制容器 level 上的 lazy-initialization,以下 example 显示:

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

自动化协作者

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

  • 自动装配可以显着减少指定 properties 或构造函数 arguments 的需要。 (其他机制,如 bean 模板在本章的其他地方讨论过在此 regard.)也很有价值
  • 随着 objects 的发展,自动装配可以更新 configuration。例如,如果需要向 class 添加依赖项,则可以自动满足该依赖项,而无需修改 configuration。因此,自动装配在开发期间尤其有用,而不会在 code base 变得更稳定时否定切换到显式布线的选项。

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

模式 说明
no (默认)无自动装配。 Bean references 必须由ref元素定义。不建议对较大的部署更改默认设置,因为明确指定协作者可以提供更好的控制和清晰度。在某种程度上,它记录了系统的结构。
byName property name 自动装配。 Spring 查找 bean,其 name 与需要自动装配的 property 相同。例如,如果 bean 定义由 name 设置为 autowire 并且它包含master property(即,它具有setMaster(..)方法),则 Spring 将查找名为master的 bean 定义并使用它来设置 property。
byType 如果容器中只存在 property 类型的一个 bean,则允许 property 自动装配。如果存在多个,则抛出致命的 exception,这表示您不能对该 bean 使用byType自动装配。如果没有匹配的 beans,则不会发生任何事情(property 未设置)。
constructor 类似于byType但适用于构造函数 arguments。如果容器中没有构造函数参数类型的一个 bean,则会引发致命错误。

使用byTypeconstructor自动装配模式,您可以连接数组和类型集合。在这种情况下,提供容器中与预期类型匹配的所有 autowire 候选者以满足依赖性。如果预期的 key 类型为String,则可以自动装配 strongly-typed Map实例。自动装配的Map实例的值由 match 预期类型的所有 bean 实例组成,Map实例的键包含相应的 bean 名称。

自动装配的限制和缺点

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

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

  • propertyconstructor-arg设置中的显式依赖项始终覆盖自动装配。您无法自动装配简单的 properties,例如 primitives,StringsClasses(以及此类简单 properties 的数组)。这个限制是 by-design。
  • 自动装配不如显式布线精确。虽然如前面的 table 所述,Spring 谨慎避免在可能产生意外结果的模糊性的情况下进行猜测。您的 Spring-managed object 之间的关系不再明确记录。
  • 可能无法为可能从 Spring 容器生成文档的工具提供接线信息。
  • 容器中的多个 bean 定义可以 match 由 setter 方法或构造函数参数指定的类型以进行自动装配。对于数组,集合或Map实例,这不一定是个问题。但是,对于期望单个 value 的依赖关系,这种歧义不是任意解决的。如果没有唯一的 bean 定义,则抛出 exception。

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

  • 放弃自动装配,支持显式布线。
  • 通过将属性设置为false,避免为 bean 定义自动装配,如下一节中所述。
  • 通过将元素的primary属性设置为true,将单个 bean 定义指定为主要候选者。
  • 使用 annotation-based configuration 实现更多细粒度控件,如Annotation-based Container Configuration中所述。

从自动装配中排除Bean

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

autowire-candidate属性旨在仅影响 type-based 自动装配。它不会影响 name 的显式 references,即使指定的 bean 未标记为 autowire 候选,它也会得到解析。因此,如果 name 匹配,name 的自动装配仍会注入 bean。

您还可以根据 pattern-matching 对 bean 名称限制 autowire 候选者。 top-level <beans/>元素在其default-autowire-candidates属性中接受一个或多个模式。对于 example,要将 autowire 候选状态限制为 name ends with Repository的任何 bean,请提供的 value。要提供多个模式,请在 comma-separated 列表中定义它们。对于 bean 定义的autowire-candidate属性,truefalse的显式 value 始终优先。对于此类 beans,pattern 匹配规则不适用。

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

方法注入

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

解决方案是放弃一些控制反转。您可以make bean 了解容器实现ApplicationContextAware接口,并通过对容器进行 getBean(“B”)调用请求(一个通常是新的)bean B 实例每 time bean A 需要它。以下 example 显示了这种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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;
}
}

前面的内容是不可取的,因为 business code 知道并耦合到 Spring Framework。方法注入,Spring IoC 容器的一些先进的 feature,让您可以干净地处理这个用例。

您可以在这篇博客文章中阅读有关方法注入动机的更多信息。

查找方法注入

Lookup 方法注入是容器覆盖 container-managed beans 方法的能力,_return 查找容器中另一个名为 bean 的查找结果。查找通常涉及原型 bean,如上一节中描述的场景。 Spring Framework 通过使用 CGLIB library 中的字节码生成来动态生成覆盖该方法的子类来实现此方法注入。

  • 为了使这个动态子类工作,Spring bean 容器子类的 class 不能是final,要覆盖的方法也不能是final
  • Unit-testing 具有abstract方法的 class 要求您自己子类化 class 并提供abstract方法的 stub implementation。
  • component 扫描也需要具体的方法,这需要具体的 classes 来获取。
  • 另一个 key 限制是查找方法不能与工厂方法一起使用,特别是在 configuration classes 中不能使用@Bean方法,因为在这种情况下,容器不负责 creating 实例,因此无法创建 runtime-generated 子类。

对于前一个 code 片段中的CommandManager class,Spring 容器会动态覆盖createCommand()方法的 implementation。 CommandManager class 没有任何 Spring 依赖项,因为重写的 example 显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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();
}

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

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

如果方法是abstract,则 dynamically-generated 子类实现该方法。否则,dynamically-generated 子类将覆盖原始 class 中定义的具体方法。考虑以下 example:

1
2
3
4
5
6
7
8
9
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>

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

bean 标识为commandManager calls 它自己的createCommand()方法,只要它需要一个myCommand bean 的新实例。您必须小心部署myCommand bean 作为原型,如果这实际上是需要的话。如果它是singleton,则每次返回相同的myCommand bean 实例。

或者,在 annotation-based component model 中,您可以通过@Lookup annotation 声明查找方法,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
public abstract class CommandManager {

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

@Lookup("myCommand")
protected abstract Command createCommand();
}

或者,更具惯用性,您可以依赖于查找方法的声明的 return 类型来解析目标 bean:

1
2
3
4
5
6
7
8
9
10
11
public abstract class CommandManager {

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

@Lookup
protected abstract MyCommand createCommand();
}

请注意,您通常应该在 order 中使用具体的 stub implementation 声明这样带注释的查找方法,以使它们与 Spring 的 component 扫描规则兼容,其中 abstract classes 默认被忽略。此限制不适用于显式注册或显式导入的 bean classes。

访问不同范围的目标 beans 的另一种方法是ObjectFactory/Provider注入点。见将 Beans 视为依赖关系

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

任意方法替换

与查找方法注入相比,一种不太有用的方法注入形式是能够使用另一种方法 implementation 替换托管 bean 中的任意方法。您可以安全地跳过本节的 rest,直到您真正需要此功能。

使用 XML-based configuration 元数据,您可以使用replaced-method元素将已存在的方法 implementation 替换为另一个,用于已部署的 bean。考虑以下 class,它有一个我们想要覆盖的名为computeValue的方法:

1
2
3
4
5
6
7
8
public class MyValueCalculator {

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

// some other methods...
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 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 ...;
}
}

部署原始 class 并指定方法覆盖的 bean 定义类似于以下 example:

1
2
3
4
5
6
7
8
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- arbitrary method replacement -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>

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

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

1
2
3
java.lang.String
String
Str

因为 arguments 的数量通常足以区分每个可能的选择,所以通过允许您只键入与参数类型匹配的最短 string,此快捷方式可以节省大量 typing。

Bean范围

创建 bean 定义时,将创建一个配方,用于创建由bean定义的class的实际实例。bean 定义是配方的 idea 很重要,因为它意味着,与 class 一样,您可以从单个配方创建许多 object 实例。

您不仅可以控制要插入到从特定 bean 定义创建的 object 的各种依赖项和 configuration 值,还可以控制从特定 bean 定义创建的 objects 的范围。这种方法功能强大且灵活,因为您可以选择通过 configuration 创建的 objects 的范围,而不必在 Java class level 的 object 范围内进行烘焙。 Beans 可以定义为部署在多个范围之一中。 Spring Framework 支持六个范围,其中四个范围仅在您使用 web-aware ApplicationContext时可用。你也可以创建自定义范围。

以下 table 描述了支持的范围:

范围 描述
singleton (默认)为每个 Spring IoC 容器的单个 object 实例定义单个 bean 定义。
原型 为任意数量的 object 实例定义单个 bean 定义。
请求 将单个 bean 定义范围限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例,该实例是在单个 bean 定义的后面创建的。仅在 web-aware Spring ApplicationContext的 context 中有效。
session 将单个 bean 定义范围限定为 HTTP Session的生命周期。仅在 web-aware Spring ApplicationContext的 context 中有效。
应用 将单个 bean 定义范围限定为ServletContext的生命周期。仅在 web-aware Spring ApplicationContext的 context 中有效。
WebSocket 将单个 bean 定义范围限定为WebSocket的生命周期。仅在 web-aware Spring ApplicationContext的 context 中有效。

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

单例范围

只管理 singleton bean 的一个共享实例,并且 beans 的所有请求都带有一个或多个 match bean 定义的 ID 导致 Spring 容器返回的一个特定 bean 实例。

换句话说,当您定义 bean 并将其范围限定为 singleton 时,Spring IoC 容器只创建该 bean 定义的 object 的一个实例。此单个实例存储在此类 singleton beans 的缓存中,并且所有后续请求和 references 都指向 bean return 缓存的 object。下图显示了 singleton 范围的工作原理:

singleton

Spring 的 singleton bean 概念与 singleton pattern 的概念不同,如四人帮(GoF)模式书中所定义的那样。 GoF singleton hard-codes object 的范围,使得每个 ClassLoader 创建一个且只有一个特定 class 的实例。 Spring singleton 的范围最好描述为 per-container 和 per-bean。这意味着,如果在单个 Spring 容器中为特定 class 定义一个 bean,则 Spring 容器将创建该_ bean 定义所定义的 class 的一个且仅一个实例。 singleton 范围是 Spring 中的默认范围。要在中将 bean 定义为 singleton,您可以定义 bean,如下面的 example 所示:

1
2
3
4
<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"/>

原型范围

bean 部署的 non-singleton 原型范围导致每次都会创建一个新的 bean 实例,并对该特定 bean 发出请求。也就是说,bean 被注入到另一个 bean 中,或者通过容器上的getBean()方法调用来请求它。通常,您应该为所有有状态 beans 使用原型范围,为 stateless beans 使用 singleton 范围。

下图说明了 Spring 原型范围:

原型

(数据访问 object(DAO)通常不配置为原型,因为典型的 DAO 不包含任何会话 state。我们更容易重用 singleton diagram.)的核心

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

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

与其他范围相比,Spring 不管理原型 bean 的完整生命周期。容器实例化,配置和组装原型 object 并将其交给 client,没有该原型实例的进一步 record。因此,尽管无论范围如何都在所有 object 上调用初始化生命周期回调方法,但在原型的情况下,不会调用已配置的销毁生命周期回调。 client code 必须清理 prototype-scoped object 并释放原型 beans 所拥有的昂贵资源。要让 Spring 容器释放 prototype-scoped beans 所拥有的资源,请尝试使用自定义bean post-processor,它包含需要清理的 beans 的 reference。

在某些方面,Spring 容器关于 prototype-scoped bean 的角色是 Java new operator 的替代品。超过该点的所有生命周期管理必须由 client 处理。 (有关 Spring 容器中 bean 生命周期的详细信息,请参阅生命周期回调

单例与原型的依赖

当您对原型 beans 使用带有依赖关系的 singleton-scoped beans 时,请注意在实例化时解析依赖关系。因此,如果原型实例是提供给 singleton-scoped bean 的唯一实例。

但是,假设您希望 singleton-scoped bean 在运行时重复获取 prototype-scoped bean 的新实例。你不能@ 如果您需要在运行时多次使用原型 bean 的新实例,请参阅方法注入

Request,Session,Application和WebSocketScopes

仅当您使用 web-aware Spring ApplicationContext implementation(例如XmlWebApplicationContext)时,requestsessionapplicationwebsocket范围才可用。如果将这些范围与常规 Spring IoC 容器(例如ClassPathXmlApplicationContext)一起使用,则会抛出抱怨未知 bean 范围的IllegalStateException

初始WebConfiguration

要在requestsessionapplicationwebsocket级别(web-scoped beans)支持 beans 的范围设定,在定义 beans 之前需要一些小的初始 configuration。 (标准范围不需要此初始设置:singletonprototype .)

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

如果在 Spring Web MVC 中访问范围 beans,实际上是在 Spring DispatcherServlet处理的请求中,则无需进行特殊设置。 DispatcherServlet已经暴露了所有相关的 state。

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

1
2
3
4
5
6
7
8
9
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>

或者,如果 listener 设置存在问题,请考虑使用 Spring 的RequestContextFilter。过滤器映射取决于周围的 web application configuration,因此您必须根据需要进行更改。以下清单显示了 web application 的过滤器部分:

1
2
3
4
5
6
7
8
9
10
11
12
<web-app>
...
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>

DispatcherServletRequestContextListenerRequestContextFilter都完全相同,即将 HTTP 请求 object 绑定到为该请求提供服务的Thread。这使 beans 在请求和 session-scoped 可进一步在调用链中可用。

RequestScope

考虑以下针对 bean 定义的 XML configuration:

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

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

使用 annotation-driven 组件或 Java configuration 时,@RequestScope annotation 可用于将 component 分配给request范围。以下 example 显示了如何执行此操作:

1
2
3
4
5
@RequestScope
@Component
public class LoginAction {
// ...
}

SessionScope

考虑以下针对 bean 定义的 XML configuration:

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

Spring 容器通过在单个 HTTP Session的生命周期中使用userPreferences bean 定义来创建UserPreferences bean 的新实例。换句话说,userPreferences bean 有效地限定在 HTTP Session level。与 request-scoped beans 一样,您可以根据需要更改创建的实例的内部 state,因为知道同样使用从同一userPreferences bean 定义创建的实例的其他 HTTP Session实例在 state 中看不到这些更改,因为它们特定于单个 HTTP Session。当最终丢弃 HTTP Session时,也将丢弃作用于该特定 HTTP Session的 bean。

使用 annotation-driven 组件或 Java configuration 时,可以使用@SessionScope annotation 将 component 分配给session范围。

1
2
3
4
5
@SessionScope
@Component
public class UserPreferences {
// ...
}

ApplicationScope

考虑以下针对 bean 定义的 XML configuration:

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

Spring 容器通过对整个 web application 使用appPreferences bean 定义一次来创建AppPreferences bean 的新实例。也就是说,appPreferences bean 的作用域为ServletContext level 并存储为常规ServletContext属性。这有点类似于 Spring singleton bean 但在两个重要方面有所不同:它是 singleton 每ServletContext,而不是 Spring’ApplicationContext’(在任何给定的 web application 中可能有几个),它实际上是暴露的,因此作为ServletContext属性可见。

使用 annotation-driven 组件或 Java configuration 时,可以使用@ApplicationScope annotation 将 component 分配给application范围。以下 example 显示了如何执行此操作:

1
2
3
4
5
@ApplicationScope
@Component
public class AppPreferences {
// ...
}

限定范围的Beans依赖

Spring IoC 容器不仅管理 objects(beans)的实例化,还管理协作者(或依赖项)的连接。如果要将 HTTP request-scoped bean 注入(对于 example)到 longer-lived 范围的另一个 bean,您可以选择 inject AOP 代理来代替作用域 bean。也就是说,您需要 inject 一个代理 object,它暴露与作用域 object 相同的公共接口,但也可以从相关范围(例如 HTTP 请求)中检索真实目标 object,并将方法 calls 委托给真实的 object。

您也可以在作为singleton的 beans 之间使用<aop:scoped-proxy/>,然后 reference 将通过可序列化的中间代理,因此能够在反序列化时 re-obtain 目标 singleton bean。

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

此外,范围代理不是以 lifecycle-safe 方式从较短范围访问 beans 的唯一方法。您还可以将注入点(即构造函数或 setter 参数或自动装配字段)声明为ObjectFactory<MyTargetBean>,允许getObject()调用在每次需要时按需检索当前实例 - 无需保留实例或存储它分别。

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

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

以下 example 中的 configuration 只有一个 line,但了解“为什么”以及它背后的“如何”非常重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

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

<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.something.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
1 定义代理的 line。

要创建这样的代理,请将 child <aop:scoped-proxy/>元素插入到作用域 bean 定义中(请参阅选择要创建的代理类型XML Schema-based configuration。为什么在requestsession和 custom-scope 级别定义 beans 的定义需要<aop:scoped-proxy/>元素?考虑以下 singleton bean 定义,并将其与您需要为上述范围定义的内容进行对比(请注意,以下userPreferences bean 定义不完整):

1
2
3
4
5
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

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

在前面的example中,singleton bean(userManager)注入一个 reference 到 HTTP Session-scoped bean(userPreferences)。这里的重点是userManager bean 是 singleton:它每个容器只实例化一次,它的依赖项(在这种情况下只有一个,userPreferences bean)也只注入一次。这意味着userManager bean 仅在完全相同的userPreferences object(即最初注入它的那个)上运行。

这不是将 shorter-lived 作用域 bean 注入 longer-lived 作用域 bean 时所需的行为(对于 example,将-scoped 协作 bean 作为依赖项注入 singleton bean)。相反,您需要一个userManager object,并且,对于 HTTP Session的生命周期,您需要一个特定于 HTTP SessionuserPreferences object。因此,容器创建一个 object,它公开与UserPreferences class 完全相同的公共接口(理想情况下是一个UserPreferences实例的 object),它可以从作用域机制中获取真正的UserPreferences object(HTTP 请求,Session等) 。容器将此代理 object 注入userManager bean,它不知道此UserPreferences reference 是代理。在此 example 中,当UserManager实例在 dependency-injected UserPreferences object 上调用方法时,它实际上是在代理上调用方法。然后代理从(在这种情况下)HTTP Session中获取真实UserPreferences object,并将方法调用委托给检索到的真实UserPreferences object。

因此,在将request-session-scoped beans 注入协作 objects 时,需要以下(正确和完整)configuration,如下面的 example 所示:

1
2
3
4
5
6
7
<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-based class 代理。

CGLIB 代理只拦截公共方法 calls!不要在这样的代理上调用 non-public 方法。它们不会委托给实际作用域的目标 object。

或者,您可以通过为<aop:scoped-proxy/>元素的proxy-target-class属性的 value 指定false来配置 Spring 容器以为此类作用域 beans 创建标准 JDK interface-based 代理。使用 JDK interface-based 代理意味着您不需要 application classpath 中的其他 libraries 来影响此类代理。但是,它还意味着作用域 bean 的 class 必须至少实现一个接口,并且注入了作用域 bean 的所有协作者必须通过其中一个接口引用 bean。以下 example 显示了基于接口的代理:

1
2
3
4
5
6
7
8
<!-- 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>

有关选择 class-based 或 interface-based 代理的更多详细信息,请参阅代理机制

自定义范围

bean 作用域机制是可扩展的。您可以定义自己的范围,甚至可以重新定义现有范围,但后者被认为是不好的做法,您无法覆盖 built-in singletonprototype范围。

创建自定义范围

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

Scope接口有四种方法可以从作用域中获取 objects,将它们从作用域中删除,然后将它们销毁。

session 范围 implementation,对于 example,返回 session-scoped bean(如果它不存在,则该方法在将__ssion 绑定到 session 以用于将来 reference 之后返回 bean 的新实例)。以下方法从基础范围返回 object:

1
Object get(String name, ObjectFactory objectFactory)

session 范围 implementation,对于 example,从基础 session 中删除 session-scoped bean。应返回 object,但如果找不到具有指定 name 的 object,则可以 return null。以下方法从基础范围中删除 object:

1
Object remove(String name)

以下方法记录范围在销毁时或范围中指定的 object 被销毁时应执行的回调:

1
void registerDestructionCallback(String name, Runnable destructionCallback)

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

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

1
String getConversationId()

每个范围的标识符都不同。对于 session 范围的 implementation,此标识符可以是 session 标识符。

使用自定义范围

在编写并测试一个或多个自定义Scope __mplement 之后,您需要让 Spring 容器知道您的新范围。以下方法是使用 Spring 容器注册新Scope的核心方法:

1
void registerScope(String scopeName, Scope scope);

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

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

假设您编写自定义Scope implementation,然后按照下一个 example 中的说明进行注册。

下一个 example 使用SimpleThreadScope,它包含在 Spring 中,但默认情况下未注册。对于您自己的自定义Scope __mplement,说明将是相同的。

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

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

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

使用自定义Scope implementation,您不仅限于范围的编程注册。您还可以使用CustomScopeConfigurer class 以声明方式执行Scope注册,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

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

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

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

</beans>

<aop:scoped-proxy/>放在FactoryBean implementation 中时,工厂 bean 本身是作用域的,而不是从getObject()返回的 object。

自定义Bean的性质

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

生命周期回调

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

JSR-250 @PostConstruct@PreDestroy 注释通常被认为是在现代 Spring application 中接收生命周期回调的最佳实践。使用这些注释意味着 beans 没有耦合到 Spring-specific 接口。有关详细信息,请参阅使用 @PostConstruct 和 @PreDestroy

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

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

除了初始化和销毁回调之外,Spring-managed objects 还可以实现Lifecycle接口,以便那些 objects 可以参与启动和关闭 process,这是由容器自身的生命周期驱动的。

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

初始化回调

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

1
void afterPropertiesSet() throws Exception;

我们建议您不要使用InitializingBean接口,因为它会不必要地将 code 耦合到 Spring。或者,我们建议使用@PostConstruct 注解或指定 POJO 初始化方法。对于 XML-based 配置元数据,可以使用init-method属性指定具有 void no-argument 签名的方法的 name。使用 Java configuration,您可以使用@BeaninitMethod属性。见接收生命周期回调。考虑以下 example:

1
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
1
2
3
4
5
6
public class ExampleBean {

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

前面的 example 与以下 example(由两个列表组成)具有几乎完全相同的效果:

1
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
1
2
3
4
5
6
public class AnotherExampleBean implements InitializingBean {

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

但是,前两个示例中的第一个不将 code 耦合到 Spring。

销毁回调

实现org.springframework.beans.factory.DisposableBean接口允许 bean 在包含它的容器被销毁时获得回调。 DisposableBean接口指定单个方法:

1
void destroy() throws Exception;

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

1
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
1
2
3
4
5
6
public class ExampleBean {

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

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

1
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
1
2
3
4
5
6
public class AnotherExampleBean implements DisposableBean {

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

但是,前两个定义中的第一个不将 code 耦合到 Spring。

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

默认初始化和销毁方法

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

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

假设您的初始化回调方法名为init(),并且您的 destroy 回调方法名为destroy()。您的 class 类似于以下 example 中的 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DefaultBlogService implements BlogService {

private BlogDao blogDao;

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

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

然后,您可以在 bean 中使用 class,类似于以下内容:

1
2
3
4
5
6
7
<beans default-init-method="init">

<bean id="blogService" class="com.something.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>

</beans>

top-level <beans/>元素属性上存在default-init-method属性会导致 Spring IoC 容器在 bean class 上识别名为init的方法作为初始化方法回调。当创建和组装 bean 时,如果 bean class 具有这样的方法,则在适当的 time 调用它。

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

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

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

结合生命周期机制

从 Spring 2.5 开始,您有三个控制 bean 生命周期行为的选项:

如果为 bean 配置了多个生命周期机制,并且每个机制配置了不同的方法 name,则每个配置的方法都在此注解后列出的 order 中执行。但是,如果配置了相同的方法 name - 对于 example,init()用于初始化方法 - 对于多个这些生命周期机制,该方法将执行一次,如前一节中所述。

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

  • @PostConstruct注解的方法
  • afterPropertiesSet()InitializingBean回调接口定义
  • 自定义配置的init()方法

Destroy 方法在同一个 order 中调用:

  • @PreDestroy注解的方法
  • destroy()DisposableBean回调接口定义
  • 自定义配置的destroy()方法

启动和关闭回调

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

1
2
3
4
5
6
7
8
public interface Lifecycle {

void start();

void stop();

boolean isRunning();
}

任何 Spring-managed object 都可以实现Lifecycle接口。然后,当ApplicationContext本身接收到启动和停止信号时(对于 example,对于运行时的 stop/restart 场景),它将那些 calls 级联到该 context 中定义的所有Lifecycle 实现。它通过委托给LifecycleProcessor来实现,如下面的清单所示:

1
2
3
4
5
6
public interface LifecycleProcessor extends Lifecycle {

void onRefresh();

void onClose();
}

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

请注意,常规org.springframework.context.Lifecycle接口是用于显式启动和停止通知的普通 contract,并不意味着 auto-startup 在 context refresh time。对于 fine-grained 控制特定 bean 的 auto-startup(包括启动阶段),请考虑实现org.springframework.context.SmartLifecycle

此外,请注意,在销毁之前不保证停止通知。在常规关闭时,所有Lifecycle beans 在传播一般销毁回调之前首先收到停止通知。但是,在 context 生命周期内的热刷新或中止刷新尝试时,仅调用 destroy 方法。

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

1
2
3
4
public interface Phased {

int getPhase();
}

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

1
2
3
4
5
6
public interface SmartLifecycle extends Lifecycle, Phased {

boolean isAutoStartup();

void stop(Runnable callback);
}

启动时,具有最低相位的 objects 首先启动。停止时,遵循反向顺序。因此,实现SmartLifecycle且其getPhase()方法返回Integer.MIN_VALUE的 object 将是第一个开始,最后一个停止。在频谱的另一端,Integer.MAX_VALUE的阶段值将指示 object 应该最后启动并首先停止(可能因为它依赖于其他进程 running)。在考虑阶段 value 时,同样重要的是要知道没有实现SmartLifecycle的任何“正常”Lifecycle object 的默认阶段是0。因此,任何负相位 value 都表示 object 应该在这些标准组件之前启动(并在它们之后停止)。对于任何正相值,反向都是 true。

SmartLifecycle定义的 stop 方法接受回调。在 implementation 的 shutdown process 完成之后,任何 implementation 都必须调用该回调的run()方法。这样可以在必要时启用异步关闭,因为LifecycleProcessor接口的默认_impleration DefaultLifecycleProcessor等待每个阶段中 objects 的 group 的超时值,以调用该回调。默认 per-phase 超时为 30 秒。您可以通过在 context 中定义名为lifecycleProcessor的 bean 来覆盖默认生命周期处理器实例。如果您只想修改超时,则定义以下内容就足够了:

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

如前所述,LifecycleProcessor接口定义了用于刷新和关闭 context 的回调方法。后者驱动 shutdown process,就像显式调用stop()一样,但是当 context 关闭时会发生。另一方面,’refresh’回调启用SmartLifecycle beans 的另一个 feature。刷新 context 时(在实例化并初始化所有 objects 之后),将调用该回调。此时,默认生命周期处理器检查每个SmartLifecycle object 的isAutoStartup()方法返回的 boolean value。如果true,那个 object 就是在那一点开始而不是等待显式调用 context 或它自己的start()方法(与 context 刷新不同,context start 不会自动发生在标准的 context implementation 中)。 phase value 和任何“depends-on”关系确定了如前所述的 startup order。

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

本节仅适用于 non-web applications。当相关的 web application 关闭时,Spring 的 web-based ApplicationContext implementations 已经有 code 来正常关闭 Spring IoC 容器。

如果在 non-web application 环境中使用 Spring 的 IoC 容器(对于 example,在富 client 桌面环境中),请向 JVM 注册 shutdown hook。这样做可确保正常关闭并 calls singleton beans 上的相关 destroy 方法,以便释放所有资源。您仍然必须正确配置和实现这些 destroy 回调。

要注册 shutdown hook,请调用ConfigurableApplicationContext接口上声明的registerShutdownHook()方法,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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...
}
}

ApplicationContextAware和BeanNameAware

ApplicationContext创建实现org.springframework.context.ApplicationContextAware接口的 object 实例时,该实例将提供的 reference。以下清单显示了ApplicationContextAware接口的定义:

1
2
3
4
public interface ApplicationContextAware {

void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

因此,beans 可以通过ApplicationContext接口以编程方式操纵创建它们的ApplicationContext,或者通过将 reference 转换为此接口的已知子类(例如ConfigurableApplicationContext,这会暴露其他功能)。一种用途是对其他 beans 进行编程检索。有时这种能力很有用。但是,一般情况下,您应该避免使用它,因为它将 code 与 Spring 耦合,并且不遵循 Inversion of Control 样式,其中协作者作为 properties 提供给 beans。 ApplicationContext的其他方法提供对文件资源的访问,发布 application events 和访问MessageSource。这些额外的 features 在ApplicationContext的附加功能中描述。

从 Spring 2.5 开始,自动装配是获得ApplicationContext的 reference 的另一种选择。 “传统”constructorbyType自动装配模式(如自动化协作者中所述)可以分别为构造函数参数或 setter 方法参数提供类型ApplicationContext的依赖关系。要获得更多灵活性,包括自动装配字段和多参数方法的功能,请使用新的 annotation-based 自动装配 features。如果这样做,ApplicationContext将自动装入一个字段,构造函数参数或方法参数,如果所涉及的字段,构造函数或方法带有@Autowired annotation,则该参数需要ApplicationContext类型。有关更多信息,请参阅使用 @Autowired

ApplicationContext创建一个实现org.springframework.beans.factory.BeanNameAware接口的 class 时,class 会提供对其关联的 object 定义中的 name 的 reference。以下清单显示了 BeanNameAware 接口的定义:

1
2
3
4
public interface BeanNameAware {

void setBeanName(String name) throws BeansException;
}

在普通 bean properties 的填充之后但在初始化回调之前(例如InitializingBeanafterPropertiesSet或自定义 init-method)之前调用回调。

其他感知接口

除了ApplicationContextAwareBeanNameAware(讨论之外,Spring 还提供了一系列Aware接口,让 beans 向容器指示它们需要某种基础结构依赖性。作为一般规则,name 是依赖类型的良好指示。以下 table 总结了最重要的Aware接口:

名称 注入依赖 解释在……
ApplicationContextAware 声明ApplicationContext ApplicationContextAware 和 BeanNameAware
ApplicationEventPublisherAware 封闭ApplicationContext的 Event 发布者。 ApplicationContext的附加功能
BeanClassLoaderAware Class loader 用于加载 bean classes。 实例化Beans
BeanFactoryAware 声明BeanFactory ApplicationContextAware 和 BeanNameAware
BeanNameAware 声明 bean 的名称。 ApplicationContextAware 和 BeanNameAware
BootstrapContextAware 资源适配器BootstrapContext容器运行。通常仅在 JCA 感知ApplicationContext实例中可用。 JCA CCI
LoadTimeWeaverAware 定义的 weaver 用于在 load time 处理 class 定义。 Load-time 在 Spring Framework 中使用 AspectJ 进行编织
MessageSourceAware 用于解析消息的已配置策略(支持参数化和国际化)。 ApplicationContext的附加功能
NotificationPublisherAware Spring JMX 通知发布者。 通知
ResourceLoaderAware 配置加载程序以 low-level 访问资源。 资源
ServletConfigAware 当前ServletConfig容器运行。仅在 web-aware Spring ApplicationContext中有效。 Spring MVC
ServletContextAware 当前ServletContext容器运行。仅在 web-aware Spring ApplicationContext中有效。 Spring MVC

请再次注意,使用这些接口会将 code 绑定到 Spring API,而不会遵循 Inversion of Control 样式。因此,我们建议将它们用于需要以编程方式访问容器的基础结构 beans。

Bean定义继承

bean 定义可以包含许多配置信息,包括构造函数参数,property 值和 container-specific 信息,例如初始化方法,静态工厂方法名等。 child bean 定义从 parent 定义继承 configuration 数据。 child 定义可以覆盖某些值或根据需要添加其他值。使用 parent 和 child bean 定义可以节省大量的 typing。实际上,这是一种模板形式。

如果以编程方式使用ApplicationContext接口,则 child bean 定义由ChildBeanDefinition class 表示。大多数用户不会在这个 level 上使用它们。相反,它们在 class(例如ClassPathXmlApplicationContext)中以声明方式配置 bean 定义。使用 XML-based configuration 元数据时,可以使用parent属性指定 child bean 定义,并将 parent bean 指定为此属性的 value。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
<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属性。

如果指定了 none,则 child bean 定义使用 parent 定义中的 bean class,但也可以覆盖它。在后一种情况下,child bean class 必须与 parent 兼容(即,它必须接受 parent 的 property 值)。

child bean 定义从 parent 继承范围,构造函数参数值,属性值和方法覆盖,并带有添加新值的选项。您指定的任何范围,初始化方法,销毁方法或static工厂方法设置都会覆盖相应的 parent 设置。

其余设置始终取自 child 定义:依赖于,autowire 模式,依赖性检查,singleton 和 lazy init。

前面的 example 使用abstract属性显式地将 parent bean 定义标记为 abstract。如果 parent 定义未指定 class,则需要将 parent bean 定义显式标记为abstract,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
<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>

parent bean 无法单独实例化,因为它不完整,并且也明确标记为abstract。当定义为abstract时,它仅可用作纯模板 bean 定义,用作 child 定义的 parent 定义。尝试使用这样的abstract parent bean,通过将其称为另一个 bean 的 ref property 或使用 parent bean ID 进行显式getBean()调用返回错误。类似地,容器的内部preInstantiateSingletons()方法忽略定义为 abstract 的 bean 定义。

ApplicationContext默认情况下先初始化Singleton Beans。因此,重要的是(至少对于 singleton beans),如果你有一个(parent)bean 定义,你打算只用作模板,并且这个定义指定一个 class,你必须确保将 abstract 属性设置为 true ,否则 application context 将实际(尝试)pre-instantiate abstract bean。

容器扩展点

通常,application 开发人员不需要子类ApplicationContext implementation classes。相反,可以通过插入特殊 integration 接口的 implementations 来扩展 Spring IoC 容器。接下来的几节将介绍这些 integration 接口。

使用BeanPostProcessor自定义Beans

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

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

BeanPostProcessor实例在 bean(或 object)实例上运行。也就是说,Spring IoC 容器实例化 bean 实例,然后BeanPostProcessor实例执行它们的工作。

BeanPostProcessor实例的范围是 per-container。仅当您使用容器层次结构时,这才是相关的。如果在一个容器中定义BeanPostProcessor,则 post-processes 仅包含该容器中的 beans。换句话说,在一个容器中定义的 beans 不是由另一个容器中定义的BeanPostProcessor post-processed,即使两个容器都是同一层次结构的一部分。

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

org.springframework.beans.factory.config.BeanPostProcessor接口恰好包含两个回调方法。当这样的 class 被容器注册为 post-processor 时,对于容器创建的每个 bean 实例,post-processor 在容器初始化方法(例如InitializingBean.afterPropertiesSet(),在任何声明的init方法之后)被调用之前都从容器获得回调,以及任何 bean 初始化回调之后。 post-processor 可以对 bean 实例执行任何操作,包括完全忽略回调。 bean post-processor 通常检查回调接口,或者它可以用代理包装 bean。一些 Spring AOP 基础结构 classes 在 order 中实现为 bean post-processors 以提供 proxy-wrapping 逻辑。

ApplicationContext自动检测在实现BeanPostProcessor接口的 configuration 元数据中定义的任何 beans。 ApplicationContext将 beans 注册为 post-processors,以便稍后在 bean 创建时调用它们。 Bean post-processors 可以以与任何其他 beans 相同的方式部署在容器中。

请注意,在 configuration class 上使用@Bean工厂方法声明BeanPostProcessor时,工厂方法的 return 类型应该是 implementation class 本身或至少org.springframework.beans.factory.config.BeanPostProcessor接口,清楚地表明该 bean 的 post-processor 性质。否则,在完全_create 之前,ApplicationContext不能按类型自动检测它。由于需要在 order 中提前实例化以应用于 context 中其他 beans 的初始化,因此这种早期类型检测至关重要。

以编程方式注册BeanPostProcessor实例虽然推荐的BeanPostProcessor注册方法是通过ApplicationContext auto-detection(如前所述),但您可以使用addBeanPostProcessor方法以编程方式对ConfigurableBeanFactory进行注册。当您需要在注册前评估条件逻辑,甚至在层次结构中跨上下文复制 bean 后处理器时,这非常有用。但请注意,以编程方式添加的BeanPostProcessor实例不尊重Ordered接口。在这里,注册的顺序决定了执行的顺序。另请注意,以编程方式注册的BeanPostProcessor实例始终在通过 auto-detection 注册的实例之前处理,而不管任何显式的 ordering。

BeanPostProcessor实例和 AOP auto-proxying

_实现BeanPostProcessor接口的类是特殊的,容器对它们的处理方式不同。它们直接 reference 的所有BeanPostProcessor实例和 beans 在启动时被实例化,作为ApplicationContext的特殊启动阶段的一部分。接下来,所有BeanPostProcessor实例都以排序方式注册,并应用于容器中的所有其他 beans。因为 AOP auto-proxying 是作为BeanPostProcessor本身实现的,所以BeanPostProcessor实例和它们直接引用的 beans 都不符合 auto-proxying 的条件,因此没有编入方面的方面。

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

如果使用自动装配或@Resource(可能会回退到自动装配)将 beans 连接到BeanPostProcessor,Spring 可能会在搜索 type-matching 依赖项候选时访问意外的 beans,因此使它们不适合 auto-proxying 或其他类型的 bean post-processing。例如,如果您有一个使用@Resource注释的依赖项,其中 field 或 setter name 不直接对应_ bean 的声明 name 且未使用 name 属性,则 Spring 会访问其他 beans 以按类型匹配它们。

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

示例:Hello World,BeanPostProcessor-style

第一个例子说明了基本用法。 example 显示了一个自定义BeanPostProcessor implementation,它调用容器创建的每个 bean 的toString()方法,并将生成的 string 打印到系统 console。

以下清单显示了自定义BeanPostProcessor implementation class 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package scripting;

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

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

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

public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}

以下beans元素使用InstantiationTracingBeanPostProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang.xsd">

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

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

</beans>

注意如何定义InstantiationTracingBeanPostProcessor。它甚至没有 name,因为它是一个 bean,它可以像任何其他 bean 一样 dependency-injected。 (前面的 configuration 还定义了一个由 Groovy 脚本支持的 bean.Spring 动态语言支持在名为动态语言支持的章节中有详细说明。

以下 Java application 运行前面的 code 和 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {

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

}

前面的 application 的输出类似于以下内容:

1
2
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMesse[emailprotected]
[emailprotected]

Example:RequiredAnnotationBeanPostProcessor

将回调接口或 annotations 与自定义BeanPostProcessor implementation 结合使用是扩展 Spring IoC 容器的常用方法。一个 example 是 Spring 的RequiredAnnotationBeanPostProcessor - 一个带有 Spring 分布的BeanPostProcessor implementation,它确保 beans 上标记有(任意)annotation 的 JavaBean properties 实际上(配置为)带有 value 的 dependency-injected。

使用BeanFactoryPostProcessor自定义Configuration元数据

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

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

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

此外,BeanFactoryPostProcessor实例的范围是 per-container。仅当您使用容器层次结构时,这才有意义。如果在一个容器中定义BeanFactoryPostProcessor,则它仅应用于该容器中的 bean 定义。一个容器中的 Bean 定义不是BeanFactoryPostProcessor在另一个容器中的BeanFactoryPostProcessor实例,即使两个容器都是同一层次结构的一部分。

工厂 post-processor 在ApplicationContext中声明时自动执行,在 order 中将更改应用于定义容器的 configuration 元数据。 Spring 包含许多预定义的 bean factory post-processors,例如PropertyOverrideConfigurerPropertyPlaceholderConfigurer。您还可以使用自定义BeanFactoryPostProcessor - for example 来注册自定义 property 编辑器。

ApplicationContext自动检测部署到其中的任何实现BeanFactoryPostProcessor接口的 beans。它在适当的 time 使用 beans 作为 bean factory post-processors。您可以像处理任何其他 bean 一样部署这些 post-processor beans。

BeanPostProcessor一样,您通常不希望为延迟初始化配置BeanFactoryPostProcessor。如果没有其他 bean references a Bean(Factory)PostProcessor,post-processor 将根本不会被实例化。因此,将忽略将其标记为延迟初始化,即使在<beans />元素的声明上将default-lazy-init属性设置为true,也会急切地实例化Bean(Factory)PostProcessor

事例:类名代替属性占位符配置器

您可以使用PropertyPlaceholderConfigurer来使用标准 Java Properties格式在单独的文件中将 bean 定义中的 property 值外部化。这样做可以使部署 application 的人员自定义 environment-specific properties,例如数据库 URL 和密码,而不会有修改主 XML 定义文件或容器的 files 的复杂性或风险。

考虑以下 XML-based configuration 元数据片段,其中定义了带占位符值的DataSource

1
2
3
4
5
6
7
8
9
10
11
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

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

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

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

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

因此,${jdbc.username} string 在运行时将替换为 value,’sa’,同样适用于 properties 文件中 match 键的其他占位符值。 PropertyPlaceholderConfigurer检查大多数 properties 中的占位符和 bean 定义的属性。此外,您可以自定义占位符前缀和后缀。

使用 Spring 2.5 中引入的context名称空间,可以使用专用的 configuration 元素配置 property 占位符。您可以在location属性中提供一个或多个位置作为 comma-separated 列表,如下面的 example 所示:

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

PropertyPlaceholderConfigurer不仅在您指定的Properties文件中查找 properties。默认情况下,如果它在指定的 properties files 中找不到 property,它还会检查 Java System properties。您可以通过使用以下三个支持的 integer 值之一设置 configurer 的systemPropertiesMode property 来自定义此行为:

  • never(0):从不检查系统 properties。
  • fallback(1):如果在指定的 properties files 中无法解析,则检查系统 properties。这是默认值。
  • override(2):在尝试指定的 properties files 之前,先检查系统 properties。这使系统 properties 可以覆盖任何其他 property 源。

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

您可以使用PropertyPlaceholderConfigurer替换 class 名称,这在您必须在运行时选择特定的 implementation class 时有用。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/something/strategy.properties</value>
</property>
<property name="properties">
<value>custom.strategy.class=com.something.DefaultStrategy</value>
</property>
</bean>

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

如果 class 在运行时无法解析为有效的 class,则 bean 的解析将在创建时失败,即

Example:PropertyOverrideConfigurer

PropertyOverrideConfigurer,另一个 bean 工厂 post-processor,类似于PropertyPlaceholderConfigurer,但与后者不同,原始定义可以为 bean properties 提供默认值或根本没有值。如果重写的Properties文件没有某个 bean property 的条目,则使用默认的 context 定义。

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

Properties 文件 configuration lines 采用以下格式:

1
beanName.property=value

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

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

此 example 文件可以与容器定义一起使用,该容器定义包含名为dataSource的 bean,该具有driverurl properties。

复合 property 名称也被支持,因为 long 作为路径的每个 component 除了被覆盖的最终 property 已经 non-null(可能由构造函数初始化)。在下面的示例中,tom bean 的fred property 的bob property 的sammy property 设置为标量 value 123

1
tom.fred.bob.sammy=123

指定的覆盖值始终是文字值。它们没有被翻译成 bean references。当 XML bean 定义中的原始 value 指定 bean reference 时,此约定也适用。

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

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

使用FactoryBean自定义实例化逻辑

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

FactoryBean接口是可插入 Spring IoC 容器的实例化逻辑的一个点。如果你有一个复杂的初始化 code,用 Java 表示,而不是(可能)冗长的 XML,你可以创建自己的FactoryBean,在 class 中编写复杂的初始化,然后将自定义FactoryBean插入容器。

FactoryBean接口提供了三种方法:

  • Object getObject():返回此工厂创建的 object 的实例。可以共享实例,具体取决于此工厂是返回单例还是原型。
  • boolean isSingleton():如果FactoryBean返回单例,则返回true,否则返回false
  • Class getObjectType():返回getObject()方法返回的 object 类型,如果事先不知道类型,则返回null

FactoryBean概念和接口用于 Spring Framework 中的许多位置。 FactoryBean接口的 50 多个 implementations 与 Spring 本身一起发布。

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

基于注解的容器配置

annotations 比 XML 更适合配置 Spring 吗?

annotation-based configuration 的引入提出了这种方法是否比 XML 更“好”的问题。简短的回答是“根据具体情况而定”。每种方法都有其优点和缺点,通常,由开发人员决定哪种策略更适合他们。由于它们的定义方式,annotations 在其声明中提供了大量的 context,从而导致更简洁,更简洁的 configuration。但是,XML 擅长在不触及 source code 或重新编译它们的情况下连接组件。一些开发人员更喜欢将布线靠近源,而其他开发人员则认为带注释的 classes 不再是 POJO,而且 configuration 变得分散且难以控制。

无论选择如何,Spring 都可以兼顾两种风格,甚至可以将它们混合在一起。值得指出的是,通过其JavaConfig选项,Spring 允许注释以 non-invasive 方式使用,而不触及目标组件 source code,并且在工具方面,Spring 工具套件支持所有 configuration 样式。

annotation-based configuration 提供了 XML 设置的替代方法,它依赖于字节码元数据来连接组件而不是 angle-bracket 声明。开发人员不使用 XML 来描述 bean 布线,而是通过在相关的 class,方法或字段声明上使用 annotations 将 configuration 移动到 component class 本身。如Example:RequiredAnnotationBeanPostProcessor中所述,将BeanPostProcessor与 annotations 结合使用是扩展 Spring IoC 容器的常用方法。例如,Spring 2.0 引入了使用@Required annotation 强制执行所需 properties 的可能性。 Spring 2.5 使得有可能采用相同的通用方法来驱动 Spring 的依赖注入。从本质上讲,@Autowired annotation 提供了与自动化协作者中描述的相同的功能,但具有更多细粒度的控制和更广泛的适用性。 Spring 2.5 还添加了对 JSR-250 注释的支持,例如@PostConstruct@PreDestroy。 Spring 3.0 添加了有关这些注释的详细信息可以在相关部分中找到。

注释注入在 XML 注入之前执行。因此,XML configuration 会覆盖通过这两种方法连接的 properties 的 annotations。

与往常一样,您可以将它们注册为单独的 bean 定义,但也可以通过在 XML-based Spring configuration 中包含以下标记来隐式注册它们(注意包含context命名空间):

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

</beans>

(隐式注册的 post-processors 包括AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 会PersistenceAnnotationBeanPostProcessor和前面提到的RequiredAnnotationBeanPostProcessor .)

<context:annotation-config/>仅在定义它的同一 application context 中查找 beans 上的 annotations。这意味着,如果将<context:annotation-config/>放在WebApplicationContextDispatcherServlet,它只检查控制器中的@Autowired beans,而不检查您的服务。有关更多信息,请参见DispatcherServlet

Required注解

@Required annotation 适用于 bean property setter 方法,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

此 annotation 指示必须在 configuration time,bean 定义中的显式 property value 或通过自动装配填充受影响的 bean property。如果尚未填充受影响的 bean property,则容器会抛出 exception。这允许急切和明确的失败,以后避免NullPointerException实例等。我们仍然建议您将断言放入 bean class 本身(对于 example,放入 init 方法)。即使在容器外部使用 class,这样做也会强制执行那些必需的 references 和值。

使用Autowired注解

在本节所包含的示例中,可以使用 JSR 330 的@Inject annotation 代替 Spring 的@Autowired annotation。有关详细信息,请参阅这里

您可以将@Autowired annotation 应用于构造函数,如以下 example 所示:

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private final CustomerPreferenceDao customerPreferenceDao;

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

// ...
}

从 Spring Framework 4.3 开始,如果目标 bean 仅定义一个开头的构造函数,则不再需要在这样的构造函数上使用@Autowired annotation。但是,如果有几个构造器可用,则必须注释至少一个构造器以教导容器使用哪一个。

您还可以将@Autowired annotation 应用于“传统”setter 方法,如以下 example 所示:

1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

您还可以将 annotation 应用于具有任意名称和多个 arguments 的方法,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MovieRecommender {

private MovieCatalog movieCatalog;

private CustomerPreferenceDao customerPreferenceDao;

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

// ...
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MovieRecommender {

private final CustomerPreferenceDao customerPreferenceDao;

@Autowired
private MovieCatalog movieCatalog;

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

// ...
}

确保您的目标组件(对于 example,MovieCatalogCustomerPreferenceDao)始终由您用于@Autowired -annotated 注入点的类型声明。否则,由于在运行时找不到类型 match,注入可能会失败。

对于通过 classpath 扫描找到的 XML-defined beans 或 component classes,容器通常预先知道具体类型。但是,对于@Bean工厂方法,您需要确保声明的 return 类型具有足够的表现力。对于实现多个接口的组件或可能由其 implementation 类型引用的组件,请考虑在工厂方法中声明最具体的 return 类型(至少与引用 bean 的注入点所需的具体类型相同)。

您还可以通过将 annotation 添加到需要该类型的 array 的字段或方法来提供ApplicationContext中特定类型的所有 beans,如下面的 example 所示:

1
2
3
4
5
6
7
public class MovieRecommender {

@Autowired
private MovieCatalog[] movieCatalogs;

// ...
}

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

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private Set<MovieCatalog> movieCatalogs;

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

// ...
}

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

您可以在 target class level 和@Bean方法上声明@Order annotation,可能是通过单独的 bean 定义(如果多个定义使用相同的 bean class)。 @Order值可能会影响注入点的优先级,但请注意它们不会影响 singleton startup order,这是由依赖关系和@DependsOn声明确定的正交关注点。

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

即使是键入的Map实例也可以自动装配为 long,因为预期的 key 类型是String。 Map 值包含期望类型的所有 beans,并且键包含相应的 bean 名称,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private Map<String, MovieCatalog> movieCatalogs;

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

// ...
}

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

1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

只能将一个带注释的构造函数 per-class 标记为必需,但可以注释多个 non-required 构造函数。在这种情况下,每个都被认为是候选者之一,Spring 使用最贪婪的构造函数,其依赖性可以得到满足 - 也就是说,具有最大数量的 arguments 的构造函数。

建议@Autowired的必需属性优于@Required annotation。 required 属性表示自动装配不需要 property。如果无法自动装配,则忽略 property。另一方面,@Required更强大,因为它强制执行由容器支持的任何方式设置的 property。如果没有注入 value,则引发相应的 exception。

或者,您可以通过 Java 8 的java.util.Optional表达特定依赖关系的 non-required 性质,如下面的示例所示:

1
2
3
4
5
6
7
public class SimpleMovieLister {

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

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

1
2
3
4
5
6
7
public class SimpleMovieLister {

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

您还可以将@Autowired用于 well-known 可解析依赖项的接口:BeanFactoryApplicationContextEnvironmentResourceLoaderApplicationEventPublisherMessageSource。这些接口及其扩展接口(如ConfigurableApplicationContextResourcePatternResolver)将自动解析,无需特殊设置。以下 example 自动装配ApplicationContext object:

1
2
3
4
5
6
7
8
9
10
public class MovieRecommender {

@Autowired
private ApplicationContext context;

public MovieRecommender() {
}

// ...
}

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

微调与@Primary基于注解的自动装配

由于按类型自动装配可能会导致多个候选项,因此通常需要对选择 process 进行更多控制。实现此目的的一种方法是使用 Spring 的@Primary annotation。 @Primary表示当多个 beans 是自动连接到 single-valued 依赖项的候选者时,应该优先选择特定的 bean。如果候选者中只存在一个主 bean,则它将成为自动连接的 value。

请考虑以下 configuration 将firstMovieCatalog定义为主MovieCatalog

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MovieConfiguration {

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

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

// ...
}

使用前面的 configuration,以下MovieRecommenderfirstMovieCatalog一起自动装配:

1
2
3
4
5
6
7
public class MovieRecommender {

@Autowired
private MovieCatalog movieCatalog;

// ...
}

相应的 bean 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

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

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

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

</beans>

微调基于注解的自动装配与限定符

当可以确定一个主要候选者时,@Primary是一种有效的方式,可以通过多个实例使用类型自动装配。当您需要更多控制选择 process 时,可以使用 Spring 的@Qualifier annotation。您可以将限定符值与特定的 arguments 相关联,缩小类型匹配集,以便为每个参数选择特定的 bean。在最简单的情况下,这可以是一个简单的描述性 value,如下面的示例所示:

1
2
3
4
5
6
7
8
public class MovieRecommender {

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

// ...
}

您还可以在单个构造函数 arguments 或方法参数上指定@Qualifier annotation,如以下 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

// ...
}

以下 example 显示了相应的 bean 定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

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

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

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

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

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

</beans>
1 具有main限定符 value 的 bean 与使用相同 value 限定的构造函数参数连接。
2 具有action限定符 value 的 bean 与使用相同 value 限定的构造函数参数连接。

对于后备 match, bean name 被视为默认限定符 value。因此,您可以使用id 而不是嵌套的限定符元素定义 bean,从而得到相同的匹配结果。但是,尽管您可以使用此约定通过 name 引用特定的 beans,@Autowired基本上是关于带有可选语义限定符的 type-driven 注入。这意味着限定符值(即使使用 bean name 回退)在类型匹配集中始终具有缩小语义。它们在语义上不表达对唯一 bean id的 reference。良好的限定符值是mainEMEApersistent,表示独立于 bean id的特定 component 的特征,如果是匿名 bean 定义(例如前面的 example 中的定义),则可能是 auto-generated。

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

允许在 type-matching 候选者中针对目标 bean 名称选择限定符值_,在注入点不需要@Qualifier annotation。如果没有其他解析指示符(例如限定符或主要标记),则对于 non-unique 依赖情况,Spring 将注入点 name(即字段 name 或参数 name)与目标 bean 名称匹配,然后选择 same-named 候选人,如果有的话。

也就是说,如果你打算用 name 表达 annotation-driven 注入,不要主要使用@Autowired,即使它能够在 type-matching 候选者中通过 bean name 进行选择。相反,使用 JSR-250 @Resource annotation,它在语义上定义为通过其唯一的 name 标识特定的目标 component,声明的类型与匹配的 process 无关。 @Autowired具有相当不同的语义:在按类型选择候选 beans 之后,仅在那些 type-selected 候选内考虑指定的String限定符 value(对于 example,将account限定符与标记有相同限定符标签的 beans 匹配)。

对于本身被定义为集合,Map或 array 类型的 beans,@Resource是一个很好的解决方案,引用特定集合或 array bean 由 unique name。也就是说,从 4.3,集合开始,你可以通过 Spring 的@Autowired类型匹配算法 match Map和 array 类型,因为 long _类型信息被保存在@Bean return 类型签名或集合继承层次结构中。在这种情况下,您可以使用限定符值在 same-typed 集合中进行选择,如上一段所述。

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

@Autowired适用于字段,构造函数和 multi-argument 方法,允许通过参数 level 中的限定符注释缩小范围。相比之下,@Resource仅支持字段,bean property setter 方法只支持一个参数。因此,如果注射目标是构造函数或 multi-argument 方法,则应该使用限定符。

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

1
2
3
4
5
6
7
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

String value();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MovieRecommender {

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

private MovieCatalog comedyCatalog;

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

// ...
}

接下来,您可以提供候选 bean 定义的信息。您可以将<qualifier/>标记添加为<bean/>标记的 sub-elements,然后将typevalue指定为匹配您的自定义限定符注释。该类型与 annotation 的 fully-qualified class name 匹配。或者,为方便起见,如果不存在冲突名称的风险,可以使用 short class name。以下示例演示了两种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

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

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

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

</beans>

Classpath 扫描和托管组件中,您可以看到以 XML 格式提供限定符元数据的 annotation-based 替代方法。具体来说,请参见使用 Annotations 提供限定符元数据

在某些情况下,使用没有 value 的 annotation 可能就足够了。当 annotation 用于更通用的目的并且可以跨多种不同类型的依赖项应用时,这可能很有用。例如,您可以提供可在没有 Internet 连接时搜索的脱机目录。首先,定义简单的 annotation,如下面的 example 所示:

1
2
3
4
5
6
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {

}

然后将 annotation 添加到字段或 property 以进行自动装配,如以下 example 所示:

1
2
3
4
5
6
7
8
public class MovieRecommender {

@Autowired
@Offline (1)
private MovieCatalog offlineCatalog;

// ...
}
1 此 line 添加@Offline annotation。

现在 bean 定义只需要一个限定符type,如下面的 example 所示:

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

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

1
2
3
4
5
6
7
8
9
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

String genre();

Format format();
}

在这种情况下,Format是 enum,定义如下:

1
2
3
public enum Format {
VHS, DVD, BLURAY
}

要自动装配的字段使用自定义限定符进行批注,并包含两个属性的值:genreformat,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MovieRecommender {

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

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

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

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

// ...
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

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

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

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

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

</beans>

使用泛型作为自动装配限定符

除了@Qualifier annotation 之外,您还可以使用 Java 泛型类型作为隐式的限定形式。例如,假设您有以下 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MyConfiguration {

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

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

假设前面的 beans 实现了一个通用接口(即Store<String>Store<Integer>),你可以@Autowire Store接口,泛型用作限定符,如下面的 example 所示:

1
2
3
4
5
@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

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

通用限定符也适用于自动装配 lists,Map实例和数组。以下 example 自动装配通用List

1
2
3
4
// 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;

使用CustomAutowireConfigurer

CustomAutowireConfigurer 上是一个BeanFactoryPostProcessor,它允许您注册自己的自定义限定符注释类型,即使它们没有使用 Spring 的@Qualifier annotation 注释。以下 example 显示了如何使用CustomAutowireConfigurer

1
2
3
4
5
6
7
8
<bean id="customAutowireConfigurer"
class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
<property name="customQualifierTypes">
<set>
<value>example.CustomQualifier</value>
</set>
</property>
</bean>

AutowireCandidateResolver通过以下方式确定 autowire 候选人:

  • 每个 bean 定义的autowire-candidate value
  • <beans/>元素上可用的任何default-autowire-candidates模式
  • @Qualifier 注释的存在以及使用CustomAutowireConfigurer注册的任何自定义注释

当多个 beans 有资格作为 autowire 候选者时,“primary”的确定如下:如果候选者中只有一个 bean 定义将primary属性设置为true,则选择它。

使用Resource注解注入

Spring 还支持使用字段上的 JSR-250 @Resource annotation 或 bean property setter 方法进行注入。这是 Java EE 5 和 6 中的 common pattern(例如,在 JSF 1.2 managed beans 或 JAX-WS 2.0 endpoints 中)。 Spring 也为 Spring-managed objects 支持此 pattern。

@Resource采用 name 属性。默认情况下,Spring 将 value 解释为要注入的 bean name。换句话说,它遵循 by-name 语义,如下面的示例所示:

1
2
3
4
5
6
7
8
9
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

如果未显式指定 name,则默认 name 派生自字段 name 或 setter 方法。如果是字段,则需要字段 name。在 setter 方法的情况下,它采用 bean property name。以下 example 将 bean 命名为movieFinder注入其 setter 方法:

1
2
3
4
5
6
7
8
9
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

_an 注释提供的 name 被CommonAnnotationBeanPostProcessor解析为如果您明确配置 Spring 的SimpleJndiBeanFactory,则可以通过 JNDI 解析名称。但是,我们建议您依赖默认行为并使用 Spring 的 JNDI 查找功能来保留间接的 level。

@Resource用法的唯一情况下,没有指定明确的 name,并且类似于@Autowired@Resource找到主要类型 match 而不是特定的名为 bean,并解析众所周知的可解析依赖项:BeanFactoryApplicationContextResourceLoaderApplicationEventPublisherMessageSource接口。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MovieRecommender {

@Resource
private CustomerPreferenceDao customerPreferenceDao;

@Resource
private ApplicationContext context; (1)

public MovieRecommender() {
}

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

使用PostConstruct注解和PreDestroy注解

CommonAnnotationBeanPostProcessor不仅识别@Resource 注解,还识别 JSR-250 生命周期注解。在 Spring 2.5 中引入,对这些注解的支持提供了初始化回调破坏回调中描述的另一种替代方法。如果在 Spring ApplicationContext中注册,则在生命周期的同一点调用带有其中一个注解的方法,作为相应的 Spring 生命周期接口方法或显式声明的回调方法。在下面的示例中,缓存在初始化时为预先填充,在销毁时清除:

1
2
3
4
5
6
7
8
9
10
11
12
public class CachingMovieLister {

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

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

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

Classpath扫描和托管组件

本章中的大多数示例都使用 XML 来指定配置元数据,该元数据在 Spring 容器中生成每个BeanDefinition。上一节(Annotation-based Container Configuration演示了如何通过 source-level annotations 提供大量 configuration 元数据。但是,即使在这些示例中,“base”bean 定义也在 XML 文件中明确定义,而 annotations 仅驱动依赖项注入。本节介绍通过扫描 classpath 隐式检测候选组件的选项。候选组件是 classes,它们与过滤条件匹配,并且在容器中注册了相应的 bean 定义。这消除了使用 XML 执行 bean 注册的需要。相反,您可以使用 annotations(用于 example,@Component),AspectJ 类型表达式或您自己的自定义过滤条件来选择哪些 classes 具有向容器注册的 bean 定义。

从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多 features 都是核心 Spring Framework 的一部分。这允许您使用 Java 定义 beans 而不是使用传统的 XML files。有关如何使用这些新 features 的示例,请查看@Configuration@Bean@Import@DependsOn注解。

Component注解和进一步构造型注解

@Repository annotation 是任何 class 的标记,它满足 repository(也称为 Data Access Object 或 DAO)的角色或构造型。该标记的用途之一是 exceptions 的自动转换,如Exception翻译中所述。

Spring 提供了进一步的构造型注释:@Component@Service@Controller@Component是任何 Spring-managed component 的通用构造型。 @Repository@Service@Controller@Component的特化,用于更具体的用例(分别在持久性,服务和表示层中)。因此,您可以使用@Component注释 component classes,但是,通过使用@Repository@Service@Controller注释它们,您的 classes 更适合通过工具处理或与方面相关联。例如,这些构造型注释成为切入点的理想目标。 @Repository@Service@Controller还可以在 Spring Framework 的未来版本中携带其他语义。因此,如果您在服务层使用@Component@Service之间进行选择,@Service显然是更好的选择。同样,如前所述,已经支持@Repository作为持久层中自动 exception 转换的标记。

使用元注解和复合注解

Spring 提供的许多注释可以在您自己的 code 中用作 meta-annotations。 meta-annotation 是一个注解,可以应用于另一个 annotation。对于 example,提到的 annotation 是 meta-annotated 和@Component,如下面的 example 所示:

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component (1)
public @interface Service {

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

您还可以组合 meta-annotations 来创建“撰写注解”。例如,Spring MVC 的@RestController 注解由@Controller@ResponseBody组成。

此外,组合的 annotations 可以选择从 meta-annotations 重新声明属性以允许自定义。当您只想公开 meta-annotation 属性的子集时,这可能特别有用。对于 example,Spring 的@SessionScope annotation 将范围 name 硬编码为session,但仍允许自定义proxyMode。以下清单显示了SessionScope annotation 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {

/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

然后,您可以使用@SessionScope而不声明proxyMode,如下所示:

1
2
3
4
5
@Service
@SessionScope
public class SessionScopedService {
// ...
}

您还可以覆盖proxyMode的 value,如下面的 example 所示:

1
2
3
4
5
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}

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

自动检测Classes并注册Bean定义

Spring 可以自动检测原型 classes 并使用ApplicationContext注册相应的BeanDefinition实例。对于 example,以下两个 classes 符合此类自动检测的条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

要自动检测这些 classes 并注册相应的 beans,您需要将@ComponentScan添加到@Configuration class,其中basePackages属性是两个 classes 的 common parent 包。 (或者,您可以指定包含每个 class.)的 parent 包的逗号或分号或 space-separated 列表

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}

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

以下替代方法使用 XML:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

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

</beans>

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

扫描 classpath 包需要在 classpath 中存在相应的目录条目。使用 Ant build JAR 时,请确保不要激活 JAR 任务的 files-only 开关。此外,在某些环境中,classpath 目录可能不会基于安全 policies 公开 - 例如,JDK 1.7.0_45 及更高版本上的独立应用程序(在清单中需要’Trusted-Library’设置 - 请参阅

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

此外,使用component-scan元素时,隐式包含AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor。这意味着这两个组件是自动检测并连接在一起的 - 所有这些都没有在 XML 中提供任何 bean configuration 元数据。

您可以通过将属性与false的_val包含在一起来禁用AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor的注册。

使用过滤器自定义扫描

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

过滤器类型 Example 表达式 描述
annotation(默认) org.example.SomeAnnotation 目标组件中 level 类型的注释。
分配 org.example.SomeClass 目标组件可分配给(扩展或实现)的 class(或接口)。
AspectJ org.example..*Service+ 要由目标组件匹配的 AspectJ 类型表达式。
正则表达式 org\.example\.Default.* 要由目标组件 class 名称匹配的正则表达式。
习惯 org.example.MyTypeFilter org.springframework.core.type .TypeFilter接口的自定义 implementation。

以下 example 显示 configuration 忽略所有@Repository 注释并使用“stub”repositories:

1
2
3
4
5
6
7
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
...
}

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

1
2
3
4
5
6
7
8
<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>

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

在组件中定义Bean元数据

Spring 组件还可以将 bean 定义元数据提供给容器。您可以使用用于在@Configuration annotated classes 中定义 bean 元数据的相同@Bean annotation 来执行此操作。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class FactoryMethodComponent {

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

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

前面的 class 是 Spring component,在doWork()方法中有 application-specific code。但是,它还提供 bean 定义,其具有引用方法publicInstance()的工厂方法。 @Bean annotation 标识工厂方法和其他 bean 定义 properties,例如通过@Qualifier annotation 的限定符 value。可以指定的其他 method-level 注释是@Scope@Lazy和自定义限定符注释。

除了 component 初始化的作用外,您还可以将@Lazy annotation 放在标有@Autowired@Inject的注入点上。在此 context 中,它会导致注入 lazy-resolution 代理。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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);
}
}

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

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

1
2
3
4
5
6
7
8
@Component
public class FactoryMethodComponent {

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

常规 Spring component 中的@Bean方法的处理方式与 Spring @Configuration class 中的对应方式不同。不同之处在于,CGLIB 不会增强@Component classes 以拦截方法和字段的调用。 CGLIB 代理是调用@Configuration classes 中@Bean方法中的方法或字段创建 bean metadata references 以协作 objects 的方法。这些方法不是用普通的 Java 语义调用的,而是通过 order 中的容器来提供 Spring beans 的常规生命周期管理和代理,即使在通过程序 calls 到@Bean方法引用其他 beans 时也是如此。相反,在普通@Component class 中调用@Bean方法中的方法或字段具有标准 Java 语义,没有应用特殊的 CGLIB 处理或其他约束。

您可以将@Bean方法声明为static,允许在不创建包含它们的配置类作为实例的情况下调用它们。这在定义 post-processor beans(对于 example,类型BeanFactoryPostProcessorBeanPostProcessor)时特别有意义,因为这样的 beans 在容器生命周期的早期初始化,并且应避免在此时触发 configuration 的其他部分。

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

@Bean方法的 Java 语言可见性对 Spring 容器中生成的 bean 定义没有立即影响。您可以根据需要在非@Configuration classes 和任何地方的静态方法中自由声明工厂方法。但是,@Configuration classes 中的常规@Bean方法需要可覆盖 - 也就是说,它们不能声明为privatefinal

@Bean方法也可以在给定 component 或 configuration class 的 base classes 上发现,也可以在 component 或 configuration class 实现的接口中声明的 Java 8 默认方法上发现。这使得在编写复杂的配置安排时具有很大的灵活性,甚至可以通过 Spring 4.2 的 Java 8 默认方法实现多重继承。

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

命名自动检测的组件

当 component 作为 scan process 的一部分自动检测时,其 bean name 由该扫描程序已知的BeanNameGenerator策略生成。默认情况下,任何包含 name value的 Spring 构造型 annotation(@Component@Repository@Service@Controller)都会将 name 提供给相应的 bean 定义。

如果这样的 annotation 不包含 name value或任何其他检测到的 component(例如自定义过滤器发现的那些),则默认的 bean name generator 将返回未大写的 non-qualified class name。对于 example,如果检测到以下 component classes,则名称将为myMovieListermovieFinderImpl

1
2
3
4
5
6
7
8
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}

如果您不想依赖默认的 bean-naming 策略,则可以提供自定义 bean-naming 策略。首先,实现BeanNameGenerator接口,并确保包含默认的 no-arg 构造函数。然后,在配置扫描程序时提供完全限定的 class name,如下面的 example annotation 和 bean 定义所示:

1
2
3
4
5
6
7
8
9
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
...
}
<beans>
<context:component-scan base-package="org.example"
name-generator="org.example.MyNameGenerator" />
</beans>

作为一般规则,考虑使用 annotation 指定 name,只要其他组件可以对其进行显式 references。另一方面,只要容器负责接线,auto-generated 名称就足够了。

为自动检测组件提供范围

与 Spring-managed 组件一样,自动检测组件的默认范围和最常见范围是singleton。但是,有时您需要一个可由@Scope annotation 指定的不同范围。您可以在 annotation 中提供范围的 name,如下面的 example 所示:

1
2
3
4
5
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}

@Scope annotations 仅在具体的 bean class(对于带注释的组件)或工厂方法(对于@Bean方法)上进行了内省。与 XML bean 定义相比,没有 bean 定义继承的概念,class level 中的继承层次结构与元数据目的无关。

有关 web-specific 范围的详细信息,例如 Spring context 中的“request”或“session”,请参阅Request,Session,Application 和 WebSocket Scopes。与这些范围的 pre-built 注释一样,您也可以使用 Spring 的 meta-annotation 方法编写自己的作用域注释:对于 example,自定义注释 meta-annotated 使用@Scope("prototype"),可能还会声明自定义 scoped-proxy 模式。

要为范围解析提供自定义策略而不是依赖于 annotation-based 方法,可以实现ScopeMetadataResolver接口。一定要包含默认的 no-arg 构造函数。然后,您可以在配置扫描程序时提供完全限定的 class name,因为 annotation 和 bean 定义的以下示例显示:

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

使用某些 non-singleton 范围时,可能需要为范围的 objects 生成代理。推理在将 Beans 视为依赖关系中描述。为此,component-scan 元素上有 scoped-proxy 属性。三个可能的值是:nointerfacestargetClass。对于 example,以下 configuration 会生成标准 JDK 动态代理:

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

使用注解提供限定符元数据

@Qualifier annotation 在Fine-tuning Annotation-based 使用限定符自动装配中讨论。该部分中的示例演示了在解析自动线候选时使用@Qualifier annotation 和自定义限定符注释来提供 fine-grained 控件。因为这些示例基于 XML bean 定义,所以通过使用 XML 中bean元素的qualifiermeta child 元素,在候选 bean 定义上提供了限定符元数据。当依赖 classpath 扫描 auto-detection 组件时,可以在候选 class 上为 type-level 注释提供限定符元数据。以下三个示例演示了此技术:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}

与大多数 annotation-based 替代方案一样,请记住 annotation 元数据绑定到 class 定义本身,而使用 XML 允许多个相同类型的 beans 提供其限定符元数据的变体,因为提供的元数据是 per-instance 而不是 per-class。

生成候选组件索引

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

要生成索引,请为包含作为 component scan 指令目标的组件的每个模块添加其他依赖项。以下 example 显示了如何使用 Maven 执行此操作:

1
2
3
4
5
6
7
8
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.1.3.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>

以下 example 显示了如何使用 Gradle 执行此操作:

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

该 process 生成一个包含在 jar 文件中的META-INF/spring.components文件。

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

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

使用JSR330标准注解

从 Spring 3.0 开始,Spring 支持 JSR-330 standard annotations(依赖注入)。这些注释的扫描方式与 Spring annotations 相同。要使用它们,您需要在 classpath 中包含相关的 jars。

如果使用 Maven,则javax.inject artifact 在标准 Maven repository)中可用。您可以将以下依赖项添加到文件 pom.xml:

1
2
3
4
5
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>

使用Inject注解和Named注解进行依赖注入

而不是@Autowired,您可以使用@javax.inject.Inject,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.inject.Inject;

public class SimpleMovieLister {

private MovieFinder movieFinder;

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

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

@Autowired一样,您可以在字段,方法和 构造参数中使用@Inject。此外,您可以将注入点声明为Provider,允许 on-demand 访问较短范围的 beans 或通过Provider.get()调用延迟访问其他 beans。以下 example 提供了前面的 example 的变体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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(...);
...
}
}

如果要对应注入的依赖项使用限定的 name,则应使用@Named annotation,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.inject.Inject;
import javax.inject.Named;

public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

@Autowired一样,@Inject也可以与java.util.Optional@Nullable一起使用。这更适用于此,因为@Inject没有required属性。以下一对示例显示了如何使用@Inject@Nullable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SimpleMovieLister {

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

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

Named注解和ManagedBean注解:Component注解的标准等价物

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.inject.Inject;
import javax.inject.Named;

@Named("movieListener") // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

在没有为 component 指定 name 的情况下使用@Component非常常见。 @Named可以以类似的方式使用,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.inject.Inject;
import javax.inject.Named;

@Named
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

使用@Named@ManagedBean时,可以使用与使用 Spring annotations 时完全相同的方式使用 component 扫描,如下面的 example 所示:

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}

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

JSR-330标准注解的局限性

当您使用标准 annotations 时,您应该知道某些重要的 features 不可用,如下面的 table 所示:

弹簧 javax.inject.* javax.inject restrictions/comments
@Autowired @Inject @Inject没有’required’属性。可以与 Java 8 的Optional一起使用。
@Component @Named/@ManagedBean JSR-330 不提供可组合的 model,只是一种识别命名组件的方法。
@Scope(“singleton”) @Singleton JSR-330 默认范围就像 Spring 的prototype。但是,为了使其与 Spring 的一般默认值保持一致,Spring 容器中声明的 JSR-330 bean 默认为singleton。在 order 中使用singleton以外的范围,您应该使用 Spring 的@Scope annotation。 javax.inject还提供@Scope 注释。然而,这个仅用于创建自己的注释。
@Qualifier @Qualifier/@Named 对于 building 自定义限定符,javax.inject.Qualifier只是 meta-annotation。具体的String限定符(如 Spring 的@Qualifier带有 value)可以通过javax.inject.Named关联。
@Value - 没有等价物
@Required - 没有等价物
@Lazy - 没有等价物
ObjectFactory 提供商 javax.inject.Provider是 Spring 的ObjectFactory的直接替代,只有更短的get()方法 name。它也可以与 Spring 的@Autowired或 non-annotated 构造函数和 setter 方法结合使用。

基于Java的容器配置

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

基本概念:Bean注解和Configuration注解

Spring 新的 Java-configuration 支持中的中心 artifacts 是@Configuration -annotated classes 和@Bean -annotated 方法。

@Bean annotation 用于指示方法实例化,配置和初始化由 Spring IoC 容器管理的新 object。对于那些熟悉 Spring 的<beans/> XML configuration 的人来说,@Bean annotation 与<bean/>元素扮演的角色相同。您可以将@Bean -annotated 方法与任何 Spring @Component一起使用。但是,它们最常用于@Configuration beans。

使用@Configuration注解 class 表示其主要目的是作为 bean 定义的源。此外,@Configuration classes 允许通过调用同一 class 中的其他@Bean方法来定义 inter-bean 依赖项。最简单的@Configuration class 如下:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

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

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

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

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

当@Bean方法在 classes 中声明未使用@Configuration注解时,它们被称为以“精简”模式处理。在@Component或甚至普通的旧 class 中声明的 Bean 方法被认为是“精简”,包含 class 的主要目的不同,@Bean方法在那里是一种奖励。对于 example,服务组件可以通过每个适用的 component class 上的附加@Bean方法将 management 视图公开给容器。在这种情况下,@Bean方法是通用工厂方法机制。

与完整的@Configuration不同,lite @Bean方法不能声明 inter-bean 依赖项。相反,它们在包含 component 的内部 state 上运行,并且可选地在它们可能声明的 arguments 上运行。因此,这样的@Bean方法不应该调用其他@Bean方法。每个这样的方法实际上只是特定 bean reference 的工厂方法,没有任何特殊的运行时语义。这里的正面 side-effect 是没有 CGLIB 子类必须在运行时应用,所以在 class 设计方面没有限制(也就是说,包含 class 可能是final等等)。

在 common 场景中,@Bean方法将在@Configuration classes 中声明,确保始终使用“完整”模式,因此 cross-method references 会被重定向到容器的生命周期 management。这可以防止通过常规 Java 调用意外地调用相同的@Bean方法,这有助于减少在“精简”模式下操作时难以跟踪的细微错误。

@Bean@Configuration 注释将在以下部分中进行深入讨论。首先,我们将介绍使用 Java-based configuration 创建 spring 容器的各种方法。

使用AnnotationConfigApplicationContext实例化Spring容器

以下部分记录了 Spring 3.0 中引入的 Spring 的AnnotationConfigApplicationContext。这个多功能的ApplicationContext implementation 不仅能够接受@Configuration classes 作为输入,还能够接受普通的@Component类和用JSR-330元数据注释的类。

@Configuration classes 作为输入提供时,@Configuration class 本身被注册为 bean 定义,class 中所有声明的@Bean方法也被注册为 bean 定义。

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

简单构造

与实例化ClassPathXmlApplicationContext时 Spring XML files 用作输入的方式大致相同,在实例化AnnotationConfigApplicationContext时可以使用@Configuration classes 作为输入。这允许完全 XML-free 使用 Spring 容器,如下面的示例所示:

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

如前所述,AnnotationConfigApplicationContext不仅限于使用@Configuration classes。任何@Component或 JSR-330 带注解的 class 都可以作为输入提供给构造函数,如下面的 example 所示:

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

前面的 example 假定MyServiceImplDependency1Dependency2使用 Spring 依赖注入注释,例如@Autowired

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

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

1
2
3
4
5
6
7
8
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}

使用scan启用组件扫描

要启用 component 扫描,您可以按如下方式注释@Configuration class:

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig {
...
}
1 此 annotation 启用 component 扫描。

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

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

在前面的 example 中,扫描com.acme包以查找任何@Component -annotated classes,并且这些 classes 在容器中注册为 Spring bean 定义。 AnnotationConfigApplicationContext公开scan(String…)方法以允许相同的 component-scanning 功能,如下面的 example 所示:

1
2
3
4
5
6
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}

请记住,@Configuration classes 是meta-annotated@Component,因此它们是 component-scanning 的候选者。在前面的 example 中,假设AppConfigcom.acme包(或下面的任何包)中声明,它在调用scan()期间被拾取。在refresh()之后,它的所有@Bean方法都被处理并在容器中注册为 bean 定义。

使用AnnotationConfigWebApplicationContext支持WebApplications

AnnotationConfigWebApplicationContextWebApplicationContext变体可用于AnnotationConfigWebApplicationContext。配置SpringContextLoaderListenerservlet listener,Spring MVC DispatcherServlet等时,可以使用此implementation。以下web.xml片段配置典型的 Spring MVC web application(注意使用contextClass context-param 和 init-param):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<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>

使用Bean注解

@Bean是 method-level annotation,是 XML <bean/>元素的直接模拟。 annotation 支持<bean/>提供的一些属性,例如: init-method destroy-method 自动装配 name

您可以在@Configuration -annotated 或@Component -annotated class 中使用@Bean annotation。

声明Bean

要声明 bean,可以使用@Bean annotation 注释方法。您可以使用此方法在指定为方法的 return value 的类型的ApplicationContext中注册 bean 定义。默认情况下, bean name 与方法 name 相同。以下 example 显示了一个@Bean方法声明:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

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

前面的 configuration 与以下 Spring XML 完全等效:

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

两个声明都在ApplicationContext中创建一个名为transferService的 bean,绑定到TransferServiceImpl类型的 object 实例,如下面的文本图像所示:

1
transferService -> com.acme.TransferServiceImpl

您还可以使用接口(或 base class)return 类型声明@Bean方法,如下面的 example 所示:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

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

但是,这会将高级类型预测的可见性限制为指定的接口类型(TransferService)。然后,只有容器已知的完整类型(TransferServiceImpl)一次,受影响的 singleton bean 已被实例化。 Non-lazy singleton beans 根据其声明 order 进行实例化,因此您可能会看到不同的类型匹配结果,具体取决于另一个 component 尝试按 non-declared 类型匹配的情况(例如@Autowired TransferServiceImpl,只有在transferService bean 实例化后才会解析)。

如果您始终通过声明的服务接口引用您的类型,则@Bean return 类型可以安全地加入该设计决策。但是,对于实现多个接口的组件或可能由其 implementation 类型引用的组件,更可能更容易地声明最具体的 return 类型(至少与引用 bean 的注入点所需的特定类型一样)。

Bean依赖项

@Bean -annotated 方法可以有任意数量的参数来描述 build bean 所需的依赖关系。例如,如果我们的TransferService需要AccountRepository,我们可以使用方法参数来实现该依赖关系,如下面的 example 所示:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

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

解决机制与 constructor-based 依赖注入非常相似。有关详细信息,请参阅相关部分

接收生命周期回调

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

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

还完全支持标准的 Aware 接口集(例如实现 BeanFactoryAwareBeanNameAwareMessageSourceAware了 ApplicationContextAware等。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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();
}
}

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

对于使用 JNDI 获取的资源,您可能希望默认执行此操作,因为其生命周期在 application 之外进行管理。特别是,确保始终为DataSource执行此操作,因为已知它在 Java EE application 服务器上存在问题。

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

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

此外,使用@Bean方法,您通常使用编程 JNDI 查找,使用 Spring 的JndiTemplateJndiLocatorDelegate帮助器或直接 JNDI InitialContext用法但不使用JndiObjectFactoryBean变体(这会强制您将 return 类型声明为FactoryBean类型而不是实际目标类型,使其更难用于其他@Bean方法中的 cross-reference calls,这些方法打算在这里引用提供的资源)。

对于前面注释上方 example 的BeanOne,在构造过程中直接调用init()方法同样有效,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class AppConfig {

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

// ...
}

当您直接使用 Java 工作时,您可以使用 objects 执行任何您喜欢的操作,并且不必总是依赖于容器生命周期。

指定Bean范围

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

使用Scope注解

您可以指定使用@Bean annotation 定义的 beans 应具有特定范围。您可以使用Bean 范围部分中指定的任何标准范围。

默认范围是singleton,但您可以使用@Scope annotation 覆盖它,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@Configuration
public class MyConfiguration {

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

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

如果使用 Java 将 XML 范围内的 scoped proxy example port(请参阅范围代理传递到我们的@Bean,它类似于以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
return new UserPreferences();
}

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

自定义Bean命名

默认情况下,configuration classes 使用@Bean方法的 name 作为生成的 bean 的 name。但是,使用name属性可以覆盖此功能,如下面的 example 所示:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

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

Bean的别名

命名 Beans中所讨论的,有时需要给出一个 bean 多个名称,也称为 bean 别名。为此,@Bean annotation 的name属性接受 String array。以下 example 显示了如何为 bean 设置多个别名:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

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

Bean描述

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

要向@Bean添加描述,可以使用@Description annotation,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@Configuration
public class AppConfig {

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

使用Configuration注解

@Configuration是 class-level annotation,表示 object 是 bean 定义的来源。 @Configuration classes 通过 public @Bean annotated 方法声明 beans。 Calls 到@Configuration classes 上的@Bean方法也可用于定义 inter-bean 依赖项。有关一般介绍,请参阅基本概念: @Bean 和 @Configuration

注入Inter-bean依赖项

当 beans 彼此依赖时,表达该依赖关系就像让一个 bean 方法调用另一个一样简单,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class AppConfig {

@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}

@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}

在前面的 example 中,beanOne通过构造函数注入接收到beanTwo的 reference。

这种声明 inter-bean 依赖项的方法仅在@Configuration class 中声明@Bean方法时才有效。您不能使用普通@Component classes 声明 inter-bean 依赖项。

查找方法注入

如前所述,查找方法注入是一种您应该很少使用的高级 feature。在 singleton-scoped bean 依赖于 prototype-scoped bean 的情况下,它很有用。使用 Java 进行此类型的 configuration 提供了实现此 pattern 的自然方法。以下 example 显示了如何使用查找方法注入:

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}

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

通过使用 Java configuration,您可以创建CommandManager的子类,其中抽象createCommand()方法被覆盖,以便查找新的(原型)命令 object。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
AsyncCommand command = new AsyncCommand();
// inject dependencies here as required
return command;
}

@Bean
public CommandManager commandManager() {
// return new anonymous implementation of CommandManager with command() overridden
// to return a new prototype Command object
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
}
}

关于基于java的配置如何在内部工作的进一步信息

考虑以下 example,它显示了一个被调用两次的@Bean注释方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class AppConfig {

@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}

@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}

@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}

clientDao()clientService1()中被调用一次,在clientService2()中被调用一次。由于此方法创建ClientDaoImpl的新实例并将其返回,因此通常需要两个实例(每个服务一个)。这肯定会有问题:在 Spring 中,实例化的 beans 默认具有singleton范围。这就是魔术的用武之地:所有@Configuration class 都在 startup-time 和CGLIB进行了子类化。在子类中,child 方法首先检查容器是否有任何缓存(作用域)beans,然后 calls parent 方法并创建一个新实例。

根据 bean 的范围,行为可能会有所不同。我们在这里谈论Singleton。

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

由于 CGLIB 在 startup-time 处动态添加 features,因此存在一些限制。特别是,configuration classes 不能是 final。但是,从 4.3 开始,configuration classes 上允许使用任何构造函数,包括使用@Autowired或单个 non-default 构造函数声明进行默认注入。

如果您希望避免任何 CGLIB-imposed 限制,请考虑在非@Configuration classes 上声明@Bean方法(对于 example,而不是在@Component classes 上)。然后,@Bean方法之间的 Cross-method calls 不会被截获,因此您必须完全依赖于构造函数或方法 level 的依赖注入。

编写Java-based配置

Spring 的 Java-based configuration feature 允许您撰写 annotations,这可以降低 configuration 的复杂性。

使用Import注解

就像在 Spring XML files 中使用<import/>元素来帮助模块化配置一样,@Import annotation 允许从另一个 configuration class 中加载@Bean定义,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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();
}
}

现在,在实例化 context 时,不需要同时指定ConfigA.classConfigB.class,只需要显式提供ConfigB,如下面的 example 所示:

1
2
3
4
5
6
7
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);
}

这种方法简化了容器实例化,因为只需要处理一个 class,而不是要求你在构造期间记住可能大量的@Configuration classes。

从 Spring Framework 4.2 开始,@Import也支持 references regular component classes,类似于AnnotationConfigApplicationContext.register方法。如果要通过使用一些 configuration classes 作为明确定义所有组件的入口点来避免 component 扫描,这将非常有用。

注入导入的Bean注解定义的依赖项

前面的 example 有效,但很简单。在大多数实际场景中,beans 在 配置类之间依赖于彼此。使用 XML 时,这不是问题,因为不涉及编译器,并且您可以在容器初始化期间声明ref="someBean"并信任 Spring 来解决它。使用@Configuration classes 时,Java 编译器会在 configuration model 上设置约束,因为对其他 beans 的引用必须是有效的 Java 语法。

幸运的是,解决这个问题很简单。作为我们已经讨论过@Bean方法可以具有任意数量的参数来描述 bean 依赖项。考虑以下更多带有几个@Configuration classes 的 real-world 场景,每个场景都取决于其他人声明的 beans:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
public class ServiceConfig {

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

@Configuration
public class RepositoryConfig {

@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return new DataSource
}
}

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}

还有另一种方法可以达到相同的效果。请记住,@Configuration classes 最终只是容器中的另一个 bean:这意味着它们可以利用@Autowired@Value注入和其他 features 与任何其他 bean 相同。

确保您以这种方式注入的依赖项仅为最简单的依赖项。在 context 初始化期间很早就处理了@Configuration classes,并且强制以这种方式注入依赖项可能会导致意外的早期初始化。尽可能采用 parameter-based 注入,如前面的 example 中所示。

另外,要特别注意通过@BeanBeanPostProcessorBeanFactoryPostProcessor定义。那些应该通常被声明为static @Bean方法,而不是触发它们包含 configuration class 的实例化。否则,@Autowired@Value在 configuration class 本身上不起作用,因为它太早创建为 bean 实例。

以下 example 显示了如何将一个 bean 自动连接到另一个 bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
public class ServiceConfig {

@Autowired
private AccountRepository accountRepository;

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

@Configuration
public class RepositoryConfig {

private final DataSource dataSource;

@Autowired
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}

@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return new DataSource
}
}

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}

仅在 Spring Framework 4.3 时支持@Configuration classes 中的构造函数注入。另请注意,如果 target bean 仅定义了一个构造函数,则无需指定@Autowired。在前面的 example 中,RepositoryConfig构造函数不需要@Autowired

Fully-qualifying导入beans以方便导航

在前面的场景中,使用@Autowired可以很好地工作并提供所需的模块化,但确定声明自动装配的 bean 定义的确切位置仍然有些模棱两可。对于 example,作为一个查看ServiceConfig的开发人员,您如何确切地知道@Autowired AccountRepository bean 的声明位置?它在 code 中并不明确,这可能就好了。请记住,Spring 工具套件提供的工具可以呈现图表,显示所有内容的连线方式,这可能就是您所需要的。此外,您的 Java IDE 可以轻松找到AccountRepository类型的所有声明和用法,并快速显示方法的位置 return 该类型。

如果这种歧义是不可接受的,并且您希望从 IDE 中直接从一个@Configuration class 导航到另一个@Configuration class,请考虑自动装配 configuration classes 本身。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ServiceConfig {

@Autowired
private RepositoryConfig repositoryConfig;

@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}

在前面的情况中,定义AccountRepository是完全明确的。但是,ServiceConfig现在与RepositoryConfig紧密耦合。这是权衡。通过使用 interface-based 或 abstract class-based @Configuration classes,可以在某种程度上减轻这种紧密耦合。考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
public class ServiceConfig {

@Autowired
private RepositoryConfig repositoryConfig;

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

@Configuration
public interface RepositoryConfig {

@Bean
AccountRepository accountRepository();
}

@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {

@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}

@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return DataSource
}

}

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}

现在ServiceConfig与具体的DefaultRepositoryConfig松散耦合,并且 built-in IDE 工具仍然有用:您可以轻松获得RepositoryConfig 实现的类型层次结构。通过这种方式,导航@Configuration classes 及其依赖项与导航 interface-based code 的常用 process 没有什么不同。

如果要影响某些 beans 的启动创建 order,请考虑将其中一些声明为@Lazy(用于在首次访问时创建而不是在启动时)或@DependsOn某些其他 beans(确保在当前 bean 之前创建特定的其他 beans) ,超出后者的直接依赖性所暗示的)。

有条件地包含ConfigurationClasses注解或Bean注解方法

基于某个任意系统 state,有条件地启用或禁用完整的@Configuration class 甚至单个@Bean方法通常很有用。一个 common example 是使用@Profile annotation 只有在 Spring Environment中启用了特定的 profile 时才激活(详见Bean 定义 Profiles

@Profile annotation 实际上是通过使用名为@Conditional的更灵活的 annotation 实现的。 @Conditional annotation 指示在注册@Bean之前应查阅的特定org.springframework.context.annotation.Condition实现。

接口的实现提供了一个返回truefalsematches(…)方法。对于 example,以下列表显示了用于@Profile的实际Condition implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
// Read the @Profile annotation attributes
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
}
return true;
}

有关更多详细信息,请参阅@Conditional javadoc。

结合Java和XML配置

Spring 的@Configuration class 支持并非旨在成为 Spring XML 的 100%完全替代品。某些工具(如 Spring XML 命名空间)仍然是配置容器的理想方法。在 XML 方便或必要的情况下,您可以选择:通过使用,example,ClassPathXmlApplicationContext以“XML-centric”方式实例化容器,或者使用AnnotationConfigApplicationContext以及@ImportResource annotation 到 import 以“Java-centric”方式实例化它根据需要使用 XML。

使用以xml为中心的Configuration注解类

最好从 XML 引导 Spring 容器,并以特别的方式包含@Configuration classes。例如,在使用 Spring XML 的大型现有代码库中,更容易在需要的基础上创建@Configuration classes 并将其包含在现有的 XML files 中。在本节的后面部分,我们将介绍在这种“XML中心的”情况下使用@Configuration classes 的选项。

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

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

以下 example 显示了 Java 中的普通 configuration class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class AppConfig {

@Autowired
private DataSource dataSource;

@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}

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

以下 example 显示了 sample system-test-config.xml文件的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>

以下 example 显示了一个可能的jdbc.properties文件:

1
2
3
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
1
2
3
4
5
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}

system-test-config.xml文件中,AppConfig <bean/>不声明id元素。虽然这样做是可以接受的,但是没有必要,因为没有其他 bean 引用它,并且不太可能通过 name 从容器中明确地获取它。类似地,DataSource bean 仅由类型自动装配,因此不严格要求显式 bean id

使用来扫描@Configuration classes

因为@Configuration是带有@Component的 meta-annotated,@Configuration -annotated classes 自动成为 component 扫描的候选者。使用与上一个 example 中描述的相同的场景,我们可以重新定义system-test-config.xml以利用 component-scanning。请注意,在这种情况下,我们不需要显式声明<context:annotation-config/>,因为<context:component-scan/>启用相同的功能。

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

1
2
3
4
5
6
7
8
9
10
11
<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注解为中心用ImportResource注解使用XML

在 application 中@Configuration classes 是配置容器的主要机制,仍然可能需要使用至少一些 XML。在这些场景中,您可以使用@ImportResource并根据需要定义尽可能多的 XML。这样做可以实现配置容器的“Java-centric”方法,并将 XML 保持在最低限度。以下 example(包括 configuration class,定义 bean,properties 文件和main class 的 XML 文件)显示了如何使用@ImportResource annotation 来实现根据需要使用 XML 的“Java-centric”configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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);
}
}
1
2
3
4
properties-config.xml
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
1
2
3
4
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
1
2
3
4
5
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}

环境抽象

环境接口是集成在容器中的抽象,它为 application 环境的两个 key 方面建模:profilesproperties

profile 是 bean 定义的命名逻辑 group,仅当给定的 profile 为 active 时才向容器注册。 Beans 可以分配给 profile,无论是用 XML 定义还是用 annotations 定义。与 profiles 相关的Environment object 的作用是确定哪些 profiles(如果有)当前是 active,以及哪些 profiles(如果有)默认情况下应该 active。

Properties 在几乎所有 applications 中都发挥着重要作用,可能来自各种来源:properties files,JVM 系统 properties,系统环境变量,JNDI,servlet context 参数,ad-hoc Properties objects,Mapobjects 等等。与 properties 相关的Environment object 的作用是为用户提供方便的服务接口,用于配置 property 源和从中解析 properties。

Bean定义Profiles

Bean 定义 profiles 在核心容器中提供了一种机制,允许在不同的环境中注册不同的 beans。 “环境”这个词对不同的用户来说意味着不同的东西,这个 feature 可以帮助许多用例,包括:

  • 在 QA 或 production 中,在开发中使用 in-memory 数据源而不是从 JNDI 查找相同的数据源。
  • 仅在将 application 部署到 performance 环境时注册监视基础结构。
  • 为客户 A 和客户 B 部署注册 beans 的自定义 implementations。

考虑一个需要DataSource的实际 application 中的第一个用例。在测试环境中,configuration 可能类似于以下内容:

1
2
3
4
5
6
7
8
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}

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

1
2
3
4
5
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是如何根据当前环境在使用这两种变体之间切换。在 time 时,Spring 用户已经设计了许多方法来完成这项工作,通常依赖于系统环境变量和包含${placeholder}标记的 XML <import/> statements 的组合,这些标记根据环境变量的 value 解析为正确的 configuration 文件路径。 Bean 定义 profiles 是一个核心容器 feature,它提供了解决此问题的方法。

如果我们概括前面 environment-specific bean 定义的 example 中显示的用例,我们最终需要在某些上下文中注册某些 bean 定义,但在其他上下文中则不需要。你可以说你想在情况 A 中注册某个 profile 的 bean 定义,在情况 B 中注册一个不同的 profile。我们首先更新我们的 configuration 以反映这种需要。

使用Profile注解

@Profile annotation 允许您指示当一个或多个指定的 profiles 为 active 时,component 符合注册条件。使用我们之前的 example,我们可以 rewrite dataSource configuration,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@Profile("development")
public class StandaloneDataConfig {

@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
@Configuration
@Profile("production")
public class JndiDataConfig {

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

如前所述,使用@Bean方法,您通常选择使用编程 JNDI 查找,使用 Spring 的JndiTemplate/JndiLocatorDelegate帮助器或前面显示的直接 JNDI InitialContext用法但不使用JndiObjectFactoryBean变体,这会强制您将 return 类型声明为FactoryBean型。

profile string 可能包含一个简单的 profile name(用于 example,production)或一个 profile 表达式。 profile 表达式允许表达更复杂的 profile 逻辑(对于 example,production & us-east)。 profile 表达式支持以下 operators:

  • !:profile 的逻辑“not”
  • &:profiles 的逻辑“和”
  • |:profiles 的逻辑“或”

如果不使用括号,则无法混合&| 运算符。对于 example,production & us-east | eu-central不是有效的表达式。它必须表示为production & (us-east | eu-central)

您可以使用@Profile作为meta-annotation来创建自定义组合注释。以下 example 定义了一个自定义@Production 注释,您可以将其用作@Profile("production")的 drop-in 替换:

1
2
3
4
5
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

如果@Configuration class 标记为@Profile,则除非一个或多个指定的 profiles 为 active,否则将绕过与该 class 关联的所有@Bean方法和@Import 注释。如果@Component@Configuration class 标有@Profile({"p1", "p2"}),则除非已激活 profiles’p1’或’p2’,否则不会注册或处理该 class。如果给定的 profile 以 NOT operator(!)作为前缀,则仅当 profile 不是 active 时才会注册带注释的元素。例如,给定@Profile({"p1", "!p2"}),如果 profile’p1’为 active 或 profile’p2’不是 active,则会发生注册。

@Profile也可以在 level 方法中声明,只包含 configuration class 的一个特定 bean(对于 example,对于特定 bean 的替代变体),如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 profile 中可用。
2 jndiDataSource方法仅在production profile 中可用。

使用@Profile on @Bean方法时,可能会应用特殊方案:对于相同 Java 方法 name 的重载@Bean方法(类似于构造函数重载),需要在所有重载方法上一致地声明@Profile条件。如果条件不一致,则只有重载方法中第一个声明的条件才重要。因此,@Profile不能用于选择具有特定参数签名的重载方法而不是另一个。在创建 time 时,Spring 的构造函数解析算法遵循同一 bean 的所有工厂方法之间的分辨率。

如果要使用不同的 profile 条件定义备用 beans,请使用@Bean name 属性指向相同 bean name 的不同 Java 方法名称,如前面的 example 所示。如果参数签名都是相同的(对于 example,所有变体都有 no-arg 工厂方法),这是首先在有效的 Java class 中表示这种排列的唯一方法(因为只有一种方法可以一个特定的 name 和参数签名)。

XMLBean定义Profiles

XML 对应物是<beans>元素的profile属性。我们之前的 sample configuration 可以在两个 XML files 中重写,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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/>元素,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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 files 中引起混乱。

XML 副本不支持前面描述的 profile 表达式。但是,可以通过使用! operator 来否定 profile。也可以通过嵌套 profiles 来应用逻辑“和”,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">

<!-- other bean definitions -->

<beans profile="production">
<beans profile="us-east">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
</beans>

在前面的 example 中,如果productionus-east profiles 都是 active,则会公开dataSource bean。

激活Profile

现在我们已经更新了 configuration,我们仍然需要指示 Spring profile 是 active。如果我们现在开始使用 sample application,我们会看到抛出NoSuchBeanDefinitionException,因为容器找不到名为dataSource的 Spring bean。

激活 profile 可以通过多种方式完成,但最直接的方法是以编程方式对Environment API 进行编程,这可以通过ApplicationContext获得。以下 example 显示了如何执行此操作:

1
2
3
4
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

此外,您还可以通过spring.profiles.active property 声明性地激活 profile,这可以通过系统环境变量,web.xml中的 JVM 系统 properties,servlet context 参数指定,甚至可以作为 JNDI 中的条目指定(请参阅PropertySource 抽象。在 integration 测试中,可以使用spring-test模块中的@ActiveProfiles annotation 声明 active profiles(请参阅Context configuration with environment profiles)。

请注意,profiles 不是“either-or”命题。您可以一次激活多个 profiles。在编程方面,您可以为setActiveProfiles()方法提供多个 profile 名称,该方法接受String… varargs。以下 example 激活多个 profiles:

1
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

声明性地,spring.profiles.active可以接受 comma-separated 列表的 profile 名称,如下面的 example 所示:

1
-Dspring.profiles.active="profile1,profile2"

默认Profile

默认的 profile 表示默认启用的 profile。考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
12
@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();
}
}

如果没有 profile 是 active,则创建dataSource。您可以将此视为一种为一个或多个 beans 提供默认定义的方法。如果启用了任何 profile,则默认的 profile 不适用。

您可以使用Environment上的setDefaultProfiles()或使用spring.profiles.default property 声明性地更改默认 profile 的 name。

PropertySource抽象

Spring 的Environment抽象提供了 property 源的可配置层次结构上的搜索操作。请考虑以下列表:

1
2
3
4
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);

在前面的代码片段中,我们看到 high-level 方式询问 Spring 是否为当前环境定义了my-property property。要回答这个问题,Environment object 会对一组PropertySource objects 执行搜索。 PropertySource是对 key-value 对的任何源的简单抽象,Spring 的StandardEnvironment配置有两个 PropertySource objects - 一个表示 JVM 系统 properties(System.getProperties())的集合,另一个表示系统环境变量集(System.getenv())。

这些默认的 property 源存在于StandardEnvironment中,用于独立的 applications。 StandardServletEnvironment填充了其他默认的 property 源,包括 servlet config 和 servlet context 参数。它可以选择启用JndiPropertySource。有关详细信息,请参阅 javadoc。

具体来说,当您使用StandardEnvironment时,如果在运行时存在my-property system property 或my-propertyi环境变量,则对env.containsProperty("my-property")的调用将返回 true。

执行的搜索是分层的。默认情况下,system properties 优先于环境变量。因此,如果在调用env.getProperty("my-property")期间恰好在两个位置都设置了my-property property,则系统 property value 将“获胜”并返回。请注意,property 值未合并,而是由前面的条目完全覆盖。

对于 common StandardServletEnvironment,完整层次结构如下,顶部有 highest-precedence 个条目:

  • ServletConfig 参数(如果适用 - 对于 example,如果是DispatcherServlet context)
  • ServletContext 参数(web.xml context-param 条目)
  • JNDI 环境变量(java:comp/env/条目)
  • JVM 系统 properties(-D command-line arguments)
  • JVM 系统环境(操作系统环境变量)

最重要的是,整个机制是可配置的。也许您有一个自定义的 properties 源要集成到此搜索中。为此,请实现并实例化您自己的PropertySource并将其添加到当前EnvironmentPropertySources集合中。以下 example 显示了如何执行此操作:

1
2
3
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在前面的 code 中,MyPropertySource在搜索中添加了最高优先级。如果它包含my-property property,则检测并返回 property,以支持任何其他PropertySource中的任何my-property property。 MutablePropertySources API 公开了许多方法,允许精确操作 property 源集。

使用PropertySource注解

@PropertySource 注解提供了一种方便的声明式机制,用于将PropertySource添加到 Spring 的Environment

给定一个名为app.properties的文件,其中包含 key-value 对testbean.name=myTestBean,以下@Configuration 类使用@PropertySource,以便调用testBean.getName()返回myTestBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

@Autowired
Environment env;

@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}

@PropertySource资源位置中存在的任何${…}占位符将根据已针对环境注册的 property 源集合进行解析,如以下 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

@Autowired
Environment env;

@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}

假设my.placeholder存在于已注册的 property 源之一中(对于 example,系统 properties 或环境变量),占位符将解析为相应的 value。如果不是,则default/path用作默认值。如果未指定缺省值且无法解析 property,则抛出IllegalArgumentException

根据 Java 8 惯例,@PropertySource 注解是可重复的。但是,所有这些@PropertySource 注解都需要在同一 level 中声明,可以直接在 configuration class 上声明,也可以在同一个自定义注解中声明为 meta-annotations。不建议混合使用直接注解和 meta-annotations,因为直接注解有效地覆盖 meta-annotations。

Statements中的占位符解析

从历史上看,元素中占位符的 value 只能针对 JVM 系统 properties 或环境变量进行解析。这已不再是这种情况。因为Environment抽象集成在整个容器中,所以很容易通过它来解析占位符。这意味着您可以以任何您喜欢的方式配置分辨率 process。您可以更改搜索系统 properties 和环境变量的优先级,或者完全删除它们。您也可以根据需要将自己的 property 源添加到组合中。

具体而言,无论customer property 的定义位置如何,以下语句都可以正常工作,只要它在环境中可用就可以:

1
2
3
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>

注册LoadTimeWeaver

LoadTimeWeaver用于在 classes 加载到 Java 虚拟机(JVM)时动态转换 class。

要启用 load-time 编织,您可以将@EnableLoadTimeWeaving添加到一个@Configuration classes 中,如下面的 example 所示:

1
2
3
4
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

或者,对于 XML configuration,您可以使用context:load-time-weaver元素:

1
2
3
<beans>
<context:load-time-weaver/>
</beans>

一旦为ApplicationContext配置,该ApplicationContext中的任何 bean 都可以实现LoadTimeWeaverAware,从而接收 load-time weaver 实例的 reference。这与Spring 的 JPA 支持结合使用特别有用,其中 load-time 编织可能是 JPA class 转换所必需的。有关更多详细信息,请参阅LocalContainerEntityManagerFactoryBean javadoc。有关 AspectJ load-time 编织的更多信息,请参阅Load-time 在 Spring Framework 中使用 AspectJ 进行编织

ApplicationContext的附加功能

正如章节介绍中所讨论的,org.springframework.beans.factory包提供了管理和操作 beans 的基本功能,包括以编程方式。除了扩展其他接口以提供更多 application framework-oriented 样式的附加功能外,org.springframework.context包还添加了扩展BeanFactory接口的ApplicationContext接口。许多人以完全声明的方式使用ApplicationContext,甚至不以编程方式创建它,而是依靠支持 class(如ContextLoader)来自动实例化ApplicationContext作为 Java EE web application 的正常启动 process 的一部分。

要以更多 framework-oriented 样式增强BeanFactory功能,context 包还提供以下功能:

  • 通过MessageSource接口访问 i18n-style 中的消息。
  • 通过ResourceLoader接口访问 URL 和 files 等资源。
  • Event 发布,即通过使用ApplicationEventPublisher接口实现ApplicationListener接口的 beans。
  • Loading 多个(分层)上下文,让每个上下文通过HierarchicalBeanFactory接口聚焦在一个特定层上,例如 application 的 web 层。

使用MessageSource进行国际化

ApplicationContext接口扩展了一个名为MessageSource的接口,因此提供了国际化(“i18n”)功能。 Spring 还提供HierarchicalMessageSource接口,可以分层次地解析消息。这些接口共同提供了 Spring 效果消息解析的基础。这些接口上定义的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc):用于从MessageSource检索消息的基本方法。如果未找到指定 locale 的消息,则使用默认消息。传入的任何 arguments 都使用标准 library 提供的MessageFormat功能成为替换值。
  • String getMessage(String code, Object[] args, Locale loc):与前一个方法基本相同,但有一点不同:无法指定默认消息。如果找不到该消息,则抛出NoSuchMessageException
  • String getMessage(MessageSourceResolvable resolvable, Locale locale):前面方法中使用的所有 properties 也包装在一个名为MessageSourceResolvable的 class 中,您可以使用此方法。

加载ApplicationContext时,它会自动搜索 context 中定义的MessageSource bean。 bean 必须具有 name messageSource。如果找到这样的 bean,则将前面方法的所有 calls 委托给消息源。如果没有找到消息源,则ApplicationContext会尝试查找包含 bean 且具有相同 name 的 parent。如果是,则使用 bean 作为MessageSource。如果ApplicationContext找不到任何消息源,则在 order 中实例化一个空DelegatingMessageSource,以便能够接受 calls 到上面定义的方法。

Spring 提供两个MessageSource implementations,ResourceBundleMessageSourceStaticMessageSource。两者都在 order 中实现HierarchicalMessageSource来执行嵌套消息传递。 StaticMessageSource很少使用,但提供了以编程方式向源添加消息。以下 example 显示ResourceBundleMessageSource

1
2
3
4
5
6
7
8
9
10
11
12
<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>

example 假定您在 classpath 中定义了三个名为formatexceptionswindows的资源包。任何解决消息的请求都以 JDK-standard 方式处理,通过ResourceBundle objects 解析消息。出于 example 的目的,假设上述两个资源包 files 的内容如下:

1
2
3
4
# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

下一个 example 显示了一个执行MessageSource功能的程序。请记住,所有ApplicationContext实现都是MessageSource 实现,因此可以强制转换为MessageSource接口。

1
2
3
4
5
public static void main(String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("message", null, "Default", null);
System.out.println(message);
}

上述程序产生的结果如下:

1
Alligators rock!

总而言之,MessageSource在名为beans.xml的文件中定义,该文件存在于 classpath 的根目录中。 messageSource bean 定义通过basenames property 引用了许多资源包。在列表中传递给basenames property 的三个 files 在 classpath 的根目录下作为 files 存在,分别称为format.propertiesexceptions.propertieswindows.properties

下一个 example 显示传递给消息查找的 arguments。这些 arguments 转换为String objects 并插入到查找消息中的占位符中。

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Example {

private MessageSource messages;

public void setMessages(MessageSource messages) {
this.messages = messages;
}

public void execute() {
String message = this.messages.getMessage("argument.required",
new Object [] {"userDao"}, "Required", null);
System.out.println(message);
}
}

调用execute()方法得到的结果如下:

1
The userDao argument is required.

关于国际化(“i18n”),Spring 的各种MessageSource __mplement 遵循与标准 JDK ResourceBundle相同的 locale 解决方案和回退规则。简而言之,继续前面定义的 example messageSource,如果要解析针对 British(en-GB)locale 的消息,您将分别创建名为format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties的 files。

通常,locale 解析由 application 的周围环境管理。在以下 example 中,手动指定解析(英国)消息的 locale:

1
2
3
4
5
6
7
8
# 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);
}

上述程序 running 的结果输出如下:

1
Ebagum lad, the 'userDao' argument is required, I say, required.

您还可以使用MessageSourceAware接口获取已定义的任何MessageSource的 reference。实现MessageSourceAware接口的ApplicationContext中定义的任何 bean 在创建和配置 bean 时都会注入 application context 的MessageSource

作为ResourceBundleMessageSource的替代,Spring 提供ReloadableResourceBundleMessageSource class。此变体支持相同的包文件格式,但比基于标准 JDK 的ResourceBundleMessageSource implementation 更灵活。特别是,它允许从任何 Spring 资源位置(不仅来自 classpath)读取 files,并支持 bundle property files 的热重新加载(同时有效地在它们之间缓存它们)。有关详细信息,请参阅ReloadableResourceBundleMessageSource javadoc。

标准和自定义事件

通过ApplicationEvent class 和ApplicationListener接口提供ApplicationContext中的 Event 处理。如果实现ApplicationListener接口的 bean 被部署到 context 中,那么每 time被发布到ApplicationContext,bean 被通知。从本质上讲,这是标准的 Observer 设计 pattern。

从 Spring 4.2 开始,event 基础结构得到了显着改进,并且提供了annotation-based model以及发布任意 event 的能力(即,不一定从ApplicationEvent延伸的 object)。当这样的 object 发布时,我们将它包装在 event 中。

以下 table 描述了 Spring 提供的标准 events:

事件 说明
ContextRefreshedEvent 初始化或刷新ApplicationContext时发布(对于 example,通过ConfigurableApplicationContext接口使用refresh()方法)。这里,“初始化”意味着所有 beans 都被加载,post-processor beans 被检测并激活,单例是 pre-instantiated,ApplicationContextobject 可以使用了。如果 context 尚未关闭_long,则可以多次触发刷新,前提是所选的ApplicationContext实际上支持这种“热”刷新。对于 example,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。
ContextStartedEvent 通过在ConfigurableApplicationContext接口上使用start()方法启动ApplicationContext时发布。这里,“已启动”意味着所有Lifecycle beans 都会收到明确的启动信号。通常,此信号用于在显式停止后重新启动 beans,但它也可用于启动尚未为自动启动配置的组件(对于 example,尚未在初始化时启动的组件)。
ContextStoppedEvent 通过在ConfigurableApplicationContext接口上使用stop()方法停止ApplicationContext时发布。这里,“停止”意味着所有Lifecycle beans 都会收到明确的停止信号。可以通过start()调用重新启动已停止的 context。
ContextClosedEvent 通过在ConfigurableApplicationContext接口上使用close()方法关闭ApplicationContext时发布。在这里,“关闭”意味着所有 singleton beans 都被销毁。封闭的 context 到达其生命的终点。它无法刷新或重新启动。
RequestHandledEvent web-specific event 告诉所有 beans 已经为 HTTP 请求提供服务。请求完成后发布此 event。此 event 仅适用于使用 Spring 的DispatcherServlet的 web applications。

您还可以创建和发布自己的自定义 events。以下 example 显示了一个简单的 class,它扩展了 Spring 的ApplicationEvent base class:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BlackListEvent extends ApplicationEvent {

private final String address;
private final String content;

public BlackListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}

// accessor and other methods...
}

要发布自定义ApplicationEvent,请在ApplicationEventPublisher上调用publishEvent()方法。通常,这是通过 creating class 实现ApplicationEventPublisherAware并将其注册为 Spring bean 来完成的。以下 example 显示了这样一个 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class EmailService implements ApplicationEventPublisherAware {

private List<String> blackList;
private ApplicationEventPublisher publisher;

public void setBlackList(List<String> blackList) {
this.blackList = blackList;
}

public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}

public void sendEmail(String address, String content) {
if (blackList.contains(address)) {
publisher.publishEvent(new BlackListEvent(this, address, content));
return;
}
// send email...
}
}

在 configuration time,Spring 容器检测EmailService实现ApplicationEventPublisherAware并自动 calls setApplicationEventPublisher()。实际上,传入的参数是 Spring 容器本身。您正在通过ApplicationEventPublisher接口与 application context 进行交互。

要接收自定义ApplicationEvent,您可以创建一个实现ApplicationListener的 class 并将其注册为 Spring bean。以下 example 显示了这样一个 class:

1
2
3
4
5
6
7
8
9
10
11
12
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

private String notificationAddress;

public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}

public void onApplicationEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}

请注意,ApplicationListener通常使用自定义 event 的类型进行参数化(前面的 example 中为BlackListEvent)。这意味着onApplicationEvent()方法可以保持 type-safe,从而避免任何向下转换的需要。您可以根据需要注册尽可能多的 event listeners,但请注意,默认情况下,event listeners 会同步接收 events。这意味着publishEvent()方法会阻塞,直到所有 listener 都已完成 event 的处理。这种同步和 single-threaded 方法的一个优点是,当 listener 接收到 event 时,如果 transaction context 可用,它将在发布者的 transaction context 内运行。如果需要另一个 event 发布策略,请参阅 Spring 的ApplicationEventMulticaster接口的 javadoc。

以下 example 显示了用于注册和配置上述每个 classes 的 bean 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="emailService" class="example.EmailService">
<property name="blackList">
<list>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
</list>
</property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
<property name="notificationAddress" value="[emailprotected]"/>
</bean>

总而言之,当调用emailService bean 的sendEmail()方法时,如果有任何应列入黑名单的电子邮件消息,则会发布类型为BlackListEvent的自定义 event。 blackListNotifier bean 注册为ApplicationListener并接收BlackListEvent,此时它可以通知相关方。

Spring 的事件机制是为在同一 application context 中 Spring beans 之间的简单通信而设计的。但是,对于更复杂的企业集成需求,单独维护的Spring Integration项目为构建 well-known Spring 编程模型的 building 轻量级pattern-oriented,event-driven 架构提供了完全支持。

基于注解的事件Listeners

从 Spring 4.2 开始,您可以使用EventListener annotation 在托管 bean 的任何公共方法上注册 event listener。 BlackListNotifier可以重写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BlackListNotifier {

private String notificationAddress;

public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}

@EventListener
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}

方法签名再次声明它侦听的 event 类型,但是,这个 time,具有灵活的 name 并且没有实现特定的 listener 接口。 event 类型也可以通过泛型缩小为 long,因为实际 event 类型在其 implementation 层次结构中解析了泛型参数。

如果您的方法应该监听多个 events,或者如果您想要根据任何参数来定义它,那么也可以在 annotation 本身上指定 event 类型。以下 example 显示了如何执行此操作:

1
2
3
4
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
...
}

还可以通过使用定义SpEL 表达的 annotation 的condition属性来添加额外的运行时过滤,应该 match 以实际调用特定 event 的方法。

以下 example 显示了只有 event 的content属性等于my-event时才能重写我们的通知程序才能被调用:

1
2
3
4
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlackListEvent(BlackListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}

每个SpEL表达式都针对专用的 context 进行求值。以下 table _llist _ltext 可用于 context 的项目,以便您可以将它们用于条件 event 处理:

名称 地点 描述
事件 root object 实际ApplicationEvent #root.event
Arguments array root object arguments(as array)用于调用目标。 #root.args[0]
参数 name evaluation context 任何方法 arguments 的 name。如果由于某种原因,名称不可用(对于 example,因为没有调试信息),参数名称也可在#a<#arg>下使用,其中#arg代表参数索引(从 0 开始)。 #blEvent#a0(您也可以使用#p0#p<#arg>表示法作为别名)

请注意,即使您的方法签名实际引用了已发布的任意 object,#root.event也允许您访问底层 event。

如果您需要发布 event 作为处理另一个 event 的结果,您可以将方法签名更改为 return 应发布的 event,如下面的 example 所示:

1
2
3
4
5
@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}

异步 listeners不支持此 feature。

这个新方法为上面的方法处理的每个BlackListEvent发布一个新的ListUpdateEvent。如果需要发布多个 events,则可以_return Collection events。

异步Listeners

如果希望特定的 listener 异步 process events,则可以重用定期 @Async 支持。以下 example 显示了如何执行此操作:

1
2
3
4
5
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}

使用异步 events 时请注意以下限制:

  • 如果 event listener 抛出Exception,则它不会传播给调用者有关详细信息,请参阅AsyncUncaughtExceptionHandler
  • 这样的 event listener 无法发送回复。如果您需要作为处理结果发送另一个 event,inject ApplicationEventPublisher手动发送 event。

监听器的顺序

如果需要在另一个 listener 之前调用一个 listener,可以将@Order annotation 添加到方法声明中,如下面的 example 所示:

1
2
3
4
5
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}

通用事件

您还可以使用泛型来进一步定义 event 的结构。考虑使用EntityCreatedEvent<T>,其中T是创建的实际实体的类型。对于 example,您可以创建以下 listener 定义以仅接收EntityCreatedEventEntityCreatedEvent

1
2
3
4
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
...
}

由于类型擦除,仅当被触发的 event 解析 event listener 过滤器的通用参数(即class PersonCreatedEvent extends EntityCreatedEvent<Person> { … }之类的东西)时,此方法才有效。

在某些情况下,如果所有 events 遵循相同的结构(如前面的 example 中的 event 的情况),这可能会变得相当繁琐。在这种情况下,您可以实现ResolvableTypeProvider来指导 framework 超出运行时环境提供的范围。以下 event 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
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()));
}
}

这不仅适用于ApplicationEvent,而且适用于您作为 event 发送的任意 object。

方便地访问底层资源

为了最佳地使用和理解application上下文,你应该熟悉 Spring 的Resource抽象,如资源中所述。

application上下文是ResourceLoader,可用于加载Resource对象。 资源本质上是JDK java.net.URL类的功能丰富版本。事实上,资源的实现在适当的时候包装了一个java.net.URL的实例。 Resource可以透明的方式从几乎任何位置获取底层资源,包括 classpath,文件系统位置,任何可用标准 URL 描述的位置,以及其他一些变体。如果资源位置 string 是一个没有任何特殊前缀的简单路径,那么这些资源来自特定且适合于实际的 application context 类型。

您可以配置部署到 application context 中的 bean 来实现特殊的回调接口ResourceLoaderAware,在初始化 time 时自动回调,application context 本身作为ResourceLoader传入。您还可以公开Resource类型的 properties,以用于访问静态资源。它们像任何其他 properties 一样被注入其中。您可以将Resource properties 指定为简单的String路径并依赖特殊的 JavaBean PropertyEditor(由 context 自动注册),以便在部署 bean 时将这些文本 strings 转换为实际的Resource object。

提供给ApplicationContext构造函数的位置路径或_path 实际上是资源 strings,并且以简单的形式根据特定的 context implementation 进行适当处理。对于 example ClassPathXmlApplicationContext,将一个简单的位置路径视为 classpath 位置。您还可以使用带有特殊前缀的 location paths(resource strings)来强制从 classpath 或 URL 中加载定义,而不管实际的 context 类型如何。

方便的ApplicationContext实例化Web应用程序

您可以通过使用ContextLoader来声明式地创建ApplicationContext实例。当然,您也可以通过使用一个ApplicationContext实现以编程方式创建ApplicationContext实例。

您可以使用ContextLoaderListener注册ApplicationContext,如下面的 example 所示:

1
2
3
4
5
6
7
8
<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,并将值用作搜索 application contexts 的位置。也支持 Ant-style 路径模式。示例是/WEB-INF/*Context.xml(对于名称以Context.xml结尾且位于WEB-INF目录中的所有 files)和/WEB-INF/**/*Context.xml(对于WEB-INF的任何子目录中的所有此类 files)。

将SpringApplicationContext部署为JavaEERAR文件

可以将 Spring ApplicationContext部署为 RAR 文件,将 context 及其所有必需的 bean classes 和 library JAR 封装在 Java EE RAR 部署单元中。这相当于能够访问 Java EE 服务器工具的 stand-alone ApplicationContext(仅托管在 Java EE 环境中)。 RAR 部署是部署无头 WAR 文件的一种更自然的替代方案 - 实际上是一个没有任何 HTTP 入口点的 WAR 文件,仅用于在 Java EE 环境中引导 Spring ApplicationContext

RAR 部署非常适用于不需要 HTTP 入口点但仅包含消息 endpoints 和预定作业的 application 上下文。 Beans 在这样的 context 中可以使用 application 服务器资源,例如 JTA 事物管理 和 JNDI-bound JDBC DataSource实例以及 JMS ConnectionFactory实例,并且还可以通过 Spring 的标准事物管理以及 JNDI 和 JMX 支持工具向平台的 JMX 服务器注册。 Application 组件也可以通过 Spring 的TaskExecutor抽象与 application 服务器的 JCA WorkManager进行交互。

有关 RAR 部署中涉及的 configuration 详细信息,请参阅SpringContextResourceAdapter class 的 javadoc。

对于将 Spring ApplicationContext 简单部署为 Java EE RAR 文件:

  • 将所有 application classes 打包到一个 RAR 文件(这是一个具有不同文件扩展名的标准 JAR 文件)。添加所有 Library JAR 都需要进入 RAR 档案的根目录。 添加 一个META-INF/ra.xml部署描述符(如SpringContextResourceAdapter 的 javadoc所示)和相应的 Spring XML bean 定义 file(s)(通常为 META-INF/applicationContext.xml)。
  • 将生成的 RAR 文件放入 application Server 的部署目录中。

此类 RAR 部署单元通常为 self-contained。它们不会将组件暴露给外部世界,甚至不会暴露给同一个应用程序的其他模块。与 RAR-based ApplicationContext的交互通常通过与其他模块共享的 JMS 目标进行。例如,RAR-based ApplicationContext也可以安排一些作业或对文件系统中的新文件作出反应(或类似)。如果它需要允许来自外部的同步访问,它可以(用于 example)export RMI endpoints,它可以被同一台机器上的其他 application 模块使用。

BeanFactory

BeanFactory API 为 Spring 的 IoC 功能提供了基础。它的特定契约主要用于与 Spring 和相关 third-party 框架的其他部分进行整合,其DefaultListableBeanFactory实现是 higher-level GenericApplicationContext容器中的 key 委托。

BeanFactory和相关接口(例如BeanFactoryAwareInitializingBeanDisposableBean)是其他 framework 组件的重要集成点。通过不需要任何注解或甚至反射,它们允许容器与其组件之间的非常有效的交互。 Application-level beans 可以使用相同的回调接口,但通常更喜欢声明性依赖注入,通过注解或通过编程配置。

请注意,核心BeanFactory API level 及其DefaultListableBeanFactory实现不会对配置格式或要使用的任何 component annotations 进行假设。所有这些风格都通过 extensions(例如XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor)进入,并作为核心元数据表示在共享BeanDefinition objects 上运行。这就是 Spring 容器如此灵活和可扩展的本质。

BeanFactory或ApplicationContext?

本节介绍BeanFactoryApplicationContext容器级别之间的差异以及对引导的影响。

您应该使用ApplicationContext,除非您有充分的理由不这样做,GenericApplicationContext及其子类AnnotationConfigApplicationContext作为自定义引导的通用实现。这些是 Spring 核心容器的主要入口点,适用于所有 common 目的:l加载配置文件,触发 classpath 扫描,以编程方式注册 bean 定义和带注解的 classes,以及(截至 5.0)注册功能 bean 定义。

因为ApplicationContext包含BeanFactory的所有功能,所以一般建议它优于普通的BeanFactory,除了需要完全控制 bean 处理的场景。在ApplicationContext(例如GenericApplicationContext implementation)中,按惯例检测到几种 beans(即 bean name 或 bean 类型 - 特别是 post-processors),而普通DefaultListableBeanFactory与任何特殊 beans 无关。

对于许多扩展容器特性,例如 annotation 处理和 AOP 代理,BeanPostProcessor 扩展点是必不可少的。如果仅使用普通DefaultListableBeanFactory,则默认情况下不会检测到并激活此类 post-processors。这种情况可能会令人困惑,因为 bean configuration 没有任何问题。相反,在这种情况下,容器需要通过额外的设置完全自举。

以下 table lists features 由BeanFactoryApplicationContext接口和 implementations 提供。

特征 BeanFactory ApplicationContext
Bean instantiation/wiring
集成生命周期管理 没有
自动BeanPostProcessor注册 没有
自动BeanFactoryPostProcessor注册 没有
方便的MessageSource访问(用于内化) 没有
Built-in ApplicationEvent出版机制 没有

要使用DefaultListableBeanFactory显式注册 bean post-processor,您需要以编程方式调用addBeanPostProcessor,如下面的 example 所示:

1
2
3
4
5
6
7
8
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions

// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

要将BeanFactoryPostProcessor应用于普通DefaultListableBeanFactory,您需要调用其postProcessBeanFactory方法,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

在这两种情况下,显式注册步骤都不方便,这就是为什么各种ApplicationContext变体比 Spring-backed applications 中的普通DefaultListableBeanFactory更受欢迎,尤其是在典型企业设置中依赖BeanFactoryPostProcessorBeanPostProcessor实例来扩展容器功能时。

AnnotationConfigApplicationContext已经注册了所有 common annotation post-processors,并且可以通过 configuration annotations(例如@EnableTransactionManagement)引入其他处理器。在 Spring 的 annotation-based configuration model 的抽象 level 中, bean post-processors 的概念变成了仅仅是内部容器细节。

资源

本章介绍 Spring 如何处理资源以及如何使用 Spring 中的资源。它包括以下主题:

介绍

遗憾的是,Java 的标准java.net.URL class 和各种 URL 前缀的标准处理程序不足以完全访问 low-level 资源。对于 example,没有标准化的URL implementation 可用于访问需要从 classpath 或相对于ServletContext获取的资源。虽然可以为专用的URL前缀注册新的处理程序(类似于http:等前缀的现有处理程序),但这通常非常复杂,并且URL接口仍然缺少一些理想的功能,例如检查是否存在的方法指向的资源。

资源接口

Spring 的Resource接口是一个更强大的接口,用于抽象对 low-level 资源的访问。以下清单显示了Resource接口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Resource extends InputStreamSource {

boolean exists();

boolean isOpen();

URL getURL() throws IOException;

File getFile() throws IOException;

Resource createRelative(String relativePath) throws IOException;

String getFilename();

String getDescription();

}

正如Resource接口的定义所示,它扩展了InputStreamSource接口。以下清单显示了InputStreamSource接口的定义:

1
2
3
4
5
public interface InputStreamSource {

InputStream getInputStream() throws IOException;

}

Resource接口中一些最重要的方法是:

  • getInputStream():找到并打开资源,返回InputStream以从资源中读取。预计每次调用都会返回一个新的InputStream。呼叫者有责任关闭流。
  • exists():返回boolean,指示此资源是否实际存在于物理形式中。
  • isOpen():返回boolean,指示此资源是否表示具有开放流的句柄。如果trueInputStream不能多次读取,必须只读一次然后关闭以避免资源泄漏。除InputStreamResource外,对所有通常的资源实现返回false。
  • getDescription():返回此资源的描述,用于处理资源时的错误输出。这通常是完全限定的文件 name 或资源的实际 URL。

其他方法允许您获取表示资源的实际URLFile object(如果基础 implementation 兼容并支持该功能)。

当需要资源时,Spring 本身广泛使用Resource抽象,作为许多方法签名中的参数类型。某些 Spring API 中的其他方法(例如各种ApplicationContext实现的构造函数)采用String,其中使用简单的形式来创建适合于 context实现的Resource,或者通过String路径上的特殊前缀,让调用者指定必须创建和使用特定的Resource实现。

虽然接口与 Spring 和 Spring 一起使用很多,但实际上非常有用的是在你自己的 code 中作为通用实用 class 使用,以便访问资源,即使你的 code 不知道或不关心任何其他的 Spring 的部分内容。虽然这会将你的 code 耦合到 Spring,但它实际上只将它耦合到这一小组实用程序 classes,它们可以作为URL的更强大的替代品,并且可以被认为等同于你将用于此目的的任何其他 library。

Resource抽象不会取代功能。它尽可能地包裹它。对于 example,UrlResource包装了一个 URL 并使用包装的URL来完成它的工作。

内置资源实现

Spring 包括以下Resource implementations:

UrlResource对象

UrlResource包装java.net.URL,可用于访问通常可通过 URL 访问的任何 object,例如 files,HTTP 目标,FTP 目标等。所有 URL 都具有标准化的String表示,使得适当的标准化前缀用于指示来自另一个的一种 URL 类型。这包括file:用于访问文件系统_path,http:用于通过 HTTP 协议访问资源,ftp:用于通过 FTP 访问资源,以及其他。

是由 Java code 通过显式使用UrlResource构造函数创建的,但是当您调用一个带有String参数的 API 方法来表示路径时,通常会隐式创建UrlResource。对于后一种情况,JavaBeans PropertyEditor最终决定创建哪种类型的Resource。如果路径 string 包含 well-known(对它,那是)前缀(例如classpath:),它会为该前缀创建一个适当的专用Resource。但是,如果它不识别前缀,则假定 string 是标准 URL string 并创建UrlResource

使用ClassPathResource

此 class 表示应从 classpath 获取的资源。它使用线程 context class 加载器,给定的 class 加载器或给定 class 来加载 loading 资源。

如果 class 路径资源驻留在文件系统中,而 classpath 资源驻留在 jar 中并且尚未(通过 servlet 引擎或任何环境)扩展到文件系统,则此Resource implementation 支持解析为java.io.File。为了解决这个问题,各种Resource implementations 总是支持解析为java.net.URL

是由 Java code 通过显式使用ClassPathResource构造函数创建的,但是当您调用一个带有String参数的 API 方法来表示路径时,通常会隐式创建ClassPathResource。对于后一种情况,JavaBeans PropertyEditor在 string 路径上识别特殊前缀classpath:,并在这种情况下创建ClassPathResource

FileSystemResource

这是java.io.Filejava.nio.file.Path句柄的Resource implementation。它支持分辨率为FileURL

ServletContextResource

这是ServletContext资源的一个资源实现,它解释相关web应用程序根目录中的相对路径。

它始终支持流访问和 URL 访问,但只有在扩展 web application 存档且资源实际位于文件系统上时才允许java.io.File访问。无论它是在文件系统上扩展还是直接从 JAR 或其他地方(如数据库)(可以想象)访问它实际上都依赖于 Servlet 容器。

InputStreamResource

InputStreamResource是给定InputStream资源实现。只有在没有特定的资源实现适用时才应该使用它。特别是,在可能的情况下,首选ByteArrayResource或任何基于文件的资源实现。

与其他资源实现相比,这是 already-opened 资源的描述符。因此,它从isOpen()返回true。如果需要将资源描述符保留在某处或者需要多次读取流,请不要使用它。

ByteArrayResource

这是给定字节 array 的资源实现。它为给定的字节 array 创建ByteArrayInputStream

它对于从任何给定的字节 array 中加载内容非常有用,而不必求助于 single-use InputStreamResource

ResourceLoader

ResourceLoader接口应由 objects 实现,它可以 return(即加载)Resource实例。以下清单显示了ResourceLoader接口定义:

1
2
3
4
5
public interface ResourceLoader {

Resource getResource(String location);

}

所有 application 上下文都实现了ResourceLoader接口。因此,所有 application 上下文都可用于获取Resource实例。

当您在特定的 application context 上调用getResource(),并且指定的位置路径没有特定的前缀时,您将返回适合该特定 application context 的Resource类型。对于 example,假设对ClassPathXmlApplicationContext实例执行了以下 code 代码段:

1
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");

对于ClassPathXmlApplicationContext,code 返回ClassPathResource。如果针对FileSystemXmlApplicationContext实例执行了相同的方法,则会返回FileSystemResource。对于WebApplicationContext,它会返回一个ServletContextResource。它同样会为每个 context 返回适当的 objects。

因此,您可以以适合特定 application context 的方式加载资源。

另一方面,您可以通过指定特殊的classpath:前缀来强制使用ClassPathResource,而不管 application context 类型,如下面的 example 所示:

1
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");

同样,您可以通过指定任何标准java.net.URL前缀来强制使用UrlResource。以下对示例使用filehttp前缀:

1
2
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt");

以下 table 总结了将String objects 转换为Resource objects 的策略:

字首 说明
classpath: classpath:com/myapp/config.xml 从 classpath 加载。
文件: file:///data/config.xml 从文件系统加载为URL。另见FileSystemResource 警告
HTTP: http://myserver/logo.png 加载为URL
(没有) /data/config.xml 取决于潜在的ApplicationContext

ResourceLoaderAware接口

ResourceLoaderAware接口是一个特殊的标记接口,用于标识期望提供ResourceLoader reference 的 objects。以下清单显示了ResourceLoaderAware接口的定义:

1
2
3
4
public interface ResourceLoaderAware {

void setResourceLoader(ResourceLoader resourceLoader);
}

当 class 实现ResourceLoaderAware并部署到 application context(作为 Spring-managed bean)时,application context 将其识别为ResourceLoaderAware。然后 application context 调用setResourceLoader(ResourceLoader),将自身作为参数提供(请记住,Spring 中的所有 application 上下文都实现了ResourceLoader接口)。

由于ApplicationContextResourceLoader,因此 bean 也可以实现ApplicationContextAware接口并直接使用提供的 application context 来加载资源。但是,一般情况下,如果您需要,最好使用专门的ResourceLoader接口。 code 只能耦合到资源 loading 接口(可以被认为是实用程序接口),而不能耦合到整个 Spring ApplicationContext接口。

从 Spring 2.5 开始,您可以依靠自动装配ResourceLoader作为实现ResourceLoaderAware接口的替代方法。 “传统的”constructorbyType自动装配模式(如自动化协作者中所述)现在能够分别为构造函数参数或 setter 方法参数提供类型ResourceLoader的依赖性。为了获得更大的灵活性(包括自动装配字段和多参数方法的能力),请考虑使用 annotation-based 自动装配 features。在这种情况下,ResourceLoader被自动装入一个字段,构造函数参数或方法参数,它们要求ResourceLoader类型为 long,因为有问题的字段,构造函数或方法带有@Autowired annotation。有关更多信息,请参阅使用 @Autowired

资源依赖关系

如果 bean 本身将通过某种动态 process 确定并提供资源路径,那么 bean 使用ResourceLoader接口加载资源可能是有意义的。对于 example,请考虑某种模板的 loading,其中所需的特定资源取决于用户的角色。如果资源是静态的,那么完全消除ResourceLoader接口的使用是有意义的,让 bean 暴露它需要的Resource properties,并期望它们被注入其中。

那么 inject 这些 properties 的重要性在于所有 application 上下文都注册并使用特殊的 JavaBeans PropertyEditor,它可以将String _path 转换为Resource objects。因此,如果myBean具有Resource类型的模板 property,则可以为该资源配置一个简单的 string,如下面的 example 所示:

1
2
3
<bean id="myBean" class="...">
<property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

请注意,资源路径没有前缀。因此,因为 application context 本身将用作ResourceLoader,所以资源本身通过ClassPathResourceFileSystemResourceServletContextResource加载,具体取决于 context 的确切类型。

如果需要强制使用特定的Resource类型,则可以使用前缀。以下两个示例显示如何强制ClassPathResourceUrlResource(后者用于访问文件系统文件):

1
2
<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>

ApplicationContexts和ResourcePaths

本节介绍如何使用资源创建 application 上下文,包括使用 XML 的快捷方式,如何使用通配符以及其他详细信息。

构建Application上下文

application context 构造函数(对于特定的 application context 类型)通常使用string或字符数组作为资源的位置路径,例如构成 context 定义的 XML files。

当这样的位置路径没有前缀时,从该路径构建并用于加载 bean 定义的特定Resource类型取决于并且适合于特定的 application context。对于 example,请考虑以下 example,它会创建ClassPathXmlApplicationContext

1
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

bean 定义是从 classpath 加载的,因为使用了ClassPathResource。但是,请考虑以下 example,它会创建一个FileSystemXmlApplicationContext

1
2
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/appContext.xml");

现在 bean 定义从文件系统位置加载(在这种情况下,相对于当前工作目录)。

请注意,在位置路径上使用特殊 classpath 前缀或标准 URL 前缀会覆盖为加载定义而创建的默认类型Resource。考虑以下 example:

1
2
ApplicationContext ctx =
new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");

使用FileSystemXmlApplicationContext从classpath加载bean定义。但是,它仍然是FileSystemXmlApplicationContext。如果它随后被用作ResourceLoader,那么任何没有前缀的路径仍然被视为文件系统路径。

构造ClassPathXmlApplicationContext实例-快捷方式

ClassPathXmlApplicationContext公开了许多构造函数以实现方便的实例化。基本的 idea 是你只能提供 string array,它只包含 XML files 本身的文件名(没有前导路径信息),并且还提供Class。然后ClassPathXmlApplicationContext从提供的 class 派生路径信息。

请考虑以下目录布局:

1
2
3
4
5
com/
foo/
services.xml
daos.xml
MessengerService.class

以下 example 显示了如何实例化ClassPathXmlApplicationContext实例,该实例由 files 中定义的名为services.xmldaos.xml(位于 classpath 上)的 beans 组成:

1
2
ApplicationContext ctx = new ClassPathXmlApplicationContext(
new String[] {"services.xml", "daos.xml"}, MessengerService.class);

有关各种构造函数的详细信息,请参阅ClassPathXmlApplicationContext javadoc。

ApplicationContext构造函数资源Paths中的通配符

application context 构造函数值中的资源 paths 可以是简单的 paths(如前所示),每个都有 one-to-one 映射到目标Resource,或者,可以包含特殊的“classpath *:”前缀或内部 Ant-style 正则表达式(匹配)通过使用 Spring 的PathMatcher实用程序)。后者都是有效的通配符。

此机制的一个用途是当您需要执行 component-style application 程序集时。所有组件都可以将 context 定义片段“发布”到 well-known 位置路径,并且当使用前缀为classpath*:的相同路径创建最终 application context 时,将自动拾取所有 component 片段。

请注意,此通配符特定于 application context 构造函数中使用资源路径(或直接使用PathMatcher实用程序 class 层次结构时),并在构造时解析。它与Resource类型本身无关。不能使用classpath*:前缀来构造实际的资源,因为资源一次只指向一个资源。

Ant-style模式

路径位置可以包含 Ant-style 模式,如下面的示例所示:

1
2
3
4
/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml

当路径位置包含 Ant-style pattern 时,解析程序遵循更复杂的过程来尝试解析通配符。它为直到最后 non-wildcard 段的路径生成Resource并从中获取 URL。如果此 URL 不是jar: URL 或 container-specific 变体(例如 WebLogic 中的zip:,WebSphere 中的wsjar等),则从中获取java.io.File并用于通过遍历文件系统来解析通配符。对于 jar URL,解析器从中获取java.net.JarURLConnection或手动解析 jar URL,然后遍历 jar 文件的内容以解析通配符。

影响可移植性

如果指定的路径已经是文件 URL(隐式,因为基本ResourceLoader是文件系统或显式),则可以保证通配符以完全可移植的方式工作。

如果指定的路径是 classpath 位置,则解析程序必须通过Classloader.getResource()调用获取最后的 non-wildcard 路径段 URL。由于这只是路径的一个节点(不是最后的文件),实际上它是未定义的(在ClassLoader javadoc 中),在这种情况下确切地返回了什么类型的 URL。实际上,它总是java.io.File表示目录(classpath 资源解析为文件系统位置)或某种 jar URL(classpath 资源解析为 jar 位置)。尽管如此,这项行动仍存在可移植性问题。

如果为最后一个 non-wildcard 段获取了 jar URL,则解析器必须能够从中获取java.net.JarURLConnection或手动解析 jar URL,以便能够遍历 jar 的内容并解析通配符。这在大多数环境中都有效,但在其他环境中无效,我们强烈建议您在依赖它之前,在特定环境中对来自 jars 的资源的通配符解析进行全面测试。

classpath*:前缀

构造基于XML的application context 时,位置 string 可以使用特殊的classpath*:前缀,如下面的 example 所示:

1
2
ApplicationContext ctx =
new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

此特殊前缀指定必须获取 match 给定 name 的所有 classpath 资源(内部,这通常通过调用ClassLoader.getResources(…))然后合并以形成最终的 application context 定义。

通配符 classpath 依赖于底层类加载器的getResources()方法。由于现在大多数 application 服务器都提供了自己的类加载器 implementation,因此行为可能会有所不同,尤其是在处理 jar files 时。检查classpath*是否有效的简单测试是使用类加载器从 class 路径上的 jar 中加载文件:getClass().getClassLoader().getResources("<someFileInsideTheJar>")。尝试使用具有相同 name 但放在两个不同位置的 files 进行此测试。如果返回了不适当的结果,请检查 application 服务器文档以获取可能影响类加载器行为的设置。

您还可以将classpath*:前缀与PathMatcher pattern 组合在位置路径的 rest 中(对于 example,classpath*:META-INF/*-beans.xml)。在这种情况下,解析策略非常简单:在最后一个 non-wildcard 路径段上使用ClassLoader.getResources()调用来获取 class 加载器层次结构中的所有匹配资源,然后,在每个资源之外,使用前面描述的相同的PathMatcher解析策略通配符子路径。

与通配符有关的其他说明

请注意,classpath*:与 Ant-style 模式结合使用时,只能在 pattern 启动之前与至少一个根目录可靠地工作,除非实际目标 files 位于文件系统中。这意味着诸如classpath*:*.xml之类的 pattern 可能无法从 jar files 的根目录中检索 files,而只能从扩展目录的根目录中检索 files。

Spring 检索 classpath 条目的能力来自 JDK 的ClassLoader.getResources()方法,该方法仅返回空 string 的文件系统位置(指示搜索的潜在根)。 Spring 也会评估URLClassLoader运行时 configuration 和 jar files 中的java.class.path清单,但这不能保证导致可移植行为。

扫描 classpath 包需要在 classpath 中存在相应的目录条目。使用 Ant build JAR 时,请不要激活 JAR 任务的 files-only 开关。此外,classpath 目录可能不会在某些环境中基于安全 policies 公开 - 例如,JDK 1.7.0_45 及更高版本上的 stand-alone applications(需要在清单中设置’Trusted-Library’.请参阅

在 JDK 9 的模块路径(Jigsaw)上,Spring 的 classpath 扫描通常按预期工作。此处强烈建议将资源放入专用目录,避免上述搜索 jar 文件根 level 时出现的可移植性问题。

如果要搜索的根包在多个 class 路径位置中可用,则不保证具有classpath:资源的 Ant-style 模式可以找到匹配的资源。考虑以下资源位置的示例:

1
com/mycompany/package1/service-context.xml

现在考虑某人可能用来尝试查找该文件的 Ant-style 路径:

1
classpath:com/mycompany/**/service-context.xml

这样的资源可能只在一个位置,但是当使用诸如前面的 example 之类的路径来尝试解析它时,解析器会处理getResource("com/mycompany");返回的(第一个)URL。如果此基本包节点存在于多个类加载器位置中,则实际的最终资源可能不存在。因此,在这种情况下,您应该更喜欢使用classpath*:和相同的 Ant-style pattern,它会搜索包含根软件包的所有 class 路径位置。

FileSystemResource警告

没有附加到FileSystemApplicationContextFileSystemResource(即,当FileSystemApplicationContext不是实际的ResourceLoader时)会像您期望的那样处理绝对和相对的路径。相对路径相对于当前工作目录,而绝对 paths 相对于文件系统的根目录。

但是,出于向后兼容性(历史)的原因,当FileSystemApplicationContextResourceLoader时,这会发生变化。 FileSystemApplicationContext强制所有附加的FileSystemResource实例将所有位置 paths 视为相对的,无论它们是否以前导斜杠开头。实际上,这意味着以下示例是等效的:

1
2
3
4
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/context.xml");
ApplicationContext ctx =
new FileSystemXmlApplicationContext("/conf/context.xml");

以下示例也是等效的(即使它们有所不同,因为一个案例是相对的而另一个案例是绝对的):

1
2
3
4
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");

在实践中,如果您需要真正的绝对文件系统路径,您应该避免使用绝对路径与FileSystemResource或FileSystemXmlApplicationContext,并通过使用file: URL前缀强制使用UrlResource。以下示例显示了如何执行此操作:

1
2
3
4
5
// 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");

验证,数据绑定和类型转换

将验证考虑为业务逻辑有利有弊,Spring提供了一种验证(和数据绑定)设计,不排除其中任何一种。具体来说,验证不应该绑定到web层,应该容易定位,它应该可以插入任何可用的验证器。考虑这些问题,Spring提供了验证器合同既基本又非常有用的应用程序在每一层。

数据绑定对于将用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。Spring提供了名称恰当的DataBinder来实现这一点。Validator和DataBinder组成了验证包,它主要用于但不限于web层。

BeanWrapper是Spring框架中的一个基本概念,在很多地方都有使用。但是,您可能不需要直接使用BeanWrapper。然而,由于这是参考文档,我们觉得应该进行一些解释。我们将在本章中解释BeanWrapper,因为如果您打算使用它,那么您很可能在尝试将数据绑定到对象时使用它。

Spring的DataBinder和底层BeanWrapper都使用PropertyEditorSupport实现来解析和格式化属性值。PropertyEditor和PropertyEditorSupport类型是JavaBeans规范的一部分,在本章中也会进行解释。Spring 3引入了一个核心。提供一般类型转换工具的转换包,以及用于格式化UI字段值的高级“格式”包。您可以使用这些包作为PropertyEditorSupport实现的更简单的替代方案。本章也将讨论它们。

应用程序可以一次全局启用Bean验证(如Java Bean验证中所描述的),并专门为所有验证需求使用它。在web层中,应用程序可以进一步为每个DataBinder注册控件本地Spring验证器实例,如配置DataBinder中所述,这对于插入自定义验证逻辑非常有用。

使用Spring的验证器接口进行验证

Spring提供一个Validator接口,可用于验证 objects。 Validator接口使用Errors object 工作,以便在验证时,验证程序可以向Errors object 报告验证失败。

考虑以下一个小数据 object 的示例:

1
2
3
4
5
6
7
public class Person {

private String name;
private int age;

// the usual getters and setters...
}

下一个 example 通过实现org.springframework.validation.Validator接口的以下两个方法为Person class提供验证行为:

  • supports(Class):这可以Validator验证提供的Class的实例吗?
  • validate(Object, org.springframework.validation.Errors):验证给定的 object,并在验证错误的情况下,使用给定的Errors object 注册那些。

实现Validator非常简单,尤其是当您知道Spring Framework也提供的ValidationUtils帮助 class 时。以下 example 为Person实例实现了Validator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PersonValidator implements Validator {

/**
* This Validator validates *only* Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}

public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}

ValidationUtils class 上的static rejectIfEmpty(..)方法用于拒绝name property,如果它是null或空 string。看一下ValidationUtils 相关 javadoc,看看它提供的功能除了前面显示的 example 之外。

虽然可以实现单个Validator class 来验证富 object 中的每个嵌套 objects,但最好将 object 的每个嵌套 class 的验证逻辑封装在它自己的Validator implementation 中。 “富”object 的简单 example 将是Customer,它由两个String properties(第一个和第二个 name)和一个复杂的Address object 组成。 Address objects 可以独立于Customer objects 使用,因此已经实现了一个独特的AddressValidator。如果您希望CustomerValidator重用AddressValidator class 中包含的逻辑而不诉诸 copy-and-paste,您可以在CustomerValidator中 dependency-inject 或实例化AddressValidator,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CustomerValidator implements Validator {

private final Validator addressValidator;

public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}

/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}

public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}

验证错误将报告给传递给验证程序的Errors object。对于 Spring Web MVC,您可以使用<spring:bind/>标记来检查错误消息,但您也可以自己检查Errors object。有关它提供的方法的更多信息可以在javadoc中找到。

将代码解析为错误消息

我们介绍了数据绑定和验证。本节介绍输出与验证错误相对应的消息。在前一节中显示的 example 中,我们拒绝了nameage字段。如果我们想使用MessageSource输出错误消息,我们可以使用我们在拒绝字段时提供的错误 code(在这种情况下为’name’和’age’)。当你通过Errors接口直接或间接地调用(例如,ValidationUtils class)rejectValue或其他reject方法之一时,底层实现不仅会记录你传入的 code,还会注册一些其他错误代码。 MessageCodesResolver确定Errors接口注册的错误代码。默认情况下,使用DefaultMessageCodesResolver,(对于 example)不仅使用您给出的 code 注册消息,还会注册包含您传递给 reject 方法的字段 name 的消息。因此,如果您使用rejectValue("age", "too.darn.old")拒绝某个字段,除了too.darn.old code 之外,Spring 还会注册too.darn.old.agetoo.darn.old.age.int(第一个包含字段 name,第二个包含字段的类型)。这样做是为了方便在定位错误消息时帮助开发人员。

有关MessageCodesResolver和默认策略的更多信息可以分别在MessageCodesResolver 的信息DefaultMessageCodesResolver。回到的 javadoc 中找到。

1
2
3
4
# in 'META-INF/spring.handlers'
http\://www.foo.com/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.com/schema/jcache/jcache.xsd=com/foo/jcache.xsd

Bean操作和BeanWrapper

org.springframework.beans包遵循 JavaBeans 标准。 JavaBean 是一个具有默认无参构造函数的 class,它遵循命名约定,其中(对于 example)名为bingoMadness的 property 将具有 setter 方法setBingoMadness(..)和 getter 方法getBingoMadness()。有关 JavaBeans 和规范的更多信息,请参阅JavaBeans

beans 包中一个非常重要的 class 是BeanWrapper接口及其相应的实现(BeanWrapperImpl)。从 javadoc 引用,BeanWrapper提供了设置和获取 property 值(单独或批量),获取property描述符和查询properties以确定它们是可读还是可写的功能。此外,BeanWrapper提供对嵌套properties的支持,使 sub-properties 上的 properties 设置为无限深度。 BeanWrapper还支持添加标准 JavaBeans PropertyChangeListenersVetoableChangeListeners的功能,而无需在目标 class 中支持 code。最后但并非最不重要的是,BeanWrapper提供了对设置索引properties的支持。 BeanWrapper通常不直接由 application code 使用,但由DataBinderBeanFactory使用。

BeanWrapper的工作方式部分由name表示:它包装 bean 以对该 bean 执行操作,例如设置和检索 properties。

设置和获取基本的和嵌套的属性

设置和获取 properties 是通过使用带有几个重载变体的setPropertyValuesetPropertyValuesgetPropertyValuegetPropertyValues方法完成的。 Springs javadoc 更详细地描述了它们。 JavaBeans 规范具有用于指示 object 的 properties 的约定。以下 table 显示了这些约定的一些示例:

表达 说明
name 表示 property name,对应于getName()isName()setName(..)方法。
account.name 指示 property account的嵌套 property name,它对应于(对于 example)getAccount().setName()getAccount().getName()方法。
account[2] 表示索引 property account的第三个元素。索引的 properties 可以是arraylist或其他自然排序的集合。
account[COMPANYNAME] 表示由account Map property 的COMPANYNAME key 索引的 map 条目的 value。

(如果你不打算直接使用BeanWrapper,那么下一部分对你来说并不重要.如果你只使用DataBinderBeanFactory以及它们的默认_i实现,你应该跳到有关 PropertyEditors 的部分

以下两个 example classes 使用BeanWrapper来获取和设置 properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Company {

private String name;
private Employee managingDirector;

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public Employee getManagingDirector() {
return this.managingDirector;
}

public void setManagingDirector(Employee managingDirector) {
this.managingDirector = managingDirector;
}
}
public class Employee {

private String name;

private float salary;

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public float getSalary() {
return salary;
}

public void setSalary(float salary) {
this.salary = salary;
}
}

以下 code 片段显示了一些如何检索和操作实例化的CompaniesEmployees的 properties 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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");

内置PropertyEditor的实现

Spring 使用PropertyEditor的概念来实现ObjectString之间的转换。以与 object 本身不同的方式表示 properties 是很方便的。例如,Date可以用人类可读的方式表示(如String'2007-14-09'),而我们仍然可以将人类可读的表单转换回原始 date(或者更好的是,转换以人类可读形式输入的任何 date 回到Date objects)。通过注册java.beans.PropertyEditor类型的自定义编辑器可以实现此行为。在BeanWrapper上注册自定义编辑器,或者在特定的 IoC 容器中注册(如上一章所述),使其了解如何将 properties 转换为所需的类型。有关PropertyEditor的更多信息,请参阅来自 Oracle 的 java.beans 包的 javadoc

在 Spring 中使用 property 编辑的几个示例:

  • 在 beans 上设置 properties 是通过使用PropertyEditor实现完成的。当您使用java.lang.String作为在 XML 文件中声明的某些 bean 的 property 的 value 时,Spring(如果相应 property 的 setter 具有Class参数)使用ClassEditor尝试将参数解析为Class object。
  • 在 Spring 的 MVC framework 中解析 HTTP 请求参数是通过使用所有类型的PropertyEditor实现来完成的,您可以在CommandController的所有子类中手动绑定。

Spring 有许多内置的 PropertyEditor实现让生活变得轻松。它们都位于org.springframework.beans.propertyeditors包中。大多数(但不是全部,如下面的 table 所示)默认情况下由BeanWrapperImpl注册。如果 property 编辑器可以某种方式配置,您仍然可以注册自己的变体来覆盖默认变体。以下 table 描述了 Spring 提供的各种PropertyEditor实现:

说明
ByteArrayPropertyEditor 字节数组的编辑器。将 strings 转换为其对应的字节表示形式。默认情况下由BeanWrapperImpl注册。
ClassEditor 解析 strings 表示 classes 到实际的 classes 和 vice-versa。如果找不到 class,则抛出IllegalArgumentException。默认情况下,由BeanWrapperImpl注册。
CustomBooleanEditor Boolean properties 的可自定义 property 编辑器。默认情况下,由BeanWrapperImpl注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomCollectionEditor Property 集合编辑器,将任何源Collection转换为给定目标Collection类型。
CustomDateEditor java.util.Date的可自定义 property 编辑器,支持自定义DateFormat。没有默认注册。必须是 user-registered,并根据需要使用适当的格式。
CustomNumberEditor 任何Number子类的可自定义 property 编辑器,例如IntegerLongFloatDouble。默认情况下,由BeanWrapperImpl注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。
FileEditor 将 strings 解析为java.io.File objects。默认情况下,由BeanWrapperImpl注册。
InputStreamEditor One-way property 编辑器,可以使用 string 并生成(通过中间ResourceEditorResource)InputStream,以便InputStream properties 可以直接设置为 strings。请注意,默认用法不会为您关闭InputStream。默认情况下,由BeanWrapperImpl注册。
LocaleEditor 可以将 strings 解析为Locale objects 和 vice-versa(string 格式为[country][variant],与LocaletoString()方法相同)。默认情况下,由BeanWrapperImpl注册。
PatternEditor 可以将 strings 解析为java.util.regex.Pattern objects 和 vice-versa。
PropertiesEditor 可以将 strings(使用java.util.Properties class 的 javadoc 中定义的格式格式化)转换为Properties objects。默认情况下,由BeanWrapperImpl注册。
StringTrimmerEditor Property 编辑器修剪 strings。 (可选)允许将空 string 转换为null value。默认情况下未注册 - 必须为 user-registered。
URLEditor 可以将 URL 的 string 表示解析为实际的URL object。默认情况下,由BeanWrapperImpl注册。

Spring 使用java.beans.PropertyEditorManager为可能需要的 property 编辑器设置搜索路径。搜索路径还包括sun.bean.editors,其中包括PropertyEditorColor等类型的PropertyEditor实现,以及大多数基本类型。另请注意,标准 JavaBeans 基础结构会自动发现PropertyEditor classes(无需显式注册),如果它们与它们处理的 class 在同一个包中,并且与 class 具有相同的 name,并附加Editor。对于 example,可以使用以下 class 和 package 结构,这足以使SomethingEditor class 被识别并用作Something Something -typed properties。

1
2
3
4
5
com
chank
pop
Something
SomethingEditor // the PropertyEditor for the Something class

请注意,您也可以在此处使用标准BeanInfo JavaBeans 机制(在某种程度上描述这里)。以下 example 使用BeanInfo机制使用关联的 class 的 properties 显式注册一个或多个PropertyEditor实例:

1
2
3
4
5
com
chank
pop
Something
SomethingBeanInfo // the BeanInfo for the Something class

引用的SomethingBeanInfo class 的以下 Java source code 将CustomNumberEditorSomething class 的age property 相关联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SomethingBeanInfo extends SimpleBeanInfo {

public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
};
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}

注册其他自定义PropertyEditor的实现

将 bean properties 设置为 string 值时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditor实现将这些 strings 转换为 property 的复杂类型。 Spring pre-registers 一些自定义的PropertyEditor 实现(用于 example,将表示为 string 的 class name 转换为Class object)。此外,Java 的标准 JavaBeans PropertyEditor查找机制允许适当地命名 class 并放置在与它提供支持的 class 相同的包中,以便可以自动找到它。

如果需要注册其他自定义PropertyEditors,可以使用多种机制。假设您有一个BeanFactory reference,最通常不方便或不推荐的手动方法是使用ConfigurableBeanFactory接口的registerCustomEditor()方法。另一个(稍微方便一点)机制是使用一个名为CustomEditorConfigurer的特殊 bean 工厂 post-processor。虽然你可以使用 bean factory post-processors 和BeanFactory 实现,但CustomEditorConfigurer有一个嵌套的 property 设置,所以我们强烈建议你将它与ApplicationContext一起使用,你可以用类似的方式将它部署到任何其他 bean,并且可以自动检测它并应用。

请注意,所有 bean 工厂和 application 上下文都会自动使用多个 built-in property 编辑器,通过使用BeanWrapper来处理 property 转换。 BeanWrapper寄存器列在上一节中的标准 property 编辑器。此外,ApplicationContexts还会覆盖或添加其他编辑器,以适合特定 application context 类型的方式处理资源查找。

标准 JavaBeans PropertyEditor实例用于将表示为 strings 的 property 值转换为 property 的实际复杂类型。您可以使用,bean factory post-processor,方便地将其他PropertyEditor实例的支持添加到ApplicationContext

考虑以下 example,它定义了一个名为ExoticType的用户 class 和另一个名为DependsOnExoticType的 class,它需要把ExoticType设置为 property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}

正确设置后,我们希望能够将 property 类型指定为 string,PropertyEditor转换为实际的ExoticType实例。以下 bean 定义显示了如何设置此关系:

1
2
3
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>

PropertyEditor实现看起来类似于以下内容:

1
2
3
4
5
6
7
8
9
// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}

最后,以下 example 显示了如何使用CustomEditorConfigurerApplicationContext注册新的PropertyEditor,然后可以根据需要使用它:

1
2
3
4
5
6
7
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
</map>
</property>
</bean>

使用PropertyEditorRegistrar

使用 Spring 容器注册 property 编辑器的另一种机制是创建和使用PropertyEditorRegistrar。当您需要在几种不同情况下使用同一组 property 编辑器时,此界面特别有用。您可以编写相应的注册商,并在每种情况下重复使用它。 PropertyEditorRegistrar实例与名为PropertyEditorRegistry的接口一起使用,该接口由 Spring BeanWrapper(和DataBinder)实现。当与CustomEditorConfigurer(描述为这里一起使用时,PropertyEditorRegistrar实例特别方便,它们公开了一个名为setPropertyEditorRegistrars(..)的 property。以这种方式添加到CustomEditorConfigurerPropertyEditorRegistrar实例可以很容易地与DataBinder和 Spring MVC 控制器共享。此外,它避免了在自定义编辑器上进行同步的需要:PropertyEditorRegistrar应该为每个 bean 创建尝试创建新的PropertyEditor实例。

以下 example 显示了如何创建自己的PropertyEditorRegistrar实现:

1
2
3
4
5
6
7
8
9
10
11
12
package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

public void registerCustomEditors(PropertyEditorRegistry registry) {

// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

// you could register as many custom property editors as are required here...
}
}

另请参阅org.springframework.beans.support.ResourceEditorRegistrar以了解 example PropertyEditorRegistrar实现。请注意,在registerCustomEditors(..)`方法的实现中,它会为每个 property 编辑器创建新实例。

下一个 example 显示了如何配置CustomEditorConfigurer和 inject 一个CustomPropertyEditorRegistrar的实例:

1
2
3
4
5
6
7
8
9
10
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean>

<bean id="customPropertyEditorRegistrar"
class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>

最后(并且与本章的重点有所不同,对于那些使用Spring 的 MVC web framework的人来说),将PropertyEditorRegistrars与 data-binding Controllers(例如SimpleFormController)结合使用可以非常方便。以下 example 在initBinder(..)方法的实现中使用PropertyEditorRegistrar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class RegisterUserController extends SimpleFormController {

private final PropertyEditorRegistrar customPropertyEditorRegistrar;

public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}

protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder binder) throws Exception {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}

// other methods to do with registering a User
}

这种PropertyEditor注册方式可以导致简洁的 code(initBinder(..)的实现只有一个 line long),让 common PropertyEditor registration code 封装在 class 中,然后根据需要在Controllers之间共享。

Spring类型转换

Spring 3 引入了一个core.convert包,它提供了一般的类型转换系统。系统定义了一个用于实现类型转换逻辑的 SPI 和一个用于在运行时执行类型转换的 API。在 Spring 容器中,您可以使用此系统替代PropertyEditor实现将外部化的 bean property value strings 转换为所需的 property 类型。您还可以在需要进行类型转换的 application 中的任何位置使用公共 API。

转换器SPI

实现类型转换逻辑的 SPI 很简单且类型很强,如下面的接口定义所示:

1
2
3
4
5
6
package org.springframework.core.convert.converter;

public interface Converter<S, T> {

T convert(S source);
}

要创建自己的转换器,请实现Converter接口并参数化S作为要转换的类型,并将T作为要转换的类型。如果S的集合或 array 需要转换为数组或 T的集合,您也可以透明地应用这样的转换器,前提是已经注册了委托的 array 或集合转换器(默认情况下DefaultConversionService)。

对于每次调用convert(S),源参数都保证不为 null。如果转换失败,您的Converter可能会抛出任何未经检查的 exception。具体来说,它应该抛出IllegalArgumentException来报告无效的源 value。注意确保Converter实现是 thread-safe。

为方便起见,core.convert.support包中提供了几个转换器实现。这些包括从 strings 到 numbers 和其他 common 类型的转换器。以下清单显示了StringToInteger class,这是一个典型的Converter 实现:

1
2
3
4
5
6
7
8
package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

public Integer convert(String source) {
return Integer.valueOf(source);
}
}

使用ConverterFactory

当您需要集中整个 class 层次结构的转换逻辑时(对于 example,当从 String 转换为 java.lang.Enum objects 时),您可以实现ConverterFactory,如下面的 example 所示:

1
2
3
4
5
6
package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

参数化 S 为要转换的类型,R 为定义可转换为 classes 范围的基本类型。然后实现 getConverter(Class ),其中 T 是 R 的子类。

StringToEnum ConverterFactory视为 example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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());
}
}
}

使用GenericConverter

当您需要复杂的Converter implementation 时,请考虑使用GenericConverter接口。使用比Converter更灵活但不太强类型的签名,GenericConverter支持在多个源类型和目标类型之间进行转换。此外,GenericConverter使您可以在实现转换逻辑时使用的源和目标字段 context。这样的 context 允许类型转换由字段 annotation 或在字段签名上声明的通用信息驱动。以下清单显示了GenericConverter的接口定义:

1
2
3
4
5
6
7
8
package org.springframework.core.convert.converter;

public interface GenericConverter {

public Set<ConvertiblePair> getConvertibleTypes();

Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

要实现GenericConverter,请return 支持的源→目标类型对。然后实现convert(Object, TypeDescriptor, TypeDescriptor)以包含转换逻辑。源TypeDescriptor提供对保存正在转换的 value 的源字段的访问。目标TypeDescriptor提供对要设置转换后的 value 的目标字段的访问。

的一个好示例是在 Java array 和集合之间进行转换的转换器。这样的ArrayToCollectionConverter内省了声明目标集合类型的字段来解析集合的元素类型。这样,在目标字段上设置集合之前,可以将源 array 中的每个元素转换为集合元素类型。

因为GenericConverter是一个更复杂的 SPI 接口,所以只有在需要时才能使用它。支持ConverterConverterFactory以满足基本类型转换需求。

使用ConditionalGenericConverter

有时,只有在特定条件保持 true 时才需要Converter run。例如,您可能希望仅在目标字段上存在特定 annotation 时运行Converter,或者只有在目标 class 上定义了特定方法(例如static valueOf方法)时才可能 run ConverterConditionalGenericConverterGenericConverterConditionalConverter接口的 union,它允许您定义这样的自定义匹配条件:

1
2
3
4
5
6
7
public interface ConditionalConverter {

boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

的良好示例是EntityConverter,它在持久实体标识符和实体 reference 之间进行转换。只有当目标实体类型声明静态查找器方法(对于 example,findAccount(Long))时,这样的EntityConverter才可能 match。您可以在matches(TypeDescriptor, TypeDescriptor)的 implementation 中执行这样的 finder 方法检查。

ConversionService应用程序接口

ConversionService定义了一个统一的 API,用于在运行时执行类型转换逻辑。转换器通常在以下 Facade 接口后面执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.springframework.core.convert;

public interface ConversionService {

boolean canConvert(Class<?> sourceType, Class<?> targetType);

<T> T convert(Object source, Class<T> targetType);

boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

大多数ConversionService implementations 也实现了ConverterRegistry,它提供了一个用于注册转换器的 SPI。在内部,ConversionService implementation 委托其注册的转换器执行类型转换逻辑。

core.convert.support包中提供了强大的ConversionService implementation。 GenericConversionService是适合在大多数环境中使用的 general-purpose implementation。 ConversionServiceFactory为 creating common ConversionService配置提供了一个方便的工厂。

配置ConversionService

ConversionService是一个 stateless object,设计用于在 application 启动时实例化,然后在多个线程之间共享。在 Spring application 中,通常为每个 Spring 容器(或ApplicationContext)配置ConversionService实例。 Spring 选择ConversionService并在 framework 需要执行类型转换时使用它。您也可以_将此ConversionService注入任何 beans 并直接调用它。

如果 Spring 没有注册ConversionService,则使用原始的PropertyEditor -based 系统。

要使用 Spring 注册默认ConversionService,请使用id conversionService添加以下 bean 定义:

1
2
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默认ConversionService可以在 strings,numbers,enums,collections,maps 和其他 common 类型之间进行转换。要使用您自己的自定义转换器补充或覆盖默认转换器,请设置converters property。 Property 值可以实现ConverterConverterFactoryGenericConverter接口中的任何一个。

1
2
3
4
5
6
7
8
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="example.MyCustomConverter"/>
</set>
</property>
</bean>

在 Spring MVC application 中使用ConversionService也是常见的。请参阅 Spring MVC 章节中的转换和格式化

在某些情况下,您可能希望在转换期间应用格式。有关使用FormattingConversionServiceFactoryBean的详细信息,请参阅FormatterRegistry SPI

以编程方式使用ConversionService

要以编程方式使用ConversionService实例,您可以像对待任何其他 bean 一样引用 reference。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class MyService {

@Autowired
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}

public void doIt() {
this.conversionService.convert(...)
}
}

对于大多数用例,您可以使用指定targetTypeconvert方法,但它不适用于更复杂的类型,例如参数化元素的集合。例如,如果要以编程方式将List Integer转换为List String,则需要提供源和目标类型的正式定义。

幸运的是,TypeDescriptor提供了各种选项,使得这么简单,如下面的示例所示:

1
2
3
4
5
6
DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ....
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

请注意,DefaultConversionService会自动注册适合大多数环境的转换器。这包括收集转换器,标量转换器和基本的Object -to - String转换器。您可以使用DefaultConversionService class 上的静态addDefaultConverters方法向任何ConverterRegistry注册相同的转换器。

value 类型的转换器可以重用于数组和集合,因此无需创建特定的转换器即可将Collection S转换为Collection T,假设标准集合处理是合适的。

Spring字段格式

如前一节所述,core.convert是 general-purpose 类型转换系统。它提供了统一的ConversionService API 以及强类型的Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。 Spring 容器使用此系统绑定 bean property 值。此外,Spring 表达式语言(SpEL)和DataBinder都使用此系统来绑定字段值。例如,当 SpEL 需要强制ShortLong以完成expression.setValue(Object bean, Object value)尝试时,core.convert系统执行强制。

现在考虑典型 client 环境的类型转换要求,例如 web 或 desktop application。在这样的环境中,您通常从String转换为支持 client postback process,以及返回String以支持视图呈现 process。此外,您经常需要本地化String值。更通用的core.convert Converter SPI 不直接解决此类格式化要求。为了直接解决它们,Spring 3 引入了一个方便的Formatter SPI,为 client 环境提供了一个简单而强大的PropertyEditor __mplement 选择。

通常,当需要实现 general-purpose 类型转换逻辑时,可以使用Converter SPI - 对于 example,用于在java.util.Datejava.lang.Long之间进行转换。在 client 环境(例如 web application)中工作时,可以使用Formatter SPI,并且需要解析和打印本地化的字段值。 ConversionService为两个 SPI 提供统一的类型转换 API。

FormatterSPI

用于实现字段格式化逻辑的Formatter SPI 简单且强类型化。以下清单显示了Formatter接口定义:

1
2
3
4
package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

FormatterPrinterParser building-block 接口扩展。以下清单显示了这两个接口的定义:

1
2
3
4
5
6
7
8
9
10
public interface Printer<T> {

String print(T fieldValue, Locale locale);
}
import java.text.ParseException;

public interface Parser<T> {

T parse(String clientValue, Locale locale) throws ParseException;
}

要创建自己的Formatter,请实现前面显示的Formatter接口。将T参数化为要格式化的 object 的类型 - 对于 example,java.util.Date。实现print()操作以打印T的实例以在 client locale 中显示。实现parse()操作以从 client locale 返回的格式化表示中解析T的实例。如果解析尝试失败,您的Formatter应该抛出ParseExceptionIllegalArgumentException。注意确保Formatter implementation 是 thread-safe。

为方便起见,format子包提供了几个Formatter __mplement。 number包提供NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter来格式化使用java.text.NumberFormatjava.lang.Number objects。 datetime包提供DateFormatter格式java.util.Date objects java.text.DateFormatdatetime.joda包基于Joda-Time library提供全面的 datetime 格式支持。

以下DateFormatter是 example Formatter implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.springframework.format.datetime;

public final class DateFormatter implements Formatter<Date> {

private String pattern;

public DateFormatter(String pattern) {
this.pattern = pattern;
}

public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}

public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}

protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}

Spring 团队欢迎 community-driven Formatter贡献见jira.spring.io贡献。

基于注解的格式化

可以按字段类型或 annotation 配置字段格式。要将注释绑定到Formatter,请实现AnnotationFormatterFactory。以下清单显示了AnnotationFormatterFactory接口的定义:

1
2
3
4
5
6
7
8
9
10
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);
}

创建 implementation :.将 A 参数化为要与格式逻辑关联的字段annotationType - 用于 example org.springframework.format.annotation.DateTimeFormat。 。 getFieldTypes() return 可以使用 annotation 的字段类型。 。 getPrinter() return Printer打印带注释字段的 value。 。 getParser() return Parser来解析带注释字段的clientValue

以下 example AnnotationFormatterFactory implementation 将@NumberFormat annotation 绑定到格式化程序,以指定数字样式或 pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public final class NumberFormatAnnotationFormatterFactory
implements AnnotationFormatterFactory<NumberFormat> {

public Set<Class<?>> getFieldTypes() {
return new HashSet<Class<?>>(asList(new Class<?>[] {
Short.class, Integer.class, Long.class, Float.class,
Double.class, BigDecimal.class, BigInteger.class }));
}

public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}

public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}

private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
if (!annotation.pattern().isEmpty()) {
return new NumberStyleFormatter(annotation.pattern());
} else {
Style style = annotation.style();
if (style == Style.PERCENT) {
return new PercentStyleFormatter();
} else if (style == Style.CURRENCY) {
return new CurrencyStyleFormatter();
} else {
return new NumberStyleFormatter();
}
}
}
}

要触发格式化,可以使用@NumberFormat 注释字段,如下面的 example 所示:

1
2
3
4
5
public class MyModel {

@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}

格式注解应用程序接口

org.springframework.format.annotation包中存在可移植格式 annotation API。您可以使用@NumberFormat格式化 java.lang.Number 字段,使用@DateTimeFormat格式化java.util.Datejava.util.Calendarjava.util.Long或 Joda-Time 字段。

以下 example 使用@DateTimeFormatjava.util.Date格式化为 ISO Date(yyyy-MM-dd):

1
2
3
4
5
public class MyModel {

@DateTimeFormat(iso=ISO.DATE)
private Date date;
}

FormatterRegistrySPI

FormatterRegistry是用于注册格式化器和转换器的 SPI。 FormattingConversionService是适用于大多数环境的FormatterRegistry的 实现。您可以使用FormattingConversionServiceFactoryBean以编程方式或声明性地将此实现配置为 Spring bean。因为此 实现还实现ConversionService,所以您可以直接将其配置为与 Spring 的DataBinder和 Spring 表达式语言(SpEL)一起使用。

以下清单显示了FormatterRegistry SPI:

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

如上面的清单所示,您可以按字段类型或 annotation 注册格式化程序。

FormatterRegistry SPI 允许您集中配置格式规则,而不是在控制器之间复制此类配置。例如,您可能希望强制所有 date 字段以某种方式格式化,或者具有特定 annotation 的字段以某种方式格式化。使用共享FormatterRegistry,您可以定义一次这些规则,并且只要需要格式化,就会应用它们。

FormatterRegistrarSPI

FormatterRegistrar是一个 SPI,用于通过 FormatterRegistry 注册格式化程序和转换器。以下清单显示了其接口定义:

1
2
3
4
5
6
package org.springframework.format;

public interface FormatterRegistrar {

void registerFormatters(FormatterRegistry registry);
}

在为给定格式类别注册多个相关转换器和格式化程序时,非常有用,例如 date 格式。它在声明性注册不足的情况下也很有用 - 例如,当格式化程序需要在与其自己的<T>不同的特定字段类型下编入索引时,或者在注册Printer/Parser对时。下一节提供有关转换器和格式化程序注册的更多信息。

在SpringMVC中配置格式

请参阅 Spring MVC 章节中的转换和格式化

配置全局日期和时间格式

默认情况下,使用DateFormat.SHORT样式从 strings 转换未使用@DateTimeFormat注释的 date 和 time 字段。如果您愿意,可以通过定义自己的 global 格式来更改此设置。

为此,您需要确保 Spring 不会注册默认格式化程序。相反,您应该手动注册所有格式化程序。使用org.springframework.format.datetime.joda.JodaTimeFormatterRegistrarorg.springframework.format.datetime.DateFormatterRegistrar class,具体取决于您是否使用 Joda-Time library。

例如,以下 Java configuration 注册 global yyyyMMdd格式(此 example 不依赖于 Joda-Time library):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class AppConfig {

@Bean
public FormattingConversionService conversionService() {

// Use the DefaultFormattingConversionService but do not register defaults
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

// Ensure @NumberFormat is still supported
conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

// Register date conversion with a specific global format
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
registrar.setFormatter(new DateFormatter("yyyyMMdd"));
registrar.registerFormatters(conversionService);

return conversionService;
}
}

如果您更喜欢基于XML的配置,则可以使用FormattingConversionServiceFactoryBean。以下 example 显示了如何执行此操作(此 time 使用 Joda Time):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd>

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="registerDefaultFormatters" value="false" />
<property name="formatters">
<set>
<bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
<property name="dateFormatter">
<bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
<property name="pattern" value="yyyyMMdd"/>
</bean>
</property>
</bean>
</set>
</property>
</bean>
</beans>

Joda-Time 提供单独的不同类型来表示datetimedate-time值。应使用JodaTimeFormatterRegistrardateFormattertimeFormatterdateTimeFormatter properties 为每种类型配置不同的格式。 DateTimeFormatterFactoryBean提供了一种创建格式化程序的便捷方法。

如果您使用 Spring MVC,请记住明确配置使用的转换服务。对于 Java-based @Configuration,这意味着扩展WebMvcConfigurationSupport class 并覆盖mvcConversionService()方法。对于 XML,您应该使用mvc:annotation-driven元素的conversion-service属性。有关详细信息,请参阅转换和格式化

JavaBean验证

Spring 3 为其验证支持引入了几项增强功能。首先,完全支持 JSR-303 Bean Validation API。其次,当以编程方式使用时,Spring 的DataBinder可以验证 objects 以及绑定它们。第三,Spring MVC 支持声明性地验证@Controller输入。

BeanValidationAPI概述

JSR-303 标准化 Java 平台的验证约束声明和元数据。通过使用此 API,您可以使用声明性验证约束来注释域 model properties,并且运行时会强制执行它们。您可以使用多个 built-in 约束。您还可以定义自己的自定义约束。

考虑以下 example,它显示了一个带有两个 properties 的简单PersonForm model:

1
2
3
4
public class PersonForm {
private String name;
private int age;
}

JSR-303 允许您为这些 properties 定义声明性验证约束,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
public class PersonForm {

@NotNull
@Size(max=64)
private String name;

@Min(0)
private int age;
}

当 JSR-303 Validator 验证此 class 的实例时,将强制执行这些约束。

有关 JSR-303 和 JSR-349 的一般信息,请参阅Bean 验证网站。有关默认 reference implementation 的特定功能的信息,请参阅Hibernate Validator文档。要了解如何将 bean 验证提供程序设置为 Spring bean,请继续阅读。

配置Bean验证提供程序

Spring 完全支持 Bean Validation API。这包括方便地支持将 JSR-303 或 JSR-349 Bean 验证提供程序作为 Spring bean 引导。这使您可以在 application 中需要验证的地方注入javax.validation.ValidatorFactoryjavax.validation.Validator

您可以使用LocalValidatorFactoryBean将默认 Validator 配置为 Spring bean,如下面的 example 所示:

1
2
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

前面的 example 中的基本 configuration 通过使用其默认引导机制触发 bean 验证进行初始化。 或 JSR-349 提供程序(例如 Hibernate Validator)应该出现在 classpath 中并自动检测。

注入验证器

LocalValidatorFactoryBean同时实现javax.validation.ValidatorFactoryjavax.validation.Validator,以及 Spring 的org.springframework.validation.Validator。您可以将这些接口中的任何一个 reference 注入到需要调用验证逻辑的 beans 中。

如果您希望直接使用 Bean Validation API,则可以注入引用javax.validation.Validator,如下面的 example 所示:

1
2
3
4
5
6
7
import javax.validation.Validator;

@Service
public class MyService {

@Autowired
private Validator validator;

如果您的 bean 需要 Spring Validation API,则可以 inject 引用org.springframework.validation.Validator,如下面的 example 所示:

1
2
3
4
5
6
7
8
import org.springframework.validation.Validator;

@Service
public class MyService {

@Autowired
private Validator validator;
}

配置自定义约束

每个 bean 验证约束由两部分组成:

  • 声明约束及其可配置属性的@Constraint注释。
  • 实现约束行为的javax.validation.ConstraintValidator接口的实现。

要将声明与实现相关联,每个@Constraint annotation references 会引用相应的ConstraintValidator实现类。在运行时,ConstraintValidatorFactory在域 model 中遇到约束注解时实例化引用的实现。

默认情况下,LocalValidatorFactoryBean配置使用 Spring 创建ConstraintValidator实例的SpringConstraintValidatorFactory。这使得自定义ConstraintValidators从依赖注入中受益,就像任何其他 Spring bean 一样。

以下 example 显示了一个自定义@Constraint声明,后跟一个使用 Spring 进行依赖注入的关联ConstraintValidator实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

@Autowired;
private Foo aDependency;

...
}

如前面的 example 所示,ConstraintValidator implementation 可以将其依赖项@Autowired与任何其他 Spring bean 一样。

Spring-driven方法验证

您可以通过MethodValidationPostProcessor bean 定义将 Bean Validation 1.1 支持的方法验证 feature(以及作为自定义扩展,也可以通过 Hibernate Validator 4.3)集成到 Spring context 中,如下所示:

1
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

要符合 Spring-driven 方法验证的条件,所有目标 classes 都需要使用 Spring 的@Validated annotation 进行注释。 (或者,您也可以将验证组声明为 use.)使用 Hibernate Validator 和 Bean Validation 1.1 提供程序查看MethodValidationPostProcessor javadoc 以获取设置详细信息。

其他Configuration选项

对于大多数情况,默认的LocalValidatorFactoryBean configuration 足够了。从消息插值到遍历解析,各种 Bean 验证结构有许多 configuration 选项。有关这些选项的更多信息,请参见LocalValidatorFactoryBean javadoc。

配置DataBinder

从 Spring 3 开始,您可以使用Validator配置DataBinder实例。配置完成后,您可以通过调用binder.validate()来调用Validator。任何验证Errors都会自动添加到 binder 的BindingResult中。

以下 example 显示了如何在 binding 到目标 object 之后以编程方式使用DataBinder来调用验证逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// bind to the target object
binder.bind(propertyValues);

// validate the target object
binder.validate();

// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

您还可以通过dataBinder.addValidatorsdataBinder.replaceValidators配置具有多个Validator实例的DataBinder。将全局配置的 bean 验证与在 DataBinder 实例上本地配置的 Spring Validator组合时,这非常有用。见[validation-mvc-configuring]

SpringMVC3验证

请参阅 Spring MVC 章节中的验证

Spring表达语言SpEL

Spring 表达式语言(简称“SpEL”)是一种强大的表达式语言,支持在运行时查询和操作 object 图。语言语法类似于 Unified EL,但提供了额外的 features,最值得注意的是方法调用和基本的 string 模板功能。

虽然有几种其他 Java 表达式语言可供选择—OGNL,MVEL 和 JBoss EL,但只有少数几个 - 创建了 Spring 表达式语言,以便为 Spring 社区提供一种支持良好的表达式语言,可以在所有产品中使用。 Spring 组合。其语言 features 由 Spring 产品组合中的项目要求驱动,包括 Eclipse-based Spring 工具套件中 code 完成支持的工具要求。也就是说,SpEL 基于一个 technology-agnostic API,可以在需要时集成其他表达式语言 implementation。

虽然 SpEL 是 Spring 组合中表达式 evaluation 的基础,但它并不直接与 Spring 绑定,可以单独使用。要自包含,本章中的许多示例都使用 SpEL,就像它是一种独立的表达式语言一样。这需要创建一些引导基础结构 classes,例如解析器。大多数 Spring 用户不需要处理这种基础结构,而只能为 evaluation 创建表达式 strings。这种典型用法的一个例子是将_Spel 整合到创建 XML 或 annotation-based bean 定义中,如表达式支持定义 bean 定义所示。

本章介绍表达式语言的 features,其 API 及其语言语法。在几个地方,InventorSociety classes 用作表达式 evaluation 的目标 objects。这些 class 声明和用于填充它们的数据列在本章末尾。

表达式语言支持以下功能:

  • 文字表达
  • Boolean 和关系 operators
  • 常用表达
  • Class 表达式
  • 访问 properties,数组,lists 和 maps
  • 方法调用
  • 关系运算符
  • 分配
  • 调用构造函数
  • Bean references
  • Array 建设
  • 内联列表
  • 内联 maps
  • 三元运营商
  • 变量
  • User-defined 功能
  • 收集投影
  • 收藏品选择
  • 模板化的表达

Evaluation

本节介绍 SpEL 接口及其表达式语言的简单使用。完整的语言 reference 可以在语言参考中找到。

以下 code 引入了 SpEL API 来评估文字 string 表达式Hello World

1
2
3
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
1 消息变量的 value 是'Hello World'

您最有可能使用的 SpEL classes 和接口位于org.springframework.expression包及其 sub-packages 中,例如spel.support

ExpressionParser接口负责解析表达式 string。在前面的 example 中,表达式 string 是由周围的单引号表示的 string 文字。 Expression接口负责评估先前定义的表达式 string。分别调用parser.parseExpressionexp.getValue时,可以抛出两个 exceptions,ParseExceptionEvaluationException

SpEL 支持各种 features,例如调用方法,访问 properties 和调用构造函数。

在下面的方法调用示例中,我们在 string 文字上调用concat方法:

1
2
3
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();
1 message的值现在是’Hello World!’。

以下_示例调用 JavaBean property calls String property Bytes

1
2
3
4
5
ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();
1 此 line 将文字转换为字节 array。

SpEL 还通过使用标准点表示法(例如prop1.prop2.prop3)和 property 值的设置来支持嵌套的 properties。也可以访问公共字段。以下 example 显示了如何使用点表示法来获取文字的长度:

1
2
3
4
5
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给出了文字的长度。

可以调用 String 的构造函数而不是使用 string 文字,如下面的 example 所示:

1
2
3
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);
1 从文字构造一个新的String并使其成为大写。

请注意泛型方法的使用:public <T> T getValue(Class<T> desiredResultType)。使用此方法无需将表达式的 value 强制转换为所需的结果类型。如果无法将 value 强制转换为T类型或使用已注册的类型转换器进行转换,则抛出EvaluationException

SpEL 的更常见用法是提供一个表达式 string,该表达式是针对特定的 object 实例(称为 root object)进行评估的。以下 example 显示了如何从Inventor class 的实例检索name property 或创建 boolean 条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression("name"); (1)
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"

exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
1 name解析为表达式。

了解EvaluationContext

在评估表达式以解析 properties,方法或字段以及帮助执行类型转换时,将使用EvaluationContext接口。 Spring 提供了两个实现。

  • SimpleEvaluationContext:为不需要 SpEL 语言语法的完整范围的表达式类别公开必要的 SpEL 语言 features 和 configuration 选项的子集,并且应该有意义地限制它们。示例包括但不限于数据 binding 表达式和 property-based 过滤器。
  • StandardEvaluationContext:公开全套 SpEL 语言 features 和 configuration 选项。您可以使用它来指定默认的根 object 并配置每个可用的 evaluation-related 策略。

SimpleEvaluationContext旨在仅支持 SpEL 语言语法的子集。它排除了 Java 类型 references,构造函数和 bean references。它还要求您明确选择表达式中 properties 和方法的 level 支持。默认情况下,create()静态工厂方法仅启用对 properties 的读访问权限。您还可以获取构建器以配置所需支持的确切 level,并针对以下一项或多项组合:

  • 仅自定义PropertyAccessor(无反射)
  • read-only 访问的数据 binding properties
  • 数据 binding properties 用于读写

类型转换

默认情况下,SpEL 使用 Spring 核心(org.springframework.core.convert.ConversionService)中提供的转换服务。此转换服务附带了许多用于 common 转换的 built-in 转换器,但也是完全可扩展的,因此您可以在类型之间添加自定义转换。另外,它是 generics-aware。这意味着,当您在表达式中使用泛型类型时,SpEL 会尝试转换以维护它遇到的任何 objects 的类型正确性。

这在实践中意味着什么?假设使用setValue()的赋值用于设置List property。 property 的类型实际上是List<Boolean>。 SpEL 识别出列表中的元素在放入之前需要转换为Boolean。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Simple {
public List<Boolean> booleanList = new ArrayList<Boolean>();
}

Simple simple = new Simple();
simple.booleanList.add(true);

EvaluationContext context = SimpleEvaluationContext().forReadOnlyDataBinding().build();

// false is passed in here as a string. SpEL and the conversion service
// correctly recognize that it needs to be a Boolean and convert it
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");

// b is false
Boolean b = simple.booleanList.get(0);

解析器配置

可以使用解析器 configuration object(org.springframework.expression.spel.SpelParserConfiguration)配置 SpEL 表达式解析器。 configuration object 控制某些表达式组件的行为。对于 example,如果索引到 array 或集合并且指定索引处的元素是null,则可以自动创建该元素。当使用由 property references 链组成的表达式时,这很有用。如果索引到 array 或列表并指定超出 array 或列表当前大小末尾的索引,则可以自动增大 array 或 list 以适应该索引。以下 example 演示了如何自动增长列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

SpEL编译

Spring Framework 4.1 包含一个基本的表达式编译器。表达式通常被解释,在 evaluation 期间提供了很多动态灵活性,但是没有提供最佳的 performance。对于偶尔的表达式使用,这很好,但是,当其他组件(如 Spring Integration)使用时,performance 非常重要,并且不需要动态。

SpEL 编译器旨在满足此需求。在 evaluation 期间,编译器生成一个真正的 Java class,它体现了表达式行为,并使用它来实现更快的表达式 evaluation。由于缺少表达式的 typing,编译器使用在执行编译时对表达式的解释评估期间收集的信息。对于 example,它不完全从表达式中知道 property reference 的类型,但是,在第一次解释 evaluation 期间,它会发现它是什么。当然,如果各种表达式元素的类型在 time 内发生变化,那么基于此信息的编译可能会导致以后出现问题。因此,编译最适合于类型信息在重复评估时不会改变的表达式。

请考虑以下基本表达式:

1
someArray[0].someProperty.someOtherProperty < 0.1

因为前面的表达式涉及 array 访问,一些 property de-referencing 和数值运算,所以 performance 增益非常明显。在一个 50000 次迭代的 example 微基准运行中,使用 interpreter 进行评估需要 75ms,使用表达式的编译 version 只需要 3ms。

编译器配置

默认情况下,编译器未打开,但您可以通过两种不同的方式打开它。您可以使用解析器 configuration process(之前讨论过)或在将 SpEL 用法嵌入到另一个 component 中时使用系统 property 来打开它。本节讨论这两个选项。

编译器可以以三种模式之一运行,这些模式在org.springframework.expression.spel.SpelCompilerMode enum 中捕获。模式如下:

  • OFF(默认值):编译器已关闭。
  • IMMEDIATE:在立即模式下,表达式会尽快编译。这通常是在第一次解释 evaluation 之后。如果编译的表达式失败(通常是由于类型更改,如前所述),则表达式 evaluation 的调用者会收到 exception。
  • MIXED:在混合模式下,表达式在 time 上以静默方式在解释和编译模式之间切换。经过一定数量的解释运行后,它们会切换到编译形式,如果编译后的表单出现问题(如类型更改,如前所述),表达式会自动再次切换回解释形式。稍后,它可能会生成另一个编译的表单并切换到它。基本上,用户在IMMEDIATE模式下获得的 exception 是在内部处理的。

存在IMMEDIATE模式,因为MIXED模式可能会导致具有副作用的表达式出现问题。如果编译后的表达式在部分成功后爆炸,则可能已经完成了影响系统 state 的事情。如果发生这种情况,调用者可能不希望它在解释模式下静默 re-run,因为表达式的一部分可能会运行两次。

选择模式后,使用SpelParserConfiguration配置解析器。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
this.getClass().getClassLoader());

SpelExpressionParser parser = new SpelExpressionParser(config);

Expression expr = parser.parseExpression("payload");

MyMessage message = new MyMessage();

Object payload = expr.getValue(message);

指定编译器模式时,还可以指定类加载器(允许传递 null)。编译表达式在提供的任何提供的 child 类加载器中定义。重要的是要确保,如果指定了类加载器,它可以看到表达式 evaluation process 中涉及的所有类型。如果未指定类加载器,则使用默认的类加载器(通常是在表达式 evaluation 期间 running 的线程的 context 类加载器)。

配置编译器的第二种方法是在 SpEL 嵌入到某个其他 component 中时使用,并且可能无法通过 configuration object 对其进行配置。在这些情况下,可以使用 system property。您可以将spring.expression.compiler.mode property 设置为SpelCompilerMode enum 值之一(offimmediatemixed)。

编译器限制

自 Spring Framework 4.1 以来,基本的编译 framework 就位。但是,framework 还不支持编译每种表达式。最初的重点是可能在 performance-critical 上下文中使用的 common 表达式。无法在 moment 中编译以下类型的表达式:

  • 涉及转让的表达
  • 表达式依赖于转换服务
  • 表达式使用自定义解析器或访问器
  • 使用选择或投影的表达式

将来可以编写更多类型的表达式。

Bean定义中的表达式

您可以将带有 XML-based 或 annotation-based configuration 元数据的 SpEL 表达式用于定义BeanDefinition实例。在这两种情况下,定义表达式的语法都是#{ <expression string> }形式。

XML配置

可以使用表达式设置 property 或构造函数参数 value,如下面的 example 所示:

1
2
3
4
5
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

<!-- other properties -->
</bean>

systemProperties变量是预定义的,因此您可以在表达式中使用它,如下面的 example 所示:

1
2
3
4
5
<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

<!-- other properties -->
</bean>

请注意,您不必在此 context 中使用#符号为预定义变量添加前缀。

您还可以通过 name 引用其他 bean properties,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
<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>

注解配置

要指定默认 value,可以将@Value annotation 放在字段,方法和方法或构造函数参数上。

以下 example _set 字段变量的默认 value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static class FieldValueTestBean

@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;

public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}

public String getDefaultLocale() {
return this.defaultLocale;
}

}

以下 example 显示了等效但在 property setter 方法上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static class PropertyValueTestBean

private String defaultLocale;

@Value("#{ systemProperties['user.region'] }")
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}

public String getDefaultLocale() {
return this.defaultLocale;
}

}

自动化方法和构造函数也可以使用@Value annotation,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SimpleMovieLister {

private MovieFinder movieFinder;
private String defaultLocale;

@Autowired
public void configure(MovieFinder movieFinder,
@Value("#{ systemProperties['user.region'] }") String defaultLocale) {
this.movieFinder = movieFinder;
this.defaultLocale = defaultLocale;
}

// ...
}
public class MovieRecommender {

private String defaultLocale;

private CustomerPreferenceDao customerPreferenceDao;

@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
@Value("#{systemProperties['user.country']}") String defaultLocale) {
this.customerPreferenceDao = customerPreferenceDao;
this.defaultLocale = defaultLocale;
}

// ...
}

语言参考

本节介绍 Spring 表达式语言的工作原理。它包括以下主题:

字面值表达式

支持的文字表达式的类型是 strings,数值(int,real,hex), boolean 和 null。 Strings 由单引号分隔。要在 string 中放置单引号,请使用两个单引号字符。

以下清单显示了 literals 的简单用法。通常,它们不是像这样单独使用,而是作为更复杂表达式的一部分使用 - 例如,在逻辑比较 operator 的一侧使用文字。

1
2
3
4
5
6
7
8
9
10
11
12
13
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();

Numbers 支持使用负号,指数表示法和 decimal 点。默认情况下,使用 Double.parseDouble()解析实 numbers。

Properties,Arrays,Lists,Maps和Indexers

使用 property references 进行导航很容易。为此,请使用句点指示嵌套的 property value。 Inventor class,pupintesla的实例填充了示例中使用的类部分中列出的数据。为了“向下”导航并获得特斯拉的出生年份和普平的出生城市,我们使用以下表达式:

1
2
3
4
// evals to 1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);

property 名称的第一个字母允许不区分大小写。数组和 lists 的内容是使用方括号表示法获得的,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);

maps 的内容是通过在括号内指定文字 key value 获得的。在下面的 example 中,因为Officers map 的键是 strings,我们可以指定 string literals:

1
2
3
4
5
6
7
8
9
10
11
12
// 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");

内联列表

您可以使用{}表示法直接在表达式中表达 lists。

1
2
3
4
// 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);

{}本身表示空列表。出于性能原因,如果列表本身完全由固定的_literal 组成,则会创建一个常量列表来表示表达式(而不是在每个 evaluation 上构建一个新列表)。

内联Maps

您还可以使用{key:value}表示法直接在表达式中表达 maps。以下 example 显示了如何执行此操作:

1
2
3
4
// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);

Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);

{:}本身表示空 map。出于性能原因,如果 map 本身由固定的_lite 或其他嵌套的常量结构(lists 或 maps)组成,则会创建一个常量 map 来表示表达式(而不是在每个 evaluation 上构建一个新的 map)。引用 map 键是可选的。上面的示例不使用带引号的键。

数组构造

您可以使用熟悉的 Java 语法 build 数组,可选地提供初始化程序以在构造 time 填充 array。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
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);

在构造 multi-dimensional array 时,当前无法提供初始化程序。

方法

您可以使用典型的 Java 编程语法调用方法。您还可以在 literals 上调用方法。还支持变量 arguments。以下示例显示了如何调用方法:

1
2
3
4
5
6
// 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);

Operators

Spring 表达式语言支持以下类型的 operators:

关系运算符

使用标准 operator 表示法支持关系 operators(等于,不等于,小于,小于或等于,大于,大于或等于)。以下列表显示了一些 operators 的示例:

1
2
3
4
5
6
7
8
// 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);

和 less-than 与null的比较遵循一个简单的规则:null被视为空(不是零)。因此,任何其他 value 总是大于null(X > null总是true),并且没有其他 value 小于零(X < null总是false)。

如果您更喜欢数字比较,请避免 number-based null比较,以支持与零进行比较(对于 example,X > 0X < 0)。

除了标准的关系 operators 之外,SpEL 还支持instanceof和常规 expression-based matches operator。以下列表显示了两者的示例:

1
2
3
4
5
6
7
8
9
10
11
// 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);

小心原始类型,因为它们立即被装箱到 wrapper 类型,所以1 instanceof T(int)计算为false1 instanceof T(Integer)计算为true,如预期的那样。

每个符号 operator 也可以指定为纯字母等价物。这避免了所使用的符号对于嵌入表达式的文档类型具有特殊含义的问题(例如在 XML 文档中)。文本等价物是:

  • lt ( < )
  • gt ( > )
  • le ( <= )
  • ge ( >= )
  • eq ( == )
  • ne ( != )
  • div ( / )
  • mod ( % )
  • not ( ! ).

所有文本 operators 都是 case-insensitive。

逻辑运算符

SpEL 支持以下逻辑 operators:

  • and
  • or
  • not

以下 example 显示了如何使用逻辑 operators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// -- 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);

数学运算符

您可以在 numbers 和 strings 上使用 addition operator。您只能在 numbers 上使用减法,乘法和除法运算符。您还可以使用模数(%)和指数幂(^)运算符。标准 operator 优先级被强制执行。以下 example 显示了正在使用的数学 operators:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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

作业运算符

要设置 property,请使用赋值 operator(=)。这通常在对setValue的调用中完成,但也可以在对getValue的调用内完成。以下清单显示了使用赋值 operator 的两种方法:

1
2
3
4
5
6
7
8
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);

类型

您可以使用特殊T operator 指定java.lang.Class(类型)的实例。使用此 operator 也可以调用静态方法。 StandardEvaluationContext使用TypeLocator查找类型,StandardTypeLocator(可以替换)是在了解java.lang包的情况下构建的。这意味着对java.lang中的类型的T() references 不需要完全限定,但所有其他类型的 references 必须是。以下 example 显示了如何使用T operator:

1
2
3
4
5
6
7
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);

构造函数

您可以使用new operator 调用构造函数。除了基本类型(intfloat等)和 String 之外,您应该使用完全限定的 class name。以下 example 显示了如何使用new operator 来调用构造函数:

1
2
3
4
5
6
7
8
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);

变量

您可以使用#variableName语法在表达式中引用变量。通过在EvaluationContext implementations 上使用setVariable方法设置变量。以下 example 显示了如何使用变量:

1
2
3
4
5
6
7
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName()) // "Mike Tesla"

#this和#root变量

始终定义#this变量并引用当前 evaluation object(解析非限定 references 的对象)。始终定义#root变量并引用根 context object。虽然#this可能会随着表达式的组件的计算而变化,但#root总是指根。以下示例显示如何使用#this#root变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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);

功能

您可以通过注册可在表达式 string 中调用的 user-defined 函数来扩展 SpEL。 function 通过EvaluationContext注册。以下 example 显示了如何注册 user-defined function:

1
2
3
4
Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

对于 example,请考虑以下实用方法来反转 string:

1
2
3
4
5
6
7
8
9
10
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();
}
}

然后,您可以注册并使用上述方法,如下面的示例所示:

1
2
3
4
5
6
7
8
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);

BeanReferences

如果 evaluation context 配置了 bean 解析程序,则可以使用@符号从表达式中查找 beans。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);

要访问工厂 bean 本身,您应该在 bean name 前面加上&符号。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
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);

三元操作员(If-Then-Else)

您可以使用三元 operator 在表达式中执行 if-then-else 条件逻辑。以下清单显示了一个最小的 example:

1
2
String falseString = parser.parseExpression(
"false ? 'trueExp' : 'falseExp'").getValue(String.class);

在这种情况下,boolean false会返回 string value 'falseExp'。一个更现实的例子如下:

1
2
3
4
5
6
7
8
9
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"

有关三元 operator 的更短语法,请参阅 Elvis operator 的下一节。

ElvisOperator

Elvis operator 是三元 operator 语法的缩写,用于Groovy语言。使用三元 operator 语法,您通常必须重复两次变量,如下面的 example 所示:

1
2
String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

相反,您可以使用 Elvis operator(以 Elvis’发型的相似性命名)。以下 example 显示了如何使用 Elvis operator:

1
2
3
4
ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(String.class);
System.out.println(name); // 'Unknown'

以下列表显示了一个更复杂的 example:

1
2
3
4
5
6
7
8
9
10
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

您可以使用 Elvis operator 在表达式中应用默认值。 folloiwng example 显示了如何在@Value表达式中使用 Elvis operator:

1
@Value("#{systemProperties['pop3.port'] ?: 25}")

这将 inject 一个系统 property pop3.port如果它被定义,或者如果没有,则为 25。

安全导航运算符

安全导航 operator 用于避免NullPointerException并来自Groovy语言。通常,当您对 object 具有 reference 时,您可能需要在访问 object 的方法或 properties 之前验证它是否为 null。为避免这种情况,安全导航 operator 返回 null 而不是抛出 exception。以下 example 显示了如何使用安全导航 operator:

1
2
3
4
5
6
7
8
9
10
11
12
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!!!

集合选择

Selection 是一种功能强大的表达式语言 feature,它允许您通过从其条目中进行选择将源集合转换为另一个集合。

Selection 使用.?[selectionExpression]的语法。它过滤集合并返回包含原始元素子集的新集合。例如,选择让我们可以轻松获得塞尔维亚发明家的列表,如下例所示:

1
2
List<Inventor> list = (List<Inventor>) parser.parseExpression(
"Members.?[Nationality == 'Serbian']").getValue(societyContext);

可以在 lists 和 maps 上进行选择。对于列表,将针对每个单独的列表元素评估选择标准。对于 map,将针对每个 map 条目(Java 类型Map.Entry的 objects)评估选择条件。每个 map 条目的 key 和 value 都可以作为 properties 访问,以便在选择中使用。

下面的表达式返回一个新的 map,它由原始 map 的那些元素组成,其中条目 value 小于 27:

1
Map newMap = parser.parseExpression("map.?[value<27]").getValue();

除了返回所有选定元素外,您还可以只检索第一个或最后一个 value。要获得与选择匹配的第一个条目,语法为.^[selectionExpression]。要获取最后一个匹配选择,语法为.$[selectionExpression]

集合投影

Projection 允许集合驱动 sub-expression 的 evaluation,结果是一个新的集合。投影的语法是.![projectionExpression]。例如,假设我们有一个发明者列表,但想要他们出生的城市列表。实际上,我们想要为发明人列表中的每个条目评估“placeOfBirth.city”。以下 example 使用投影来执行此操作:

1
2
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");

您还可以使用 map 来驱动投影,在这种情况下,将根据 map 中的每个条目(表示为 Java Map.Entry)来计算投影表达式。跨 map 的投影结果是一个列表,其中包含针对每个 map 条目的投影表达式的 evaluation。

表达模板

表达式模板允许将文本文本与一个或多个 evaluation 块混合。每个 evaluation 块都使用您可以定义的前缀和后缀字符分隔。 common 的选择是使用#{ }作为分隔符,如下面的 example 所示:

1
2
3
4
5
String randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"

string 是通过将文本文本'random number is '与评估#{ }分隔符内表达式的结果(在这种情况下,调用random()方法的结果)连接来计算的。 parseExpression()方法的第二个参数是ParserContext类型。 ParserContext接口用于影响在 order 中解析表达式的方式,以支持表达式模板功能。 TemplateParserContext的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TemplateParserContext implements ParserContext {

public String getExpressionPrefix() {
return "#{";
}

public String getExpressionSuffix() {
return "}";
}

public boolean isTemplate() {
return true;
}
}

Classes在示例中使用

本节列出了本章中示例中使用的 classes。

示例 1. Inventor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package org.spring.samples.spel.inventor;

import java.util.Date;
import java.util.GregorianCalendar;

public class Inventor {

private String name;
private String nationality;
private String[] inventions;
private Date birthdate;
private PlaceOfBirth placeOfBirth;

public Inventor(String name, String nationality) {
GregorianCalendar c= new GregorianCalendar();
this.name = name;
this.nationality = nationality;
this.birthdate = c.getTime();
}

public Inventor(String name, Date birthdate, String nationality) {
this.name = name;
this.nationality = nationality;
this.birthdate = birthdate;
}

public Inventor() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getNationality() {
return nationality;
}

public void setNationality(String nationality) {
this.nationality = nationality;
}

public Date getBirthdate() {
return birthdate;
}

public void setBirthdate(Date birthdate) {
this.birthdate = birthdate;
}

public PlaceOfBirth getPlaceOfBirth() {
return placeOfBirth;
}

public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
this.placeOfBirth = placeOfBirth;
}

public void setInventions(String[] inventions) {
this.inventions = inventions;
}

public String[] getInventions() {
return inventions;
}
}

示例 2. PlaceOfBirth.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package org.spring.samples.spel.inventor;

public class PlaceOfBirth {

private String city;
private String country;

public PlaceOfBirth(String city) {
this.city=city;
}

public PlaceOfBirth(String city, String country) {
this(city);
this.country = country;
}

public String getCity() {
return city;
}

public void setCity(String s) {
this.city = s;
}

public String getCountry() {
return country;
}

public void setCountry(String country) {
this.country = country;
}

}

示例 3. Society.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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;
}

}

使用Spring进行面向切面编程

Aspect-oriented 编程(AOP)通过提供另一种思考程序结构的方式来补充 Object-oriented 编程(OOP)。 OOP 中的 key 模块化单元是 class,而在 AOP 中,模块化单元是 aspect。方面可以实现跨越多种类型和对象的关注点(例如 transaction management)的模块化。 (这种担忧在 AOP 中通常被称为“横切”问题 literature.)

Spring 的 key 组件之一是 AOP framework。虽然 Spring IoC 容器不依赖于 AOP(意味着如果您不想使用 AOP,则不需要使用 AOP),AOP 补充了 Spring IoC 以提供非常强大的中间件解决方案。

Spring 2.0 AOP

Spring 2.0 引入了一种更简单,更强大的方法,通过使用schema-based 接近@AspectJ 注释风格来编写自定义方面。这两种样式都提供完全类型的建议和使用 AspectJ 切入点语言,同时仍然使用 Spring AOP 进行编织。

本章讨论 Spring 2.0 schema-和@AspectJ-based AOP 支持。 A支持,如 Spring 1.2 applications 中常见的那样,在以下章节中讨论。

AOP 在 Spring Framework 中用于:

  • 提供声明性企业服务,尤其是作为 EJB 声明性服务的替代品。最重要的服务是声明式事务管理
  • 让用户实现自定义方面,补充他们使用 AOP 的 OOP。

如果您只对通用声明性服务或其他 pre-packaged 声明性中间件服务(如池)感兴趣,则无需直接使用 Spring AOP,并且可以跳过本章的大部分内容。

AOP概念

让我们首先定义一些中心 AOP 概念和术语。这些条款不是 Spring-specific。不幸的是,AOP 术语不是特别直观。但是,如果 Spring 使用自己的术语,那将更加令人困惑。

  • Aspect:跨越多个 classes 的关注点的模块化。事务管理是企业 Java applications 中横切关注点的一个很好的例子。在 Spring AOP 中,方面是使用常规 classes(schema-based 接近或使用@Aspect annotation(@AspectJ 风格注释的常规 classes 实现的。
  • 连接点:程序执行期间的一个点,例如方法的执行或 exception 的处理。在 Spring AOP 中,连接点始终表示方法执行。
  • 通知:aspect 在特定连接点采取的操作。不同类型的建议包括“周围”,“之前”和“之后”建议。 (建议类型被讨论 later.)许多 AOP 框架,包括 Spring,model 作为拦截器的建议,并在连接点周围维护一系列拦截器。
  • 切入点:匹配连接点的谓词。建议与切入点表达式相关联,并在切入点匹配的任何连接点处运行(对于 example,执行具有特定 name 的方法)。由切入点表达式匹配的连接点的概念是 AOP 的核心,而 Spring 默认使用 AspectJ 切入点表达式语言。
  • 简介:代表类型声明其他方法或字段。 Spring AOP 允许您向任何建议的 object 引入新接口(以及相应的 implementation)。对于 example,您可以使用简介使 bean 实现IsModified接口,以简化缓存。 (引言在 AspectJ 中被称为 inter-type 声明 community.)
  • 目标对象:由一个或多个方面建议的 object。也称为“建议对象”。由于 Spring AOP 是使用运行时代理实现的,因此 object 始终是代理 object。
  • AOP 代理:由 order 中的 AOP framework 创建的 object,用于实现 aspect contracts(建议方法执行等)。在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
  • 编织:将方面与其他 application 类型或 objects 链接以创建一个建议的 object。这可以在 compile time(使用 AspectJ 编译器,example),load time 或运行时完成。与其他纯 Java AOP 框架一样,Spring AOP 在运行时执行编织。

Spring AOP 包括以下类型的通知:

  • 前置通知:在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它抛出 exception)。
  • 后置通知:在连接点正常完成后通知为 run(对于 example,如果方法返回而不抛出 exception)。
  • 异常通知:如果方法通过抛出 exception 退出,则执行通知。
  • 最终通知:无论连接点退出的方式(正常或异常 return),都要执行通知。
  • 围绕通知:围绕连接点的通知,例如方法调用。这是最有力的通知。 around 通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续加入点还是通过返回自己的 return value 或抛出 exception 来快速通知的方法执行。

围绕通知是最普遍的通知。由于 Spring AOP(如 AspectJ)提供了全方位的通知类型,因此我们建议您使用可以实现所需行为的最不强大的通知类型。例如,如果您只需要使用方法的 return value 更新缓存,那么最好实现返回后的通知,而不是周围的通知,尽管周围的通知可以完成同样的事情。使用最具体的建议类型可以提供更简单的编程模型,减少错误的可能性。对于 example,您不需要在用于 around 通知的JoinPoint上调用proceed()方法,因此,您无法调用它。

在 Spring 2.0 中,所有通知参数都是静态类型的,因此您可以使用相应类型的通知参数(来自 example 的方法执行的 return value 的类型)而不是Object数组。

由切入点匹配的连接点的概念是 AOP 的 key,它将其与仅提供拦截的旧技术区分开来。切入点使得建议可以独立于 object-oriented 层次结构进行定向。例如,您可以将一个提供声明性事务管理的建议应用于 span 多个 objects(例如服务层中的所有业务操作)的一组方法。

SpringAOP能力和目标

Spring AOP 是用纯 Java 实现的。不需要特殊的编译 process。 Spring AOP 不需要控制 class 加载器层次结构,因此适合在 servlet 容器或 application 服务器中使用。

Spring AOP 目前仅支持方法执行连接点(建议在 Spring beans 上执行方法)。虽然可以在不破坏核心 Spring AOP API 的情况下添加对字段拦截的支持,但未实现字段拦截。如果您需要建议字段访问和更新连接点,请考虑使用 AspectJ 等语言。

Spring AOP 的 AOP 方法与大多数其他 AOP 框架的方法不同。目的不是提供最完整的 AOP implementation(尽管 Spring AOP 非常强大)。相反,目的是在 AOP implementation 和 Spring IoC 之间提供紧密的整合,以帮助解决企业应用中的 common 问题。

因此,例如,Spring Framework 的 AOP 功能通常与 Spring IoC 容器一起使用。通过使用普通 bean 定义语法来配置方面(尽管这允许强大的“auto-proxying”功能)。这是与其他 AOP implementations 的重要区别。使用 Spring AOP 无法轻松或有效地完成某些操作,例如建议非常 fine-grained objects(通常是域 objects)。在这种情况下,AspectJ 是最佳选择。但是,我们的经验是 Spring AOP 为适合 AOP 的企业 Java 应用程序中的大多数问题提供了出色的解决方案。

Spring AOP 从未努力与 AspectJ 竞争,以提供全面的 AOP 解决方案。我们认为,像 Spring AOP 这样的 proxy-based 框架和像 AspectJ 这样的 full-blown 框架都是有价值的,它们是互补的,而不是竞争。 Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,以在一致的 Spring-based application architecture 中实现 AOP 的所有使用。此 integration 不会影响 Spring AOP API 或 AOP Alliance API。 Spring AOP 仍为 backward-compatible。有关 Spring AOP API 的讨论,请参阅以下章节

Spring Framework 的核心原则之一是 non-invasiveness。这是 idea,您不应该被迫将 framework-specific classes 和接口引入您的业务或域 model。但是,在某些地方,Spring Framework 确实为您提供了将 Spring Framework-specific 依赖项引入代码库的选项。为您提供此类选项的基本原理是,在某些情况下,以这种方式读取或编码某些特定功能可能更容易。但是,Spring Framework(几乎)始终为您提供选择:您可以自由决定哪种选项最适合您的特定用例或场景。

与本章相关的一个选择是选择哪种 AOP framework(以及哪种 AOP 样式)。您可以选择 AspectJ,Spring AOP 或两者。您还可以选择 @AspectJ annotation-style 方法或 Spring XML configuration-style 方法。本章选择首先介绍@AspectJ-style 方法的事实不应被视为 Spring 团队倾向于 @AspectJ annotation-style 方法而不是 Spring XML configuration-style。

有关每种样式的“为什么和为何如此”的更完整的讨论,请参见选择要使用的 AOP 声明样式

AOP代理

Spring AOP 默认使用 AOP 代理的标准 JDK 动态代理。这使得任何接口(或接口集)都可以被代理。

Spring AOP 也可以使用 CGLIB 代理。这对代理 classes 而不是接口是必要的。默认情况下,如果 business object 没有实现接口,则使用 CGLIB。由于编程接口而不是 classes 是一种好的做法,因此业务 classes 通常会实现一个或多个业务接口。在那些(希望很少见的)情况下,你需要建议一个未在接口上声明的方法,或者你需要将一个代理的 object 作为一个具体类型传递给一个方法,这可能是强制使用 CGLIB

掌握 Spring AOP 是 proxy-based 的事实很重要。请参阅了解 AOP 代理以详细了解此实现详细信息的实际含义。

AspectJ注解支持

@AspectJ 指的是将方面声明为使用 annotations 注释的常规 Java classes 的样式。作为 AspectJ 5 版本的一部分,AspectJ 项目引入了 @AspectJ 样式。 Spring 使用 AspectJ 提供的 library 解释与 AspectJ 5 相同的注释,用于切入点解析和匹配。但是,AOP 运行时仍然是纯粹的 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

使用 AspectJ 编译器和 weaver 可以使用完整的 AspectJ 语言,并在将 AspectJ 与 Spring Applications 一起使用中讨论。

启用AspectJ注解支持

要在 Spring configuration 中使用 @AspectJ 方面,您需要启用 Spring 支持,以根据 @AspectJ 方面配置 Spring AOP,并根据这些方面是否建议 auto-proxying beans。通过 auto-proxying,我们的意思是,如果 Spring 确定 bean 被一个或多个方面建议,它会自动为该 bean 生成一个代理来拦截方法调用,并确保根据需要执行建议。

可以使用 XML-或 Java-style configuration 启用 @AspectJ 支持。在任何一种情况下,您还需要确保 AspectJ 的aspectjweaver.jar library 位于 application 的 classpath(version 1.8 或更高版本)。这个 library 可以在 AspectJ 发行版的lib目录中找到,也可以从 Maven Central repository 中找到。

使用JavaConfiguration启用AspectJ注解支持

要使用 Java @Configuration启用 @AspectJ 支持,请添加@EnableAspectJAutoProxy annotation,如下面的 example 所示:

1
2
3
4
5
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

使用XMLConfiguration启用AspectJ注解支持

要使用 XML-based configuration 启用 @AspectJ 支持,请使用aop:aspectj-autoproxy元素,如下面的 example 所示:

1
<aop:aspectj-autoproxy/>

这假定您使用_s中描述的 schema 支持。有关如何 import aop命名空间中的标记,请参阅AOP schema

声明Aspect

启用 @AspectJ 支持后,_appring context 中定义的任何带有 class @AspectJ aspect(具有@Aspect annotation)的 bean 将由 Spring 自动检测并用于配置 Spring AOP。接下来的两个示例显示了 not-very-useful aspect 所需的最小定义。

两个 example 中的第一个在 application context 中显示了一个常规的 bean 定义,该定义指向具有@Aspect annotation 的 bean class:

1
2
3
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>

这两个示例中的第二个显示了NotVeryUsefulAspect class 定义,该定义使用org.aspectj.lang.annotation.Aspect annotation 注释;

1
2
3
4
5
6
7
package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

方面(class 用@Aspect注释)可以有方法和字段,与任何其他 class 相同。它们还可以包含切入点,建议和简介(inter-type)声明。

通过 component 扫描自动检测方面 您可以在 Spring XML configuration 中将 aspect classes 注册为常规 beans,或者通过 classpath 扫描自动检测它们 - 与任何其他 Spring-managed bean 相同。但是,请注意@Aspect annotation 不足以在 classpath 中自动检测。为此,您需要添加一个单独的@Component annotation(或者,根据 Spring 的 component 扫描器的规则,可以定义符合条件的自定义构造型 annotation)。

与其他方面的方面建议? 在 Spring AOP 中,方面本身不能成为其他方面建议的目标。 class 上的@Aspect annotation 将其标记为 aspect,因此将其从 auto-proxying 中排除。

声明切入点

切入点确定感兴趣的连接点,从而使我们能够控制建议何时执行。 Spring AOP 仅支持 Spring beans 的方法执行连接点,因此您可以将切入点视为匹配 Spring beans 上方法的执行。切入点声明有两个部分:一个包含 name 的签名和任何参数以及一个精确确定我们感兴趣的方法执行的切入点表达式。在 AOP 的 @AspectJ annotation-style 中,切入点签名由常规方法定义提供,并且通过使用@Pointcut annotation 指示切入点表达式(用作切入点签名的方法必须具有void return 类型)。

__ample 可以帮助区分切入点签名和切入点表达式。以下 example 定义名为anyOldTransfer的切入点,该切入点与名为transfer的任何方法的执行相匹配:

1
2
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

形成@Pointcut annotation 的 value 的切入点表达式是常规的 AspectJ 5 切入点表达式。有关 AspectJ 的切入点语言的完整讨论,请参阅AspectJ 编程指南(和,对于 extensions,AspectJ 5 开发人员的笔记本)或 AspectJ 上的一本书(例如,Colyer 等人的 Eclipse AspectJ,或者 Ramnivas Laddad 的 AspectJ in Action) 。

支持的切入点指示符

Spring AOP 支持以下 AspectJ 切入点指示符(PCD)用于切入点表达式:

  • execution:用于匹配方法执行连接点。这是使用 Spring AOP 时使用的主要切入点指示符。
  • within:限制匹配某些类型中的连接点(使用 Spring AOP 时执行在匹配类型中声明的方法)。
  • this:限制与连接点的匹配(使用 Spring AOP 时执行方法),其中 bean reference(Spring AOP 代理)是给定类型的实例。
  • target:限制与连接点的匹配(使用 Spring AOP 时执行方法),其中目标 object(application object 被代理)是给定类型的实例。
  • args:限制匹配连接点(使用 Spring AOP 时执行方法),其中 arguments 是给定类型的实例。
  • @target:限制与连接点的匹配(使用 Spring AOP 时执行方法),其中执行 object 的 class 具有给定类型的 annotation。
  • @args:限制与连接点的匹配(使用 Spring AOP 时执行方法),其中传递的实际 arguments 的运行时类型具有给定类型的注释。
  • @within:限制匹配到具有给定 annotation 的类型中的连接点(使用 Spring AOP 时执行在具有给定 annotation 的类型中声明的方法)。
  • @annotation:限制连接点的匹配,其中连接点的主题(在 Spring AOP 中执行的方法)具有给定的 annotation。

其他切入点类型

完整的 AspectJ 切入点语言支持 Spring 中不支持的其他切入点指示符:callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@this@withincode。在 Spring AOP 解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException

Spring AOP 支持的切入点指示符集可以在将来的版本中进行扩展,以支持更多的 AspectJ 切入点指示符。

因为 Spring AOP 限制仅与方法执行连接点匹配,所以前面对切入点指示符的讨论给出了比在 AspectJ 编程指南中找到的更窄的定义。此外,AspectJ 本身具有 type-based 语义,并且在执行连接点,thistarget都引用相同的 object:object 执行该方法。 Spring AOP 是一个 proxy-based 系统,它区分代理 object 本身(绑定到this)和代理后面的目标 object(绑定到target)。

由于 Spring 的 AOP framework 的 proxy-based 性质,根据定义,目标 object 中的 calls 不会被截获。对于 JDK 代理,只能拦截代理上的公共接口方法 calls。使用 CGLIB,代理上的公共和受保护方法 calls 被截获(如果需要,甚至是 package-visible 方法)。但是,通过代理的 common 交互应始终通过公共签名进行设计。

请注意,切入点定义通常与任何截获的方法匹配。如果切入点严格意味着 public-only,即使在通过代理进行潜在 non-public 交互的 CGLIB 代理方案中,也需要相应地进行定义。

如果您的拦截需要在目标 class 中包含方法 calls 甚至构造函数,请考虑使用 Spring-driven 原生 AspectJ 编织而不是 Spring 的 proxy-based AOP framework。这构成了具有不同特征的不同 AOP 使用模式,因此在做出决定之前一定要熟悉编织。

Spring AOP 还支持另一个名为bean的 PCD。此 PCD 允许您将连接点的匹配限制为特定的名为 Spring bean 或一组名为 Spring beans(使用通配符时)。 bean PCD 具有以下形式:

1
bean(idOrNameOfBean)

idOrNameOfBean标记可以是任何 Spring bean 的 name。提供了使用*字符的有限通配符支持,因此,如果为 Spring beans 建立了一些命名约定,则可以编写bean PCD 表达式来选择它们。与其他切入点指示符的情况一样,bean PCD 也可以与&&(和),||(或)和!(否定)运算符一起使用。

bean PCD 仅在 Spring AOP 中受支持,而不在原生 AspectJ 编织中受支持。它是 AspectJ 定义的标准 PCD 的 Spring-specific 扩展,因此不适用于@Aspect model 中声明的方面。

bean PCD 在 level 实例(Spring bean name 概念上的 building)上运行,而不是仅在 level 类型上运行(weaving-based AOP 受限)。 Instance-based 切入点指示符是 Spring 的 proxy-based AOP framework 的特殊功能,它与 Spring bean 工厂紧密结合,在 name 中识别特定 beans 是自然而直接的。

组合切入点表达式

您可以使用&&, ||!组合切入点表达式。您还可以通过 name 引用切入点表达式。以下 example 显示了三个切入点表达式:

1
2
3
4
5
6
7
8
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} (1)

@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {} (2)

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} (3)
1 如果方法执行连接点表示任何公共方法的执行,则anyPublicOperation匹配。
2 如果方法执行在交易模块中,inTrading匹配。
3 如果方法执行表示交易模块中的任何公共方法,则tradingOperation匹配。

如前所示,最好的做法是从较小的命名组件中构建更复杂的切入点表达式。当通过 name 引用切入点时,将应用常规 Java 可见性规则(您可以看到相同类型的私有切入点,层次结构中的受保护切入点,任何位置的公共切入点等)。可见性不会影响切入点匹配。

共享Common切入点定义

在使用 enterprise applications 时,开发人员通常希望从几个方面引用 application 的模块和特定的操作集。我们建议定义一个“SystemArchitecture”aspect,为此目的捕获 common 切入点表达式。这样的 aspect 通常类似于以下 example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}

/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}

/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}

/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}

/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}

}

您可以在需要切入点表达式的任何位置引用此类 aspect 中定义的切入点。对于 example,要使服务层 transactional,您可以编写以下内容:

1
2
3
4
5
6
7
8
9
10
11
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>

<aop:config><aop:advisor>元素在Schema-based AOP 支持中讨论。 transaction 元素在Transaction Management中讨论。

例子

Spring AOP 用户可能最常使用execution切入点指示符。执行表达式的格式如下:

1
2
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)

除返回类型 pattern(前面的代码片段中的ret-type-pattern), name pattern 和参数 pattern 之外的所有部分都是可选的。返回类型 pattern 确定方法的 return 类型必须在 order 中才能匹配连接点。 *最常用作返回类型 pattern。它匹配任何 return 类型。仅当方法返回给定类型时,fully-qualified 类型 name 才匹配。 name pattern 与方法 name 匹配。您可以使用*通配符作为 name pattern 的全部或部分。如果指定声明类型 pattern,请包含尾随.以将其连接到 name pattern component。参数 pattern 稍微复杂一些:()匹配不带参数的方法,而(..)匹配任何数量(零个或多个)参数。 (*) pattern 匹配一个接受任何类型的一个参数的方法。 (*,String)匹配一个带两个参数的方法。第一个可以是任何类型,而第二个必须是String。有关更多信息,请参阅 AspectJ 编程指南的语言语义学部分。

以下示例显示了一些 common 切入点表达式:

  • 执行任何公共方法:
1
execution(public * *(..))
  • 执行 name 以set开头的任何方法:
1
execution(* set*(..))
  • 执行AccountService接口定义的任何方法:
1
execution(* com.xyz.service.AccountService.*(..))
  • 执行service包中定义的任何方法:
1
execution(* com.xyz.service.*.*(..))
  • 执行服务包中定义的任何方法或其中一个 sub-packages:
1
execution(* com.xyz.service..*.*(..))
  • 服务包中的任何连接点(仅在 Spring AOP 中执行方法):
1
within(com.xyz.service.*)
  • 服务包中的任何连接点(仅在 Spring AOP 中执行方法)或其中一个 sub-packages:
1
within(com.xyz.service..*)
  • 代理实现AccountService接口的任何连接点(仅在 Spring AOP 中执行方法):
1
this(com.xyz.service.AccountService)

‘this’更常用于 binding 形式。有关如何在建议正文中提供代理 object 的信息,请参阅宣布建议部分。

  • 目标 object 实现AccountService接口的任何连接点(仅在 Spring AOP 中执行方法):
1
target(com.xyz.service.AccountService)

‘target’更常用于 binding 形式。请参阅宣布建议部分,了解如何在建议体中提供目标 object。

  • 采用单个参数的任何连接点(仅在 Spring AOP 中执行方法)以及在运行时传递的参数为Serializable
1
args(java.io.Serializable)

‘args’更常用于 binding 形式。请参阅宣布建议部分,了解如何在建议体中提供方法 arguments。

请注意,此 example 中给出的切入点与execution(* *(java.io.Serializable))不同。如果在运行时传递的参数是Serializable,则 args version 匹配,如果方法签名声明了Serializable类型的单个参数,则执行 version 匹配。

  • 目标 object 具有@Transactional annotation 的任何连接点(仅在 Spring AOP 中执行方法):
1
@target(org.springframework.transaction.annotation.Transactional)

您还可以在 binding 表单中使用“@target”。请参阅宣布建议部分,了解如何在建议体中提供 annotation object。

  • 任何连接点(仅在 Spring AOP 中执行方法),其中目标 object 的声明类型具有@Transactional annotation:
1
@within(org.springframework.transaction.annotation.Transactional)

您还可以在 binding 表单中使用“@within”。请参阅宣布建议部分,了解如何在建议体中提供 annotation object。

  • 任何连接点(仅在 Spring AOP 中执行方法),其中执行方法具有@Transactional annotation:
1
@annotation(org.springframework.transaction.annotation.Transactional)

您还可以在 binding 表单中使用“@annotation”。请参阅宣布建议部分,了解如何在建议体中提供 annotation object。

  • 任何连接点(仅在 Spring AOP 中执行方法),它接受一个参数,并且传递的参数的运行时类型具有@Classified annotation:
1
@args(com.xyz.security.Classified)

您还可以在 binding 表单中使用“@args”。请参阅建议正文中的宣布建议部分如何使 annotation object(s 可用。

  • 名为tradeService的 Spring bean 上的任何连接点(仅在 Spring AOP 中执行方法):
1
bean(tradeService)
  • Spring beans 上的任何连接点(仅在 Spring AOP 中执行方法)具有匹配通配符表达式*Service的名称:
1
bean(*Service)

编写好的切入点

在编译期间,AspectJ 处理 order 中的切入点以优化匹配 performance。检查 code 并确定每个连接点是否匹配(静态或动态)给定的切入点是一个代价高昂的 process。 (动态 match 表示无法通过静态分析完全确定 match,并且在 code 中放置一个测试以确定 code 是否正在运行时是否存在实际的 match)。在第一次遇到切入点声明时,AspectJ 将其重写为匹配 process 的最佳形式。这是什么意思?基本上,切入点在 DNF(析取范式)中重写,并且切入点的组件被排序,以便首先检查那些评估更便宜的组件。这意味着您不必担心理解各种切入点指示符的 performance,并且可以在切入点声明中的任何 order 中提供它们。

但是,AspectJ 只能使用它所说的内容。为了获得最佳的匹配性能,您应该考虑他们想要实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三组:kinded,scoping 和 contextual:

  • Kinded 指示符选择一种特定的连接点:executiongetsetcallhandler
  • 范围指示符选择一组感兴趣的连接点(可能有多种类型):withinwithincode
  • 上下文指示符 match(并且可选地绑定)基于 context:thistarget@annotation

一个写得很好的切入点应至少包括前两种类型(kinded 和 scoping)。您可以根据连接点 context 将上下文指示符包含到 match 中,或者绑定 context 以在建议中使用。由于额外的处理和分析,仅提供一个 kinded 指示符或仅提供上下文指示符,但可能会影响编织 performance(time 和 memory used)。范围界定指示符非常快匹配,并且使用它们意味着 AspectJ 可以非常快速地解除不应该进一步处理的连接点组。如果可能,一个好的切入点应该总是包含一个。

声明通知

建议与切入点表达式相关联,并在切入点匹配的方法执行之前,之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是在适当位置声明的切入点表达式。

前置通知

您可以使用@Before annotation 在 aspect 中的通知之前声明:

1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}

}

如果我们使用 in-place 切入点表达式,我们可以将前面的 example 写为以下 example:

1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}

}

后置通知

返回建议后,匹配的方法执行正常返回。您可以使用@AfterReturning annotation 声明它:

1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}

}

您可以在同一个 aspect 中拥有多个建议声明(以及其他成员)。我们在这些示例中仅显示一个建议声明,以集中每个声明的效果。

有时,您需要在建议正文中访问返回的实际 value。您可以使用绑定 return value 的@AfterReturning形式来获取该访问权限,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}

}

returning属性中使用的 name 必须与 advice 方法中参数的 name 相对应。当方法执行返回时,return value 将作为相应的参数 value 传递给 advice 方法。 returning子句还将匹配仅限于那些 return 指定类型的 value 的方法执行(在这种情况下,Object,它匹配任何 return value)。

请注意,在返回建议后使用时,无法 return 完全不同的 reference。

异常通知

抛出建议运行时,匹配的方法执行通过抛出 exception 退出。您可以使用@AfterThrowing annotation 声明它,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}

}

通常,只有在抛出给定类型的 exceptions 时才需要 run 建议,并且您还经常需要访问通知体中抛出的 exception。您可以使用throwing属性来限制匹配(如果需要,否则使用Throwable作为 exception 类型)并将抛出的 exception 绑定到 advice 参数。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}

}

throwing属性中使用的 name 必须与 advice 方法中参数的 name 相对应。当通过抛出 exception 退出方法时,exception 将作为相应的参数 value 传递给 advice 方法。 throwing子句还将匹配仅限于那些抛出指定类型的 exception 的方法执行(在本例中为DataAccessException)。

最终通知

在匹配的方法执行退出之后(最终)建议运行之后。它是使用@After annotation 声明的。在建议之后必须准备好处理正常和 exception return 条件。它通常用于释放资源和类似目的。以下 example 显示了最终建议后如何使用:

1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}

}

环绕通知

最后一种建议是建议。周围的建议围绕匹配方法的执行运行。它有机会在方法执行之前和之后完成工作,并确定何时,如何,甚至方法实际上都可以执行。如果您需要在方法执行之前和之后以 thread-safe 方式共享 state(启动和停止计时器,对于 example),通常会使用 around 建议。始终使用符合您要求的最不强大的建议形式(也就是说,如果建议之前不要使用建议)。

使用@Around annotation 声明 around 建议。 advice 方法的第一个参数必须是ProceedingJoinPoint类型。在通知的主体内,在ProceedingJoinPoint上调用proceed()会导致执行基础方法。 proceed方法也可以传入Object[]。 array 中的值在进行时用作方法执行的 arguments。

使用Object[]调用时proceed的行为与由 AspectJ 编译器编译的周围建议的proceed行为略有不同。对于使用传统 AspectJ 语言编写的周围建议,传递给proceed的 arguments 的数量必须匹配传递给 around 建议的 arguments 的数量(不是底层连接点采用的 arguments 的数量),并且 value 传递给给定的参数位置取代了 value 所绑定的实体的连接点处的原始 value(如果现在没有意义,请不要担心)。 Spring 采用的方法比其 proxy-based,execution-only 语义更简单,更好匹配。如果编译为 Spring 编写的 @AspectJ 方面并使用带有 ARguments 的proceed与 AspectJ 编译器和 weaver,则只需要知道这种差异。有一种方法可以在 Spring AOP 和 AspectJ 上编写 100%兼容的方面,这在以下有关建议参数的部分中有所讨论。

以下 example 显示了如何使用 around 建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}

}

around 建议返回的 value 是方法调用者看到的 return value。例如,一个简单的缓存 aspect 可以从缓存中返回 value(如果有的话),如果没有则调用proceed()。请注意,proceed可以在 around 建议的主体内调用一次,多次或根本不调用。所有这些都是合法的。

通知参数

Spring 提供了完全类型的建议,这意味着您在建议签名中声明了所需的参数(正如我们之前看到的返回和抛出示例),而不是在 time中使用Object[]数组。我们将在本节后面的内容中看到如何使建议和其他上下文值可用。首先,我们来看看如何编写通用建议,以便了解建议目前建议的方法。

访问当前JoinPoint

任何通知方法都可以声明一个类型为org.aspectj.lang.JoinPoint的参数作为其第一个参数(请注意,需要使用 around 通知声明ProceedingJoinPoint类型的第一个参数,它是JoinPoint的子类.JoinPoint接口提供了许多有用的方法:

  • getArgs():返回方法 arguments。
  • getThis():返回代理 object。
  • getTarget():返回目标 object。
  • getSignature():返回正在建议的方法的描述。
  • toString():打印建议方法的有用描述。

有关详细信息,请参阅javadoc

将参数传递给通知

我们已经看到了如何绑定返回的 value 或 exception value(在返回之后和抛出建议之后使用)。要使参数值可用于建议体,可以使用args的 binding 形式。如果在 args 表达式中使用参数 name 代替类型 name,则在调用通知时,相应参数的 value 将作为参数 value 传递。 示例应该使这更清楚。假设您想建议执行以Account object 作为第一个参数的 DAO 操作,并且您需要访问建议体中的帐户。你可以写下面的内容:

1
2
3
4
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}

切入点表达式的args(account,..)部分有两个目的。首先,它将匹配仅限于那些方法至少采用一个参数的方法执行,而传递给该参数的参数是Account的实例。其次,它通过account参数使实际的Account object 可用于建议。

另一种编写方法是声明一个切入点,当它与连接点匹配时“提供”Account object value,然后从建议中引用指定的切入点。这看起来如下:

1
2
3
4
5
6
7
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}

有关更多详细信息,请参阅 AspectJ 编程指南。

代理 object(this),目标 object(target)和 annotations(@within@target@annotation@args)都可以以类似的方式绑定。接下来的两个示例显示如何匹配使用@Auditable annotation 注释的方法的执行并提取 audit code:

这两个示例中的第一个显示了@Auditable annotation 的定义:

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}

这两个示例中的第二个显示了与@Auditable方法的执行相匹配的建议:

1
2
3
4
5
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
建议参数和泛型

Spring AOP 可以处理 class 声明和方法参数中使用的泛型。假设您有一个如下所示的泛型类型:

1
2
3
4
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}

通过将 advice 参数 typing 到要拦截方法的参数类型,可以将方法类型的拦截限制为某些参数类型:

1
2
3
4
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}

此方法不适用于通用集合。因此,您无法按如下方式定义切入点:

1
2
3
4
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}

为了使这项工作,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理null值。要实现与此类似的操作,您必须将参数键入Collection<?>并手动检查元素的类型。

确定参数名称

通知调用中的参数 binding 依赖于切入点表达式中使用的匹配名称与通知和切入点方法签名中声明的参数名称。参数名称不能通过 Java 反射获得,因此 Spring AOP 使用以下策略来确定参数名称:

  • 如果用户已明确指定参数名称,则使用指定的参数名称。 advice 和 pointcut annotations 都有一个可选的argNames属性,您可以使用它来指定带注释的方法的参数名称。这些参数名称在运行时可用。以下 example 显示了如何使用argNames属性:
1
2
3
4
5
6
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}

如果第一个参数是JoinPointProceedingJoinPointJoinPoint.StaticPart类型,则可以从argNames属性的 value 中省略参数的 name。对于 example,如果修改前面的建议以接收连接点 object,则argNames属性不需要包含它:

1
2
3
4
5
6
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}

JoinPointProceedingJoinPointJoinPoint.StaticPart类型的第一个参数赋予的特殊处理对于不收集任何其他连接点 context 的通知实例特别方便。在这种情况下,您可以省略argNames属性。对于 example,以下建议无需声明argNames属性:

1
2
3
4
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
  • 使用'argNames'属性有点笨拙,所以如果没有指定'argNames'属性,Spring AOP 会查看 class 的调试信息,并尝试从局部变量 table 中确定参数名称。此信息以 long 形式显示,因为 classes 已使用调试信息(至少'-g:vars')进行编译。使用此 flag 进行编译的后果是:(1)您的 code 稍微容易理解(逆向工程),(2)class 文件大小略大(通常无关紧要),(3)优化删除未使用的本地变量未由编译器应用。换句话说,你应该通过 building 与 flag 一起遇到任何困难。

如果即使没有调试信息,AspectJ 编译器(ajc)也编译了 @AspectJ aspect,则无需添加argNames属性,因为编译器会保留所需的信息。

  • 如果在没有必要的调试信息的情况下编译了 code,Spring AOP 会尝试推断 binding 变量与参数的配对(对于 example,如果只有一个变量绑定在切入点表达式中,并且 advice 方法只接受一个参数,那么配对很明显)。如果给定可用信息,变量的 binding 是不明确的,则抛出AmbiguousBindingException
  • 如果上述所有策略都失败,则抛出IllegalArgumentException
继续Arguments

我们之前评论过,我们将描述如何使用 arguments 编写proceed调用,这些调用在 Spring AOP 和 AspectJ 中一致地工作。解决方案是确保通知签名绑定 order 中的每个方法参数。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}

在许多情况下,无论如何都要进行 binding(如前面的 example)。

通知顺序

当多条建议都想在同一个连接点运行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定建议执行的 order。最高优先级的建议首先“在路上”(因此,给出两条之前的建议,优先级最高的建议首先运行)。从连接点出来的“出路”中,最高优先级建议最后运行(因此,给出两条后建议,具有最高优先级的建议将以第二次运行)。

当在不同方面定义的两条建议都需要在同一个连接点运行时,除非另行指定,否则执行的 order 是未定义的。您可以通过指定优先级来控制执行的 order。这是通过在 aspect class 中实现org.springframework.core.Ordered接口或使用Order annotation 注释它,以正常的 Spring 方式完成的。给定两个方面,aspect 从Ordered.getValue()(或 annotation value)返回较低的 value 具有更高的优先级。

当在同一个 aspect 中定义的两条建议都需要在同一个连接点运行时,ordering 是未定义的(因为没有办法通过 javac-compiled classes 的反射来检索声明 order)。考虑将这些建议方法折叠到每个 aspect class 中每个连接点的一个建议方法中,或者将这些建议重构为单独的 aspect classes,您可以在 aspect level 中对其进行排序。

引入

引入(在 AspectJ 中称为 inter-type 声明)使 aspect 能够声明通知的 objects 实现给定的接口,并代表那些 objects 提供该接口的实现。

您可以使用@DeclareParents注解进行引入。此 annotation 用于声明匹配类型具有新的 parent(因此为 name)。对于 example,给定一个名为UsageTracked的接口和一个名为DefaultUsageTracked的接口的实现,以下 aspect 声明服务接口的所有实现者也实现UsageTracked接口(通过 JMX 为 example 公开统计信息):

1
2
3
4
5
6
7
8
9
10
11
12
@Aspect
public class UsageTracking {

@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;

@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}

}

要实现的接口由注释字段的类型确定。 @DeclareParents annotation 的value属性是 AspectJ 类型 pattern。匹配类型的任何 bean 都实现UsageTracked接口。请注意,在前面的 example 的 前置通知中,service beans 可以直接用作UsageTracked接口的实现。如果以编程方式访问 bean,您将编写以下内容:

1
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

Aspect实例化模型

这是一个高级的 topic。如果您刚刚开始使用 AOP,您可以安全地跳过它直到稍后。

默认情况下,application context 中的每个 aspect 都有一个实例。 AspectJ calls singleton instantiation model。可以使用备用生命周期定义方面。 Spring 支持 AspectJ 的perthispertarget实例化模型(目前不支持percflow, percflowbelow,pertypewithin)。

您可以通过在@Aspect annotation 中指定perthis子句来声明perthis aspect。考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {

private int someState;

@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}

}

在前面的示例中,'perthis'子句的作用是为执行业务服务的每个唯一服务 object 创建一个 aspect 实例(每个唯一的 object 在切入点表达式匹配的连接点处绑定到’this’)。 aspect 实例是在服务 object 上调用方法的第一个 time 创建的。当服务 object 超出范围时,aspect 超出范围。在创建 aspect 实例之前,不执行其中的任何建议。一旦创建了 aspect 实例,其中声明的建议就会在匹配的连接点执行,但仅当服务 object 是与此 aspect 关联的服务时才执行。有关per子句的更多信息,请参阅 AspectJ 编程指南。

pertarget instantiation model 的工作方式与perthis完全相同,但它会在匹配的连接点为每个唯一目标 object 创建一个 aspect 实例。

AOP例子

现在您已经了解了所有组成部分的工作原理,我们可以将它们放在一起做一些有用的事情。

由于并发问题,业务服务的执行有时会失败(例如,死锁失败者)。如果重试该操作,则可能在下一次尝试时成功。对于适合在这种情况下重试的业务服务(幂等操作不需要返回给用户进行冲突解决),我们希望透明地重试操作以避免 client 看到PessimisticLockingFailureException。这是明确跨越服务层中的多个服务的要求,因此非常适合通过 aspect 实现。

因为我们想要重试操作,所以我们需要使用 around 建议,以便我们可以多次调用proceed。以下清单显示了基本的 aspect implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Aspect
public class ConcurrentOperationExecutor implements Ordered {

private static final int DEFAULT_MAX_RETRIES = 2;

private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;

public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}

public int getOrder() {
return this.order;
}

public void setOrder(int order) {
this.order = order;
}

@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}

}

请注意,aspect 实现Ordered接口,以便我们可以将 aspect 的优先级设置为高于 transaction 建议(我们希望每次 time 时都重新执行 transaction)。 maxRetriesorder properties 都由 Spring 配置。主要行动发生在doConcurrentOperation周围的建议中。请注意,对于 moment,我们将重试逻辑应用于每个businessService()。我们试图继续,如果我们失败PessimisticLockingFailureException,我们再试一次,除非我们已经用尽所有的重试尝试。

相应的 Spring configuration 如下:

1
2
3
4
5
6
<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>

要优化 aspect 以便它只重试幂等操作,我们可以定义以下Idempotent annotation:

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}

然后我们可以使用 annotation 来注释服务操作的 implementation。对 aspect 的更改仅重试幂等操作涉及改进切入点表达式,以便只有@Idempotent操作 match,如下所示:

1
2
3
4
5
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
...
}

基于Schema的AOP支持

如果您更喜欢 XML-based 格式,Spring 还支持使用新的aop名称空间标记定义方面。支持与使用 @AspectJ 样式时完全相同的切入点表达式和建议种类。因此,在本节中,我们将重点放在新语法上,并将 reader 引用到上一节(@AspectJ 支持)中的讨论,以便了解编写切入点表达式和_conding advice 参数。

要使用本节中描述的 aop 命名空间标记,您需要 import spring-aop schema,如XML Schema-based configuration中所述。有关如何 import aop命名空间中的标记,请参阅AOP schema

在 Spring 配置中,所有 aspect 和 advisor 元素必须放在<aop:config>元素中(在 application context configuration 中可以有多个<aop:config>元素)。 <aop:config>元素可以包含切入点,顾问程序和 aspect 元素(请注意,这些元素必须在该 order 中声明)。

<aop:config>样式的 configuration 大量使用 Spring 的auto-proxying机制。如果您已经通过使用BeanNameAutoProxyCreator或类似的东西使用显式 auto-proxying,这可能会导致问题(例如建议不被编织)。推荐用法 pattern 是仅使用<aop:config>样式或仅使用AutoProxyCreator样式,并且永远不要混用它们。

声明Aspect

使用 schema 支持时,aspect 是在 Spring application context 中定义为 bean 的常规 Java object。 state 和行为在 object 的字段和方法中捕获,切入点和建议信息在 XML 中捕获。

您可以使用< hh:// +4512+ 3 >元素声明 aspect,并使用ref属性引用支持 bean,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>

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

支持 aspect(在这种情况下为aBean)的 bean 当然可以配置和依赖注入,就像任何其他 Spring bean 一样。

声明切入点

您可以在<aop:config>元素中声明一个命名切入点,让切入点定义在多个方面和顾问之间共享。

表示服务层中任何业务服务执行的切入点可以定义如下:

1
2
3
4
5
6
<aop:config>

<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

请注意,切入点表达式本身使用与@AspectJ 支持中描述的相同的 AspectJ 切入点表达式语言。如果使用基于 schema 的声明样式,则可以引用切入点表达式中类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下:

1
2
3
4
5
6
<aop:config>

<aop:pointcut id="businessService"
expression="com.xyz.myapp.SystemArchitecture.businessService()"/>

</aop:config>

假设您有一个SystemArchitecture aspect,如共享 Common 切入点定义中所述。

然后在 aspect 中声明切入点与声明 top-level 切入点非常相似,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
<aop:config>

<aop:aspect id="myAspect" ref="aBean">

<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>

...

</aop:aspect>

</aop:config>

与 @AspectJ aspect 的方式大致相同,使用基于 schema 的定义样式声明的切入点可以收集连接点 context。对于 example,以下切入点将this object 收集为连接点 context 并将其传递给建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

必须声明建议以通过包含匹配名称的参数来接收收集的连接点 context,如下所示:

1
2
3
public void monitor(Object service) {
...
}

组合切入点 sub-expressions 时,&&在 XML 文档中很难处理,因此您可以分别使用andornot关键字代替&&||!。对于 example,可以更好地编写上一个切入点,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<aop:config>

<aop:aspect id="myAspect" ref="aBean">

<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service..(..)) and this(service)"/>

<aop:before pointcut-ref="businessService" method="monitor"/>

...
</aop:aspect>
</aop:config>

请注意,以这种方式定义的切入点由 XML id引用,不能用作命名切入点来形成复合切入点。因此,schema-based 定义样式中的命名切入点支持比 @AspectJ 样式提供的更有限。

声明通知

schema-based AOP 支持使用与 @AspectJ 样式相同的五种建议,它们具有完全相同的语义。

前置通知

在匹配的方法执行之前运行建议之前。它使用< hh:// +4545+ 3 >元素在<aop:aspect>中声明,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
<aop:aspect id="beforeExample" ref="aBean">

<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>

...

</aop:aspect>

这里,dataAccessOperation是在顶部(<aop:config>)level 定义的切入点的id。要改为内联切入点,请将pointcut-ref属性替换为pointcut属性,如下所示:

1
2
3
4
5
6
7
8
9
<aop:aspect id="beforeExample" ref="aBean">

<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>

...

</aop:aspect>

正如我们在讨论 @AspectJ 样式时所提到的,使用命名切入点可以显着提高 code 的可读性。

method属性标识提供建议正文的方法(doAccessCheck)。必须为包含建议的 aspect 元素引用的 bean 定义此方法。在执行数据访问操作(由切入点表达式匹配的方法执行连接点)之前,将调用 aspect bean 上的doAccessCheck方法。

后置通知

在匹配的方法执行正常完成后返回通知运行。它以与建议之前相同的方式在<aop:aspect>中声明。以下 example 显示了如何声明它:

1
2
3
4
5
6
7
8
9
<aop:aspect id="afterReturningExample" ref="aBean">

<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>

...

</aop:aspect>

与 @AspectJ 样式一样,您可以在建议体内获取 return value。为此,请使用 returns 属性指定 return value 应传递到的参数的 name,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
<aop:aspect id="afterReturningExample" ref="aBean">

<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>

...

</aop:aspect>

doAccessCheck方法必须声明一个名为retVal的参数。此参数的类型以与@AfterReturning描述的相同方式约束匹配。对于 example,您可以按如下方式声明方法签名:

1
public void doAccessCheck(Object retVal) {...

异常通知

抛出建议执行时,匹配的方法执行通过抛出 exception 退出。它通过使用 after-throwing 元素在<aop:aspect>内声明,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
<aop:aspect id="afterThrowingExample" ref="aBean">

<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>

...

</aop:aspect>

与 @AspectJ 样式一样,您可以在通知主体中获得抛出的 exception。为此,请使用 throwing 属性指定 exception 应传递到的参数的 name,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
<aop:aspect id="afterThrowingExample" ref="aBean">

<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>

...

</aop:aspect>

doRecoveryActions方法必须声明一个名为dataAccessEx的参数。此参数的类型以与@AfterThrowing描述的相同方式约束匹配。对于 example,方法签名可以声明如下:

1
public void doRecoveryActions(DataAccessException dataAccessEx) {...

最终通知

在(最终)建议运行之后,无论匹配的方法执行如何退出。您可以使用after元素声明它,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
<aop:aspect id="afterFinallyExample" ref="aBean">

<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>

...

</aop:aspect>

环绕通知

最后一种建议是建议。周围的建议围绕匹配的方法执行运行。它有机会在方法执行之前和之后完成工作,并确定何时,如何,甚至方法实际上都可以执行。 around 方法通常用于在 thread-safe 方式执行方法之前和之后共享 state(启动和停止计时器,用于 example)。始终使用符合您要求的最不强大的建议形式。如果在建议之前可以使用 job,请不要使用周围的建议。

您可以使用aop:around元素声明周围的建议。 advice 方法的第一个参数必须是ProceedingJoinPoint类型。在通知体内,在ProceedingJoinPoint上调用proceed()会导致执行基础方法。也可以使用Object[]调用proceed方法。 array 中的值在进行时用作方法执行的 arguments。有关使用Object[]调用proceed的注释,请参阅围绕建议。以下 example 显示了如何在 XML 中声明建议:

1
2
3
4
5
6
7
8
9
<aop:aspect id="aroundExample" ref="aBean">

<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>

...

</aop:aspect>

doBasicProfiling建议的 implementation 可以与 @AspectJ example(当然减去 annotation)完全相同,如下面的 example 所示:

1
2
3
4
5
6
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}

通知参数

schema-based 声明样式支持完全类型化的建议,方法与 @AspectJ 支持描述的方式相同 - 通过 name 匹配建议方法参数的切入点参数。有关详细信息,请参阅建议参数。如果您希望显式指定通知方法的参数名称(不依赖于前面描述的检测策略),可以使用通知元素的arg-names属性来实现,该属性的处理方式与argNames属性的处理方式相同。 advice annotation(如确定参数名称中所述)。以下 example 显示了如何在 XML 中指定参数 name:

1
2
3
4
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>

arg-names属性接受 comma-delimited 参数名称列表。

以下稍微涉及方法的示例显示了一些与一些强类型参数一起使用的建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
package x.y.service;

public interface PersonService {

Person getPerson(String personName, int age);
}

public class DefaultFooService implements FooService {

public Person getPerson(String name, int age) {
return new Person(name, age);
}
}

接下来是 aspect。请注意profile(..)方法接受多个 strongly-typed 参数的事实,第一个参数恰好是用于继续方法调用的连接点。此参数的存在表示profile(..)将用作around建议,如以下 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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());
}
}
}

最后,以下 example XML configuration 会影响特定连接点的前一个建议的执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="x.y.service.DefaultPersonService"/>

<!-- this is the actual advice itself -->
<bean id="profiler" class="x.y.SimpleProfiler"/>

<aop:config>
<aop:aspect ref="profiler">

<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* x.y.service.PersonService.getPerson(String,int))
and args(name, age)"/>

<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>

</aop:aspect>
</aop:config>

</beans>

请考虑以下驱动程序脚本:

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;

public final class Boot {

public static void main(final String[] args) throws Exception {
BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
PersonService person = (PersonService) ctx.getBean("personService");
person.getPerson("Pengo", 12);
}
}

使用这样的 Boot class,我们将在标准输出上获得类似于以下内容的输出:

1
2
3
4
5
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)

通知顺序

当多个建议需要在同一个连接点(执行方法)执行时,ordering 规则如通知顺序中所述。方面之间的优先级是通过将Order annotation 添加到支持 aspect 的 bean 或 bean 实现Ordered接口来确定的。

引入

介绍(在 AspectJ 中称为 inter-type 声明)让 aspect 声明建议 objects 实现给定的接口并代表那些 objects 提供该接口的 implementation。

您可以使用aop:aspect中的aop:declare-parents元素进行介绍。您可以使用aop:declare-parents元素声明匹配类型具有新的 parent(因此为 name)。对于 example,给定名为UsageTracked的接口和名为DefaultUsageTracked的接口的 implementation,以下 aspect 声明服务接口的所有实现者也实现UsageTracked接口。 (在_中通过 JMX 为 example.)公开统计数据

1
2
3
4
5
6
7
8
9
10
11
12
13
<aop:aspect id="usageTrackerAspect" ref="usageTracking">

<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

<aop:before
pointcut="com.xyz.myapp.SystemArchitecture.businessService()
and this(usageTracked)"
method="recordUsage"/>

</aop:aspect>

支持usageTracking bean 的 class 将包含以下方法:

1
2
3
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}

要实现的接口由implement-interface属性确定。 types-matching属性的 value 是 AspectJ 类型 pattern。匹配类型的任何 bean 都实现UsageTracked接口。请注意,在前面的 example 的 before 建议中,service beans 可以直接用作UsageTracked接口的 implementation。要以编程方式访问 bean,您可以编写以下内容:

1
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

切面实例模型

schema-defined 方面唯一支持的实例化 model 是 singleton model。未来的版本可能支持其他实例化模型。

通知器

“顾问”的概念来自 Spring 中定义的 AOP 支持,并且在 AspectJ 中没有直接的等价物。通知器就像一个小的 self-contained aspect,有一条通知。通知本身由 bean 表示,并且必须实现Spring 中的通知类型中描述的通知接口之一。顾问可以利用 AspectJ 切入点表达式。

Spring 支持带有<aop:advisor>元素的顾问程序概念。您最常见的是它与 transactional advice 一起使用,它在 Spring 中也有自己的命名空间支持。以下 example 显示了一个顾问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>

除了前面的 example 中使用的pointcut-ref属性之外,您还可以使用pointcut属性来内联定义切入点表达式。

要定义顾问程序的优先级以便建议可以参与 ordering,请使用order属性来定义顾问程序的Ordered value。

AOP模式实例

本节说明当使用 schema 支持重写时,并发锁定失败如何从AOP Example重试 example。

由于并发问题,业务服务的执行有时会失败(例如,死锁失败者)。如果重试该操作,则可能在下一次尝试时成功。对于适合在这种情况下重试的业务服务(幂等操作不需要返回给用户进行冲突解决),我们希望透明地重试操作以避免 client 看到PessimisticLockingFailureException。这是明确跨越服务层中的多个服务的要求,因此非常适合通过 aspect 实现。

因为我们想要重试操作,所以我们需要使用 around 建议,以便我们可以多次调用proceed。以下清单显示了基本的 aspect实现(这是一个使用 schema 支持的常规 Java class):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;
}

}

请注意,aspect 实现Ordered接口,以便我们可以将 aspect 的优先级设置为高于 transaction 通行(我们希望每次都重新执行 transaction)。 maxRetriesorder properties 都由 Spring 配置。主要操作发生在doConcurrentOperation around advice 方法中。我们试着继续。如果我们以PessimisticLockingFailureException失败,我们会再次尝试,除非我们已经用尽所有重试尝试。

此 class 与 @AspectJ example 中使用的 class 相同,但删除了 annotations。

相应的 Spring configuration 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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>

请注意,对于 time,我们假设所有业务服务都是幂等的。如果不是这种情况,我们可以通过引入Idempotent annotation 并使用 annotation 来注释服务操作的实现来优化 aspect 以便它只重试真正的幂等操作,如下面的 example 所示:

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}

对 aspect 的更改仅重试幂等操作涉及改进切入点表达式,以便只有@Idempotent操作 match,如下所示:

1
2
3
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>

选择要使用的AOP声明样式

一旦确定 aspect 是实现给定需求的最佳方法,您如何决定使用 Spring AOP 或 AspectJ 以及 Aspect 语言(code)样式, @AspectJ annotation 样式还是 Spring XML 样式?这些决策受到许多因素的影响,包括应用要求,开发工具和团队对 AOP 的熟悉程度。

SpringAOP或FullAspectJ?

使用最简单的方法。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要将 AspectJ 编译器/ weaver 引入您的开发和 build 进程。如果您只需要通知在 Spring beans 上执行操作,Spring AOP 是正确的选择。如果您需要通知不由 Spring 容器管理的 objects(例如域 objects),则需要使用 AspectJ。如果您希望通知除简单方法执行之外的连接点(对于 example,字段 get 或 set join points 等),还需要使用 AspectJ。

使用 AspectJ 时,您可以选择 AspectJ 语言语法(也称为“code 样式”)或 @AspectJ annotation 样式。显然,如果您不使用 Java 5,则可以选择:使用 code 样式。如果方面在您的设计中扮演重要角色,并且您能够使用 Eclipse 的AspectJ 开发工具(AJDT)插件,则 AspectJ 语言语法是首选选项。它更清晰,更简单,因为该语言是专门为写作方面而设计的。如果您不使用 Eclipse 或只有几个方面在您的 application 中没有主要作用,您可能需要考虑使用 @AspectJ 样式,在 IDE 中坚持使用常规 Java 编译,并在其中添加 aspect 编织阶段 build 脚本。

AspectJ注解或SpringAOP的XML?

如果您已选择使用 Spring AOP,则可以选择 @AspectJ 或 XML 样式。需要考虑各种权衡。

XML 样式可能是现有 Spring 用户最熟悉的,并且由真正的 POJO 支持。当使用 AOP 作为配置企业服务的工具时,XML 可能是一个不错的选择(一个好的测试是你是否认为切入点表达式是你可能想要独立改变的 configuration 的一部分)。使用 XML 样式,可以从您的 configuration 中清楚地看出系统中存在哪些方面。

XML 风格有两个缺点。首先,它没有完全封装它在一个地方解决的要求的实现。 DRY 原则规定,系统中的任何知识都应该有单一,明确,权威的表示。使用 XML 样式时,有关如何实现需求的知识将分支到 back bean class 的声明和 configuration 文件中的 XML。使用 @AspectJ 样式时,此信息封装在单个模块中:aspect。其次,XML 样式在它表达的内容方面比 @AspectJ 样式稍微受限:仅支持“singleton”aspect instantiation model,并且不可能组合在 XML 中声明的命名切入点。对于 example,在 @AspectJ 样式中,您可以编写如下内容:

1
2
3
4
5
6
7
8
@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

在 XML 样式中,您可以声明前两个切入点:

1
2
3
4
5
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>

XML 方法的缺点是您无法通过组合这些定义来定义accountPropertyAccess切入点。

@AspectJ 样式支持其他实例化模型和更丰富的切入点组合。它具有将 aspect 保持为模块化单元的优点。它还具有 @AspectJ 方面可以被 Spring AOP 和 AspectJ 理解(并因此消耗)的优点。因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,则可以轻松迁移到 AspectJ-based 方法。总而言之,只要您的方面不仅仅是简单的企业服务配置,Spring 团队更喜欢 @AspectJ 风格。

混合Aspect类型

通过使用 auto-proxying 支持,schema-defined <aop:aspect>切面,`声明的顾问程序,甚至在同一 configuration 中使用 Spring 1.2 样式定义的代理和拦截器,完全可以混合 @AspectJ 样式方面。所有这些都是通过使用相同的底层支持机制实现的,并且可以毫无困难地 co-exist。

代理机制

Spring AOP 使用 JDK 动态代理或 CGLIB 为给定目标 object 创建代理。 (只要有选择,JDK 动态代理就是首选)。

如果要代理的目标 object 实现至少一个接口,则使用 JDK 动态代理。目标类型实现的所有接口都是代理的。如果目标 object 未实现任何接口,则会创建 CGLIB 代理。

如果要强制使用 CGLIB 代理(对于 example,代理为目标 object 定义的每个方法,而不仅仅是那些由其接口实现的方法),您可以这样做。但是,您应该考虑以下问题:

  • 无法建议final方法,因为它们无法被覆盖。
  • 从 Spring 3.2 开始,不再需要将 CGLIB 添加到项目 classpath 中,因为 CGLIB classes 在org.springframework下重新打包并直接包含在 spring-core JAR 中。这意味着 CGLIB-based 代理支持“正常工作”,就像 JDK 动态代理一样。
  • 从 Spring 4.0 开始,代理 object 的构造函数不再被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。只有当您的 JVM 不允许构造函数绕过时,您才可以从 Spring 的 AOP 支持中看到 double 调用和相应的 debug log 条目。

要强制使用 CGLIB 代理,请将<aop:config>元素的proxy-target-class属性的 value 设置为 true,如下所示:

1
2
3
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>

要在使用 @AspectJ auto-proxy 支持时强制 CGLIB 代理,请将<aop:aspectj-autoproxy>元素的proxy-target-class属性设置为true,如下所示:

1
<aop:aspectj-autoproxy proxy-target-class="true"/>

多个<aop:config/>部分在运行时折叠为单个统一的 auto-proxy 创建者,这将应用指定的任何<aop:config/>部分(通常来自不同的 XML bean definition files)的最强代理设置。这也适用于<tx:annotation-driven/><aop:aspectj-autoproxy/>元素。

需要明确的是,在<tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/>元素上使用proxy-target-class="true"会强制对所有这三个元素使用 CGLIB 代理。

了解AOP代理

Spring AOP 是 proxy-based。在编写自己的aspect或使用 Spring Framework 提供的任何 Spring AOP-based 方面之前,掌握最后一个语句实际意味着什么的语义是非常重要的。

首先考虑具有 plain-vanilla,un-proxied,nothing-special-about-it,直 object reference 的场景,如下面的 code 片段所示:

1
2
3
4
5
6
7
8
9
10
11
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...
}
}

如果在 object reference 上调用方法,则直接在该 object reference 上调用该方法,如下图所示:

aop proxy plain pojo call

1
2
3
4
5
6
7
8
9
10
public class Main {

public static void main(String[] args) {

Pojo pojo = new SimplePojo();

// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}

当 client code 具有的 reference 是代理时,事情会略有变化。请考虑以下图表和 code 片段:

aop 代理电话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {

public static void main(String[] args) {

ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());

Pojo pojo = (Pojo) factory.getProxy();

// this is a method call on the proxy!
pojo.foo();
}
}

这里要理解的关键是Main class 的main(..)方法中的 client code 对代理有一个 reference。这意味着 object reference 上的方法 calls 是代理上的 calls。因此,代理可以委托给与该特定方法调用相关的所有拦截器(通知)。但是,一旦调用最终到达目标 object(在这种情况下是SimplePojo,reference),它可能会对或this.foo()进行调用的任何方法 calls 将被调用this reference,而不是代理。这具有重要意义。这意味着 self-invocation 不会导致与方法调用相关的通知有机会执行。

好的,那么该怎么办呢?最好的方法(术语“最好”,在这里松散地使用)是重构你的 code,以便 self-invocation 不会发生。这确实需要你做一些工作,但这是最好的,least-invasive 方法。接下来的方法是绝对可怕的,我们毫不犹豫地指出它,正是因为它是如此可怕。您可以(对我们来说很痛苦)将 class 中的逻辑完全绑定到 Spring AOP,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
public class SimplePojo implements Pojo {

public void foo() {
// this works, but... gah!
((Pojo) AopContext.currentProxy()).bar();
}

public void bar() {
// some logic...
}
}

这完全将你的 code 耦合到 Spring AOP,它使 class 本身意识到它正在 AOP context 中使用,它在 AOP 面前飞行。在创建代理时,它还需要一些额外的 configuration,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {

public static void main(String[] args) {

ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.adddInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);

Pojo pojo = (Pojo) factory.getProxy();

// this is a method call on the proxy!
pojo.foo();
}
}

最后,必须注意的是 AspectJ 没有这个 self-invocation 问题,因为它不是 proxy-based AOP framework。

程序化创建AspectJ注解代理

除了使用<aop:config><aop:aspectj-autoproxy>在 configuration 中声明方面之外,还可以以编程方式创建建议目标 objects 的代理。有关 Spring 的 AOP API 的完整详细信息,请参阅下一章。在这里,我们希望专注于使用 @AspectJ 方面自动创建代理的能力。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory class 为一个或多个 @AspectJ 方面建议的目标 object 创建代理。这个 class 的基本用法非常简单,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

有关详细信息,请参阅javadoc

在Spring应用程序中使用AspectJ

到目前为止,我们在本章中介绍的所有内容都是纯粹的 Spring AOP。在本节中,我们将介绍如何使用 AspectJ 编译器或 weaver 代替 Spring AOP 或者除了 Spring AOP 之外,如果您的需求超出了 Spring AOP 提供的功能。

Spring 附带一个小的 AspectJ aspect library,它在你的发行版中可以 stand-alone 作为spring-aspects.jar。您需要将此添加到 order 中的 classpath 以使用其中的方面。 使用 AspectJ 与依赖关系 Inject Domain Objects with SpringAspectJ 的其他 Spring 方面讨论了这个 library 的内容以及如何使用它。 使用 Spring IoC 配置 AspectJ 方面讨论了如何依赖 inject 使用 AspectJ 编译器编织的 AspectJ 方面。最后,Load-time 在 Spring Framework 中使用 AspectJ 进行编织介绍了 load-time 编织使用 AspectJ 的 Spring applications。

使用AspectJ用Spring依赖注入域对象

Spring 容器实例化并配置 application context 中定义的 beans。在给定包含要应用的 configuration 的 bean 定义的 name 的情况下,也可以要求 bean 工厂配置 pre-existing object。 spring-aspects.jar包含 annotation-driven aspect,它利用此功能允许依赖注入任何 object。该支持旨在用于在任何容器控制之外创建的 objects。域 objects 通常属于此类别,因为它们通常使用new operator 或 ORM 工具以编程方式创建,这是数据库查询的结果。

@Configurable annotation 将 class 标记为符合 Spring-driven configuration 的条件。在最简单的情况下,您可以纯粹使用它作为标记 annotation,如下面的 example 所示:

1
2
3
4
5
6
7
8
package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
// ...
}

当以这种方式用作标记接口时,Spring 通过使用与 fully-qualified 类型 name(com.xyz.myapp.domain.Account)具有相同 name 的 bean 定义(通常为 prototype-scoped)来配置带注释类型的新实例(在本例中为Account)。由于 bean 的默认 name 是其类型的 fully-qualified name,因此声明原型定义的便捷方法是省略id属性,如下面的 example 所示:

1
2
3
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要显式指定要使用的原型 bean 定义的 name,可以直接在 annotation 中执行此操作,如下面的 example 所示:

1
2
3
4
5
6
7
8
package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
// ...
}

Spring 现在查找名为account的 bean 定义,并将其用作配置新Account实例的定义。

您还可以使用自动装配来避免必须指定专用的 bean 定义。要让 Spring 应用自动装配,请使用@Configurable 注释的autowire property。您可以分别按类型或 name 指定@Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME进行自动装配。作为替代,从 Spring 2.5 开始,最好通过在字段或方法 level 上使用@Autowired@Inject@Configurable beans 指定显式的 annotation-driven 依赖注入(有关详细信息,请参阅Annotation-based Container Configuration

最后,您可以使用dependencyCheck属性(对于 example,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))为新创建和配置的 object 中的 object references 启用 Spring 依赖性检查。如果此属性设置为true,则 Spring 会在 configuration 之后验证是否已设置所有 properties(不是 primitives 或集合)。

请注意,使用 annotation 本身不会做任何事情。 spring-aspects.jar中的AnnotationBeanConfigurerAspect作用于 annotation 的存在。本质上,aspect 说,“在从@Configurable注释的类型的新 object 初始化返回后,根据 annotation 的 properties 使用 Spring 配置新创建的 object”。在此 context 中,“初始化”是指新实例化的 objects(对于 example,objects 用new operator 实例化)以及正在进行反序列化的Serializable objects(对于 example,通过readResolve())。

上段中的一个 key 短语是“实质上”。对于大多数情况,“在从新的 object 初始化返回之后”的确切语义很好。在这个 context 中,“初始化之后”意味着在构造 object 之后注入依赖项。这意味着依赖项不可用于 class 的构造函数体。如果希望在构造函数体执行之前注入依赖项,从而可以在构造函数体中使用,则需要在@Configurable声明中定义它,如下所示:

1
@Configurable(preConstruction=true)

您可以在AspectJ 编程指南的 AspectJ 在本附录中中找到有关各种切入点类型的语言语义的更多信息。

为此,必须使用 AspectJ 编织器编写带注释的类型。您可以使用 build-time Ant 或 Maven 任务执行此操作(请参阅示例,AspectJ 开发环境指南)或 load-time 编织(请参阅Load-time 在 Spring Framework 中使用 AspectJ 进行编织AnnotationBeanConfigurerAspect本身需要由 Spring 配置(在 order 中以获取 bean 工厂的 reference 用于配置新的 objects)。如果使用 Java-based configuration,则可以将@EnableSpringConfigured添加到任何@Configuration class,如下所示:

1
2
3
4
5
@Configuration
@EnableSpringConfigured
public class AppConfig {

}

如果您更喜欢基于 XML 的 configuration,Spring context 命名空间定义了一个方便的context:spring-configured元素,您可以按如下方式使用它:

1
<context:spring-configured/>

在配置 aspect 之前创建的@Configurable objects 实例会导致向 debug log 发出消息,并且不会发生 object 的 configuration。 example 可能是 Spring configuration 中的 bean,它在 Spring 初始化时创建域 objects。在这种情况下,您可以使用depends-on bean 属性手动指定 bean 依赖于 configuration aspect。以下 example 显示了如何使用depends-on属性:

1
2
3
4
5
6
7
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

<!-- ... -->

</bean>

不要通过 bean configurer aspect 激活@Configurable处理,除非你真的想在运行时依赖它的语义。特别是,请确保不要在 bean classes 上使用@Configurable作为常规 Spring beans 与容器一起注册。这样做会导致 double 初始化,一次通过容器,一次通过 aspect。

单元测试Configurable注解Objects

@Configurable支持的目标之一是启用域 objects 的独立单元测试,而没有与 hard-coded 查找相关的困难。如果@Configurable类型尚未由 AspectJ 编织,则 annotation 在单元测试期间不会产生任何影响。您可以在测试的 object 中设置 mock 或 stub property references 并正常进行。如果_J类型已由 AspectJ 编织,您仍然可以正常地在容器外部进行单元测试,但是每次 time 时都会看到一条警告消息,表明它尚未由 Spring 配置@Configurable object。

使用多个Application上下文

用于实现@Configurable支持的AnnotationBeanConfigurerAspect是 AspectJ singleton aspect。 singleton aspect 的范围与static成员的范围相同:每个类加载器都有一个 aspect 实例来定义类型。这意味着,如果在同一个类加载器层次结构中定义多个 application 上下文,则需要考虑定义@EnableSpringConfigured bean 的位置以及将spring-aspects.jar放在 classpath 的位置。

考虑一个典型的 Spring web application configuration,它具有一个共享 parent application context,用于定义 common 业务服务,支持这些服务所需的一切,以及一个 child application context,用于每个 servlet(包含特定于该 servlet 的定义)。所有这些上下文都在同一个类加载器层次结构中 co-exist,因此AnnotationBeanConfigurerAspect只能对其中一个进行 reference。在这种情况下,我们建议在 shared(parent)application context 中定义@EnableSpringConfigured bean。这定义了您可能希望 inject into domain objects 的服务。结果是你无法通过使用 @Configurable 机制(可能不是你想要做的事情)将 references 配置为 references 到 child(servlet-specific)上下文中定义的_bends。

在同一容器中部署多个 web applications 时,请确保每个 web application 都使用自己的类加载器加载spring-aspects.jar中的类型(对于 example,将spring-aspects.jar放在'WEB-INF/lib'中)。如果spring-aspects.jar仅添加到 container-wide classpath(因此由共享 parent 类加载器加载),则所有 web applications 共享相同的 aspect 实例(这可能不是您想要的)。

AspectJ的其他Spring切面

除了@Configurable aspect 之外,spring-aspects.jar还包含一个 AspectJ aspect,您可以使用它来驱动 Spring 的 transaction management 用于使用@Transactional annotation 注释的类型和方法。这主要适用于想要在 Spring 容器之外使用 Spring Framework 的 transaction 支持的用户。

解释@Transactional annotations 的 aspect 是AnnotationTransactionAspect。使用此 aspect 时,必须注释 implementation class(或 class 中的方法或两者),而不是 class 实现的接口(如果有)。 AspectJ 遵循 Java 的规则,即接口上的注释不会被继承。

class 上的@Transactional annotation 指定了 class 中执行任何公共操作的默认 transaction 语义。

class 中的方法上的@Transactional annotation 覆盖 class annotation(如果存在)给出的默认 transaction 语义。可以注释任何可见性的方法,包括私有方法。直接注释 non-public 方法是获得 transaction 划分以执行此类方法的唯一方法。

从 Spring Framework 4.2 开始,spring-aspects提供了一个类似的 aspect,为标准javax.transaction.Transactional annotation 提供了完全相同的 features。检查JtaAnnotationTransactionAspect以获取更多详细信息。

对于想要使用 Spring configuration 和 transaction management 支持但不想(或不能)使用 annotations 的 AspectJ 程序员,spring-aspects.jar还包含abstract方面,您可以扩展以提供自己的切入点定义。有关更多信息,请参阅AbstractBeanConfigurerAspectAbstractTransactionAspect方面的来源。作为示例,以下摘录显示了如何编写 aspect 来配置域 model 中定义的 objects 的所有实例,方法是使用 match 完全限定的 class 名称的原型 bean 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

public DomainObjectConfiguration() {
setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
}

// the creation of a new bean (any object in the domain model)
protected pointcut beanCreation(Object beanInstance) :
initialization(new(..)) &&
SystemArchitecture.inDomainModel() &&
this(beanInstance);

}

使用SpringIoC配置AspectJ切面

当您使用带有 Spring applications 的 AspectJ 方面时,很自然地希望并期望能够使用 Spring 配置这些方面。 AspectJ 运行时本身负责 aspect 创建,通过 Spring 配置 AspectJ-created 方面的方法取决于 aspect 使用的 AspectJ 实例化 model(per-xxx子句)。

AspectJ 的大多数方面都是 singleton 方面。 这些方面的配置很容易。您可以创建一个 bean 定义,它将_sepect 类型作为常规引用,并包含factory-method="aspectOf" bean 属性。这可以确保 Spring 通过询问 AspectJ 来获取 aspect 实例,而不是尝试自己创建实例。以下 example 显示了如何使用factory-method="aspectOf"属性:

1
2
3
4
5
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf"> (1)

<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
1 注意factory-method="aspectOf"属性

Non-singleton 方面更难配置。但是,可以通过 creating 原型 bean 定义并使用spring-aspects.jar@Configurable支持来配置 aspect 实例,一旦它们具有 AspectJ 运行时创建的 bean,就可以这样做。

如果你想要使用 AspectJ 编织个方面(例如,使用 load-time 编织域 model 类型)和其他想要与 Spring AOP 一起使用的 @AspectJ 方面,并且这些方面都在 Spring 中配置,你需要告诉 Spring AOP @AspectJ auto-proxying 支持_配置中定义的 @AspectJ 方面的哪个确切子集应该用于 auto-proxying。您可以通过在<aop:aspectj-autoproxy/>声明中使用一个或多个<include/>元素来完成此操作。每个<include/>元素指定一个 name pattern,只有名称与至少一个模式匹配的 beans 用于 Spring AOP auto-proxy configuration。以下 example 显示了如何使用<include/>元素:

1
2
3
4
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

不要被<aop:aspectj-autoproxy/>元素的 name 误导。使用它会导致创建 Spring AOP 代理。这里使用 @AspectJ 样式的 aspect 声明,但不涉及 AspectJ 运行时。

Load-time在SpringFramework中使用AspectJ进行编织

Load-time weaving(LTW)是指在将 AspectJ 方面加载到 Java 虚拟机(JVM)中时将其编织到 application 的 class files 中的过程。本节的重点是在 Spring Framework 的特定 context 中配置和使用 LTW。本节不是 LTW 的一般介绍。有关 LTW 细节的详细信息以及仅使用 AspectJ 配置 LTW(完全不涉及 Spring),请参阅AspectJ 开发环境指南的 LTW 部分

Spring Framework 为 AspectJ LTW 带来的 value 在编织 process 上实现了很多 finer-grained 控制。 ‘Vanilla’AspectJ LTW 通过使用 Java(5)代理来实现,该代理通过在启动 JVM 时指定 VM 参数来启用。因此,它是一个 JVM-wide 设置,在某些情况下可能会很好,但通常有点过于粗糙。 Spring-enabled LTW 允许您以__ 1 为基础打开 LTW,这更多 fine-grained 并且在’single-JVM-multiple-application’环境中更有意义(例如在典型的 application 服务器环境中找到的)。

此外,在某些环境中,此支持允许 load-time 编织而不对 application 服务器的启动脚本进行任何修改,这需要添加-javaagent:path/to/aspectjweaver.jar或(如本节后面所述)-javaagent:path/to/org.springframework.instrument-{version}.jar(以前称为spring-agent.jar)。开发人员修改一个或多个构成 application context 的 files 以启用 load-time 编织,而不是依赖于通常负责部署 configuration 的管理员,例如启动脚本。

现在销售宣传已经结束,让我们首先介绍使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍 example 中引入的元素。有关完整的 example,请参阅Petclinic sample application

AFirstExample

假设您是一名 application 开发人员,他的任务是诊断系统中某些 performance 问题的原因。我们将开启一个简单的分析 aspect,而不是 break 出分析工具,让我们快速获得一些 performance metrics。然后,我们可以立即将 finer-grained 分析工具应用于该特定区域。

此处提供的 example 使用 XML configuration。您还可以使用Java configuration配置和使用 @AspectJ 。具体来说,您可以使用@EnableLoadTimeWeaving annotation 作为<context:load-time-weaver/>的替代方法(有关详细信息,请参阅下面

以下 example 显示了分析 aspect,它不是花哨的 - 它是一个使用@AspectJ-style aspect 声明的 time-based 分析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

@Around("methodsToBeProfiled()")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
StopWatch sw = new StopWatch(getClass().getSimpleName());
try {
sw.start(pjp.getSignature().getName());
return pjp.proceed();
} finally {
sw.stop();
System.out.println(sw.prettyPrint());
}
}

@Pointcut("execution(public * foo..*.*(..))")
public void methodsToBeProfiled(){}
}

我们还需要创建一个META-INF/aop.xml文件,以通知 AspectJ weaver 我们想要将ProfilingAspect编织到我们的 classes 中。此文件约定,即 Java classpath 上名为META-INF/aop.xml的文件(或 files)的存在是标准 AspectJ。以下 example 显示了aop.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

<weaver>
<!-- only weave classes in our application-specific packages -->
<include within="foo.*"/>
</weaver>

<aspects>
<!-- weave in just this aspect -->
<aspect name="foo.ProfilingAspect"/>
</aspects>

</aspectj>

现在我们可以继续 configuration 的 Spring-specific 部分。我们需要配置LoadTimeWeaver(稍后解释)。这个 load-time weaver 是必不可少的 component,负责将一个或多个META-INF/aop.xml files 中的 aspect configuration 编织到 application 中的 classes 中。好处是它不需要很多 configuration(你可以指定更多的选项,但稍后会详细介绍),如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- a service object; we will be profiling its methods -->
<bean id="entitlementCalculationService"
class="foo.StubEntitlementCalculationService"/>

<!-- this switches on the load-time weaving -->
<context:load-time-weaver/>
</beans>

现在所有必需的 artifacts(aspect,META-INF/aop.xml文件和 Spring configuration)都已到位,我们可以使用main(..)方法创建以下驱动程序 class 来演示 LTW 的运行情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

EntitlementCalculationService entitlementCalculationService
= (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

// the profiling aspect is 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}

我们还有最后一件事要做。本节的介绍确实说可以用 Spring 选择性地在_ClassLoader基础上打开 LTW,这是 true。但是,对于这个例子,我们使用 Java 代理(随 Spring 提供)来打开 LTW。我们使用以下命令来运行前面显示的Main class:

1
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent是用于指定和启用代理程序来检测在 JVM 上运行的程序的 flag。 Spring Framework 附带了一个代理InstrumentationSavingAgent,它包装在spring-instrument.jar中,作为前面 example 中-javaagent参数的 value 提供。

执行Main程序的输出看起来像下一个 example。 (我在calculateEntitlement() implementation 中引入了Thread.sleep(..)语句,以便探查器实际捕获 0 毫秒以外的东西(01234毫秒不是 AOP 引入的开销)。下面的清单显示了运行探查器时得到的输出:

1
2
3
4
5
6
7
Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement

由于这个 LTW 是通过使用 full-blown AspectJ 实现的,因此我们不仅限于建议 Spring beans。 Main程序的以下细微变化产生相同的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

public static void main(String[] args) {

new ClassPathXmlApplicationContext("beans.xml", Main.class);

EntitlementCalculationService entitlementCalculationService =
new StubEntitlementCalculationService();

// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}

请注意,在前面的程序中,我们如何引导 Spring 容器,然后在 Spring 的 context 之外创建StubEntitlementCalculationService的新实例。剖析建议仍在编织中。

不可否认,示例过于简单化。但是,Spring 中 LTW 支持的基础知识都已在前面的示例中引入,本节的内容详细解释了 configuration 和用法的每一位背后的“原因”。

此 example 中使用的ProfilingAspect可能是基本的,但它非常有用。它是开发人员在开发过程中可以使用的 development-time aspect 的一个很好的例子,然后很容易从部署到 UAT 或 production 的 application 的构建中排除。

Aspect

您在 LTW 中使用的方面必须是 AspectJ 方面。您可以使用 AspectJ 语言本身编写它们,也可以在@AspectJ-style 中编写方面。那么你的方面都是有效的 AspectJ 和 Spring AOP 方面。此外,编译的 aspect classes 需要在 classpath 上可用。

META-INF/aop.xml

AspectJ LTW 基础结构是使用 Java classpath 上的一个或多个META-INF/aop.xml files 配置的(直接或更常见地,在 jar files 中)。

LTW 部分AspectJ reference 文档详细介绍了该文件的结构和内容。因为 aop.xml 文件是 100%AspectJ,所以我们在此不再进一步描述。

必需libraries(JARS)

至少,您需要以下 libraries 来使用 Spring Framework 对 AspectJ LTW 的支持:

  • spring-aop.jar(version 2.5 或更高版本,加上所有必需的依赖项)
  • aspectjweaver.jar(version 1.6.8 或更高版本)

如果您使用Spring-provided 代理启用检测,您还需要:

  • spring-instrument.jar

Spring配置

Spring 的 LTW 支持中的 key component 是LoadTimeWeaver接口(在org.springframework.instrument.classloading包中),以及与 Spring 发行版一起提供的众多 implementations。 LoadTimeWeaver负责在运行时向ClassLoader添加一个或多个java.lang.instrument.ClassFileTransformers,这为各种有趣的 applications 打开了大门,其中一个恰好是方面的 LTW。

如果您不熟悉运行时 class 文件转换的 idea,请在继续之前查看java.lang.instrument包的 javadoc API 文档。虽然该文档并不全面,但至少可以看到 key 接口和 classes(在阅读本节时为 reference)。

为特定的ApplicationContext配置LoadTimeWeaver可以像添加一个 line 一样简单。 (请注意,您几乎肯定需要使用ApplicationContext作为 Spring 容器 - 通常,BeanFactory是不够的,因为 LTW 支持使用BeanFactoryPostProcessors .)

要启用 Spring Framework 的 LTW 支持,您需要配置LoadTimeWeaver,通常使用@EnableLoadTimeWeaving annotation 来完成,如下所示:

1
2
3
4
5
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {

}

或者,如果您更喜欢 XML-based configuration,请使用<context:load-time-weaver/>元素。请注意,该元素在context名称空间中定义。以下 example 显示了如何使用<context:load-time-weaver/>

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:load-time-weaver/>

</beans>

前面的 configuration 会自动为您定义和注册许多 LTW-specific infrastructure beans,例如LoadTimeWeaverAspectJWeavingEnabler。默认的LoadTimeWeaverDefaultContextLoadTimeWeaver class,它试图装饰自动检测到的LoadTimeWeaver。 “自动检测”的确切类型LoadTimeWeaver取决于您的运行时环境。以下 table 总结了各种LoadTimeWeaver __mplement:

运行环境 LoadTimeWeaver 实现
在 Oracle 的WebLogic中运行 WebLogicLoadTimeWeaver
在 Oracle 的GlassFish中运行 GlassFishLoadTimeWeaver
Apache Tomcat运行 TomcatLoadTimeWeaver
Running in Red Hat 的JBoss ASWildFly JBossLoadTimeWeaver
在 IBM 的WebSphere运行 WebSphereLoadTimeWeaver
JVM 以 Spring InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar)开头 InstrumentationLoadTimeWeaver
回退,期望底层的 ClassLoader 遵循 common 约定(对于适用于TomcatInstrumentableClassLoader树脂的 example) ReflectiveLoadTimeWeaver

请注意,table lists 仅列出使用DefaultContextLoadTimeWeaver时自动检测的LoadTimeWeavers。您可以准确指定要使用的LoadTimeWeaver implementation。

要使用 Java configuration 指定特定的LoadTimeWeaver,请实现LoadTimeWeavingConfigurer接口并覆盖getLoadTimeWeaver()方法。以下 example 指定ReflectiveLoadTimeWeaver

1
2
3
4
5
6
7
8
9
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}

如果使用 XML-based configuration,则可以将完全限定的类名指定为<context:load-time-weaver/>元素上weaver-class属性的 value。同样,以下 example 指定ReflectiveLoadTimeWeaver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>

由 configuration 定义和注册的LoadTimeWeaver可以稍后使用众所周知的 name,loadTimeWeaver从 Spring 容器中检索。请记住,LoadTimeWeaver仅作为 Spring 的 LTW 基础设施添加一个或多个ClassFileTransformers的机制存在。执行 LTW 的实际ClassFileTransformerClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime包)class。有关详细信息,请参阅ClassPreProcessorAgentAdapter class 的 class-level javadoc,因为编织实际如何实现的细节超出了本文档的范围。

还有一个 configuration 的最后一个属性要讨论:aspectjWeaving属性(如果使用 XML,则为aspectj-weaving)。此属性控制是否启用 LTW。它接受三个可能值中的一个,如果该属性不存在,则默认 value 为autodetect。以下 table 总结了三个可能的值:

Annotation Value XML Value 说明
ENABLED on AspectJ 编织打开,方面在 load-time 处编织。
DISABLED off LTW 已关闭。没有 aspect 编织在 load-time。
AUTODETECT autodetect 如果 Spring LTW 基础结构可以找到至少一个META-INF/aop.xml文件,则 AspectJ 编织开启。否则,它关闭。这是默认值。

特定环境配置

最后一节包含在 application 服务器和 web 容器等环境中使用 Spring 的 LTW 支持时所需的任何其他设置和 configuration。

Tomcat

从历史上看,Apache Tomcat的默认 class 加载器不支持 class 转换,这就是 Spring 提供满足此需求的增强 implementation 的原因。命名为TomcatInstrumentableClassLoader,加载程序在 Tomcat 6.0 及以上版本上运行。

不要在 Tomcat 8.0 及更高版本上定义TomcatInstrumentableClassLoader。相反,让 Spring 通过TomcatLoadTimeWeaver策略自动使用 Tomcat 的新原生InstrumentableClassLoader工具。

如果仍需要使用TomcatInstrumentableClassLoader,则可以为每个 web application 单独注册,如下所示:

  • org.springframework.instrument.tomcat.jar复制到$CATALINA_HOME/lib,其中$CATALINA_HOME表示 Tomcat 安装的根目录
  • 通过编辑 web application context 文件指示 Tomcat 使用自定义 class 加载程序(而不是默认值),如下面的 example 所示:
1
2
3
4
<Context path="/myWebApp" docBase="/my/webApp/location">
<Loader
loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
</Context>

Apache Tomcat 6.0 支持多个 context 位置:

  • 服务器配置文件:$CATALINA_HOME/conf/server.xml
  • 默认 context configuration:$CATALINA_HOME/conf/context.xml,它影响所有已部署的 web applications
  • per-web application configuration,可以部署在 server-side $CATALINA_HOME/conf/[enginename]/[hostname]/[webapp]-context.xml上,也可以嵌入 web-app 归档内部META-INF/context.xml

为了提高效率,我们建议使用嵌入式 per-web application configuration 样式,因为它只影响使用自定义 class 加载器的 applications,并且不需要对服务器 configuration 进行任何更改。有关可用 context 位置的更多详细信息,请参阅 Tomcat 6.0.x 文件

或者,考虑使用 Spring-provided 泛型 VM 代理,在 Tomcat 的启动脚本中指定(在本节前面介绍过)。这使得所有已部署的 web applications 都可以使用检测,无论它们发生在上。

WebLogic,WebSphere,Resin,GlassFish和JBoss

最新版本的 WebLogic Server(version 10 及更高版本),IBM WebSphere Application Server(version 7 及更高版本),Resin(version 3.1 及更高版本)和 JBoss(version 6.x 或更高版本)提供了能够进行本地检测的ClassLoader。 Spring 的原生 LTW 利用此类 ClassLoader implementations 来启用 AspectJ 编织。您可以通过激活 load-time 编织来启用 LTW,如如前所述。具体来说,您无需修改启动脚本即可添加-javaagent:path/to/spring-instrument.jar

请注意,GlassFish instrumentation-capable ClassLoader仅在其 EAR 环境中可用。对于 GlassFish web applications,请按照 Tomcat 设置说明前面概述过进行操作。

请注意,在 JBoss 6.x 上,您需要禁用应用服务器扫描以防止它在 application 实际启动之前 classes classes。一个快速的解决方法是使用以下内容向 artifact 添加名为WEB-INF/jboss-scanning.xml的文件:

1
<scanning xmlns="urn:jboss:scanning:1.0"/>
通用JAVA配置

如果在不支持或不支持现有LoadTimeWeaver implementations 的环境中需要 class 检测,则 JDK 代理可以是唯一的解决方案。对于这种情况,Spring 提供InstrumentationLoadTimeWeaver,这需要一个 Spring-specific(但非常一般)VM 代理,org.springframework.instrument-{version}.jar(以前称为spring-agent.jar)。

要使用它,必须通过提供以下 JVM 选项来启动具有 Spring 代理的虚拟机:

1
-javaagent:/path/to/org.springframework.instrument-{version}.jar

请注意,这需要修改 VM 启动脚本,这可能会阻止您在 application Server 环境中使用它(取决于您的操作 policies)。此外,JDK 代理可以检测整个 VM,这可能很昂贵。

出于性能原因,我们建议您仅在目标环境(例如码头)没有(或不支持)专用 LTW 时才使用此 configuration。

更多资源

有关 AspectJ 的更多信息可以在AspectJ 网站上找到。

Eclipse AspectJ 由 Adrian Colyer 等人。人。 (Addison-Wesley,2005)为 AspectJ 语言提供了全面的介绍和参考。

AspectJ in Action,第二版由 Ramnivas Laddad(Manning,2009)强烈推荐。本书的重点是AspectJ,但是很多一般的AOP主题都在探索中(在某种程度上)。

Spring面向切面API

前一章描述了 Spring 对 AOP 的支持,包括 @AspectJ 和 schema-based aspect 定义。在本章中,我们将讨论 lower-level Spring AOP API 以及 Spring 1.2 applications 中通常使用的 AOP 支持。对于新的 applications,我们建议使用前一章中描述的 Spring 2.0 和更高版本的 AOP 支持。但是,当您使用现有的 applications 时(或者当您阅读书籍和文章时),您可能会遇到 Spring 1.2-style 示例。 Spring 5 保持向后兼容 Spring 1.2,Spring 5 完全支持本章中描述的所有内容。

Spring中的切入点API

本节描述 Spring 如何处理关键切入点概念。

概念

Spring 的切入点 model 可以独立于通知类型启用切入点重用。您可以使用相同的切入点来定位不同的建议。

org.springframework.aop.Pointcut接口是中央接口,用于将建议定位到特定的 classes 和方法。完整的界面如下:

1
2
3
4
5
6
7
public interface Pointcut {

ClassFilter getClassFilter();

MethodMatcher getMethodMatcher();

}

Pointcut接口拆分为两部分允许重用 class 和方法匹配部分以及 fine-grained 组合操作(例如使用另一个方法匹配器执行“union”)。

ClassFilter接口用于将切入点限制为给定的一组目标 classes。如果matches()方法始终返回 true,则匹配所有目标 classes。以下清单显示了ClassFilter接口定义:

1
2
3
4
public interface ClassFilter {

boolean matches(Class clazz);
}

MethodMatcher接口通常更重要。完整的界面如下:

1
2
3
4
5
6
7
8
public interface MethodMatcher {

boolean matches(Method m, Class targetClass);

boolean isRuntime();

boolean matches(Method m, Class targetClass, Object[] args);
}

matches(Method, Class)方法用于测试此切入点是否与目标 class 上的给定方法匹配。创建 AOP 代理时可以执行此 evaluation,以避免在每次方法调用时都需要测试。如果 two-argument matches方法为给定方法返回true,并且 MethodMatcher 的isRuntime()方法返回true,则在每次方法调用时都会调用 three-argument matches 方法。这使得切入点可以在执行目标通知之前立即查看传递给方法调用的 arguments。

大多数MethodMatcher实现都是静态的,这意味着它们的isRuntime()方法返回false。在这种情况下,永远不会调用 three-argument matches方法。

如果可能,尝试使切入点成为静态,允许 AOP framework 在创建 AOP 代理时缓存切入点 evaluation 的结果。

切入点的操作

Spring 支持切入点上的操作(尤其是 union 和 intersection)。

Union 表示切入点匹配的方法。交叉意味着两个切入点 match 的方法。 Union 通常更有用。您可以使用org.springframework.aop.support.Pointcuts class 中的静态方法或在同一个包中使用ComposablePointcut class 来组合切入点。但是,使用 AspectJ 切入点表达式通常是一种更简单的方法。

AspectJ表达式切入点

从 2.0 开始,Spring 使用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一个切入点,它使用 AspectJ-supplied library 来解析 AspectJ 切入点表达式 string。

有关受支持的 AspectJ 切入点 primitives 的讨论,请参阅前一章

方便的切入点实现

Spring 提供了几个方便的切入点实现。您可以直接使用其中一些。其他人打算在 application-specific 切入点中进行子类化。

静态切入点

静态切入点基于方法和目标 class,不能考虑方法的 arguments。对于大多数用途,静态切入点足够 - 并且最好。当首次调用方法时,Spring 只能评估一次静态切入点。之后,无需再次使用每个方法调用来评估切入点。

本节的 rest 描述了 Spring 中包含的一些静态切入点 implementations。

正则表达式切入点

指定静态切入点的一种显而易见的方法是正则表达式。除了 Spring 之外,几个 AOP 框架使这成为可能。 org.springframework.aop.support.JdkRegexpMethodPointcut是一个通用的正则表达式切入点,它使用 JDK 中的正则表达式支持。

使用JdkRegexpMethodPointcut class,您可以提供 pattern strings 的列表。如果其中任何一个是 match,则切入点的计算结果为true。 (所以,结果实际上是 pointcuts.)的 union

以下 example 显示了如何使用JdkRegexpMethodPointcut

1
2
3
4
5
6
7
8
9
<bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>

Spring 提供了一个名为RegexpMethodPointcutAdvisor的便捷 class,它允许我们 reference Advice(记住Advice可以是一个拦截器,在建议之前,抛出建议等)。在幕后,Spring 使用JdkRegexpMethodPointcut。使用RegexpMethodPointcutAdvisor简化了布线,因为_ bean 封装了切入点和建议,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref bean="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>

您可以将RegexpMethodPointcutAdvisor与任何Advice类型一起使用。

Attribute-driven切入点

一种重要的静态切入点是 metadata-driven 切入点。这使用元数据属性的值(通常是 source-level 元数据)。

动态切入点

与静态切入点相比,动态切入点的评估成本更高。它们考虑了方法 arguments 以及静态信息。这意味着必须使用每个方法调用来评估它们,并且不能缓存结果,因为 arguments 会有所不同。

主要示例是control flow切入点。

控制流切入点

Spring 控制流切入点在概念上类似于 AspectJ cflow切入点,虽然功能较弱。 (目前无法指定切入点在另一个 pointcut.)匹配的连接点下执行。控制流切入点与当前调用堆栈匹配。对于 example,如果连接点是由com.mycompany.web包中的方法调用的,则可能会触发。通过SomeCaller class。使用org.springframework.aop.support.ControlFlowPointcut class 指定控制流切入点。

在运行时评估控制流切入点的成本远远高于其他动态切入点。在 Java 1.4 中,成本大约是其他动态切入点的五倍。

切入点超类

Spring 提供了有用的切入点超类来帮助您实现自己的切入点。

因为静态切入点最有用,所以您应该将StaticMethodMatcherPointcut子类化。这需要只实现一个抽象方法(尽管您可以覆盖其他方法来自定义行为)。以下 example 显示了如何子类化StaticMethodMatcherPointcut

1
2
3
4
5
6
class TestStaticPointcut extends StaticMethodMatcherPointcut {

public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}

还有动态切入点的超类。

您可以在 Spring 1.0 RC2 及更高版本中使用任何建议类型的自定义切入点。

自定义切入点

因为 Spring AOP 中的切入点是 Java classes 而不是语言 features(如在 AspectJ 中),所以您可以声明自定义切入点,无论是静态还是动态。 Spring 中的自定义切入点可以是任意复杂的。但是,如果可以,我们建议使用 AspectJ 切入点表达式语言。

更高版本的 Spring 可能会为 JAC 提供的“语义切入点”提供支持 - 例如,“所有改变目标 object 中实例变量的方法”。

Spring中的通知API

现在我们可以检查 Spring AOP 如何处理通知。

通知生命周期

每个通知都是 Spring bean。通知实例可以在所有通知的 objects 之间共享,或者对每个通知的 object 都是唯一的。这对应于 per-class 或 per-instance 通知。

Per-class 通知最常用。它适用于通用通知,例如 transaction 顾问。这些不依赖于代理 object 的 state 或添加新的 state。它们仅仅依赖于方法和 arguments。

Per-instance 通知适合介绍,以支持 mixins。在这种情况下,通知将 state 添加到代理的 object。

您可以在同一个 AOP 代理中混合使用共享和 per-instance 通知。

Spring中的通知类型

Spring 提供了几种通知类型,并且可以扩展以支持任意通知类型。本节介绍基本概念和标准通知类型。

围绕通知拦截

Spring 中最基本的通知类型是围绕通知进行拦截。

Spring 符合 AOP Alliance接口,用于使用方法拦截的周围通知。实现MethodInterceptor并实现通知的 Classes 也应该实现以下接口:

1
2
3
4
public interface MethodInterceptor extends Interceptor {

Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke()方法的MethodInvocation参数公开了被调用的方法,目标连接点,AOP 代理和方法的 arguments。 invoke()方法应该返回调用的结果:连接点的 return value。

以下 example 显示了一个简单的MethodInterceptor实现:

1
2
3
4
5
6
7
8
9
public class DebugInterceptor implements MethodInterceptor {

public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
System.out.println("Invocation returned");
return rval;
}
}

注意对MethodInvocationproceed()方法的调用。这沿拦截器链向下进入连接点。大多数拦截器调用此方法并 return 其 return value。但是,MethodInterceptor,就像任何一个建议一样,可以 return 一个不同的 value 或抛出一个 exception 而不是调用 proceed 方法。但是,如果没有充分的理由,您不希望这样做。

MethodInterceptor implementations 提供与其他 AOP Alliance-compliant AOP implementations 的互操作性。本节其余部分讨论的其他建议类型实现了 common AOP 概念,但是以 Spring-specific 方式实现。虽然使用最具体的建议类型有一个优势,但如果您可能想要在另一个 AOP framework 中运行 aspect,请坚持使用MethodInterceptor。请注意,切入点目前在框架之间不可互操作,AOP 联盟目前不定义切入点接口。

前置通知

更简单的通知类型是之前的通知。这不需要MethodInvocation object,因为只在进入方法之前调用它。

之前通知的主要优点是不需要调用proceed()方法,因此不会无意中无法继续拦截链。

以下清单显示了MethodBeforeAdvice接口:

1
2
3
4
public interface MethodBeforeAdvice extends BeforeAdvice {

void before(Method m, Object[] args, Object target) throws Throwable;
}

(Spring 的 API 设计允许在通知之前使用字段,尽管通常的 objects 适用于字段拦截,并且 Spring 不太可能实现 it.)

请注意 return 类型是void。在通知可以在连接点执行之前插入自定义行为但不能更改 return value。如果 before advice 抛出了 exception,则会中止拦截器链的进一步执行。 exception 传播回拦截器链。如果取消选中它或在被调用方法的签名上,它将直接传递给 client。否则,它被 AOP 代理包装在未经检查的 exception 中。

以下 example 显示 Spring 中的 before 通知,它计算所有方法调用:

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
}

之前建议可以与任何切入点一起使用。

异常通知

如果连接点抛出 exception,则在连接点的 return 之后调用抛出通知。 Spring 提供类型投掷通知。请注意,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一个标记接口,用于标识给定的 object 实现一个或多个类型化的 throws 通知方法。这些应该是以下形式:

1
afterThrowing([Method, args, target], subclassOfThrowable)

只需要最后一个参数。方法签名可以有一个或四个 arguments,具体取决于 advice 方法是否对方法和 arguments 感兴趣。接下来的两个列表显示了作为 throws 建议示例的 classes。

如果抛出RemoteException(包括从子类),则调用以下通知:

1
2
3
4
5
6
public class RemoteThrowsAdvice implements ThrowsAdvice {

public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}

与前面的通知不同,下一个 example 声明了四个 arguments,因此它可以访问被调用的方法,方法 arguments 和 target object。如果抛出ServletException,则调用以下通知:

1
2
3
4
5
6
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}

最后的 example 说明了如何在处理RemoteExceptionServletException的单个 class 中使用这两个方法。可以在单个 class 中组合任意数量的 throws 通知方法。以下清单显示了最终的 example:

1
2
3
4
5
6
7
8
9
10
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
}
}

如果 throws-advice 方法本身抛出一个 exception,它会覆盖原始的 exception(也就是说,它会更改抛出给用户的 exception)。覆盖的 exception 通常是 RuntimeException,它与任何方法签名兼容。但是,如果 throws-advice 方法抛出已检查的 exception,则它必须_匹配目标方法的声明的 exceptions,因此在某种程度上耦合到特定的目标方法签名。不要抛出与目标方法的签名不兼容的未声明的已检查 exception!

抛出建议可以与任何切入点一起使用。

后置通知

在 Spring 中返回后的通知必须实现org.springframework.aop.AfterReturningAdvice接口,如下所示:

1
2
3
4
5
public interface AfterReturningAdvice extends Advice {

void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}

返回后的通知可以访问 return value(它无法修改),调用的方法,方法的 arguments 和目标。

返回通知后的以下内容计算所有未抛出 exceptions 的成功方法调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

此通知不会更改执行路径。如果它抛出一个 exception,它会被拦截链而不是 return value 抛出。

返回通知后可以使用任何切入点。

Introduction通知

Spring 将介绍通知视为一种特殊的拦截建议。

简介需要IntroductionAdvisorIntroductionInterceptor来实现以下接口:

1
2
3
4
public interface IntroductionInterceptor extends MethodInterceptor {

boolean implementsInterface(Class intf);
}

从 AOP Alliance MethodInterceptor接口继承的invoke()方法必须实现介绍。也就是说,如果调用的方法在引入的接口上,则引入拦截器负责处理方法调用 - 它不能调用proceed()

引言通知不能与任何切入点一起使用,因为它仅适用于 class,而不是方法 level。您只能使用IntroductionAdvisor的介绍通知,它具有以下方法:

1
2
3
4
5
6
7
8
9
10
11
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

ClassFilter getClassFilter();

void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

Class[] getInterfaces();
}

没有MethodMatcher,因此没有Pointcut与介绍建议相关联。只有 class 过滤是合乎逻辑的。

getInterfaces()方法返回此顾问程序引入的接口。

内部使用validateInterfaces()方法来查看引入的接口是否可以由配置的IntroductionInterceptor实现。

考虑 Spring 测试套件中的 example,并假设我们要将以下接口引入一个或多个 objects:

1
2
3
4
5
public interface Lockable {
void lock();
void unlock();
boolean locked();
}

这说明了一个混合。我们希望能够将建议的 objects 转换为Lockable,无论其类型如何,并调用锁定和解锁方法。如果我们调用lock()方法,我们希望所有 setter 方法都抛出LockedException。因此,我们可以添加一个 aspect,它提供了使 objects 不可变的能力,而不需要它们知道它:AOP 的一个很好的例子。

首先,我们需要IntroductionInterceptor来完成繁重的任务。在这种情况下,我们扩展org.springframework.aop.support.DelegatingIntroductionInterceptor便利 class。我们可以直接实现IntroductionInterceptor,但在大多数情况下使用DelegatingIntroductionInterceptor是最好的。

DelegatingIntroductionInterceptor旨在委托对引入的接口的实际 implementation 的介绍,隐藏使用拦截这样做。您可以使用构造函数参数将委托设置为任何 object。默认委托(使用 no-argument 构造函数时)为this。因此,在下一个 example 中,委托是DelegatingIntroductionInterceptorLockMixin子类。给定一个委托(默认情况下,它本身),DelegatingIntroductionInterceptor实例查找委托实现的所有接口(除了IntroductionInterceptor)并支持对其中任何接口的介绍。诸如LockMixin之类的子类可以调用suppressInterface(Class intf)方法来抑制不应该公开的接口。但是,无论IntroductionInterceptor准备支持多少个接口,IntroductionAdvisor都使用控制哪些接口实际暴露。引入的接口隐藏了目标对同一接口的任何 implementation。

因此,LockMixin扩展DelegatingIntroductionInterceptor并实现Lockable本身。超类自动选择可以支持引入Lockable,因此我们不需要指定。我们可以用这种方式引入任意数量的接口。

注意使用locked实例变量。这有效地将额外的 state 添加到目标 object 中的 state。

以下 example 显示了 example LockMixin class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

private boolean locked;

public void lock() {
this.locked = true;
}

public void unlock() {
this.locked = false;
}

public boolean locked() {
return this.locked;
}

public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
throw new LockedException();
}
return super.invoke(invocation);
}

}

通常,您不需要覆盖invoke()方法。 DelegatingIntroductionInterceptor implementation(如果引入方法,calls delegate方法,否则进入连接点)通常就足够了。在本例中,我们需要添加一个检查:如果处于锁定模式,则不能调用 setter 方法。

所需的介绍只需要保存一个不同的LockMixin实例并指定引入的接口(在这种情况下,只有Lockable)。更复杂的 example 可能会对引入拦截器(将被定义为原型)进行 reference。在这种情况下,没有与LockMixin相关的 configuration,因此我们使用new创建它。以下 example 显示了我们的LockMixinAdvisor class:

1
2
3
4
5
6
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}

我们可以非常简单地应用此顾问程序,因为它不需要 configuration。 (但是,如果没有IntroductionAdvisor .)就不可能使用IntroductionInterceptor与通常一样,顾问必须是 per-instance,因为它是有状态的。对于每个建议的 object,我们需要LockMixinAdvisor的不同实例,因此需要LockMixin。顾问包括建议 object 的 state 的一部分。

我们可以像在任何其他顾问中一样,使用 XML configuration 中的Advised.addAdvisor()方法或(推荐方式)以编程方式应用此顾问程序。下面讨论的所有代理创建选项,包括“自动代理创建器”,正确处理引入和有状态混合。

Spring中的通知API

在 Spring 中,Advisor 是一个 aspect,它只包含一个与切入点表达式相关联的建议 object。

除了介绍的特殊情况,任何顾问都可以使用任何建议。 org.springframework.aop.support.DefaultPointcutAdvisor是最常用的顾问 class。它可以与MethodInterceptorBeforeAdviceThrowsAdvice一起使用。

可以在同一个 AOP 代理中混合 Spring 中的顾问程序和通知类型。例如,您可以在一个代理 configuration 中使用拦截建议,抛出建议和建议之前。 Spring 自动创建必要的拦截链。

使用ProxyFactoryBean创建AOP代理

如果您使用 Spring IoC 容器(ApplicationContextBeanFactory)为您的业务 objects(你应该!),你想使用 Spring 的 AOP FactoryBean implementations 之一。 (请记住,工厂 bean 引入了一个间接层,让它创建一个不同的 type.)的对象

Spring AOP 支持还使用了工厂 beans。

在 Spring 中创建 AOP 代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。这样可以完全控制切入点,任何适用的建议以及它们的 ordering。但是,如果您不需要此类控件,则可以使用更简单的选项。

Basics

与其他 Spring FactoryBean __mplementations 一样,ProxyFactoryBean引入了 level 间接。如果你定义ProxyFactoryBean名为foo,objects reference foo看不到ProxyFactoryBean实例本身,而是由ProxyFactoryBean中的getObject()方法的 implementation 创建的 object。此方法创建一个包装目标 object 的 AOP 代理。

使用ProxyFactoryBean或另一个 IoC-aware class 创建 AOP 代理的最重要的好处之一是 IoC 可以管理建议和切入点。这是一个强大的 feature,可以实现其他 AOP 框架难以实现的某些方法。对于 example,一个建议本身可以 reference application objects(除了目标,它应该在任何 AOP framework 中可用),受益于 Dependency Injection 提供的所有可插拔性。

JavaBeanProperties

在 common 中,Spring 提供了大多数FactoryBean __mplement,ProxyFactoryBean class 本身就是一个 JavaBean。它的 properties 用于:

一些 key properties 继承自org.springframework.aop.framework.ProxyConfig(Spring 中所有 AOP 代理工厂的超类)。这些 key properties 包括以下内容:

  • proxyTargetClasstrue如果要代理目标 class,而不是目标 class 的接口。如果此 property value 设置为true,则会创建 CGLIB 代理(但也请参见JDK-和 CGLIB-based 代理
  • optimize:控制是否将积极优化应用于通过 CGLIB 创建的代理。除非您完全了解相关 AOP 代理如何处理优化,否则您不应轻易使用此设置。目前仅用于 CGLIB 代理。它对 JDK 动态代理没有影响。
  • frozen:如果代理 configuration 为frozen,则不再允许更改 configuration。这既可以作为轻微优化,也可以用于在创建代理后不希望调用者能够操作代理(通过Advised接口)的情况。此 property 的默认 value 为false,因此允许更改(例如添加其他建议)。
  • exposeProxy:确定当前代理是否应该在ThreadLocal中公开,以便目标可以访问它。如果目标需要获取代理并且exposeProxy property 设置为true,则目标可以使用AopContext.currentProxy()方法。

特定于ProxyFactoryBean的其他 properties 包括以下内容:

  • proxyInterfacesString接口名称的 array。如果未提供,则使用目标 class 的 CGLIB 代理(但也请参见JDK-和 CGLIB-based 代理
  • interceptorNamesAdvisor _ar的Advisor,拦截器或其他建议名称。 Ordering 很重要,在第一个 come-first 服务的基础上。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。

名称是当前工厂中的 bean 名称,包括来自祖先工厂的 bean 名称。你不能在这里提到 bean references,因为这样做会导致ProxyFactoryBean忽略建议的 singleton 设置。

您可以使用星号(*)附加拦截器 name。这样做会导致应用所有顾问程序 beans,其名称以要应用星号之前的部分开头。您可以在使用“Global”顾问中找到使用此 feature 的 example 示例。

  • singleton:无论调用getObject()方法的频率如何,工厂是否应该_return 一个 object。几个FactoryBean implementations 提供了这样一种方法。默认的 value 是true。如果你想使用有状态的建议 - 对于 example,对于有状态的 mixins - 使用原型建议和false的 singleton value。

JDK和CGLIB-based代理

本节作为关于ProxyFactoryBean如何选择为特定目标 object(将被代理)创建 JDK-based 代理或 CGLIB-based 代理的权威文档。

关于创建 JDK-或 CGLIB-based 代理的行为在 Spring 的 1.2.x 和 2.0 版本之间发生了变化。对于 auto-detecting 接口,ProxyFactoryBean现在表现出与TransactionProxyFactoryBean class 相似的语义。

如果要代理的目标 object 的 class(以下简称为 target class)未实现任何接口,则会创建 CGLIB-based 代理。这是最简单的方案,因为 JDK 代理是 interface-based,没有接口意味着甚至不可能进行 JDK 代理。您可以通过设置interceptorNames property 来插入目标 bean 并指定拦截器列表。请注意,即使ProxyFactoryBeanproxyTargetClass property 已设置为false,也会创建 CGLIB-based 代理。 (这样做是没有意义的,最好从 bean 定义中删除,因为它最多是冗余的,最坏的情况是 confusing.)

如果目标 class 实现一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean的 configuration。

如果ProxyFactoryBeanproxyTargetClass property 已设置为true,则会创建 CGLIB-based 代理。这是有道理的,并且符合最少惊喜的原则。即使ProxyFactoryBeanproxyInterfaces property 已设置为一个或多个完全限定的接口名称,proxyTargetClass property 设置为true这一事实会导致 CGLIB-based 代理生效。

如果ProxyFactoryBeanproxyInterfaces property 已设置为一个或多个完全限定的接口名称,则会创建 JDK-based 代理。创建的代理实现proxyInterfaces property 中指定的所有接口。如果目标 class 碰巧实现了比proxyInterfaces property 中指定的接口多得多的接口,那么这一切都很好,但返回的代理并没有实现这些额外的接口。

如果尚未设置ProxyFactoryBeanproxyInterfaces property,但目标 class 确实实现了一个(或多个)接口,则ProxyFactoryBean auto-detects 目标 class 实际上实现了至少一个接口,并创建了 JDK-based 代理。实际代理的接口是 target class 实现的所有接口。实际上,这与提供目标 class 为proxyInterfaces property 实现的每个接口的列表相同。但是,它的工作量明显减少,并且不易出现印刷错误。

代理接口

考虑一下ProxyFactoryBean的简单示例。这个例子涉及:

  • 代理的目标 bean。这是 example 中的personTarget bean 定义。
  • AdvisorInterceptor用于提供建议。
  • AOP 代理 bean 定义,用于指定目标 object(personTarget bean),代理接口和要应用的建议。

以下清单显示了 example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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 property 采用String列表,该列表包含当前工厂中拦截器或顾问程序的 bean 名称。您可以在返回之前,之后使用顾问程序,拦截器,并抛出建议 objects。顾问的排序非常重要。

您可能想知道为什么列表不包含 bean references。原因是,如果ProxyFactoryBean的 singleton property 设置为false,它必须能够 return 独立的代理实例。如果任何顾问本身就是原型,则需要返回一个独立的实例,因此必须能够从工厂获得原型的实例。持有参考是不够的。

前面显示的person bean 定义可用于代替Person implementation,如下所示:

1
Person person = (Person) factory.getBean("person");

同一个 IoC context 中的其他 beans 可以表达对它的强类型依赖,就像普通的 Java object 一样。以下 example 显示了如何执行此操作:

1
2
3
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>

此 example 中的PersonUser class 显示Person类型的 property。就其而言,AOP 代理可以透明地用于代替“真正的”人实现。但是,它的 class 将是一个动态代理 class。可以将它转换为Advised接口(稍后讨论)。

您可以使用匿名内部 bean 隐藏目标和代理之间的区别。只有ProxyFactoryBean定义是不同的。该建议仅用于完整性。以下 example 显示了如何使用匿名内部 bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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类型的 object。如果我们想要阻止 application context 的用户获取 un-advised object 的 reference 或者需要避免 Spring IoC 自动装配的任何歧义,这将非常有用。可以说,ProxyFactoryBean定义为 self-contained 也有一个优点。但是,有时能够从工厂获得 un-advised 目标实际上可能是一个优势(例如,在某些测试场景中)。

代理Classes

如果您需要代理 class 而不是一个或多个接口,该怎么办?

想象一下,在我们之前的例子中,没有Person接口。我们需要建议一个没有实现任何业务接口的 class Person。在这种情况下,您可以将 Spring 配置为使用 CGLIB 代理而不是动态代理。为此,请将前面显示的ProxyFactoryBean上的proxyTargetClass property 设置为true。虽然最好是编程到接口而不是 classes,但在使用 legacy code 时,建议不实现接口的 classes 的功能非常有用。 (一般来说,Spring 不是规定性的.虽然它可以很容易地应用好的做法,但它避免强迫特定的 approach.)

如果您愿意,即使您有接口,也可以在任何情况下强制使用 CGLIB。

CGLIB 代理通过在运行时生成目标 class 的子类来工作。 Spring 将此生成的子类配置为将方法 calls 委托给原始目标。子类用于实现 Decorator pattern,在通知中编织。

CGLIB 代理通常应对用户透明。但是,有一些问题需要考虑:

  • 无法建议Final方法,因为它们无法被覆盖。
  • 无需将 CGLIB 添加到 classpath。从 Spring 3.2 开始,CGLIB 被重新打包并包含在 spring-core JAR 中。换句话说,CGLIB-based AOP“开箱即用”,JDK 动态代理也是如此。

CGLIB 代理和动态代理之间几乎没有性能差异。从 Spring 1.0 开始,动态代理略快一些。但是,这可能会在未来发生变化。 在这种情况下,绩效不应该是一个决定性的考虑因素。

使用全局Advisors

通过在拦截器 name 上附加一个星号,所有具有 bean 名称的顾问都会将星号之前的部分匹配添加到顾问程序链中。如果您需要添加一组标准的“global”顾问程序,这可以派上用场。以下 example 定义了两个 global 顾问程序:

1
2
3
4
5
6
7
8
9
10
11
<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"/>

简明的代理定义

特别是在定义 transactional 代理时,最终可能会有许多类似的代理定义。使用 parent 和 child bean 定义以及内部 bean 定义可以产生更清晰,更简洁的代理定义。

首先,我们为代理创建 parent,template,bean 定义,如下所示:

1
2
3
4
5
6
7
8
9
<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>

这从未实例化,因此它实际上可能是不完整的。然后,需要创建的每个代理都是 child bean 定义,它将代理的目标包装为内部 bean 定义,因为目标永远不会单独使用。以下 example 显示了这样的 child bean:

1
2
3
4
5
6
<bean id="myService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MyServiceImpl">
</bean>
</property>
</bean>

您可以从 parent 模板覆盖 properties。在以下 example 中,我们覆盖 transaction 传播设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

请注意,在 parent bean example 中,我们通过将abstract属性设置为true显式地将 parent bean 定义标记为抽象,如先前所述,因此实际上可能无法实例化它。 Application 上下文(但不是简单的 bean 工厂),默认情况下,pre-instantiate 所有单例。因此,重要的是(至少对于 singleton beans),如果你有一个(parent)bean 定义,你打算只用作模板,并且这个定义指定一个 class,你必须确保将abstract属性设置为true。否则,application context 实际上会尝试 pre-instantiate 它。

使用ProxyFactory以编程方式创建AOP代理

使用 Spring 以编程方式创建 AOP 代理很容易。这使您可以使用 Spring AOP 而不依赖于 Spring IoC。

target object 实现的接口会自动代理。以下清单显示了为目标 object 创建代理,其中包含一个拦截器和一个顾问程序:

1
2
3
4
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

第一个 step 是构造一个org.springframework.aop.framework.ProxyFactory类型的 object。您可以使用目标 object 创建它,如前面的 example,或指定要在备用构造函数中代理的接口。

您可以添加建议(使用拦截器作为专门的建议),顾问或两者,并在ProxyFactory的生命周期中操纵它们。如果添加IntroductionInterceptionAroundAdvisor,则可以使代理实现其他接口。

ProxyFactory(继承自AdvisedSupport)也有便捷方法,可以添加其他建议类型,例如 before 和 throws 建议。 AdvisedSupportProxyFactoryProxyFactoryBean的超类。

在大多数应用程序中,将 AOP 代理创建与 IoC framework 集成是最佳实践。我们建议您通常使用 AOP 来外部化来自 Java code 的 configuration。

操纵通知的Objects

但是,您创建 AOP 代理,可以使用org.springframework.aop.framework.Advised接口操作它们。无论它实现哪个其他接口,任何 AOP 代理都可以转换为此接口。该界面包括以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Advisor[] getAdvisors();

void addAdvice(Advice advice) throws AopConfigException;

void addAdvice(int pos, Advice advice) throws AopConfigException;

void addAdvisor(Advisor advisor) throws AopConfigException;

void addAdvisor(int pos, Advisor advisor) throws AopConfigException;

int indexOf(Advisor advisor);

boolean removeAdvisor(Advisor advisor) throws AopConfigException;

void removeAdvisor(int index) throws AopConfigException;

boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;

boolean isFrozen();

getAdvisors()方法为每个已添加到工厂的顾问程序,拦截器或其他通知类型返回Advisor。如果添加了Advisor,则此索引处返回的顾问程序是您添加的 object。如果您添加了一个拦截器或其他建议类型,Spring 将其包装在一个顾问程序中,并且切入点始终返回true。因此,如果添加了MethodInterceptor,则为此索引返回的顾问程序是DefaultPointcutAdvisor,它返回MethodInterceptor以及与所有 classes 和方法匹配的切入点。

addAdvisor()方法可用于添加任何Advisor。通常,持有切入点和建议的顾问程序是通用DefaultPointcutAdvisor,您可以将其用于任何建议或切入点(但不适用于介绍)。

默认情况下,即使创建了代理,也可以添加或删除顾问程序或拦截器。唯一的限制是不可能添加或删除介绍顾问,因为工厂的现有代理不显示接口更改。 (您可以从工厂获取新代理以避免此 problem.)

以下 example 显示将 AOP 代理转换为Advised接口并检查和操作其建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);

尽管毫无疑问,合法的使用案例是否可行(无双关语)修改 production 中的商业 object 建议是值得怀疑的。但是,它在开发中非常有用(例如,在测试中)。我们有时发现能够以拦截器或其他建议的形式添加 test code 非常有用,进入我们想要测试的方法调用。 (例如,建议可以进入为该方法创建的 transaction,也许在运行 SQL 以检查数据库是否已正确更新,然后标记 transaction for roll back.)

根据您创建代理的方式,通常可以设置frozen flag。在这种情况下,Advised isFrozen()方法返回true,任何通过添加或删除修改建议的尝试都会导致AopConfigException。在某些情况下,冻结建议的 object 的 state 的功能很有用(例如,为了防止调用 code 删除安全拦截器)。如果已知不需要运行时建议修改,它也可以在 Spring 1.1 中使用以允许积极优化。

使用”auto-proxy”工具

到目前为止,我们已经考虑使用ProxyFactoryBean或类似的工厂 bean 显式创建 AOP 代理。

Spring 还允许我们使用“auto-proxy” bean 定义,它可以自动代理所选的 bean 定义。这是基于 Spring 的“bean post processor”基础结构构建的,它可以在容器加载时修改任何 bean 定义。

在此 model 中,您在 XML bean 定义文件中设置了一些特殊的 bean 定义来配置 auto-proxy 基础结构。这允许您声明符合 auto-proxying 的目标。你不需要使用ProxyFactoryBean

有两种方法可以做到这一点:

  • 通过使用引用当前 context 中特定 beans 的 auto-proxy 创建者。
  • 一个特殊的 auto-proxy 创建案例值得单独考虑:auto-proxy 创建由 source-level 元数据属性驱动。

Auto-proxyBean定义

本节介绍org.springframework.aop.framework.autoproxy包提供的 auto-proxy 创建者。

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator class 是BeanPostProcessor,它自动为 beans 创建 AOP 代理,其名称为 match 文字值或通配符。以下 example 显示了如何创建BeanNameAutoProxyCreator bean:

1
2
3
4
5
6
7
8
<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 property 而不是拦截器列表,以允许原型顾问的正确行为。命名为“拦截器”可以是顾问或任何建议类型。

与 auto-proxying 一般,使用BeanNameAutoProxyCreator的主要目的是将相同的 configuration 一致地应用于多个 objects,并使用最小的 configuration 量。将声明性 transactions 应用于多个 objects 是一种流行的选择。

Bean 定义的名称 match,例如前面的 example 中的jdkMyBeanonlyJdk,是带有目标 class 的普通旧 bean 定义。 BeanNameAutoProxyCreator自动创建 AOP 代理。相同的建议适用于所有匹配的 beans。请注意,如果使用顾问程序(而不是前面的 example 中的拦截器),则切入点可能会以不同的方式应用于不同的 beans。

DefaultAdvisorAutoProxyCreator

一个更通用且极其强大的 auto-proxy 创建者是DefaultAdvisorAutoProxyCreator。这会在当前 context 中自动应用符合条件的顾问程序,而无需在 auto-proxy 顾问程序的 bean 定义中包含特定的 bean 名称。它提供了与BeanNameAutoProxyCreator一致的 configuration 和避免重复的相同优点。

使用此机制涉及:

  • 指定DefaultAdvisorAutoProxyCreator bean 定义。
  • 在相同或相关的上下文中指定任意数量的顾问程序。请注意,这些必须是顾问,而不是拦截器或其他建议。这是必要的,因为必须有一个切入点来评估,以检查每个建议是否符合候选 bean 定义。

DefaultAdvisorAutoProxyCreator自动评估每个顾问程序中包含的切入点,以查看它应该应用于每个业务 object 的建议(如果有的话)(例如 example 中的businessObject1businessObject2)。

这意味着可以自动将任意数量的顾问程序应用于每个业务 object。如果任何顾问程序中没有切入点与业务 object 中的任何方法匹配,则 object 不会被代理。当为新业务 objects 添加 bean 定义时,如有必要,它们会自动代理。

Auto-proxying 通常具有使调用者或依赖项无法获得 un-advised object 的优点。在此ApplicationContext上调用getBean("businessObject1")将返回 AOP 代理,而不是目标业务 object。 (前面显示的“内部 bean”成语也提供了这个 benefit.)

以下 example 创建DefaultAdvisorAutoProxyCreator bean 以及本节中讨论的其他元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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"/>

如果要将相同的建议一致地应用于许多业务 objects,则DefaultAdvisorAutoProxyCreator非常有用。基础结构定义到位后,您可以添加新的业务 objects,而不包括特定的代理 configuration。您还可以轻松地删除其他方面(对于 example,跟踪或 performance 监视方面),只需对 configuration 进行最少的更改。

DefaultAdvisorAutoProxyCreator提供对过滤的支持(通过使用命名约定,以便仅评估某些顾问程序,这允许在同一工厂中使用多个,不同配置的 AdvisorAutoProxyCreators)和 ordering。如果这是一个问题,顾问可以实现org.springframework.core.Ordered接口以确保正确的 ordering。前面的 example 中使用的TransactionAttributeSourceAdvisor具有可配置的 order value。默认设置是无序的。

使用TargetSource实现

Spring 提供TargetSource的概念,在org.springframework.aop.TargetSource接口中表示。该接口负责返回实现连接点的“target object”。在 AOP 代理处理方法调用的每个 time 时,都要求TargetSource implementation 提供目标实例。

使用 Spring AOP 的开发人员通常不需要直接使用TargetSource implementations,但这提供了支持池,热插拔和其他复杂目标的强大方法。例如,通过使用池来管理实例,池TargetSource可以为每次调用返回不同的目标实例。

如果未指定TargetSource,则使用默认 implementation 来包装本地 object。每次调用都会返回相同的目标(正如您所期望的那样)。

本节的 rest 描述了 Spring 提供的标准目标源以及如何使用它们。

使用自定义目标源时,您的目标通常需要是原型而不是 singleton bean 定义。这允许 Spring 在需要时创建新的目标实例。

Hot-swappable目标来源

org.springframework.aop.target.HotSwappableTargetSource的存在是为了让 AOP 代理的目标切换,同时让调用者保持对它的 references。

更改目标源的目标会立即生效。 HotSwappableTargetSource是 thread-safe。

您可以使用 HotSwappableTargetSource 上的swap()方法更改目标,如下面的 example 所示:

1
2
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);

以下 example 显示了所需的 XML 定义:

1
2
3
4
5
6
7
8
9
<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 持有 reference 的客户端不知道更改但立即开始命中新目标。

虽然这个 example 没有添加任何建议(没有必要添加使用TargetSource的建议),但任何TargetSource都可以与任意建议一起使用。

池目标来源

使用池化目标源为 stateless session EJB 提供了类似的编程 model,其中维护了相同实例的池,方法调用将在池中释放 objects。

Spring 池和 SLSB 池之间的一个重要区别是 Spring 池可以应用于任何 POJO。与 Spring 一样,此服务可以 non-invasive 方式应用。

Spring 为 Commons _Pool 2.2 提供支持,它提供了一个相当有效的池 implementation。您需要 application 的 classpath 上的commons-pool Jar 才能使用此 feature。您还可以将org.springframework.aop.target.AbstractPoolingTargetSource子类化为支持任何其他池 API。

Commons _Pool 1.5 也受支持,但从 Spring Framework 4.2 开始不推荐使用。

以下清单显示了 example configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

请注意,目标 object(前面的 example 中的businessObjectTarget)必须是原型。这使得PoolingTargetSource implementation 可以根据需要创建目标的新实例来扩展池。有关其 properties 的信息,请参阅AbstractPoolingTargetSource 的 javadoc和您希望使用的具体子类。 maxSize是最基本的,并且始终保证存在。

在这种情况下,myInterceptor是需要在同一 IoC context 中定义的拦截器的 name。但是,您无需指定拦截器来使用池。如果您只想要池化而没有其他建议,请不要设置interceptorNames property。

您可以将 Spring 配置为能够将任何池化的 object 转换为org.springframework.aop.target.PoolingConfig接口,该接口通过介绍公开有关 configuration 和池当前大小的信息。您需要定义类似于以下内容的顾问程序:

1
2
3
4
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="poolTargetSource"/>
<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>

通过在AbstractPoolingTargetSource class 上调用便捷方法获得此顾问程序,因此使用MethodInvokingFactoryBean。此顾问程序的 name(poolConfigAdvisor,此处)必须位于ProxyFactoryBean中的拦截器名称列表中,以暴露池化的 object。

演员阵容定义如下:

1
2
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());

通常不需要池化 stateless 服务 objects。我们不相信它应该是默认选择,因为大多数 stateless objects 自然是线程安全的,并且如果资源被缓存,实例池是有问题的。

使用 auto-proxying 可以实现更简单的池化。您可以设置任何 auto-proxy 创建者使用的TargetSource __mplement。

原型目标来源

设置“原型”目标源类似于设置池TargetSource。在这种情况下,将在每次方法调用时创建目标的新实例。虽然在现代 JVM 中创建一个新的 object 的成本并不高,但连接新的 object(满足其 IoC 依赖性)的成本可能更高。因此,如果没有充分的理由,就不应该使用这种方法。

为此,您可以修改前面显示的poolTargetSource定义,如下所示(为清晰起见,我们还更改了 name):

1
2
3
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>

唯一的 property 是目标 bean 的 name。在TargetSource implementations 中使用继承来确保一致的命名。与池化目标源一样,目标 bean 必须是原型 bean 定义。

ThreadLocal目标源

如果需要为每个传入请求创建 object(每个线程),ThreadLocal目标源很有用。 ThreadLocal的概念提供了一个 JDK-wide 工具来透明地将一个资源与一个线程一起存储。设置ThreadLocalTargetSource与其他类型的目标源所解释的几乎相同,如下面的示例所示:

1
2
3
<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
</bean>

ThreadLocal实例在 multi-threaded 和 multi-classloader 环境中错误地使用它们时会出现严重问题(可能导致 memory 泄漏)。您应该始终考虑将 threadlocal 包装在其他 class 中,并且永远不要直接使用ThreadLocal本身(wrapper class 除外)。此外,您应该始终记住正确设置和取消设置(后者只需要调用ThreadLocal.set(null))线程的本地资源。在任何情况下都应该进行取消,因为不取消它可能会导致有问题的行为。 Spring 的ThreadLocal支持为您执行此操作,应始终考虑使用ThreadLocal实例而无需其他正确处理 code。

定义新的通知类型

Spring AOP 旨在可扩展。虽然拦截 implementation 策略目前在内部使用,但除了拦截建议之外,之前,抛出建议和返回建议之后,还可以支持任意建议类型。

org.springframework.aop.framework.adapter包是一个 SPI 包,可以在不更改核心 framework 的情况下添加对新自定义通知类型的支持。自定义Advice类型的唯一约束是它必须实现org.aopalliance.aop.Advice标记接口。

有关详细信息,请参阅org.springframework.aop.framework.adapter javadoc。

Null-safety

尽管 Java 不允许您使用其类型系统表达 null-safety,但 Spring Framework 现在在org.springframework.lang包中提供以下注释,以便您声明 API 和字段的可为空性:

  • @NonNull:注解表示特定参数 returnvalue 或字段不能null(参数和 return value 不需要@NonNullApi@NonNullFields适用)。
  • @Nullable:注解表示特定参数 returnvalue 或 field 可以是null
  • @NonNullApi:包 level 的 Annotation 声明 non-null 作为参数和 return 值的默认行为。
  • @NonNullFields:包 level 的 Annotation 声明 non-null 作为字段的默认行为。

Spring Framework 利用这些注释,但它们也可以在任何基于 Spring 的 Java 项目中使用,以声明 null-safe API 和可选的 null-safe 字段。尚未支持泛型类型 arguments,varargs 和 array 元素的可空性,但应该在即将发布的版本中,请参阅SPR-15942以获取 up-to-date 信息。预计 Spring Framework 发布之间的可空性声明为 fine-tuned,包括次要声明。方法体内使用的类型的可为空性超出了此 feature 的范围。

_Reactor 或 Spring Data 这样的库提供了使用此 feature 的 null-safe API。

用例

除了为 Spring Framework API 可空性提供显式声明之外,IDE(例如 IDEA 或 Eclipse)可以使用这些注释来提供与 null-safety 相关的有用警告,以避免在运行时NullPointerException

它们还用于在 Kotlin 项目中制作 Spring API null-safe,因为 Kotlin 本身支持null-safety。更多详情可在Kotlin 支持文档中找到。

JSR305元注解

Spring annotations 是 meta-annotated with JSR 305 annotations(一个休眠但广泛传播的 JSR)。 JSR 305 meta-annotations 让像 IDEA 或 Kotlin 这样的工具供应商以通用方式提供 null-safety 支持,而不必支持 Spring annotations。

没有必要也不建议在项目 classpath 中添加 JSR 305 依赖项以利用 Spring null-safe API。只有在其代码库中使用 null-safety annotations 的 Spring-based libraries 等项目才应添加带有compileOnly Gradle configuration 或 Maven provided范围的com.google.code.findbugs:jsr305:3.0.2以避免编译警告。

数据缓冲区和编解码器

Java NIO 提供ByteBuffer,但许多 libraries 在它们自己的字节缓冲区 API 之上构建,特别是对于使用直接缓冲区重用缓冲区 and/or 有利于 performance 的网络操作。对于 example Netty 具有ByteBuf层次结构,Undertow 使用 XNIO,Jetty 使用池化字节缓冲区并释放回调,依此类推。 spring-core模块提供了一组抽象来处理各种字节缓冲区 API,如下所示:

DataBufferFactory

DataBufferFactory用于以两种方式之一创建数据缓冲区:

  • 分配一个新的数据缓冲区,可选择预先指定容量(如果已知),即使DataBuffer的实现可以根据需要增长和缩小,这也会更有效。
  • 包装现有的byte[]java.nio.ByteBuffer,它使用DataBuffer implementation 来装饰给定数据,并且不涉及分配。

请注意,WebFlux applications 不直接创建DataBufferFactory,而是通过 client 侧的ServerHttpResponseClientHttpRequest访问它。工厂类型取决于底层的客户端或服务器 e.g. NettyDataBufferFactory表示 Reactor Netty,DefaultDataBufferFactory表示其他人。

DataBuffer

DataBuffer接口提供与java.nio.ByteBuffer类似的操作,但也带来一些额外的好处,其中一些受 Netty ByteBuf的启发。以下是部分福利清单:

  • 读写独立职位,i.e。不需要调用flip()来在读写之间交替。
  • java.lang.StringBuilder一样,按需扩容。
  • 通过PooledDataBuffer汇总缓冲区和 reference 计数。
  • 将缓冲区视为java.nio.ByteBufferInputStreamOutputStream
  • 确定给定字节的索引或最后一个索引。

PooledDataBuffer

正如 Javadoc for 字节缓冲区中所解释的,字节缓冲区可以是直接缓冲区或 non-direct。直接缓冲区可以驻留在 Java 堆之外,这样就无需复制本机 I/O 操作。这使得直接缓冲区对于通过 socket 接收和发送数据特别有用,但是它们创建和释放的成本也更高,这导致了池缓冲区的 idea。

PooledDataBufferDataBuffer的扩展,它有助于 reference 计数,这对于字节缓冲池是必不可少的。它是如何工作的?分配PooledDataBuffer时,reference 计数为 1. Calls 到retain()递增计数,而 calls 到release()递减计数。当计数大于 0 时_long,保证缓冲区不被释放。当计数减少到 0 时,可以释放池化缓冲区,这实际上可能意味着缓冲区的保留 memory 返回到 memory 池。

请注意,不是直接在PooledDataBuffer上操作,在大多数情况下最好使用DataBufferUtils中的方便方法,只有当它是PooledDataBuffer的实例时才应用释放或保留到DataBuffer

DataBufferUtils

DataBufferUtils提供了许多实用程序方法来操作数据缓冲区:

  • 将数据缓冲区流连接到单个缓冲区,可能没有副本 e.g. 通过复合缓冲区,如果底层字节缓冲区 API 支持。
  • InputStream或 NIO Channel转换为Flux<DataBuffer>,反之亦然Publisher<DataBuffer>转入OutputStream或 NIO Channel
  • 如果缓冲区是PooledDataBuffer的实例,则释放或保留DataBuffer的方法。
  • 从字节流中跳过或取出,直到特定的字节数。

编解码器

org.springframework.core.codec包提供以下策略接口:

  • EncoderPublisher<T>编码为数据缓冲区流。
  • DecoderPublisher<DataBuffer>解码为更高 level objects 的流。

spring-core模块提供byte[]ByteBufferDataBufferResourceString编码器和解码器 implementations。 spring-web模块添加了 Jackson JSON,Jackson Smile,JAXB2,Protocol Buffers 和其他编码器和解码器。请参阅 WebFlux 部分中的编解码器

使用DataBuffer

使用数据缓冲区时,必须特别注意确保释放缓冲区,因为它们可能是汇集。我们将使用编解码器来说明它是如何工作的,但概念更普遍适用。让我们看看内部编解码器必须在内部管理数据缓冲区。

在创建更高 level objects 之前,Decoder是读取输入数据缓冲区的最后一个,因此它必须按如下方式释放它们:

  • 如果Decoder只是读取每个输入缓冲区并准备立即释放它,它可以通过DataBufferUtils.release(dataBuffer)来实现。
  • 如果Decoder正在使用FluxMono operators,例如flatMapreduce和其他在内部预取和缓存数据项的操作符,或者正在使用filterskip等其他省略项的操作符,则必须将doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)添加到组合中确保这些缓冲区在被丢弃之前被释放,也可能因此导致错误或取消信号。
  • 如果Decoder以任何其他方式保留一个或多个数据缓冲区,则必须确保在完全读取时释放它们,或者在读取和释放高速缓存数据缓冲区之前发生错误或取消信号。

请注意,DataBufferUtils#join提供了一种安全有效的方法,可将数据缓冲区流聚合到单个数据缓冲区中。同样,skipUntilByteCounttakeUntilByteCount是解码器使用的附加安全方法。

Encoder分配其他人必须读取(和释放)的数据缓冲区。所以Encoder没什么可做的。但是,如果在使用数据填充缓冲区时发生序列化错误,则Encoder必须注意释放数据缓冲区。例如:

1
2
3
4
5
6
7
8
9
10
11
12
DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
// serialize and populate buffer..
release = false;
}
finally {
if (release) {
DataBufferUtils.release(buffer);
}
}
return buffer;

的 consumer 负责释放它接收的数据缓冲区。在 WebFlux application 中,Encoder的输出用于写入 HTTP 服务器响应或 client HTTP 请求,在这种情况下,释放数据缓冲区是 code 写入服务器响应或 client 的责任。请求。

请注意,在 Netty 上运行时,会有排除缓冲区泄漏的调试选项。

附录

XML模式

附录的这一部分列出了与核心容器相关的 XML 模式。

utilSchema

正如 name 所暗示的那样,util标签处理 common,实用程序配置问题,例如配置集合,引用常量等等。要使用util schema 中的标记,您需要在 Spring XML configuration 文件的顶部添加以下前导码(片段中的文本引用正确的 schema,以便util命名空间中的标记可供您使用):

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

<!-- bean definitions here -->
</beans>

使用<util:constant/>

考虑以下 bean 定义:

1
2
3
4
5
6
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>

前面的 configuration 使用 Spring FactoryBean 实现(FieldRetrievingFactoryBean)将 bean 上的isolation property 的 value 设置为java.sql.Connection.TRANSACTION_SERIALIZABLE常量的 value。这一切都很好,但它很冗长,并且(不必要地)将 Spring 的内部管道暴露给最终用户。

以下 XML Schema-based version 更简洁,清楚地表达了开发人员的意图(“_in this constant value”),并且它读起来更好:

1
2
3
4
5
<bean id="..." class="...">
<property name="isolation">
<util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</property>
</bean>
从字段Value设置BeanProperty或Constructor参数

FieldRetrievingFactoryBeanFactoryBean,它检索static或 non-static 字段 value。它通常用于检索public static final常量,然后可以使用这些常量为另一个 bean 设置 property value 或 constructor 参数。

以下 example 通过使用staticField property 显示如何公开static字段:

1
2
3
4
<bean id="myField"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
<property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>

还有一个便利用法表单,其中static字段被指定为 bean name,如下面的 example 所示:

1
2
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>

这确实意味着 bean id不再有任何选择(因此引用它的任何其他 bean 也必须使用这个更长的 name),但这个表单定义非常简洁,非常方便用作内部 bean 因为不必为 bean reference 指定,如下面的 example 所示:

1
2
3
4
5
6
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>

您还可以访问另一个 bean 的 non-static(实例)字段,如FieldRetrievingFactoryBean class 的 API 文档中所述。

在 Spring 中将枚举值作为 property 或构造函数 arguments 注入到 beans 中很容易。你实际上不需要做任何事情或知道关于 Spring 内部的任何事情(甚至关于_c 等类的_c_)。以下 example 枚举显示了注入 enum value 是多么容易:

1
2
3
4
5
6
7
package javax.persistence;

public enum PersistenceContextType {

TRANSACTION,
EXTENDED
}

现在考虑以下类型PersistenceContextType的 setter 和相应的 bean 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
package example;

public class Client {

private PersistenceContextType persistenceContextType;

public void setPersistenceContextType(PersistenceContextType type) {
this.persistenceContextType = type;
}
}
<bean class="example.Client">
<property name="persistenceContextType" value="TRANSACTION"/>
</bean>

使用<util:property-path/>

考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 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"/>

前面的 configuration 使用 Spring FactoryBean implementation(PropertyPathFactoryBean)来创建一个 bean(类型为int),名为testBean.age,其 value 等于testBean bean 的age property。

现在考虑以下 example,它添加一个<util:property-path/>元素:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>

<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>

<property-path/>元素的path属性的 value 遵循beanName.beanProperty的形式。在这种情况下,它会选择名为testBean的 bean 的age property。 age property 的 value 是10

使用<util:property-path/>设置BeanProperty或ConstructorArgument

PropertyPathFactoryBeanFactoryBean,它评估给定目标 object 上的 property 路径。目标 object 可以直接指定,也可以由 bean name 指定。然后,您可以在另一个 bean 定义中将此 value 用作 property value 或 constructor 参数。

以下 example 显示了一个用于另一个 bean 的路径,由 name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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 评估路径:

1
2
3
4
5
6
7
8
9
10
<!-- 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 name 是 property 路径。以下 example 显示了快捷方式:

1
2
3
<!-- 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 的 name 中没有选择。任何 reference 它也必须使用相同的id,这是路径。如果用作内部 bean,则根本不需要引用它,如下面的示例所示:

1
2
3
4
5
6
<bean id="..." class="...">
<property name="age">
<bean id="person.age"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
</property>
</bean>

您可以在实际定义中专门设置结果类型。对于大多数用例来说,这不是必需的,但它有时可能很有用。有关此 feature 的更多信息,请参阅 javadoc。

使用<util:properties/>

考虑以下 example:

1
2
3
4
<!-- 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>

前面的 configuration 使用 Spring FactoryBean implementation(PropertiesFactoryBean)来实例化一个java.util.Properties实例,其值从提供的资源)位置加载。

以下 example 使用util:properties元素来表示更简洁的表示:

1
2
<!-- 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/>

考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
<!-- creates a java.util.List instance with values loaded from the supplied 'sourceList' -->
<bean id="emails" class="org.springframework.beans.factory.config.ListFactoryBean">
<property name="sourceList">
<list>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
</list>
</property>
</bean>

前面的 configuration 使用 Spring FactoryBean implementation(ListFactoryBean)来创建java.util.List实例,并使用从提供的sourceList获取的值对其进行初始化。

以下 example 使用<util:list/>元素来表示更简洁的表示:

1
2
3
4
5
6
7
<!-- creates a java.util.List instance with the supplied values -->
<util:list id="emails">
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
</util:list>

您还可以使用<util:list/>元素上的list-class属性显式控制实例化和填充的List的确切类型。例如,如果我们确实需要实例化java.util.LinkedList,我们可以使用以下 configuration:

1
2
3
4
5
6
<util:list id="emails" list-class="java.util.LinkedList">
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>d'[emailprotected]</value>
</util:list>

如果未提供list-class属性,则容器将选择List implementation。

使用<util:map/>

考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
<!-- creates a java.util.Map instance with values loaded from the supplied 'sourceMap' -->
<bean id="emails" class="org.springframework.beans.factory.config.MapFactoryBean">
<property name="sourceMap">
<map>
<entry key="pechorin" value="[emailprotected]"/>
<entry key="raskolnikov" value="[emailprotected]"/>
<entry key="stavrogin" value="[emailprotected]"/>
<entry key="porfiry" value="[emailprotected]"/>
</map>
</property>
</bean>

前面的 configuration 使用 Spring FactoryBean implementation(MapFactoryBean)来创建一个java.util.Map实例,该实例使用从提供的'sourceMap'获取的 key-value 对进行初始化。

以下 example 使用<util:map/>元素来表示更简洁的表示:

1
2
3
4
5
6
7
<!-- creates a java.util.Map instance with the supplied key-value pairs -->
<util:map id="emails">
<entry key="pechorin" value="[emailprotected]"/>
<entry key="raskolnikov" value="[emailprotected]"/>
<entry key="stavrogin" value="[emailprotected]"/>
<entry key="porfiry" value="[emailprotected]"/>
</util:map>

您还可以使用<util:map/>元素上的'map-class'属性显式控制实例化和填充的Map的确切类型。例如,如果我们确实需要实例化java.util.TreeMap,我们可以使用以下 configuration:

1
2
3
4
5
6
<util:map id="emails" map-class="java.util.TreeMap">
<entry key="pechorin" value="[emailprotected]"/>
<entry key="raskolnikov" value="[emailprotected]"/>
<entry key="stavrogin" value="[emailprotected]"/>
<entry key="porfiry" value="[emailprotected]"/>
</util:map>

如果未提供'map-class'属性,则容器将选择Map implementation。

使用<util:set/>

考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
<!-- creates a java.util.Set instance with values loaded from the supplied 'sourceSet' -->
<bean id="emails" class="org.springframework.beans.factory.config.SetFactoryBean">
<property name="sourceSet">
<set>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
</set>
</property>
</bean>

前面的 configuration 使用 Spring FactoryBean implementation(SetFactoryBean)来创建一个java.util.Set实例,该实例使用从提供的sourceSet获取的值进行初始化。

以下 example 使用<util:set/>元素来表示更简洁的表示:

1
2
3
4
5
6
7
<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
</util:set>

您还可以使用<util:set/>元素上的set-class属性显式控制实例化和填充的Set的确切类型。例如,如果我们确实需要实例化java.util.TreeSet,我们可以使用以下 configuration:

1
2
3
4
5
6
<util:set id="emails" set-class="java.util.TreeSet">
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
<value>[emailprotected]</value>
</util:set>

如果未提供set-class属性,则容器将选择Set implementation。

AOPSchema

aop标签用于配置 Spring 中的所有 AOP,包括使用 AspectJ AOP framework 的 Spring 自己的 proxy-based AOP framework 和 Spring 的 integration。这些标签在题为使用 Spring 进行面向对象编程的章节中全面介绍。

为了完整性,要使用aop schema 中的标记,您需要在 Spring XML configuration 文件的顶部放置以下前导码(片段中的文本引用正确的 schema,以便aop命名空间中的标记为你可以得到):

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- bean definitions here -->
</beans>

ContextSchema

context标签处理与管道相关的ApplicationContext configuration - 也就是说,通常不是 beans 对 end-user 很重要,而是 beans 在 Spring 中执行很多“咕噜”工作,比如BeanfactoryPostProcessors。以下代码段 references 正确的 schema,以便context命名空间中的元素可供您使用:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<!-- bean definitions here -->
</beans>

使用<property-placeholder/>

此元素激活${…}占位符的替换,这些占位符是针对指定的 properties 文件(作为Spring 资源位置解析的。这个元素是一个方便的机制,为你设置一个PropertyPlaceholderConfigurer。如果您需要更多地控制PropertyPlaceholderConfigurer,您可以自己明确定义一个。

使用<annotation-config/>

此元素激活 Spring 基础结构以检测 bean classes 中的 annotations:

  • Spring 的@Required@Autowired
  • JSR 250 的@PostConstruct@PreDestroy@Resource(如果有的话)
  • JPA 的@PersistenceContext@PersistenceUnit(如果可用)。

或者,您可以选择为那些注释显式激活单个BeanPostProcessors

此元素不会激活 Spring 的@Transactional annotation 的处理。您可以将< 1 >元素用于此目的。

使用<component-scan/>

这个元素在Annotation-based container configuration中有详细说明。

使用<load-time-weaver/>

这个元素在Load-time 在 Spring Framework 中使用 AspectJ 进行编织中有详细说明。

使用<spring-configured/>

这个元素在使用 AspectJ 依赖 inject 域 objects 与 Spring中有详细说明。

使用<mbean-export/>

这个元素在配置基于 annotation 的 MBean export中有详细说明。

BeansSchema

最后但同样重要的是,我们在beans schema 中有元素。自 framework 出现以来,这些元素一直存在于 Spring 中。这里没有显示beans schema 中各种元素的示例,因为它们在依赖关系和 configuration 详细(实际上,整个章节中得到了相当全面的介绍。

请注意,您可以向<bean/> XML 定义添加零个或多个 key-value 对。如果有的话,使用这些额外的元数据完成的工作完全取决于您自己的自定义逻辑(因此,如果您按照标题为XML Schema Authoring的附录中所述编写自己的自定义元素,通常只会使用它。

下面的 example 显示了周围<bean/>的 context 中的<meta/>元素(请注意,没有任何逻辑可以解释它,元数据实际上是无用的)。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="foo" class="x.y.Foo">
<meta key="cacheName" value="foo"/> (1)
<property name="name" value="Rick"/>
</bean>

</beans>
1 这是 example meta元素

在前面的 example 的情况下,您可以假设有一些逻辑使用 bean 定义并设置一些使用提供的元数据的缓存基础结构。

XMLSchema编写

自 version 2.0 以来,Spring 具有一种机制,可以将 schema-based extensions 添加到基本的 Spring XML 格式,以便定义和配置 beans。本节介绍如何编写自己的自定义 XML bean 定义解析器,并将这些解析器集成到 Spring IoC 容器中。

为了便于创作使用 schema-aware XML 编辑器的 configuration files,Spring 的可扩展 XML configuration 机制基于 XML Schema。如果您不熟悉标准 Spring 发行版附带的 Spring 当前的 XML configuration extensions,则应首先阅读标题为[xsd-config]的附录。

要创建新的 XML configuration extensions:

  • Author用于描述自定义 element(s 的 XML schema。
  • Code自定义NamespaceHandler 实现。
  • Code一个或多个BeanDefinitionParser实现(这是真正的工作完成的地方)。
  • Register你的新 artifacts 与 Spring。

对于统一的 example,我们创建了一个 XML 扩展(一个自定义 XML 元素),它允许我们配置SimpleDateFormat类型的 objects(来自java.text包)。完成后,我们将能够定义SimpleDateFormat类型的 bean 定义,如下所示:

1
2
3
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>

(我们在本附录中稍后会包含更详细的示例.第一个简单示例的目的是引导您完成制作自定义的基本步骤 extension.)

编辑Schema

创建用于 Spring 的 IoC 容器的 XML configuration 扩展,首先创建一个 XML Schema 来描述扩展。对于我们的 example,我们使用以下 schema 来配置SimpleDateFormat objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- myns.xsd (inside package org/springframework/samples/xml) -->

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.com/schema/myns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.mycompany.com/schema/myns"
elementFormDefault="qualified"
attributeFormDefault="unqualified">

<xsd:import namespace="http://www.springframework.org/schema/beans"/>

<xsd:element name="dateformat">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType"> (1)
<xsd:attribute name="lenient" type="xsd:boolean"/>
<xsd:attribute name="pattern" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
1 指示的 line 包含所有可识别标记的扩展名基础(意味着它们具有id属性,我们可以将其用作容器中的 bean 标识符)。我们可以使用此属性,因为我们导入了 Spring-provided beans命名空间。

前面的 schema 允许我们使用<myns:dateformat/>元素直接在 XML application context 文件中配置SimpleDateFormat objects,如下面的 example 所示:

1
2
3
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>

请注意,在我们创建了基础结构 classes 之后,前面的 XML 代码段基本上与以下 XML 代码段相同:

1
2
3
4
<bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-HH-dd HH:mm"/>
<property name="lenient" value="true"/>
</bean>

前两个片段中的第二个在容器中创建 bean(由SimpleDateFormat类型的 name dateFormat标识),并设置了几个 properties。

方法 creating configuration 格式允许使用具有 schema-aware XML 编辑器的 IDE 进行紧密整合。通过使用正确创作的 schema,您可以使用自动完成功能让用户在枚举中定义的几个 configuration 选项之间进行选择。

编码NamespaceHandler

除了 schema 之外,我们还需要一个NamespaceHandler来解析 Spring 在解析 configuration files 时遇到的这个特定命名空间的所有元素。对于这个例子,NamespaceHandler应该处理myns:dateformat元素的解析。

NamespaceHandler接口 features 有三种方法:

  • init():允许初始化NamespaceHandler,并在使用处理程序之前由 Spring 调用。
  • BeanDefinition parse(Element, ParserContext):Spring 遇到 top-level 元素时调用(不嵌套在 bean 定义或不同的命名空间内)。此方法本身可以注册 bean 定义,return bean 定义,或两者。
  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext):当 Spring 遇到不同命名空间的属性或嵌套元素时调用。使用Spring 支持的范围(对于 example)使用一个或多个 bean 定义的修饰。我们首先突出显示一个简单的 example,不使用装饰,之后我们在一个更高级的 example 中显示装饰。

虽然您可以为整个命名空间编码自己的NamespaceHandler(因此提供解析命名空间中每个元素的 code),但通常情况是 Spring XML configuration 文件中的每个 top-level XML 元素都会产生一个 bean 定义(就像在我们的例子中,单个<myns:dateformat/>元素导致单个SimpleDateFormat bean 定义)。 Spring features 支持这种情况的许多便利 classes。在下面的示例中,我们使用NamespaceHandlerSupport class:

1
2
3
4
5
6
7
8
9
10
11
package org.springframework.samples.xml;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class MyNamespaceHandler extends NamespaceHandlerSupport {

public void init() {
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
}

}

你可能会注意到这个 class 中实际上并没有很多解析逻辑。实际上,NamespaceHandlerSupport class 有一个 built-in 授权概念。它支持在需要解析其名称空间中的元素时注册任意数量的BeanDefinitionParser实例。这种干净的关注分离让NamespaceHandler处理其命名空间中所有自定义元素的解析编排,同时委托BeanDefinitionParsers来完成 XML 解析的繁重工作。这意味着每个BeanDefinitionParser只包含解析单个自定义元素的逻辑,我们可以在下一个 step 中看到。

使用BeanDefinitionParser

如果NamespaceHandler遇到已映射到特定 bean 定义解析器的类型的 XML 元素(在本例中为dateformat),则使用BeanDefinitionParser。换句话说,BeanDefinitionParser负责解析 schema 中定义的一个不同的 top-level XML 元素。在解析器中,我们可以访问 XML 元素(以及它的子元素),以便我们可以解析自定义 XML 内容,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.springframework.samples.xml;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

import java.text.SimpleDateFormat;

public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { (1)

protected Class getBeanClass(Element element) {
return SimpleDateFormat.class; (2)
}

protected void doParse(Element element, BeanDefinitionBuilder bean) {
// this will never be null since the schema explicitly requires that a value be supplied
String pattern = element.getAttribute("pattern");
bean.addConstructorArg(pattern);

// this however is an optional property
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}

}
1 我们使用 Spring-provided AbstractSingleBeanDefinitionParser来处理 creating 单个BeanDefinition的许多基本 grunt 工作。
2 我们为AbstractSingleBeanDefinitionParser超类提供单个BeanDefinition所代表的类型。

在这个简单的例子中,这就是我们需要做的一切。我们的单个BeanDefinition的创建由AbstractSingleBeanDefinitionParser超类处理,bean 定义的唯一标识符的提取和设置也是如此。

注册Handler和Schema

编码完成。剩下要做的就是让 Spring XML 解析基础架构了解我们的自定义元素。我们通过在两个 special-purpose properties files 中注册我们的自定义namespaceHandler和自定义 XSD 文件来实现。这些 properties files 都放在 application 的META-INF目录中,对于 example,可以与 JAR 文件中的二进制 class 一起分发。 Spring XML 解析基础架构通过 consuming 这些特殊的 properties files 自动选择你的新扩展,其格式将在接下来的两节中详细介绍。

编写META-INF/spring.handlers

名为spring.handlers的 properties 文件包含 XML Schema URI 到名称空间处理程序 classes 的映射。对于我们的 example,我们需要编写以下内容:

1
http\://www.mycompany.com/schema/myns=org.springframework.samples.xml.MyNamespaceHandler

(:字符是 Java properties 格式的有效分隔符,因此 URI 中的:字符需要使用 backslash.)进行转义

key-value 对的第一部分(key)是与自定义命名空间扩展相关联的 URI,需要完全匹配targetNamespace属性的 value,如自定义 XSD schema 中指定的那样。

编写META-INF/spring.schemas

名为spring.schemas的 properties 文件包含 XML Schema 位置的映射(与 schema 声明一起使用 schema 作为xsi:schemaLocation属性的一部分的 XML files)与 classpath 资源的映射。需要此文件来防止 Spring 绝对必须使用需要 Internet 访问权限的默认EntityResolver才能检索 schema 文件。如果在此 properties 文件中指定映射,Spring 将在 classpath 上搜索 schema(在本例中为org.springframework.samples.xml包中的myns.xsd)。以下代码段显示了我们需要为自定义 schema 添加的 line:

1
http\://www.mycompany.com/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd

(请记住,:字符必须是 escaped.)

建议您在 classpath 上的NamespaceHandlerBeanDefinitionParser classes 旁边部署 XSD 文件(或 files)。

在SpringXMLConfiguration中使用自定义扩展

使用自己实现的自定义扩展与使用 Spring 提供的“自定义”extensions 没有什么不同。以下 example 使用 Spring XML configuration 文件中前面步骤中开发的自定义<dateformat/>元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:myns="http://www.mycompany.com/schema/myns"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.mycompany.com/schema/myns http://www.mycompany.com/schema/myns/myns.xsd">

<!-- as a top-level bean -->
<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/> (1)

<bean id="jobDetailTemplate" abstract="true">
<property name="dateFormat">
<!-- as an inner bean -->
<myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
</property>
</bean>

</beans>
1 我们的自定义 bean。

更详细的例子

本节介绍自定义 XML extensions 的一些更详细的示例。

在自定义元素中嵌套自定义元素

本节中提供的 example 显示了如何编写满足以下 configuration 目标所需的各种 artifact:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:foo="http://www.foo.com/schema/component"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.foo.com/schema/component http://www.foo.com/schema/component/component.xsd">

<foo:component id="bionic-family" name="Bionic-1">
<foo:component name="Mother-1">
<foo:component name="Karate-1"/>
<foo:component name="Sport-1"/>
</foo:component>
<foo:component name="Rock-1"/>
</foo:component>

</beans>

前面的 configuration 将自定义 extensions 嵌套在一起。 <foo:component/>元素实际配置的 class 是Component class(显示在下一个 example 中)。注意Component class 如何不公开components property 的 setter 方法。这使得使用 setter 注入为Component class 配置 bean 定义变得困难(或者更不可能)。以下清单显示了Component class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.foo;

import java.util.ArrayList;
import java.util.List;

public class Component {

private String name;
private List<Component> components = new ArrayList<Component> ();

// mmm, there is no setter method for the 'components'
public void addComponent(Component component) {
this.components.add(component);
}

public List<Component> getComponents() {
return components;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}

此问题的典型解决方案是创建一个自定义FactoryBean,为components property 公开 setter property。以下清单显示了这样的自定义FactoryBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.foo;

import org.springframework.beans.factory.FactoryBean;

import java.util.List;

public class ComponentFactoryBean implements FactoryBean<Component> {

private Component parent;
private List<Component> children;

public void setParent(Component parent) {
this.parent = parent;
}

public void setChildren(List<Component> children) {
this.children = children;
}

public Component getObject() throws Exception {
if (this.children != null && this.children.size() > 0) {
for (Component child : children) {
this.parent.addComponent(child);
}
}
return this.parent;
}

public Class<Component> getObjectType() {
return Component.class;
}

public boolean isSingleton() {
return true;
}

}

这很好用,但它向最终用户公开了很多 Spring 管道。我们要做的是编写一个自定义扩展,隐藏所有这些 Spring 管道。如果我们坚持前面描述的步骤,我们首先通过创建 XSD schema 来定义自定义标记的结构,如下面的清单所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.com/schema/component"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.com/schema/component"
elementFormDefault="qualified"
attributeFormDefault="unqualified">

<xsd:element name="component">
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="component"/>
</xsd:choice>
<xsd:attribute name="id" type="xsd:ID"/>
<xsd:attribute name="name" use="required" type="xsd:string"/>
</xsd:complexType>
</xsd:element>

</xsd:schema>

再次关注前面描述的 process,然后我们创建一个自定义NamespaceHandler

1
2
3
4
5
6
7
8
9
10
11
package com.foo;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

public void init() {
registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
}

}

接下来是自定义BeanDefinitionParser。请记住,我们正在创建描述ComponentFactoryBeanBeanDefinition。以下清单显示了我们的自定义BeanDefinitionParser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.foo;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import java.util.List;

public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser {

protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
return parseComponentElement(element);
}

private static AbstractBeanDefinition parseComponentElement(Element element) {
BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class);
factory.addPropertyValue("parent", parseComponent(element));

List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
if (childElements != null && childElements.size() > 0) {
parseChildComponents(childElements, factory);
}

return factory.getBeanDefinition();
}

private static BeanDefinition parseComponent(Element element) {
BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
component.addPropertyValue("name", element.getAttribute("name"));
return component.getBeanDefinition();
}

private static void parseChildComponents(List<Element> childElements, BeanDefinitionBuilder factory) {
ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
for (Element element : childElements) {
children.add(parseComponentElement(element));
}
factory.addPropertyValue("children", children);
}

}

最后,通过修改META-INF/spring.handlersMETA-INF/spring.schemas files,需要在 Spring XML 基础结构中注册各种 artifact,如下所示:

1
2
3
4
# in 'META-INF/spring.handlers'
http\://www.foo.com/schema/component=com.foo.ComponentNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.com/schema/component/component.xsd=com/foo/component.xsd

“普通”元素的自定义属性

编写自己的自定义解析器和关联的 artifacts 并不难。但是,有时这不是正确的做法。考虑需要向现有 bean 定义添加元数据的场景。在这种情况下,您当然不希望编写自己的整个自定义扩展。相反,您只想在现有的 bean 定义元素中添加其他属性。

通过另一个例子,假设您为服务 object 定义 bean 定义(对它来说是未知的)访问集群JCache,并且您希望确保在周围的 cluster 中急切地启动命名的 JCache 实例。以下清单显示了这样一个定义:

1
2
3
4
<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
jcache:cache-name="checking.account">
<!-- other dependencies here... -->
</bean>

然后,我们可以在解析'jcache:cache-name'属性时创建另一个BeanDefinition。这个BeanDefinition然后为我们初始化命名的 JCache。我们还可以修改'checkingAccountService'的现有BeanDefinition,以便它依赖于这个新的 JCache-initializing BeanDefinition。以下清单显示了我们的JCacheInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.foo;

public class JCacheInitializer {

private String name;

public JCacheInitializer(String name) {
this.name = name;
}

public void initialize() {
// lots of JCache API calls to initialize the named cache...
}

}

现在我们可以转到自定义扩展。首先,我们需要编写描述自定义属性的 XSD schema,如下所示:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.com/schema/jcache"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.com/schema/jcache"
elementFormDefault="qualified">

<xsd:attribute name="cache-name" type="xsd:string"/>

</xsd:schema>

接下来,我们需要创建关联的NamespaceHandler,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
package com.foo;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class JCacheNamespaceHandler extends NamespaceHandlerSupport {

public void init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
new JCacheInitializingBeanDefinitionDecorator());
}

}

接下来,我们需要创建解析器。请注意,在这种情况下,因为我们要解析 XML 属性,所以我们写一个BeanDefinitionDecorator而不是BeanDefinitionParser。以下清单显示了我们的BeanDefinitionDecorator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.foo;

import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {

private static final String[] EMPTY_STRING_ARRAY = new String[0];

public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder,
ParserContext ctx) {
String initializerBeanName = registerJCacheInitializer(source, ctx);
createDependencyOnJCacheInitializer(holder, initializerBeanName);
return holder;
}

private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder,
String initializerBeanName) {
AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition());
String[] dependsOn = definition.getDependsOn();
if (dependsOn == null) {
dependsOn = new String[]{initializerBeanName};
} else {
List dependencies = new ArrayList(Arrays.asList(dependsOn));
dependencies.add(initializerBeanName);
dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY);
}
definition.setDependsOn(dependsOn);
}

private String registerJCacheInitializer(Node source, ParserContext ctx) {
String cacheName = ((Attr) source).getValue();
String beanName = cacheName + "-initializer";
if (!ctx.getRegistry().containsBeanDefinition(beanName)) {
BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class);
initializer.addConstructorArg(cacheName);
ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition());
}
return beanName;
}

}

最后,我们需要通过修改META-INF/spring.handlersMETA-INF/spring.schemas files 向 Spring XML 基础结构注册各种 artifact,如下所示:

1
2
3
4
# in 'META-INF/spring.handlers'
http\://www.foo.com/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.com/schema/jcache/jcache.xsd=com/foo/jcache.xsd