11. Spring 面向切面的编程

11.1 Introduction

面向方面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。 OOP 中模块化的关键单位是类,而在 AOP 中模块化的单位是方面。方面使关注点模块化,例如跨多种类型和对象的事务 Management。 (在 AOP 文献中,此类关注点通常被称为“跨领域”关注点.)

Spring 的关键组件之一是* AOP framework *。尽管 Spring IoC 容器不依赖于 AOP,这意味着您不需要使用 AOP,但 AOP 是对 Spring IoC 的补充,以提供功能强大的中间件解决方案。

Spring 2.0+ AOP

Spring 2.0 引入了一种使用schema-based approach@AspectJ 注解样式编写自定义方面的更简单,更强大的方法。这两种样式都提供了完全类型化的建议并使用了 AspectJ 切入点语言,同时仍使用 Spring AOP 进行编织。

本章讨论了基于 Spring 2.0 模式和基于@AspectJ 的 AOP 支持。 接下来的章节中讨论了 Spring 1.2 应用程序中常见的较低级别的 AOP 支持。

AOP 在 Spring 框架中用于…

  • …提供声明性企业服务,尤其是代替 EJB 声明性服务。最重要的此类服务是声明式 TransactionManagement

  • …允许用户实现自定义方面,并通过 AOP 补充其对 OOP 的使用。

Note

如果您只对通用声明性服务或其他预打包的声明性中间件服务(例如池)感兴趣,则无需直接使用 Spring AOP,并且可以跳过本章的大部分内容。

11.1.1 AOP 概念

让我们首先定义一些重要的 AOP 概念和术语。这些术语不是特定于 Spring 的...不幸的是,AOP 术语不是特别直观。但是,如果使用 Spring 自己的术语,将会更加令人困惑。

    • Aspect *:关注点的模块化,跨多个类。事务 Management 是企业 Java 应用程序中横切关注的一个很好的例子。在 Spring AOP 中,方面是使用常规类(schema-based approach)或使用@Aspect注解(@AspectJ style)Comments 的常规类实现的。
  • 连接点:程序执行期间的一个点,例如方法执行或异常处理。在 Spring AOP 中,连接点“总是”表示方法的执行。

  • 忠告:方面在特定的连接点处采取的操作。不同类型的建议包括“周围”,“之前”和“之后”建议。 (建议类型将在下面讨论.)包括 Spring 在内的许多 AOP 框架都将建议建模为* interceptor ,并在连接点附近保持一系列拦截器。

    • Pointcut *:匹配连接点的谓词。建议与切入点表达式关联,并在与该切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。切入点表达式匹配的连接点的概念是 AOP 的核心,并且 Spring 默认使用 AspectJ 切入点表达语言。
  • 简介:代表类型声明其他方法或字段。 Spring AOP 允许您向任何建议对象引入新接口(和相应的实现)。例如,您可以使用简介使 Bean 实现IsModified接口,以简化缓存。 (在 AspectJ 社区中,介绍被称为类型间声明.)

  • 目标对象:一个或多个方面建议的对象。也称为“建议”对象。由于 Spring AOP 是使用运行时代理实现的,因此该对象将始终是代理对象。

    • AOP 代理*:由 AOP 框架创建的一个对象,用于实现方面协定(建议方法执行等)。在 Spring 框架中,AOP 代理将是 JDK 动态代理或 CGLIB 代理。
  • 编织:将方面与其他应用程序类型或对象链接以创建建议的对象。这可以在编译时(例如,使用 AspectJ 编译器),加载时或在运行时完成。像其他纯 Java AOP 框架一样,Spring AOP 在运行时执行编织。

咨询类型:

    • Before advisor *:在连接点之前执行的建议,但是它不能阻止执行流前进到连接点(除非它引发异常)。
  • 返回建议之后:连接点正常完成后要执行的建议:例如,如果某个方法返回而没有引发异常。

  • 抛出建议后:如果方法因抛出异常而退出,则执行建议。

  • (最终)建议之后:无论连接点退出的方式如何(正常或特殊返回),都将执行建议。

  • 围绕建议:围绕连接点的建议,例如方法调用。这是最有力的建议。周围建议可以在方法调用之前和之后执行自定义行为。它还负责选择是返回连接点还是通过返回其自身的返回值或引发异常来捷径建议的方法执行。

围绕建议是最通用的建议。由于 Spring AOP 与 AspectJ 一样,提供了各种建议类型,因此我们建议您使用功能最弱的建议类型,以实现所需的行为。例如,如果您只需要使用方法的返回值更新缓存,则最好使用返回后的建议而不是周围的建议,尽管周围的建议可以完成相同的事情。使用最具体的建议类型可以提供更简单的编程模型,并减少出错的可能性。例如,您不需要在用于周围建议的JoinPoint上调用proceed()方法,因此不会失败。

在 Spring 2.0 中,所有建议参数都是静态类型的,因此您可以使用适当类型的建议参数(例如,从方法执行返回的值的类型)而不是Object数组。

连接点的概念以及切入点是 AOP 的关键,它使 AOP 与仅提供拦截功能的旧技术区分开。切入点使建议的目标独立于面向对象的层次结构。例如,提供声明式事务 Management 的环绕建议可以应用于跨越多个对象(例如服务层中的所有业务操作)的一组方法。

11.1.2 Spring AOP 功能和目标

Spring AOP 是用纯 Java 实现的。不需要特殊的编译过程。 Spring AOP 不需要控制类加载器的层次结构,因此适合在 Servlet 容器或应用程序服务器中使用。

Spring AOP 当前仅支持方法执行连接点(建议在 Spring Bean 上执行方法)。尽管可以在不破坏核心 Spring AOP API 的情况下添加对字段拦截的支持,但并未实现字段拦截。如果需要建议字段访问和更新连接点,请考虑使用诸如 AspectJ 之类的语言。

Spring AOP 的 AOP 方法不同于大多数其他 AOP 框架。目的不是提供最完整的 AOP 实现(尽管 Spring AOP 相当强大);而是在 AOP 实现和 Spring IoC 之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。

因此,例如,通常将 Spring Framework 的 AOP 功能与 Spring IoC 容器结合使用。使用常规的 bean 定义语法来配置方面(尽管这允许强大的“自动代理”功能):这是与其他 AOP 实现的关键区别。使用 Spring AOP 有些事情无法轻松或高效地完成,例如建议非常细粒度的对象(通常是域对象):在这种情况下,AspectJ 是最佳选择。但是,我们的经验是,Spring AOP 为企业 Java 应用程序中适合 AOP 的大多数问题提供了出色的解决方案。

Spring AOP 决不会与 AspectJ 竞争以提供全面的 AOP 解决方案。我们认为,基于代理的框架(如 Spring AOP)和成熟的框架(如 AspectJ)都是有价值的,并且它们是互补的,而不是竞争。 Spring 无缝地将 Spring AOP 和 IoC 与 AspectJ 集成在一起,从而能够在基于 Spring 的一致应用程序体系结构中满足 AOP 的所有使用。这种集成不会影响 Spring AOP API 或 AOP Alliance API:Spring AOP 保持向后兼容。有关 Spring AOP API 的讨论,请参见接下来的章节

Note

Spring 框架的中心宗旨之一是“非侵入性”。这是一个想法,您不应被迫将特定于框架的类和接口引入业务/域模型。但是,在某些地方,Spring Framework 确实为您提供了将特定于 Spring Framework 的依赖项引入您的代码库的选项:之所以提供此类选项,是因为在某些情况下,读取或编码其中的某些特定部分可能很容易这种功能。 Spring 框架(几乎)总是为您提供选择:您可以自由地就哪个选项最适合您的特定用例或场景做出明智的决定。

与本章相关的一种选择是选择哪种 AOP 框架(以及哪种 AOP 样式)。您可以选择 AspectJ 和/或 Spring AOP,也可以选择@AspectJComments 样式方法或 Spring XML 配置样式方法。本章选择首先介绍@AspectJ 风格的方法这一事实不应被视为表明 Spring 团队比 Spring XML 配置风格更喜欢@AspectJComments 风格的方法。

有关每种样式的理由的详细讨论,请参见第 11.4 节“选择要使用的 AOP 声明样式”

11.1.3 AOP 代理

Spring AOP 默认将标准 JDK 动态代理用于 AOP 代理。这使得可以代理任何接口(或一组接口)。

Spring AOP 也可以使用 CGLIB 代理。这对于代理类而不是接口是必需的。如果业务对象未实现接口,则默认情况下使用 CGLIB。最好的做法是对接口进行编程,而不是对类进行编程。业务类通常将实现一个或多个业务接口。在某些情况下(可能极少发生),您需要建议未在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法,则可以使用强制使用 CGLIB

重要的是要掌握 Spring AOP 是基于“代理”的事实。有关此实现细节实际含义的彻底检查,请参见第 11.6.1 节“了解 AOP 代理”

11.2 @AspectJ 支持

@AspectJ 是一种将方面声明为带有 Comments 的常规 Java 类的样式。 @AspectJ 样式是AspectJ project作为 AspectJ 5 版本的一部分引入的。 Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ 5 相同的 Comments。但是,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

Note

使用 AspectJ 编译器和 weaver 可以使用完整的 AspectJ 语言,将在第 11.8 节“将 AspectJ 与 Spring 应用程序一起使用”中进行讨论。

11.2.1 启用@AspectJ 支持

要在 Spring 配置中使用@AspectJ 方面,您需要启用 Spring 支持以基于@AspectJ 方面来配置 Spring AOP,并基于这些方面是否建议来对 bean 进行“自动代理”。通过自动代理,我们的意思是如果 Spring 确定一个或多个方面建议使用 bean,它将自动为该 bean 生成一个代理,以拦截方法调用并确保按需执行建议。

可以通过 XML 或 Java 样式配置启用@AspectJ 支持。无论哪种情况,您都需要确保 AspectJ 的aspectjweaver.jar库位于应用程序的 Classpath(版本 1.6.8 或更高版本)上。该库在 AspectJ 发行版的'lib'目录中或通过 Maven Central 存储库可用。

通过 Java 配置启用@AspectJ 支持

要使用 Java @Configuration启用@AspectJ 支持,请添加@EnableAspectJAutoProxy注解:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

通过 XML 配置启用@AspectJ 支持

要对基于 XML 的配置启用@AspectJ 支持,请使用aop:aspectj-autoproxy元素:

<aop:aspectj-autoproxy/>

假设您正在使用第 41 章,基于 XML Schema 的配置中所述的架构支持。有关如何在aop名称空间中导入标签的信息,请参见第 41.2.7 节“ aop 模式”

11.2.2 声明一个方面

启用@AspectJ 支持后,Spring 会自动检测在应用程序上下文中使用@AspectJ 方面(具有@AspectComments)的类定义的任何 bean,并将其用于配置 Spring AOP。以下示例显示了一个不太有用的方面所需的最小定义:

应用程序上下文中的常规 bean 定义,指向具有@Aspect注解的 bean 类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of aspect here as normal -->
</bean>

NotVeryUsefulAspect类定义,并带有org.aspectj.lang.annotation.AspectComments;

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

方面(带有@Aspect的类)可能具有与其他任何类一样的方法和字段。它们可能还包含切入点,建议和介绍(类型间)声明。

Note

您可以将方面类注册为 Spring XML 配置中的常规 bean,也可以像其他任何 Spring 托管 bean 一样,通过 Classpath 扫描自动检测它们。但是,请注意,* @ Aspect Comments 不足以在 Classpath 中进行自动检测:为此,您需要添加一个单独的 @ Component *Comments(或替代地,按照规则进行限定的自定义构造型 Comments) Spring 的组件扫描器)。

Note

在 Spring AOP 中,不可能使自己成为其他方面的建议目标。类上的* @ Aspect *Comments 将其标记为一个方面,因此将其从自动代理中排除。

11.2.3 声明切入点

回想一下,切入点确定了感兴趣的连接点,从而使我们能够控制何时执行建议。 * Spring AOP 仅支持 Spring bean 的方法执行连接点,因此您可以将切入点视为与 Spring bean 上的方法执行相匹配。切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入表达式,该表达式精确地确定我们感兴趣的方法执行。在 AOP 的@AspectJ 注解样式中,切入点签名由 AOP 提供。常规方法定义,并使用@PointcutComments 指示切入点表达式(用作切入点签名的方法必须*具有void返回类型)。

一个示例将有助于使切入点签名和切入点表达式之间的区别更加清晰。下面的示例定义一个名为'anyOldTransfer'的切入点,该切入点将匹配任何名为'transfer'的方法的执行:

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

形成@Pointcut注解的值的切入点表达式是一个常规的 AspectJ 5 切入点表达式。有关 AspectJ 的切入点语言的完整讨论,请参见AspectJ 编程指南(以及 extensionsAspectJ 5 开发人员笔记本)或有关 AspectJ 的书之一,例如 Colyer 等人的“ Eclipse AspectJ”。等或 Ramnivas Laddad 的“ AspectJ in Action”。

支持的切入点指示符

Spring AOP 支持以下在切入点表达式中使用的 AspectJ 切入点指示符(PCD):

Other pointcut types

完整的 AspectJ 切入点语言支持 Spring 不支持的其他切入点指示符。它们是:call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this@withincode。在 Spring AOP 解释的切入点表达式中使用这些切入点指示符将导致抛出IllegalArgumentException

Spring AOP 支持的切入点指示符集合可能会在将来的版本中扩展,以支持更多的 AspectJ 切入点指示符。

    • execution *-用于匹配方法执行连接点,这是在使用 Spring AOP 时将使用的主要切入点指示符
    • in **-限制对某些类型内的连接点的匹配(仅在使用 Spring AOP 时在匹配类型内声明的方法的执行)
    • this *-限制匹配到连接点(使用 Spring AOP 时方法的执行),其中 bean 引用(Spring AOP 代理)是给定类型的实例
    • target *-限制匹配到连接点(使用 Spring AOP 时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例
    • args *-限制匹配点(参数是给定类型的实例)的连接点(使用 Spring AOP 时方法的执行)
    • @ target *-限制匹配到连接点(使用 Spring AOP 时方法的执行),其中执行对象的类具有给定类型的 Comments
    • @ args *-限制匹配的连接点(使用 Spring AOP 时方法的执行),其中传递的实际参数的运行时类型具有给定类型的 Comments
    • @ within *-限制匹配到具有给定 Comments 的类型内的连接点(使用 Spring AOP 时,使用给定 Comments 的类型中声明的方法的执行)
    • @ annotation *-将匹配点限制为其中连接点的主题(在 Spring AOP 中执行的方法)具有给定 Comments 的连接点

由于 Spring AOP 仅将匹配限制为仅方法执行连接点,因此,与“ AspectJ 编程指南”相比,上面对切入点指示符的讨论所提供的定义范围更窄。另外,AspectJ 本身具有基于类型的语义,并且在执行连接点处thistarget均引用同一对象-执行该方法的对象。 Spring AOP 是基于代理的系统,可区分代理对象本身(绑定到this)和代理后面的目标对象(绑定到target)。

Note

由于 Spring 的 AOP 框架基于代理的性质,因此按定义***不会拦截目标对象内的调用。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,将拦截代理上的公共方法和受保护方法,甚至在必要时甚至对程序包可见的方法也将被拦截。但是,通常应通过公共签名设计通过代理进行的常见交互。

请注意,切入点定义通常与任何拦截方法匹配。如果严格地将切入点设置为仅公开使用,即使在 CGLIB 代理方案中通过代理存在潜在的非公开交互作用,也需要相应地进行定义。

如果您的拦截需要在目标类中包括方法调用甚至构造函数,请考虑使用 Spring 驱动的原生 AspectJ 编织而不是 Spring 的基于代理的 AOP 框架。这构成了具有不同特性的 AOP 使用模式,因此请确保在做出决定之前先熟悉编织。

Spring AOP 还支持名为bean的附加 PCD。此 PCD 允许您将连接点的匹配限制为特定的命名 Spring Bean 或一组命名 Spring Bean(使用通配符时)。 bean PCD 具有以下形式:

bean(idOrNameOfBean)

idOrNameOfBean令牌可以是任何 Spring bean 的名称:使用*字符提供了有限的通配符支持,因此,如果您为 Spring bean 构建了一些命名约定,则可以很容易地编写bean PCD 表达式来选择它们。与其他切入点指示符一样,bean PCD 可以&&'ed | || ed 和! (否定)。

Note

请注意,Spring AOP 仅支持bean PCD,而本机 AspectJ 编织则不支持bean PCD。它是 AspectJ 定义的标准 PCD 的特定于 Spring 的扩展,因此不适用于@Aspect模型中声明的方面。

bean PCD 在* instance *级别(基于 Spring bean 名称概念构建)而不是仅在类型级别(这是基于编织的 AOP 所限制)上运行。基于实例的切入点指示符是 Spring 基于代理的 AOP 框架及其与 Spring bean 工厂的紧密集成的一种特殊功能,在该工厂中自然而直接地通过名称来标识特定的 bean。

组合切入点表达式

切入点表达式可以使用'&&','||'进行组合和'!'。也可以通过名称引用切入点表达式。下面的示例显示三个切入点表达式:anyPublicOperation(如果方法执行连接点代表任何公共方法的执行,则它们匹配); inTrading(如果 Transaction 模块中有方法执行,则匹配)和tradingOperation(如果 Transaction 模块中的方法代表任何公共方法,则匹配)。

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如上所示。当按名称引用切入点时,将应用常规的 Java 可见性规则(您可以看到相同类型的私有切入点,层次结构中受保护的切入点,任何地方的公共切入点等)。可见性不会影响切入点* matching *。

共享常用切入点定义

在使用企业应用程序时,您通常希望从多个方面引用应用程序的模块和特定的操作集。我们建议为此定义一个“ SystemArchitecture”方面,以捕获常见的切入点表达式。典型的方面如下所示:

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() {}

}

在一个方面中定义的切入点可以在需要切入点表达式的任何地方引用。例如,要使服务层具有事务性,您可以编写:

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

第 11.3 节“基于模式的 AOP 支持”中讨论了<aop:config><aop:advisor>元素。Transaction 元素在第十七章,TransactionManagement中讨论。

Examples

Spring AOP 用户可能最常使用execution切入点指示符。执行表达式的格式为:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)

除了返回类型模式(上面的片段中为 ret-type-pattern),名称模式和参数模式以外的所有部分都是可选的。返回类型模式确定该方法的返回类型必须是什么才能使连接点匹配。最常见的是,您将使用*作为返回类型模式,该模式与任何返回类型匹配。仅当方法返回给定类型时,标准类型名称才匹配。名称模式与方法名称匹配。您可以将*通配符用作名称模式的全部或一部分。如果指定了声明类型模式,则在其末尾加上.以将其连接到名称模式组件。参数模式稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量的参数(零个或多个)。模式(*)匹配采用任何类型的一个参数的方法,(*,String)匹配采用两个参数的方法,第一个可以是任何类型,第二个必须是 String。有关更多信息,请查阅 AspectJ 编程指南的Language Semantics部分。

常见切入点表达式的一些示例在下面给出。

  • 任何公共方法的执行:
execution(public * *(..))
  • 名称以“ set”开头的任何方法的执行:
execution(* set*(..))
  • AccountService接口定义的任何方法的执行:
execution(* com.xyz.service.AccountService.*(..))
  • 服务包中定义的任何方法的执行:
execution(* com.xyz.service.*.*(..))
  • 服务包或子包中定义的任何方法的执行:
execution(* com.xyz.service..*.*(..))
  • 服务包中的任何连接点(仅在 Spring AOP 中执行方法):
within(com.xyz.service.*)
  • 服务包或子包中的任何连接点(仅在 Spring AOP 中执行方法):
within(com.xyz.service..*)
  • 代理实现AccountService接口的任何连接点(仅在 Spring AOP 中执行方法):
this(com.xyz.service.AccountService)

Note

'this'通常以绑定形式使用:-有关如何使通知对象在建议正文中可用的信息,请参阅以下有关建议的部分。

  • 目标对象实现AccountService接口的任何连接点(仅在 Spring AOP 中执行方法):
target(com.xyz.service.AccountService)

Note

“目标”通常以绑定形式使用:-有关如何使目标对象在建议正文中可用的信息,请参阅以下有关建议的部分。

  • 任何采用单个参数且运行时传递的参数为Serializable的连接点(仅在 Spring AOP 中是方法执行):
args(java.io.Serializable)

Note

'args'通常以绑定形式使用:-有关如何使方法参数在建议正文中可用的信息,请参阅以下有关建议的部分。

请注意,此示例中给出的切入点不同于execution(* *(java.io.Serializable)):如果在运行时传递的参数为 Serializable,则 args 版本匹配,如果方法签名声明单个类型为Serializable的参数,则执行版本匹配。

  • 目标对象具有@Transactional注解的任何连接点(仅在 Spring AOP 中执行方法):
@target(org.springframework.transaction.annotation.Transactional)

Note

'@target'也可以以绑定形式使用:-有关如何使 Comments 对象在建议正文中可用的信息,请参阅以下有关建议的部分。

  • 目标对象的声明类型具有@TransactionalComments 的任何连接点(仅在 Spring AOP 中是方法执行):
@within(org.springframework.transaction.annotation.Transactional)

Note

'@within'也可以以绑定形式使用:-有关如何使 Comments 对象在建议正文中可用的信息,请参阅以下建议部分。

  • 执行方法带有@Transactional注解的任何连接点(仅在 Spring AOP 中是方法执行):
@annotation(org.springframework.transaction.annotation.Transactional)

Note

'@annotation'也可以以绑定形式使用:-有关如何使 Comments 对象在建议正文中可用的信息,请参阅以下建议部分。

  • 任何采用单个参数的连接点(仅在 Spring AOP 中是方法执行),并且传递的参数的运行时类型具有@Classified注解:
@args(com.xyz.security.Classified)

Note

'@args'也可以以绑定形式使用:-有关如何使 Comments 对象在建议正文中可用的信息,请参阅以下有关建议的部分。

  • 名为tradeService的 Spring bean 上的任何连接点(仅在 Spring AOP 中执行方法):
bean(tradeService)
  • 名称与通配符表达式*Service匹配的 Spring bean 上的任何连接点(仅在 Spring AOP 中是方法执行):
bean(*Service)

编写好的切入点

在编译期间,AspectJ 处理切入点,以尝试并优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个昂贵的过程。 (动态匹配意味着无法从静态分析中完全确定匹配,并且将在代码中进行测试以确定在代码运行时是否存在实际匹配)。首次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点是用 DNF(析取范式)重写的,并且对切入点的组件进行了排序,以便首先检查那些较便宜的组件。这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何 Sequences 提供它们。

但是,AspectJ 只能使用所告知的内容,为了获得最佳的匹配性能,您应该考虑他们正在尝试实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的代号自然分为以下三类之一:亲切,范围界定和上下文:

  • 亲切的指示符是选择特定种类的连接点的指示符。例如:执行,获取,设置,调用,处理程序

  • 作用域指定者是那些选择一组感兴趣的连接点的人(可能很多种)。例如:内部,内部代码

  • 上下文指示符是基于上下文匹配(并可选地绑定)的那些。例如:this,target,@ annotation

写得好的切入点应该尝试至少包括前两种类型(种类和作用域),而如果希望基于连接点上下文进行匹配或将上下文绑定以用于建议中,则可以包括上下文指示符。仅提供同类的指示符或仅提供上下文的指示符都可以,但是由于所有额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指定符的匹配非常快,它们的使用意味着 AspectJ 可以非常快地消除不应进一步处理的连接点组-这就是为什么一个好的切入点应始终包括一个切入点的原因。

11.2.4 前置通知

建议与切入点表达式关联,并且在切入点匹配的方法执行之前,之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。

Before advice

使用@Before在切面中声明前置通知:

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() {
        // ...
    }

}

如果使用就地切入点表达式,我们可以将上面的示例重写为:

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声明:

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() {
        // ...
    }

}

Note

注意:在同一个方面内当然可以有多个建议声明以及其他成员。在这些示例中,我们仅显示单个建议声明,以关注当时正在讨论的问题。

有时您需要在建议正文中访问返回的实际值。您可以使用@AfterReturning的形式为此绑定返回值:

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属性中使用的名称必须与 advice 方法中的参数名称相对应。当方法执行返回时,该返回值将作为相应的参数值传递到通知方法。 returning子句还将匹配仅限制为返回指定类型值的方法执行(在这种情况下为Object,它将匹配任何返回值)。

请注意,使用归还建议时,不可能返回完全不同的参考。

异常通知

当执行方法执抛出异常时运行异常通知。使用@AfterThrowing声明异常通知:

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() {
        // ...
    }

}

通常,您希望建议仅在引发给定类型的异常时才运行,并且通常还需要访问通知正文中的异常。使用throwing属性既可以限制匹配(如果需要,也可以使用Throwable作为异常类型),并将抛出的异常绑定到 advice 参数。

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属性中使用的名称必须与 advice 方法中的参数名称相对应。当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给通知方法。 throwing子句还将匹配仅限制为引发指定类型异常(在这种情况下为DataAccessException)的那些方法执行。

后置通知

在方法退出后,无论是否抛出异常,都会运行后置通知。后置通知使用@After进行声明。后置通知必须处理正常或异常返回状况,它通常用于释放资源等。

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() {
        // ...
    }

}

Around advice

最后一种建议是围绕建议。围绕建议在匹配的方法执行过程中“围绕”运行。它有机会在执行该方法之前和之后进行工作,并确定何时,如何以及即使该方法 true 开始执行。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用绕行建议。始终使用满足您要求的功能最弱的建议形式(即在建议可以使用之前,不要在建议的周围使用简单的建议)。

周围的建议使用@AroundComments 声明。咨询方法的第一个参数必须为ProceedingJoinPoint类型。在建议的正文中,在ProceedingJoinPoint上调用proceed()会使底层方法执行。 proceed方法也可以称为传递Object[]传递-数组中的值将在 continue 执行时用作方法执行的参数。

Note

当用 Object []调用时,procedes 的行为与 AspectJ 编译器所编译的 aroundadvice 的行为略有不同。对于使用传统 AspectJ 语言编写的环绕通知,传递给 proc 的参数数量必须与传递给环绕通知的参数数量(而不是基础连接点采用的参数数量)相匹配,并且传递给给定参数位置会取代该值绑定到的实体的连接点处的原始值(不要担心,如果这现在没有意义!)。 Spring 采取的方法更简单,并且更好地匹配基于代理的仅执行语义。如果要编译为 Spring 编写的@AspectJ 方面,并在 AspectJ 编译器和 weaver 中使用 continue 处理参数,则只需要知道这种区别。有一种方法可以在 Spring AOP 和 AspectJ 之间 100%兼容,并且在下面有关建议参数的部分中对此进行了讨论。

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

}

周围建议返回的值将是方法的调用者看到的返回值。例如,一个简单的缓存方面可以从缓存中返回一个值(如果有的话),并调用 proceed()(如果没有)。请注意,proced 可能在遍历建议的正文中被调用一次,多次或完全不被调用,所有这些都是相当合法的。

Advice parameters

Spring 提供了完全类型化的建议-意味着您可以在建议签名中声明所需的参数(正如我们在上面的返回和抛出示例中所看到的),而不是一直使用Object[]数组。稍后我们将介绍如何使参数和其他上下文值可用于建议主体。首先,让我们看一下如何编写通用建议,以了解建议当前建议的方法。

访问当前的 JoinPoint

任何通知方法都可以将类型org.aspectj.lang.JoinPoint的参数声明为其第一个参数(请注意,必须*周围建议以声明类型ProceedingJoinPoint的第一个参数,该参数是JoinPoint的子类.JoinPoint接口提供了许多有用的方法例如getArgs()(返回方法参数),getThis()(返回代理对象),getTarget()(返回目标对象),getSignature()(返回所建议的方法的说明)和toString()(打印正在使用的方法的有用说明)建议)。有关详细信息,请查阅 javadocs。

将参数传递给建议

我们已经看到了如何绑定返回的值或异常值(在返回和引发建议之后使用)。要使参数值可用于建议正文,可以使用args的绑定形式。如果在 args 表达式中使用参数名称代替类型名称,则在调用建议时,相应参数的值将作为参数值传递。一个例子应该使这一点更清楚。假设您要建议执行以 Account 对象为第一个参数的 dao 操作,并且需要访问通知正文中的帐户。您可以编写以下内容:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

切入点表达式的args(account,..)部分有两个用途:首先,它将匹配限制为仅方法采用至少一个参数,并且传递给该参数的参数是Account的实例的方法执行;其次,它通过account参数使实际的Account对象可用于建议。

编写此代码的另一种方法是声明一个切入点,当切入点Account对象值与连接点匹配时,该切入点“提供”,然后仅从通知中引用命名的切入点。如下所示:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

有兴趣的 Reader 可以再次参考 AspectJ 编程指南以获取更多详细信息。

代理对象(this),目标对象(target)和 Comments(@within, @target, @annotation, @args)都可以以类似的方式绑定。下面的示例说明如何匹配用@AuditableCommentsComments 的方法的执行,并提取审计代码。

首先定义@Auditable注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

然后是与@Auditable方法的执行相匹配的建议:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}
建议参数和泛型

Spring AOP 可以处理类声明和方法参数中使用的泛型。假设您具有如下通用类型:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

您可以通过简单地将 advice 参数键入要针对以下方法来拦截方法的参数类型,来将方法类型的拦截限制为某些参数类型:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

正如我们上面已经讨论的那样,这种工作原理非常明显。但是,值得指出的是,这不适用于通用集合。因此,您无法定义这样的切入点:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

为了完成这项工作,我们将必须检查集合中的每个元素,这是不合理的,因为我们也无法决定通常如何处理null值。要实现类似目的,您必须将参数键入Collection<?>并手动检查元素的类型。

确定参数名称

建议调用中的参数绑定依赖于切入点表达式中使用的名称与(建议和切入点)方法签名中声明的参数名称的匹配。通过 Java 反射*不能使用参数名称,因此 Spring AOP 使用以下策略来确定参数名称:

  • 如果用户已明确指定参数名称,则使用指定的参数名称:建议和切入点 Comments 均具有可选的“ argNames”属性,可用于指定带 Comments 方法的参数名称-这些参数名称*在运行时可用。例如:
@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”属性的值中省略参数的名称。例如,如果您修改前面的建议以接收连接点对象,则“ argNames”属性不必包含它:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

对于不收集任何其他连接点上下文的建议,对JoinPointProceedingJoinPointJoinPoint.StaticPart类型的第一个参数进行特殊处理特别方便。在这种情况下,您可以简单地省略“ argNames”属性。例如,以下建议无需声明“ argNames”属性:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}
  • 使用'argNames'属性有点笨拙,因此,如果未指定'argNames'属性,则 Spring AOP 将查看该类的调试信息,并尝试从局部变量表中确定参数名称。只要已使用调试信息编译了类(至少'-g:vars'),该信息就会存在。启用此标志时进行编译的结果是:(1)您的代码将更易于理解(逆向工程),(2)类文件的大小将非常大(通常无关紧要),(3)要删除的优化未使用的局部变量将不会被您的编译器应用。换句话说,使用该标志进行构建不会遇到任何困难。

Note

如果即使没有调试信息,AspectJ 编译器(ajc)都已编译@AspectJ 方面,则无需添加 argNames 属性,因为编译器将保留所需的信息。

  • 如果在没有必要调试信息的情况下编译了代码,则 Spring AOP 将尝试推断绑定变量与参数的配对(例如,如果切入点表达式中仅绑定了一个变量,并且 advice 方法仅接受一个参数,配对很明显!)。如果在给定可用信息的情况下变量的绑定不明确,则将抛出AmbiguousBindingException

  • 如果以上所有策略均失败,则将引发IllegalArgumentException

处理参数

前面我们说过,我们将描述如何编写带有参数*的 procedure 调用,该调用在 Spring AOP 和 AspectJ 上始终有效。解决方案只是确保建议签名按 Sequences 绑定每个方法参数。例如:

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

在许多情况下,无论如何您都将执行此绑定(如上例所示)。

Advice ordering

当多条建议都希望在同一连接点上运行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定建议执行的 Sequences。优先级最高的建议首先“在途中”运行(因此,给定两条优先建议,则优先级最高的建议首先运行)。从连接点“出路”中,优先级最高的建议将最后运行(因此,给定两条后置通知,优先级最高的建议将第二次运行)。

当在不同方面中定义的两条建议都需要在同一连接点上运行时,除非您另外指定,否则执行 Sequences 是不确定的。您可以通过指定优先级来控制执行 Sequences。通过在 Aspect 类中实现org.springframework.core.Ordered接口或使用Order注解对其进行 Comments,可以通过普通的 Spring 方法来完成。给定两个方面,从Ordered.getValue()返回较低值(或 Comments 值)的方面具有较高的优先级。

当在相同方面定义的两条建议都需要在同一连接点上运行时,其 Sequences 是未定义的(因为无法通过反射来获取 javac 编译类的声明 Sequences)。考虑将这些建议方法折叠为每个方面类中每个连接点的一个建议方法,或将建议重构为单独的方面类-可以在方面级别进行排序。

11.2.5 Introductions

简介(在 AspectJ 中称为类型间声明)使方面可以声明建议对象实现给定的接口,并代表那些对象提供该接口的实现。

使用@DeclareParentsComments 进行介绍。此注解用于声明匹配类型具有新的父代(因此而得名)。例如,给定一个接口UsageTracked以及该接口DefaultUsageTracked的实现,以下方面声明,服务接口的所有实现者也都实现UsageTracked接口。 (例如,为了通过 JMX 公开统计信息.)

@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

要实现的接口由带 Comments 的字段的类型确定。 @DeclareParents注解的value属性是 AspectJ 类型的模式:-任何匹配类型的 bean 都将实现 UsageTracked 接口。请注意,在上述示例的之前建议中,服务 Bean 可以直接用作UsageTracked接口的实现。如果以编程方式访问 bean,则应编写以下内容:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

11.2.6 方面实例化模型

Note

(这是一个高级主题,因此,如果您刚开始使用 AOP,则可以放心地跳过它,直到以后.)

默认情况下,应用程序上下文中每个方面都有一个实例。 AspectJ 将此称为单例实例化模型。可以用备用生命周期定义方面:-Spring 支持 AspectJ 的perthispertarget实例化模型(当前不支持percflow, percflowbelow,pertypewithin)。

通过在@Aspect注解中指定perthis子句来声明“ perthis”方面。让我们看一个例子,然后我们将解释它是如何工作的。

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {

    private int someState;

    @Before(com.xyz.myapp.SystemArchitecture.businessService())
    public void recordServiceUsage() {
        // ...
    }

}

'perthis'子句的作用是,将为每个执行业务服务的唯一服务对象创建一个方面实例(每个唯一对象在与切入点表达式匹配的连接点处绑定到“ this”的每个对象)。方面实例是在服务对象上首次调用方法时创建的。当服务对象超出范围时,方面将超出范围。在创建方面实例之前,其中的任何建议都不会执行。创建方面实例后,在其中声明的建议将在匹配的连接点处执行,但仅当服务对象与此方面相关联时才执行。有关每个条款的更多信息,请参见 AspectJ 编程指南。

'pertarget'实例化模型的工作方式与 perthis 完全相同,但是在匹配的连接点为每个唯一目标对象创建一个方面实例。

11.2.7 Example

既然您已经了解了所有组成部分的工作方式,那么让我们将它们放在一起做一些有用的事情!

有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很可能在下一轮成功。对于适合在这种情况下重试的业务服务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作以避免 Client 端看到PessimisticLockingFailureException。这项要求清楚地跨越了服务层中的多个服务,因此非常适合通过方面实施。

因为我们想重试该操作,所以我们将需要使用围绕建议,以便可以多次调用 proced。这是基本方面实现的外观:

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

}

请注意,方面实现了Ordered接口,因此我们可以将方面的优先级设置为高于事务建议(每次重试时都希望有新的事务)。 maxRetriesorder属性都将由 Spring 配置。主要动作发生在doConcurrentOperation周围建议中。注意,目前我们将重试逻辑应用于所有businessService()s。我们尝试 continue,如果失败并失败了PessimisticLockingFailureException,我们将再次尝试,除非我们用尽了所有的重试尝试。

相应的 Spring 配置为:

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

为了改进方面,使其仅重试幂等操作,我们可以定义Idempotent注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

并使用 Comments 来 Comments 服务操作的实现。将方面更改为仅重试幂等操作仅涉及优化切入点表达式,以便只有@Idempotent个操作匹配:

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    ...
}

11.3 基于架构的 AOP 支持

如果您更喜欢基于 XML 的格式,那么 Spring 还支持使用新的“ aop”名称空间标签来定义方面。与使用@AspectJ 样式时,支持完全相同的切入点表达式和建议类型,因此,在本节中,我们将重点介绍新的语法,并向 Reader 介绍上一节(第 11.2 节“ @AspectJ 支持”)的讨论,以使他们对写作有所了解。切入点表达式和建议参数的绑定。

要使用本节中描述的 aop 名称空间标记,您需要按照第 41 章,基于 XML Schema 的配置所述导入spring-aop模式。有关如何在aop名称空间中导入标签的信息,请参见第 41.2.7 节“ aop 模式”

在您的 Spring 配置中,所有方面和顾问元素都必须放在<aop:config>元素内(在应用程序上下文配置中可以有多个<aop:config>元素)。 <aop:config>元素可以包含切入点,顾问和方面元素(请注意,这些元素必须按此 Sequences 声明)。

Warning

<aop:config>样式的配置大量使用了 Spring 的auto-proxying机制。如果您已经通过使用BeanNameAutoProxyCreator或类似方法来使用显式自动代理,则可能会导致问题(例如未编制建议)。推荐的用法模式是仅使用<aop:config>样式,或者仅使用AutoProxyCreator样式。

11.3.1 声明一个方面

使用模式支持,方面只是在 Spring 应用程序上下文中定义为 Bean 的常规 Java 对象。状态和行为在对象的字段和方法中捕获,切入点和建议信息在 XML 中捕获。

使用\ <>元素声明一个方面,并使用ref属性引用该支持 bean:

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

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

支持方面(在本例中为"aBean")的 bean 当然可以像配置其他任何 Spring bean 一样进行配置并注入依赖项。

11.3.2 声明切入点

可以在\ <>元素内声明命名的切入点,从而使切入点定义可以在多个方面和顾问程序之间共享。

可以定义代表服务层中任何业务服务的执行的切入点:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

注意,切入点表达式本身使用的是与第 11.2 节“ @AspectJ 支持”中所述的 AspectJ 切入点表达式语言。如果使用基于架构的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法是:

<aop:config>

    <aop:pointcut id="businessService"
        expression="com.xyz.myapp.SystemArchitecture.businessService()"/>

</aop:config>

假设您具有称为“共享通用切入点定义”的部分中所述的SystemArchitecture方面。

在一个方面中声明一个切入点与声明一个顶级切入点非常相似:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        ...

    </aop:aspect>

</aop:config>

在@AspectJ 方面,使用相同的方式,使用基于架构的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点收集“ this”对象作为连接点上下文,并将其传递给通知:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) &amp;&amp; this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...

    </aop:aspect>

</aop:config>

必须声明建议,以通过包含匹配名称的参数来接收收集的连接点上下文:

public void monitor(Object service) {
    ...
}

当组合切入点子表达式时,&&在 XML 文档中很尴尬,因此可以使用关键字andornot代替&&||!。例如,上一个切入点可能更好地写为:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service..(..)) and this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>
</aop:config>

请注意,以这种方式定义的切入点由其 XML ID 引用,并且不能用作命名切入点以形成复合切入点。因此,基于架构的定义样式中的命名切入点支持比@AspectJ 样式所提供的更受限制。

11.3.3 声明建议

支持与@AspectJ 样式相同的五种建议类型,它们具有完全相同的语义。

Before advice

在运行匹配的方法之前,建议运行之前。使用\ <>元素在<aop:aspect>内部声明它。

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

dataAccessOperation是在最高(<aop:config>)级别定义的切入点的 ID。要改为定义切入点内联,请将pointcut-ref属性替换为pointcut属性:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
        method="doAccessCheck"/>

    ...

</aop:aspect>

正如我们在@AspectJ 样式的讨论中所指出的那样,使用命名的切入点可以显着提高代码的可读性。

method 属性标识提供建议正文的方法(doAccessCheck)。必须为包含建议的 Aspect 元素所引用的 bean 定义此方法。在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用方面 Bean 上的“ doAccessCheck”方法。

返回建议后

返回的建议在匹配的方法执行正常完成时运行。在<aop:aspect>内部以与建议之前相同的方式声明它。例如:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

就像@AspectJ 样式一样,可以在建议正文中保留返回值。使用 returning 属性指定返回值应传递到的参数的名称:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

doAccessCheck 方法必须声明一个名为retVal的参数。该参数的类型以与@AfterReturning 中所述相同的方式约束匹配。例如,方法签名可以声明为:

public void doAccessCheck(Object retVal) {...

提出建议后

抛出建议后,当匹配的方法执行通过抛出异常退出时执行建议。使用后抛出元素在<aop:aspect>内部声明它:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

就像@AspectJ 样式一样,可以在通知正文中获取引发的异常。使用 throwing 属性指定异常应传递到的参数的名称:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

doRecoveryActions 方法必须声明一个名为dataAccessEx的参数。该参数的类型以与@AfterThrowing 中所述相同的方式约束匹配。例如,方法签名可以声明为:

public void doRecoveryActions(DataAccessException dataAccessEx) {...

(最终)建议后

建议(最终)运行后,匹配的方法执行退出。它使用after元素声明:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>

Around advice

最后一种建议是围绕建议。围绕建议在匹配的方法执行过程中“围绕”运行。它有机会在执行该方法之前和之后进行工作,并确定何时,如何以及即使该方法 true 开始执行。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用绕行建议。始终使用最不可行的建议形式来满足您的要求;如果在建议之前会做的简单,请不要在建议周围使用。

周围的建议使用aop:around元素声明。咨询方法的第一个参数必须为ProceedingJoinPoint类型。在建议的正文中,在ProceedingJoinPoint上调用proceed()会导致基础方法执行。 proceed方法也可能正在调用传入Object[]传递-数组中的值将在 continue 执行时用作方法执行的参数。有关调用 continueObject[]的注意事项,请参见称为“周围建议”的部分

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...

</aop:aspect>

doBasicProfiling建议的实现与@AspectJ 示例中的实现完全相同(当然要减去 Comments):

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

Advice parameters

基于架构的声明样式以与@AspectJ 支持相同的方式支持完全类型的建议-通过按名称将切入点参数与建议方法参数进行匹配。有关详情,请参见称为“建议参数”的部分。如果您希望显式指定建议方法的参数名称(不依赖于先前描述的检测策略),则可以使用建议元素的arg-names属性来完成此操作,该属性与_Name 中的“ argNames”属性相同如名为“确定参数名称”的部分中所述的建议 Comments。例如:

<aop:before
    pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
    method="audit"
    arg-names="auditable"/>

arg-names属性接受逗号分隔的参数名称列表。

在下面找到一个稍微复杂一些的基于 XSD 的方法示例,该示例说明了与许多强类型参数结合使用的一些建议。

package x.y.service;

public interface FooService {

    Foo getFoo(String fooName, int age);
}

public class DefaultFooService implements FooService {

    public Foo getFoo(String name, int age) {
        return new Foo(name, age);
    }
}

接下来是方面。请注意,profile(..)方法接受许多强类型参数,这一事实恰好是用于进行方法调用的连接点:此参数的存在表明profile(..)将用作around建议:

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class SimpleProfiler {

    public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
        StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
        try {
            clock.start(call.toShortString());
            return call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
    }
}

最后,这是实现特定联接点的上述建议所需的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 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="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomeFooServiceMethod"
                expression="execution(* x.y.service.FooService.getFoo(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod"
                method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>

如果我们具有以下驱动程序脚本,我们将在标准输出上获得类似以下的输出:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.FooService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        FooService foo = (FooService) ctx.getBean("fooService");
        foo.getFoo("Pengo", 12);
    }
}
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)

Advice ordering

当需要在同一连接点(执行方法)上执行多个建议时,排序规则如“建议 Order”部分中所述。方面之间的优先级是通过将Order注解添加到支持方面的 bean 或通过使 bean 实现Ordered接口来确定的。

11.3.4 Introductions

简介(在 AspectJ 中称为类型间声明)使方面可以声明建议对象实现给定的接口,并代表那些对象提供该接口的实现。

使用aop:aspect内的aop:declare-parents元素进行了介绍。该元素用于声明匹配类型具有新的父对象(因此称为名称)。例如,给定一个接口UsageTracked以及该接口DefaultUsageTracked的实现,以下方面声明服务接口的所有实现者也都实现UsageTracked接口。 (例如,为了通过 JMX 公开统计信息.)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.SystemArchitecture.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

支持usageTracking bean 的类将包含以下方法:

public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

要实现的接口由implement-interface属性确定。 types-matching属性的值是 AspectJ 类型模式:-任何匹配类型的 bean 都将实现UsageTracked接口。注意,在以上示例的之前建议中,服务 Bean 可以直接用作UsageTracked接口的实现。如果以编程方式访问 bean,则应编写以下内容:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

11.3.5 Aspect 实例化模型

模式定义方面唯一受支持的实例化模型是单例模型。在将来的版本中可能会支持其他实例化模型。

11.3.6 Advisors

“顾问”的概念是从 Spring 中定义的 AOP 支持中提出来的,在 AspectJ 中没有直接等效的概念。顾问就像一个独立的小方面,只有一条建议。通知本身由 Bean 表示,并且必须实现第 12.3.2 节“ Spring 的通知类型”中描述的建议接口之一。顾问可以利用 AspectJ 切入点表达式。

Spring 通过<aop:advisor>元素支持顾问程序概念。您最常会看到它与事务建议结合使用,事务建议在 Spring 中也有其自己的名称空间支持。外观如下:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor
        pointcut-ref="businessService"
        advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

除了以上示例中使用的pointcut-ref属性,您还可以使用pointcut属性来内联定义切入点表达式。

要定义顾问程序的优先级,以便该建议书可以参与 Order,请使用order属性来定义顾问程序的Ordered值。

11.3.7 Example

让我们看看使用模式支持重写第 11.2.7 节“示例”中的并发锁定失败重试示例时的外观。

有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很有可能在下一轮成功。对于适合在这种情况下重试的业务服务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作以避免 Client 端看到PessimisticLockingFailureException。这项要求清楚地跨越了服务层中的多个服务,因此非常适合通过方面实施。

因为我们想重试该操作,所以我们需要使用周围的建议,以便我们可以多次调用 procedure。这是基本方面实现的外观(这是使用模式支持的常规 Java 类):

public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

请注意,方面实现了Ordered接口,因此我们可以将方面的优先级设置为高于事务建议(每次重试时都希望有新的事务)。 maxRetriesorder属性都将由 Spring 配置。主要动作发生在doConcurrentOperation周围建议方法中。我们尝试 continue,如果失败并失败了PessimisticLockingFailureException,我们将再次尝试,除非我们用尽了所有的重试尝试。

Note

该类与@AspectJ 示例中使用的类相同,但是除去了 Comments。

相应的 Spring 配置为:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

请注意,目前我们假定所有业务服务都是幂等的。如果不是这种情况,我们可以通过引入IdempotentComments 来优化方面,使其仅重试 true 的幂等操作:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

并使用 Comments 来 Comments 服务操作的实现。更改为仅重试幂等操作的方面仅涉及优化切入点表达式,以便仅@Idempotent个操作匹配:

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

11.4 选择要使用的 AOP 声明样式

一旦确定方面是实现给定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ,以及在 Aspect 语言(代码)样式,@ AspectJComments 样式或 Spring XML 样式之间做出决定?这些决定受许多因素影响,包括应用程序需求,开发工具以及团队对 AOP 的熟悉程度。

11.4.1 Spring AOP 或完整的 AspectJ?

使用最简单的方法即可。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/编织器。如果您只需要建议在 Spring bean 上执行操作,那么 Spring AOP 是正确的选择。如果您需要建议不受 Spring 容器 Management 的对象(通常是域对象),那么您将需要使用 AspectJ。如果您希望建议除简单方法执行以外的连接点(例如,字段获取或设置连接点,等等),则还需要使用 AspectJ。

使用 AspectJ 时,可以选择 AspectJ 语言语法(也称为“代码样式”)或@AspectJComments 样式。显然,如果您不使用 Java 5,那么您已经选择了……使用代码样式。如果方面在您的设计中起着重要的作用,并且您能够使用AspectJ 开发工具(AJDT)插件用于 Eclipse,则 AspectJ 语言语法是首选的选择:它更简洁,更简单,因为该语言是专为编写方面而设计的。如果您不使用 Eclipse,或者只有少数几个方面在您的应用程序中不起作用,那么您可能需要考虑使用@AspectJ 样式并在 IDE 中坚持常规 Java 编译,并添加一个方面编织阶段到您的构建脚本。

11.4.2 用于 Spring AOP 的@AspectJ 或 XML?

如果选择使用 Spring AOP,则可以选择@AspectJ 或 XML 样式。有各种折衷考虑。

XML 样式将是现有 Spring 用户最熟悉的,并且由 true 的 POJO 支持。当使用 AOP 作为配置企业服务的工具时,XML 是一个不错的选择(一个很好的测试是您是否将切入点表达式视为配置的一部分,您可能希望独立更改)。可以说,使用 XML 样式可以从您的配置中更清楚地了解系统中存在哪些方面。

XML 样式有两个缺点。首先,它没有完全将要解决的需求的实现封装在一个地方。 DRY 原则说,系统中的任何知识都应该有单一,明确,Authority 的表示形式。使用 XML 样式时,“如何实现”需求的知识会分散在支持 Bean 类的声明和配置文件中的 XML 中。使用@AspectJ 样式时,只有一个模块-方面-将该信息封装在其中。其次,与@AspectJ 样式相比,XML 样式在表达能力上有更多限制:仅支持“单例”方面实例化模型,并且无法组合以 XML 声明的命名切入点。例如,使用@AspectJ 样式,您可以编写如下内容:

@Pointcut(execution(* get*()))
public void propertyAccess() {}

@Pointcut(execution(org.xyz.Account+ *(..))
public void operationReturningAnAccount() {}

@Pointcut(propertyAccess() && operationReturningAnAccount())
public void accountPropertyAccess() {}

在 XML 样式中,我可以声明前两个切入点:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML 方法的缺点是无法通过组合这些定义来定义accountPropertyAccess切入点。

@AspectJ 样式支持其他实例化模型和更丰富的切入点组合。它具有将方面保持为模块化单元的优势。它还具有 Spring AOP 和 AspectJ 都可以理解(并因此使用)@AspectJ 方面的优点-因此,如果您以后决定需要 AspectJ 的功能来实现附加要求,则很容易迁移到 AspectJ 基于方法。总而言之,只要您具有比简单的企业服务“配置”功能更多的方面,Spring 团队就喜欢@AspectJ 样式。

11.5 混合方面类型

完全有可能使用自动代理支持,使用模式定义的<aop:aspect>方面,<aop:advisor>声明的顾问程序甚至使用 Spring 1.2 样式在同一配置中定义的代理和拦截器来混合@AspectJ 样式的方面。所有这些都是使用相同的基础支持机制实现的,并且将毫无困难地共存。

11.6 代理机制

Spring AOP 使用 JDK 动态代理或 CGLIB 创建给定目标对象的代理。 (只要有选择,首选 JDK 动态代理)。

如果要代理的目标对象实现至少一个接口,则将使用 JDK 动态代理。目标类型实现的所有接口都将被代理。如果目标对象未实现任何接口,则将创建 CGLIB 代理。

如果要强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅仅是代理由其接口实现的方法),则可以这样做。但是,有一些问题要考虑:

  • 不能建议final方法,因为它们不能被覆盖。

  • 从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的项目 Classpath 中,因为 CGLIB 类在 org.springframework 下重新打包并直接包含在 spring-core JAR 中。这意味着基于 CGLIB 的代理支持“可以正常工作”,就像 JDK 动态代理始终具有的一样。

  • 从 Spring 4.0 开始,由于将通过 Objenesis 创建 CGLIB 代理实例,因此将不再两次调用您的代理对象的构造函数。仅当您的 JVM 不允许绕过构造函数时,您才可能从 Spring 的 AOP 支持中看到两次调用和相应的调试日志条目。

要强制使用 CGLIB 代理,请将<aop:config>元素的proxy-target-class属性的值设置为 true:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用@AspectJ 自动代理支持时强制 CGLIB 代理,请将<aop:aspectj-autoproxy>元素的'proxy-target-class'属性设置为true

<aop:aspectj-autoproxy proxy-target-class="true"/>

Note

多个<aop:config/>节在运行时折叠到一个统一的自动代理创建器中,该创建器将应用<aop:config/>节中的任何(通常来自不同 XML Bean 定义文件)指定的“最强”代理设置。这也适用于<tx:annotation-driven/><aop:aspectj-autoproxy/>元素。

要清楚:在<tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/>元素上使用proxy-target-class="true"将强制对所有三个元素*使用 CGLIB 代理。

11.6.1 了解 AOP 代理

Spring AOP 是基于代理的。在编写自己的方面或使用 Spring Framework 随附的任何基于 Spring AOP 的方面之前,掌握最后一条语句实际含义的语义至关重要。

首先考虑以下情形:您有一个普通的,未经代理的,没有什么特别的,直接的对象引用,如以下代码片段所示。

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果在对象引用上调用方法,则该方法在该对象引用上被“直接”调用,如下所示。

aop 代理普通 pojo 电话

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 端代码具有的引用是代理时,情况会稍有变化。考虑以下图表和代码片段。

aop 代理呼叫

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类的main(..)内部的 Client 端代码具有对 proxy 的引用。这意味着该对象引用上的方法调用将是代理上的调用,因此代理将能够委派给与该特定方法调用相关的所有拦截器(建议)。但是,一旦调用最终到达目标对象(在这种情况下为SimplePojo引用),它可能会对自身调用的任何方法调用(例如this.bar()this.foo())都将针对 this 引用而不是 代理。这具有重要的意义。这意味着自调用不会导致与方法调用相关的建议得到执行的机会。

好吧,那么该怎么办?最好的方法(在这里宽松地使用“最好”一词)是重构代码,以便不会发生自调用。当然,这确实需要您做一些工作,但这是最好的,侵入性最小的方法。下一种方法绝对可怕,而我几乎不愿指出这一点,因为它是如此可怕。通过执行以下操作,您可以(ch 住!)将类中的逻辑完全绑定到 Spring AOP:

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

这将您的代码完全耦合到 Spring AOP,并且使类本身意识到它在 AOP 上下文中使用的事实,而 AOP 上下文却是这样。创建代理时,它还需要一些其他配置:

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 没有此自调用问题,因为它不是基于代理的 AOP 框架。

11.7 以编程方式创建@AspectJ 代理

除了使用<aop:config><aop:aspectj-autoproxy>声明配置中的各个方面外,还可以通过编程方式创建建议目标对象的代理。有关 Spring 的 AOP API 的完整详细信息,请参见下一章。在这里,我们要重点介绍使用@AspectJ 方面自动创建代理的功能。

类别org.springframework.aop.aspectj.annotation.AspectJProxyFactory可用于为一个或多个@AspectJ 方面建议的目标对象创建代理。此类的基本用法非常简单,如下所示。有关完整信息,请参见 javadocs。

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

11.8 将 AspectJ 与 Spring 应用程序一起使用

到目前为止,我们在本章中介绍的所有内容都是纯 Spring AOP。在本节中,我们将研究如果您的需求超出了 Spring AOP 所提供的功能,那么如何使用 AspectJ 编译器/编织器代替 Spring AOP 或作为其补充。

Spring 附带了一个小的 AspectJ 方面库,该库在您的发行版中可以作为spring-aspects.jar独立使用;您需要将其添加到 Classpath 中才能使用其中的方面。 第 11.8.1 节“使用 AspectJ 依赖 Spring 注入域对象”第 11.8.2 节“ AspectJ 的其他 Spring 方面”讨论了该库的内容以及如何使用它。 第 11.8.3 节“使用 Spring IoC 配置 AspectJ 方面”讨论如何依赖注入使用 AspectJ 编译器编织的 AspectJ 方面。最后,第 11.8.4 节“在 Spring 框架中使用 AspectJ 进行加载时编织”介绍了使用 AspectJ 为 Spring 应用程序进行加载时编织。

11.8.1 使用 AspectJ 依赖于 Spring 注入域对象

Spring 容器实例化并配置在您的应用程序上下文中定义的 bean。给定包含要应用的配置的 Bean 定义的名称,也可以要求 Bean 工厂配置一个“预先存在的”对象。 spring-aspects.jar包含 Comments 驱动的方面,该方面利用此功能来允许* any object 的依赖项注入。该支持旨在用于在任何容器的控制范围之外*创建的对象。域对象通常属于此类,因为它们通常是使用new运算符以编程方式创建的,或者是由于数据库查询而由 ORM 工具创建的。

@ConfigurableComments 将一个类标记为符合 Spring 驱动的配置。在最简单的情况下,它可以用作标记 Comments:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

当以这种方式用作标记接口时,Spring 将使用具有与完全限定类型名称(com.xyz.myapp.domain.Account)同名的 bean 定义(通常为原型作用域)来配置带 Comments 类型的新实例(在本例中为Account)。由于 Bean 的默认名称是其类型的完全限定名称,因此声明原型定义的简便方法是简单地省略id属性:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要显式指定要使用的原型 bean 定义的名称,则可以直接在注解中这样做:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring 现在将查找一个名为“ account”的 bean 定义,并将其用作配置新的Account实例的定义。

您也可以使用自动装配来避免完全指定专用的 bean 定义。要让 Spring 应用自动装配,请使用@Configurable注解的autowire属性:分别通过类型或名称指定@Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME进行自动装配。或者,从 Spring 2.5 开始,最好在字段或方法级别使用@Autowired@Inject@Configurable bean 指定显式的,Comments 驱动的依赖项注入(有关更多详细信息,请参见第 7.9 节“基于 Comments 的容器配置”)。

最后,您可以使用dependencyCheck属性(例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))为新创建和配置的对象中的对象引用启用 Spring 依赖项检查。如果此属性设置为 true,那么 Spring 将在配置后验证是否已设置了所有属性(不是基本类型或集合)。

单独使用 Comments 当然没有任何作用。Comments 中存在的是spring-aspects.jar中的AnnotationBeanConfigurerAspect。本质上,该方面说“从初始化带有@Configurable的类型的新对象返回后,根据 Comments 的属性使用 Spring 配置新创建的对象”。在这种情况下,“初始化”是指新实例化的对象(例如,用new运算符实例化的对象)以及正在反序列化(例如,通过readResolve())的Serializable对象。

Note

上段中的关键短语之一是“ 本质上”。在大多数情况下,“ 从新对象的初始化返回之后”的确切语义会很好……在这种情况下,“ 初始化之后”意味着将在对象构建之后注入依赖项-这意味着依赖关系将无法在类的构造函数体中使用。如果您希望构造函数体执行之前注入依赖项,从而可以在构造函数体中使用这些依赖项,那么您需要在@Configurable声明中进行定义,如下所示:

@Configurable(preConstruction=true)

您可以在AspectJ 编程指南的 AspectJ 在本附录中中找到有关各种切入点类型的语言语义的更多信息。

为此,带 Comments 的类型必须与 AspectJ 编织器一起编织-您可以使用构建时的 Ant 或 Maven 任务来执行此操作(例如,参见AspectJ 开发环境指南),也可以使用加载时的编织(请参见第 11.8.4 节“在 Spring 框架中使用 AspectJ 进行加载时编织”)。 AnnotationBeanConfigurerAspect本身需要由 Spring 进行配置(以便获得对将用于配置新对象的 Bean 工厂的引用)。如果使用基于 Java 的配置,只需将@EnableSpringConfigured添加到任何@Configuration类。

@Configuration
@EnableSpringConfigured
public class AppConfig {

}

如果您更喜欢基于 XML 的配置,Spring context namespace定义一个方便的context:spring-configured元素:

<context:spring-configured/>

在配置方面之前*之前创建的@Configurable个对象的实例将导致向调试日志发出一条消息,并且不进行任何对象配置。一个示例可能是 Spring 配置中的 bean,当它由 Spring 初始化时会创建域对象。在这种情况下,您可以使用“ depends-on” bean 属性来手动指定该 bean 取决于配置方面。

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>

Note

除非您真的想在运行时依赖它的语义,否则不要通过 bean configurer 方面激活@Configurable处理。特别是,请确保不要在通过容器注册为常规 Spring Bean 的 Bean 类上使用@Configurable:否则,您将获得两次初始化,一次是通过容器,一次是通过方面。

单元测试@Configurable 对象

@Configurable支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果 AspectJ 尚未编织@Configurable类型,则 Comments 在单元测试期间不起作用,您可以在被测对象中简单地设置模拟或存根属性引用,然后照常进行。如果 AspectJ 编织了@Configurable类型,那么您仍然可以像往常一样在容器外部进行单元测试,但是每次构造@Configurable对象时,您都会看到一条警告消息,指示该对象尚未由 Spring 配置。

处理多个应用程序上下文

用于实现@Configurable支持的AnnotationBeanConfigurerAspect是 AspectJ 单例方面。单例方面的范围与static成员的范围相同,也就是说,每个类加载器都有一个方面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在哪里定义@EnableSpringConfigured bean 以及在哪里将spring-aspects.jar放在 Classpath 上。

考虑一个典型的 Spring Web 应用程序配置,其中有一个共享的父应用程序上下文,用于定义通用的业务服务以及支持它们的一切,并且每个 servlet 都有一个子应用程序上下文,其中包含该 servlet 的特定定义。所有这些上下文将共存于相同的类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能保留对其中一个的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured bean:这定义了您可能想注入域对象中的服务。结果是,您不能使用@Configurable 机制使用在子(特定于 servlet 的)上下文中定义的 Bean 的引用来配置域对象(可能不是您想做的事情!)。

在同一容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序使用其自己的类加载器(例如,将spring-aspects.jar放在'WEB-INF/lib'中)加载spring-aspects.jar中的类型。如果仅将spring-aspects.jar添加到容器范围的 Classpath 中(并因此由共享的父类加载器加载),则所有 Web 应用程序将共享同一方面的实例,这可能不是您想要的。

11.8.2 AspectJ 的其他 Spring 方面

除了@Configurable方面之外,spring-aspects.jar还包含一个 AspectJ 方面,该方面可用于驱动对@TransactionalComments 所 Comments 的类型和方法的 Spring 事务 Management。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。

解释@TransactionalComments 的方面是AnnotationTransactionAspect。使用此方面时,必须 Comments* implementation *类(和/或该类中的方法),而不是 Comments 该类所实现的接口(如果有)。 AspectJ 遵循 Java 的规则,即“不继承”接口上的 Comments。

类上的@TransactionalComments 指定用于执行该类中任何* public *操作的默认事务语义。

类内方法上的@TransactionalComments 将覆盖类 Comments(如果存在)给出的默认事务语义。可以标注任何可见性的方法,包括私有方法。直接 Comments 非公共方法是执行此类方法而获得事务划分的唯一方法。

Tip

从 Spring Framework 4.2 开始,spring-aspects提供了类似的方面,为标准javax.transaction.TransactionalComments 提供了完全相同的功能。检查JtaAnnotationTransactionAspect了解更多详细信息。

对于希望使用 Spring 配置和事务 Management 支持但又不想(或不能)使用 Comments 的 AspectJ 程序员,spring-aspects.jar还包含abstract个方面,您可以扩展它们以提供自己的切入点定义。有关更多信息,请参见AbstractBeanConfigurerAspectAbstractTransactionAspect方面的资源。例如,以下摘录显示了如何编写一个方面以使用与完全合格的类名匹配的原型 Bean 定义来配置域模型中定义的对象的所有实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

    public DomainObjectConfiguration() {
        setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
    }

    // the creation of a new bean (any object in the domain model)
    protected pointcut beanCreation(Object beanInstance) :
        initialization(new(..)) &&
        SystemArchitecture.inDomainModel() &&
        this(beanInstance);

}

11.8.3 使用 Spring IoC 配置 AspectJ 方面

当将 AspectJ 方面与 Spring 应用程序一起使用时,既自然又希望能够使用 Spring 配置此类方面。 AspectJ 运行时本身负责方面的创建,而通过 Spring 配置 AspectJ 创建的方面的方法取决于方面所使用的 AspectJ 实例化模型(per-xxx子句)。

AspectJ 的大多数方面都是* singleton *方面。这些方面的配置非常简单:只需正常地创建一个参照方面类型的 bean 定义,并包含 bean 属性'factory-method="aspectOf"'即可。这可以确保 Spring 通过向 AspectJ 索要长宽比实例,而不是尝试自己创建实例来获得长宽比实例。例如:

<bean id="profiler" class="com.xyz.profiler.Profiler"
        factory-method="aspectOf">

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

非单一方面很难配置:但是,可以通过创建原型 bean 定义并使用spring-aspects.jar中的@Configurable支持来配置方面实例(一旦它们由 AspectJ 运行时创建了 bean)来实现。

如果您有一些要与 AspectJ 编织的@AspectJ 方面(例如,对域模型类型使用加载时编织)以及要与 Spring AOP 一起使用的其他@AspectJ 方面,那么这些方面都已使用 Spring 进行了配置,那么您将需要告诉 Spring AOP @AspectJ 自动代理支持,应使用配置中定义的@AspectJ 方面的确切子集进行自动代理。您可以使用<aop:aspectj-autoproxy/>声明中的一个或多个<include/>元素来完成此操作。每个<include/>元素指定一个名称模式,只有名称与至少一个模式匹配的 bean 才会用于 Spring AOP 自动代理配置:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

Note

不要被<aop:aspectj-autoproxy/>元素的名称所迷惑:使用它会导致创建* Spring AOP 代理*。方面声明的@AspectJ 样式仅在此处使用,但不涉及 AspectJ 运行时。

11.8.4 在 Spring Framework 中使用 AspectJ 进行加载时编织

加载时编织(LTW)是指在将 AspectJ 方面加载到应用程序的类文件中时将它们编织到 Java 虚拟机(JVM)中的过程。本节的重点是在 Spring 框架的特定上下文中配置和使用 LTW:虽然本节不是 LTW 的介绍。有关 LTW 的详细信息以及仅使用 AspectJ 配置 LTW(完全不涉及 Spring)的详细信息,请参见AspectJ 开发环境指南的 LTW 部分

Spring 框架为 AspectJ LTW 带来的增值在于能够实现对编织过程的更精细控制。 “ Vanilla” AspectJ LTW 使用 Java(5)代理来实现,该代理通过在启动 JVM 时指定 VM 参数来打开。因此,它是 JVM 范围的设置,在某些情况下可能很好,但通常有点太粗糙了。启用了 Spring 的 LTW 使您能够按*每个 ClassLoader *开启 LTW,这显然是更细粒度的,并且在“单个 JVM-多应用程序”环境中更有意义(例如典型的应用服务器环境)。

此外,在某些环境中,这种支持使加载时编织无需对应用服务器的启动脚本进行任何修改即可添加-javaagent:path/to/aspectjweaver.jar或(如本节中稍后所述)-javaagent:path/to/org.springframework.instrument-{version}.jar(以前称为spring-agent.jar)。开发人员只需修改构成应用程序上下文的一个或多个文件即可启用加载时编织,而不必依赖通常负责部署配置(例如启动脚本)的 Management 员。

现在销售工作已经结束,让我们首先使用 Spring 快速浏览 AspectJ LTW 的示例,然后在以下示例中介绍有关元素的详细信息。有关完整示例,请参见Petclinicsample 申请

第一个例子

让我们假设您是一位负责诊断系统中某些性能问题的原因的应用程序开发人员。我们要做的不是打开分析工具,而是打开一个简单的分析方面,这将使我们能够快速获得一些性能 Metrics,以便我们可以立即将更细粒度的分析工具应用于该特定区域。之后。

Note

此处提供的示例使用 XML 样式配置,还可以使用Java Configuration配置和使用@AspectJ。特别地,@EnableLoadTimeWeavingComments 可以用作<context:load-time-weaver/>的替代方法(有关详细信息,请参见below)。

这是分析方面。没什么花哨的,只是使用@AspectJ 风格的方面声明的基于时间的快速分析器。

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}

我们还需要创建一个META-INF/aop.xml文件,以通知 AspectJ 编织者我们要将ProfilingAspect编织到类中。此文件约定,即在 JavaClasspath 上名为META-INF/aop.xml的文件,是标准 AspectJ。

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

现在到配置的特定于 Spring 的部分。我们需要配置一个LoadTimeWeaver(稍后会进行详细说明,现在就信任它)。此加载时织布器是必不可少的组件,负责将一个或多个META-INF/aop.xml文件中的方面配置编织到应用程序的类中。好处是,它不需要很多配置,如下所示(您可以指定一些其他选项,但稍后会详细介绍)。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        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>

现在,所有必需的工件(方面,META-INF/aop.xml文件和 Spring 配置)均已准备就绪,让我们使用main(..)方法创建一个简单的驱动程序类,以演示实际的 LTW。

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService
            = (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

        // the profiling aspect is 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

最后一件事要做。本节的引言确实说过,可以使用 Spring 以ClassLoader为基础选择性地打开 LTW,这是事实。但是,仅在此示例中,我们将使用 Java 代理(Spring 随附)打开 LTW。这是我们将用来运行上面的Main类的命令行:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent是用于指定和启用代理来检测在 JVM 上运行的程序的标志。 Spring 框架附带了这样的代理InstrumentationSavingAgent,该代理包装在spring-instrument.jar中,在上面的示例中作为-javaagent参数的值提供。

Main程序执行的输出如下所示。 (我在calculateEntitlement()实现中引入了Thread.sleep(..)语句,以便探查器实际上捕获的不是 0 毫秒的内容-01234毫秒不是* AOP 引入的开销:)

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

由于该 LTW 是使用成熟的 AspectJ 来实现的,因此我们不仅限于为 Spring Bean 提供建议;以下对Main程序的细微改动将产生相同的结果。

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {

        new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
            new StubEntitlementCalculationService();

        // the profiling aspect will be 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

请注意,在上面的程序中,我们是如何简单地引导 Spring 容器,然后完全在 Spring 上下文之外创建StubEntitlementCalculationService的新实例……分析建议仍然被编织。

坦率地说,该示例很简单……但是,在上面的示例中,已经介绍了 Spring 对 LTW 支持的基础,本节的其余部分将详细解释每一个配置和用法背后的“原因”。

Note

在此示例中使用的ProfilingAspect可能是基本的,但很有用。这是开发时方面的一个很好的示例,开发人员可以在开发期间使用它(当然),然后很容易地将其从部署到 UAT 或 Producing 的应用程序构建中排除。

Aspects

您在 LTW 中使用的方面必须是 AspectJ 方面。它们可以用 AspectJ 语言本身编写,也可以用@AspectJ 样式编写。这意味着您的方面既是有效的 AspectJ *又是 Spring AOP 方面。此外,编译的方面类需要在 Classpath 上可用。

'META-INF/aop.xml'

AspectJ LTW 基础结构是使用一个或多个META-INF/aop.xml文件配置的,这些文件位于 JavaClasspath 上(直接或通常在 jar 文件中)。

该文件的结构和内容在 AspectJ 主参考文档中进行了详细说明,感兴趣的 Reader 是提到该资源。 (我很高兴看到这部分内容很简短,但是aop.xml文件是 100%AspectJ 的-没有适用于它的特定于 Spring 的信息或语义,因此也没有任何我可以贡献的额外价值),所以而不是重新介绍 AspectJ 开发人员编写的令人满意的部分,我只是将您引向那里。)

必需的库(JARS)

至少需要使用以下库来使用 Spring Framework 对 AspectJ LTW 的支持:

  • spring-aop.jar(2.5 版或更高版本,以及所有强制性依赖项)

  • aspectjweaver.jar(1.6.8 版或更高版本)

如果您使用的是Spring 提供的代理程序可实现检测,则还需要:

  • spring-instrument.jar

Spring configuration

Spring 的 LTW 支持中的关键组件是LoadTimeWeaver接口(在org.springframework.instrument.classloading包中),以及 Spring 发行版附带的众多实现。 LoadTimeWeaver负责在运行时向ClassLoader添加一个或多个java.lang.instrument.ClassFileTransformers,这为各种有趣的应用程序打开了大门,其中之一恰好是方面的 LTW。

Tip

如果您不熟悉运行时类文件转换的概念,建议您在 continue 之前阅读java.lang.instrument软件包的 javadoc API 文档。这并不是一件繁琐的事,因为那里(非常烦人)那里有一些宝贵的文档……关键的接口和类至少会摆在您面前,以供您阅读本节时参考。

为特定的ApplicationContext配置LoadTimeWeaver就像添加一行一样容易。 (请注意,您几乎肯定需要使用ApplicationContext作为您的 Spring 容器-通常BeanFactory是不够的,因为 LTW 支持使用BeanFactoryPostProcessors.)

要启用 Spring Framework 的 LTW 支持,您需要配置LoadTimeWeaver,通常使用@EnableLoadTimeWeaving注解完成。

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {

}

或者,如果您更喜欢基于 XML 的配置,请使用<context:load-time-weaver/>元素。请注意,该元素是在context名称空间中定义的。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        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>

上面的配置将自动为您定义并注册许多 LTW 特定的基础结构 Bean,例如LoadTimeWeaverAspectJWeavingEnabler。默认的LoadTimeWeaverDefaultContextLoadTimeWeaver类,它将尝试修饰自动检测到的LoadTimeWeaver:将被“自动检测到”的LoadTimeWeaver的确切类型取决于您的运行时环境(下表中概述)。

表 11.1. DefaultContextLoadTimeWeaver LoadTimeWeavers

Runtime EnvironmentLoadTimeWeaver实施
在 Oracle 的WebLogic中运行WebLogicLoadTimeWeaver
在 Oracle 的GlassFish中运行GlassFishLoadTimeWeaver
Apache Tomcat中运行TomcatLoadTimeWeaver
在 Red Hat 的JBoss ASWildFly中运行JBossLoadTimeWeaver
在 IBM 的WebSphere中运行WebSphereLoadTimeWeaver
JVM 从 Spring InstrumentationSavingAgent开始((java -javaagent:path/to/spring-instrument.jar)*InstrumentationLoadTimeWeaver
回退,期望基础 ClassLoader 遵循通用约定(例如,适用于TomcatInstrumentableClassLoaderResin)ReflectiveLoadTimeWeaver

请注意,这些只是使用DefaultContextLoadTimeWeaver时会自动检测到的LoadTimeWeavers:当然,可以精确指定要使用的LoadTimeWeaver实现。

要使用 Java 配置指定特定的LoadTimeWeaver,请实现LoadTimeWeavingConfigurer接口并覆盖getLoadTimeWeaver()方法:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}

如果使用的是基于 XML 的配置,则可以将标准类名指定为<context:load-time-weaver/>元素上weaver-class属性的值:

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

稍后可以使用众所周知的名称loadTimeWeaver从 Spring 容器中检索由配置定义和注册的LoadTimeWeaver。请记住,LoadTimeWeaver作为 Spring 的 LTW 基础结构添加一个或多个ClassFileTransformers的一种机制而存在。执行 LTW 的实际ClassFileTransformerClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime程序包)类。有关更多详细信息,请参见ClassPreProcessorAgentAdapter类的类级 javadocs,因为实际上如何实现编织的细节不在本节的讨论范围之内。

还需要讨论配置的最后一个属性:aspectjWeaving属性(如果使用 XML,则为aspectj-weaving)。这是一个简单的属性,用于控制是否启用 LTW。它是如此简单。它接受下面概述的三个可能值之一,如果属性不存在,则默认值为autodetect

表 11.2 AspectJ 编织属性值

Annotation ValueXML ValueExplanation
ENABLEDonAspectJ 编织已开始,并且将在加载时适当地编织各方面。
DISABLEDoffLTW 已关闭…在加载时不会编织任何方面。
AUTODETECTautodetect如果 Spring LTW 基础结构可以找到至少一个META-INF/aop.xml文件,则说明 AspectJ 编织已打开,否则已关闭。这是默认值。

Environment-specific configuration

最后一部分包含在应用程序服务器和 Web 容器等环境中使用 Spring 的 LTW 支持时所需的所有其他设置和配置。

Tomcat

从历史上看,Apache Tomcat的默认类加载器不支持类转换,这就是 Spring 提供增强的实现以解决此需求的原因。名为TomcatInstrumentableClassLoader的加载程序可在 Tomcat 6.0 及更高版本上运行。

Tip

不再在 Tomcat 8.0 及更高版本上定义TomcatInstrumentableClassLoader。相反,让 Spring 通过TomcatLoadTimeWeaver策略自动使用 Tomcat 的新本机InstrumentableClassLoader工具。

如果您仍然需要使用TomcatInstrumentableClassLoader,可以按如下方式分别为每个 Web 应用程序注册:

  • org.springframework.instrument.tomcat.jar复制到* $ CATALINA_HOME /lib 中,其中 $ CATALINA_HOME *表示 Tomcat 安装的根目录)

  • 通过编辑 Web 应用程序上下文文件,指示 Tomcat 使用自定义类加载器(而不是默认值):

<Context path="/myWebApp" docBase="/my/webApp/location">
    <Loader
        loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
</Context>

Apache Tomcat(6.0)支持多个上下文位置:

  • 服务器配置文件-* $ CATALINA_HOME/conf/server.xml *

  • 默认上下文配置-* $ CATALINA_HOME/conf/context.xml *-影响所有已部署的 Web 应用程序

  • 每个 Web 应用程序配置,可以在服务器端* $ CATALINA_HOME/conf/[enginename]/[hostname]/[webapp] -context.xml 上部署,也可以嵌入在 META- INF/context.xml *

为了提高效率,建议使用嵌入式的每个 Web 应用程序配置样式,因为它只会影响使用自定义类加载器的应用程序,并且不需要对服务器配置进行任何更改。有关可用上下文位置的更多详细信息,请参见 Tomcat 6.0.x documentation

或者,考虑使用在 Tomcat 的启动脚本中指定的 Spring 提供的通用 VM 代理(请参见上文)。这将使仪器可用于所有已部署的 Web 应用程序,无论它们恰好在什么 ClassLoader 上运行。

WebLogic,WebSphere,Resin,GlassFish,JBoss

WebLogic Server(10 版及更高版本),IBM WebSphere Application Server(7 版及更高版本),Resin(3.1 及更高版本)和 JBoss(6.x 或更高版本)的最新版本提供了能够进行本地检测的 ClassLoader。 Spring 的本地 LTW 利用此类 ClassLoader 来实现 AspectJ 编织。您可以通过简单地激活加载时编织来启用 LTW,如前所述。具体来说,您不需要修改启动脚本来添加-javaagent:path/to/spring-instrument.jar

请注意,具有 GlassFish 仪器功能的 ClassLoader 仅在其 EAR 环境中可用。对于 GlassFish Web 应用程序,请按照上面概述的 Tomcat 设置说明进行操作。

请注意,在 JBoss 6.x 上,需要禁用应用程序服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是将名为WEB-INF/jboss-scanning.xml的文件添加到您的工件中,其中包含以下内容:

<scanning xmlns="urn:jboss:scanning:1.0"/>
通用 Java 应用程序

在不支持或现有LoadTimeWeaver实现不支持的环境中需要类检测时,JDK 代理可以是唯一的解决方案。对于这种情况,Spring 提供了InstrumentationLoadTimeWeaver,这需要一个 Spring 特定(但非常通用)的 VM 代理org.springframework.instrument-{version}.jar(以前称为spring-agent.jar)。

要使用它,必须通过提供以下 JVM 选项来使用 Spring 代理启动虚拟机:

-javaagent:/path/to/org.springframework.instrument-{version}.jar

请注意,这需要修改 VM 启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的操作策略)。另外,JDK 代理将对整个 VM 进行检测,这可能证明是昂贵的。

出于性能原因,建议仅在目标环境(例如Jetty)没有(或不支持)专用 LTW 的情况下才使用此配置。

11.9 其他资源

可以在AspectJ website上找到有关 AspectJ 的更多信息。

Adrian Colyer 等人的《 Eclipse AspectJ》一书。等(Addison-Wesley,2005 年)为 AspectJ 语言提供了全面的介绍和参考。

强烈推荐 Ramnivas Laddad(Manning,2009)写的《 AspectJ in Action,第二版》。本书的重点是 AspectJ,但是在一定程度上探讨了许多通用的 AOP 主题。