使用 Spring 进行 11. Aspect Oriented Programming

11.1 简介

Aspect-Oriented 编程(AOP)通过提供另一种思考程序结构的方式来补充 Object-Oriented 编程(OOP)。 OOP 中的 key 模块化单元是 class,而在 AOP 中,模块化单元是 aspect。方面实现了诸如 transaction management 之类的关注点的模块化,这些问题涉及多种类型和 objects。 (这种担忧通常被称为 AOP 中的横切关注 literature.)

Spring 的 key 组件之一是 AOP framework。虽然 Spring IoC 容器不依赖于 AOP,但这意味着如果您不需要使用 AOP,AOP 将补充 Spring IoC 以提供非常强大的中间件解决方案。


春天 2.0 AOP

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

本章将讨论 Spring 2.0 schema-和@AspectJ-based AOP 支持。 A支持,如 Spring 1.2 applications 中常见的那样,在以下章节中讨论。


在 Spring Framework 中使用 AOP 来...

  • ...提供声明性企业服务,尤其是作为 EJB 声明性服务的替代品。最重要的服务是声明性 transaction management

  • ...允许用户实现自定义方面,补充他们使用 AOP 的 OOP。

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

11.1.1 AOP 概念

让我们首先定义一些中心 AOP 概念和术语。这些术语不是 Spring-specific ......不幸的是,AOP 术语不是特别直观;然而,如果 Spring 使用自己的术语,那将更加令人困惑。

  • Aspect:跨越多个 classes 的关注点的模块化。 Transaction management 是企业 Java applications 中横切关注点的一个很好的例子。在 Spring AOP 中,方面是使用常规 classes(schema-based 接近)或使用@Aspect annotation(@AspectJ 风格)注释的常规 classes 实现的。

  • 连接点:程序执行期间的一个点,例如方法的执行或 exception 的处理。在 Spring AOP 中,连接点始终表示方法执行。

  • 建议:aspect 在特定连接点采取的操作。不同类型的建议包括“周围”,“之前”和“之后”建议。 (建议类型被讨论 below.)许多 AOP 框架,包括 Spring,model 作为拦截器的建议,在连接点周围维护一系列拦截器。

  • 切入点:匹配连接点的谓词。建议与切入点表达式相关联,并在切入点匹配的任何连接点处运行(对于 example,执行具有特定 name 的方法)。由切入点表达式匹配的连接点的概念是 AOP 的核心,而 Spring 默认使用 AspectJ 切入点表达式语言。

  • 简介:代表类型声明其他方法或字段。 Spring AOP 允许您向任何建议的 object 引入新接口(和相应的 implementation)。对于 example,您可以使用简介使 bean 实现IsModified接口,以简化缓存。 (引言在 AspectJ 中被称为 inter-type 声明 community.)

  • 目标 object:object 由一个或多个方面建议。也称为建议 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 在运行时执行编织。

建议类型:

  • 建议之前:在连接点之前执行但不能阻止执行流程进入连接点的建议(除非它抛出 exception)。

  • 返回建议后:在连接点正常完成后执行的建议:对于 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 层次结构进行定向。例如,提供声明性 transaction management 的 around 建议可以应用于跨多个 objects 的一组方法(例如服务层中的所有业务操作)。

11.1.2 Spring AOP 的功能和目标

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 之间提供一个紧密的 integration 来帮助解决企业应用程序中的 common 问题。

因此,例如,Spring Framework 的 AOP 功能通常与 Spring IoC 容器一起使用。使用普通 bean 定义语法配置方面(虽然这允许强大的“自动代理”功能):这是与其他 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,你不应该被迫在你的 business/domain model 中引入 framework-specific classes 和接口。但是,在某些地方,Spring Framework 确实为您提供了将 Spring Framework-specific 依赖项引入代码库的选项:给出这些选项的基本原理是因为在某些情况下,读取或 code 某些特定功能可能更容易这样的方式。 Spring Framework(几乎)总是为您提供选择:您可以自由决定哪种选项最适合您的特定用例或场景。

与本章相关的一个选择是选择哪种 AOP framework(以及哪种 AOP 样式)。您可以选择 AspectJ and/or Spring AOP,也可以选择 @AspectJ annotation-style 方法或 Spring XML configuration-style 方法。本章选择首先介绍@AspectJ-style 方法的事实不应被视为 Spring 团队倾向于 @AspectJ annotation-style 方法而不是 Spring XML configuration-style。

有关每种样式的原因和原因的更完整的讨论,请参见第 11.4 节,“选择使用哪种 AOP 声明样式”

11.1.3 AOP Proxies

Spring AOP 默认使用 AOP 代理的标准 JDK 动态代理。这使得任何接口(或接口集)都可以被代理。

Spring AOP 也可以使用 CGLIB 代理。这对代理 classes 而不是接口是必要的。如果 business object 没有实现接口,则默认使用 CGLIB。因为优良的做法是编程接口而不是 classes; business classes 通常会实现一个或多个业务接口。在那些(希望很少见)需要建议未在接口上声明的方法,或者需要将代理的 object 作为具体类型传递给方法的情况下,可能会强制使用 CGLIB

掌握 Spring AOP 是 proxy-based 的事实很重要。请参阅第 11.6.1 节,“了解 AOP 代理”以详细了解此 implementation 详细信息的实际含义。

11.2 @AspectJ 支持

@AspectJ 指的是将方面声明为使用 annotations 注释的常规 Java classes 的样式。作为 AspectJ 5 版本的一部分,AspectJ 项目引入了 @AspectJ 样式。 Spring 使用 AspectJ 提供的 library 解释与 AspectJ 5 相同的注释,用于切入点解析和匹配。 AOP 运行时仍然是纯粹的 Spring AOP,并且不依赖于 AspectJ 编译器或 weaver。

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

11.2.1 启用 @AspectJ 支持

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

可以使用 XML 或 Java 样式 configuration 启用 @AspectJ 支持。在任何一种情况下,您还需要确保 AspectJ 的aspectjweaver.jar library 位于 application 的 classpath(version 1.6.8 或更高版本)。这个 library 可以在 AspectJ 发行版的'lib'目录中找到,也可以通过 Maven Central repository 获得。

使用 Java configuration 启用 @AspectJ 支持

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

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

使用 XML configuration 启用 @AspectJ 支持

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

<aop:aspectj-autoproxy/>

这假定您正在使用 schema 支持,如第 41 章,XML Schema-based configuration中所述。有关如何 import aop命名空间中的标记,请参阅第 41.2.7 节,“aop schema”

11.2.2 声明 aspect

启用 @AspectJ 支持后,_appring context 中定义的任何带有 class @AspectJ aspect(具有@Aspect annotation)的 bean 将由 Spring 自动检测并用于配置 Spring AOP。以下 example 显示了 not-very-useful aspect 所需的最小定义:

application context 中的常规 bean 定义,指向具有@Aspect annotation 的 bean class:

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

NotVeryUsefulAspect class 定义,用org.aspectj.lang.annotation.Aspect annotation 注释;

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

@Aspect
public class NotVeryUsefulAspect {

}

方面(class 用@Aspect注释)可能具有与任何其他 class 一样的方法和字段。它们还可能包含切入点,建议和引入(inter-type)声明。

您可以在 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 中排除。

11.2.3 声明一个切入点

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

__ample 将有助于区分切入点签名和切入点表达式。以下 example 定义了一个名为'anyOldTransfer'的切入点,它将匹配任何名为'transfer'的方法的执行:

@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)用于切入点表达式:


其他切入点类型

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

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


  • 执行 - 对于匹配方法执行连接点,这是在使用 Spring AOP 时将使用的主要切入点指示符

  • within - 限制匹配某些类型中的连接点(只是在使用 Spring AOP 时执行在匹配类型中声明的方法)

  • this - 限制匹配连接点(使用 Spring AOP 时执行方法)bean reference(Spring AOP proxy)是给定类型的实例

  • target - 限制匹配到连接点(使用 Spring AOP 时执行方法),其中目标 object(application object 被代理)是给定类型的实例

  • args - 限制匹配连接点(使用 Spring AOP 时执行方法),其中 arguments 是给定类型的实例

  • @target - 限制匹配连接点(使用 Spring AOP 时执行方法),其中执行 object 的 class 具有给定类型的 annotation

  • @args - 限制匹配连接点(使用 Spring AOP 时执行方法),其中传递的实际 arguments 的运行时类型具有给定 type(s 的注释

  • @within - 限制匹配以在具有给定 annotation 的类型中连接点(使用 Spring AOP 时,使用给定 annotation 在类型中声明的方法的执行)

  • @annotation - 限制匹配到连接点的主题,其中连接点的主题(在 Spring AOP 中执行的方法)具有给定的 annotation

因为 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 具有以下形式:

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 显示了三个切入点表达式:anyPublicOperation(如果方法执行连接点表示任何公共方法的执行,则匹配); inTrading(如果方法执行在交易模块中,则匹配)和tradingOperation(如果方法执行代表交易模块中的任何公共方法,则匹配)。

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

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

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

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

共享 common 切入点定义

使用 enterprise applications 时,您经常需要从几个方面引用 application 的模块和特定的_set 操作。我们建议定义一个“SystemArchitecture”aspect,为此目的捕获 common 切入点表达式。典型的这样的 aspect 看起来如下:

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,您可以编写:

<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>元素在第 11.3 节,“Schema-based AOP 支持”中讨论。 transaction 元素在第 17 章,交易管理中讨论。

例子

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

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 才会 match。 name pattern 与方法 name 匹配。您可以将*通配符用作 name pattern 的全部或部分。如果指定声明类型 pattern,则包含尾随.以将其连接到 name pattern component。参数 pattern 稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量的参数(零或更多)。 pattern (*)匹配一个采用任何类型的参数的方法,(*,String)匹配一个采用两个参数的方法,第一个可以是任何类型,第二个必须是 String。有关更多信息,请参阅 AspectJ 编程指南的语言语义学部分。

下面给出了 common 切入点表达式的一些示例。

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

'this'更常用于 binding 形式: - 请参阅以下有关如何在建议体中提供代理 object 的建议。

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

'target'更常用于 binding 形式: - 请参阅以下有关如何在建议体中提供目标 object 的建议。

  • 任何连接点(仅在 Spring AOP 中执行方法),它接受一个参数,并且在运行时传递的参数为Serializable
args(java.io.Serializable)

'args'更常用于 binding 形式: - 请参阅以下有关如何在建议体中使用方法 arguments 的建议。

请注意,此 example 中给出的切入点与execution(* *(java.io.Serializable))不同:如果在运行时传递的参数是 Serializable,则 args version 匹配,如果方法签名声明了类型Serializable的单个参数,则 executeversion 匹配。

  • 任何连接点(仅在 Spring AOP 中执行方法),其中目标 object 具有@Transactional annotation:
@target(org.springframework.transaction.annotation.Transactional)

'@target'也可以用于 binding 形式: - 请参阅以下有关如何在建议体中提供 annotation object 的建议。

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

'@within'也可以用于 binding 形式: - 请参阅以下有关如何在建议体中提供 annotation object 的建议。

  • 任何连接点(仅在 Spring AOP 中执行方法),其中执行方法具有@Transactional annotation:
@annotation(org.springframework.transaction.annotation.Transactional)

'@annotation'也可以用于 binding 形式: - 请参阅以下有关如何在建议体中提供 annotation object 的建议。

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

'@args'也可以 binding 形式使用: - 请参阅以下有关如何在建议体中提供 annotation object(s 的建议。

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

写出好的切入点

在编译期间,AspectJ 处理 order 中的切入点以尝试和优化匹配 performance。检查 code 并确定每个连接点是否匹配(静态或动态)给定的切入点是一个代价高昂的 process。 (动态 match 表示无法通过静态分析完全确定 match,并且将在 code 中放置一个测试,以确定 code 是 running 时是否存在实际的 match)。在第一次遇到切入点声明时,AspectJ 会将其写入匹配 process 的最佳形式。这是什么意思?基本上,切入点在 DNF(析取范式)中重写,并且切入点的组件被排序,以便首先检查那些评估成本更低的组件。这意味着您不必担心理解各种切入点指示符的 performance,并且可以在切入点声明中的任何 order 中提供它们。

但是,AspectJ 只能处理它所说的内容,并且为了获得最佳的匹配性能,您应该考虑它们想要实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三组:kinded,scoping 和 context:

  • Kinded 指示符是指选择特定类型的连接点的指示符。对于 example:执行,获取,设置,调用,处理程序

  • 范围界定指示符是那些选择一组感兴趣的连接点(可能是多种类型)的指示符。对于 example:within,withincode

  • 上下文指示符是基于 context 的 match(并且可选地绑定)的指示符。对于 example:this,target, @annotation

一个写得很好的切入点应该尝试包括至少前两种类型(kinded 和 scoping),而如果希望基于连接点 context match,或者绑定 context 以在建议中使用,则可以包括上下文指示符。只提供一个 kinded 指示符或只提供一个上下文指示符将起作用,但由于所有额外的处理和分析,可能会影响编织 performance(time 和 memory used)。范围界定指示符对 match 非常快,它们的使用意味着 AspectJ 可以非常快速地解除不应该进一步处理的连接点组 - 这就是为什么一个好的切入点应该总是包含一个如果可能的话。

11.2.4 宣布建议

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

在建议之前

在使用@Before annotation 在 aspect 中声明建议之前:

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 写为:

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

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 中有多个建议声明和其他成员。我们只是在这些示例中展示了一条建议声明,专注于 time 讨论的问题。

有时您需要在建议体中访问返回的实际 value。您可以使用的形式绑定 return value:

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,这将 match 任何 return value)。

请注意,使用 after-returning 建议时,无法 return 完全不同的 reference。

投掷建议后

抛出建议运行时,匹配的方法执行通过抛出 exception 退出。它是使用@AfterThrowing annotation 声明的:

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 参数。

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 条件。它通常用于释放资源等。

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 建议。始终使用满足您要求的最不强大的建议形式(i.e.如果在建议之前,请不要使用简单的建议)。

使用@Around annotation 声明 around 建议。 advice 方法的第一个参数必须是ProceedingJoinPoint类型。在通知的主体内,在ProceedingJoinPoint上调用proceed()会导致执行基础方法。也可以调用proceed方法传入Object[] - array 中的值将在方法执行时用作方法执行的 arguments。

使用 Object [188]调用时,proceed 的行为与由 AspectJ 编译器编译的 around 建议的行为略有不同。对于使用传统 AspectJ 语言编写的周围建议,传递给 continue 的 arguments 的数量必须 match 传递给 around 建议的 arguments 的数量(不是底层连接点采用的 arguments 的数量),并且 value 传递给给定的参数位置取代了 value 所绑定的实体的连接点处的原始 value(如果现在没有意义,请不要担心!)。 Spring 采用的方法比 proxy-based 更简单,更好 match,只执行语义。如果要编译为 Spring 编写的 @AspectJ 方面并使用带有 AspectJ 编译器和 weaver 的 arguments 继续,则只需要了解这种差异。有一种方法可以在 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;
    }

}

around 建议返回的 value 将是方法调用者看到的 return value。对于 example 的一个简单的缓存 aspect,如果它有一个 value,它可以从缓存中返回 value,如果没有,则调用 proceed()。请注意,可以在周围建议的主体内调用一次,多次或根本不调用,所有这些都是非常合法的。

建议参数

Spring 提供完全类型的建议 - 这意味着您在建议签名中声明了所需的参数(正如我们在上面看到的返回和抛出示例所见),而不是在 time中使用Object[]数组。我们将看到如何在 moment 中为建议体提供参数和其他上下文值。首先让我们来看看如何编写通用建议,以便了解建议目前建议的方法。

访问当前的 JoinPoint

任何通知方法都可以声明为第一个参数,类型为org.aspectj.lang.JoinPoint的参数(请注意,需要使用 around 通知声明ProceedingJoinPoint类型的第一个参数,它是JoinPoint的子类.JoinPoint接口提供了许多有用的方法,如getArgs()(返回方法 arguments),getThis()(返回代理 object),getTarget()(返回目标 object),getSignature()(返回正在建议的方法的描述)和toString()(打印建议方法的有用描述)请查阅 javadocs 了解详细信息。

将参数传递给建议

我们已经看到了如何绑定返回的 value 或 exception value(在返回之后和抛出建议之后使用)。要使参数值可用于建议体,可以使用args的 binding 形式。如果在 args 表达式中使用参数 name 代替类型 name,则在调用通知时,相应参数的 value 将作为参数 value 传递。 示例应该使这更清楚。假设您要建议执行以 Account object 作为第一个参数的 dao 操作,并且您需要访问建议体中的帐户。你可以写下面的内容:

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

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

另一种编写方法是声明一个切入点,当它与连接点匹配时“提供”Account object value,然后从提示中引用指定的切入点。这看起来如下:

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

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

感兴趣的 reader 再次被引用到 AspectJ 编程指南以获取更多细节。

代理 object(this),目标 object(target)和 annotations(@within, @target, @annotation, @args)都可以以类似的方式绑定。以下 example 显示了如何__chch 执行使用@Auditable annotation 注释的方法,并提取 audit code。

首先是@Auditable annotation 的定义:

@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 可以处理 class 声明和方法参数中使用的泛型。假设您有这样的泛型类型:

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

您可以通过简单地将 advice 参数 typing 到要拦截方法的参数类型来限制方法类型截取到某些参数类型:

@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<?>并手动检查元素的类型。

确定参数名称

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

  • 如果用户明确指定了参数名称,则使用指定的参数名称:advice 和 pointcut annotations 都有一个可选的“argNames”属性,可用于指定带注释的方法的参数名称 - 这些参数名称在运行时可用。例如:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

如果第一个参数是JoinPointProceedingJoinPointJoinPoint.StaticPart类型,则可以从“argNames”属性的 value 中省略参数的 name。例如,如果修改前面的建议以接收连接点 object,则“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
}

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

@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)要删除的优化编译器不会应用未使用的局部变量。换句话说,你应该遇到这个 flag 没有遇到任何困难。

如果即使没有调试信息,AspectJ 编译器(ajc)也编译了 @AspectJ aspect,那么就不需要添加 argNames 属性,因为编译器将保留所需的信息。

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

  • 如果以上所有策略都失败,那么将抛出IllegalArgumentException

继续 arguments

我们之前评论过,我们将描述如何使用 arguments 编写一个继续调用,该调用在 Spring AOP 和 AspectJ 中一致地工作。解决方案只是确保通知签名绑定 order 中的每个方法参数。例如:

@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)。

建议 ordering

当多条建议都想在同一个连接点运行时会发生什么? 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 中进行排序。

11.2.5 介绍

简介(在 AspectJ 中称为 inter-type 声明)使 aspect 能够声明建议的 objects 实现给定的接口,并代表那些 objects 提供该接口的 implementation。

使用@DeclareParents annotation 进行介绍。此 annotation 用于声明匹配类型具有新的 parent(因此为 name)。对于 example,给定一个接口UsageTracked,以及该接口DefaultUsageTracked的 implementation,以下 aspect 声明服务接口的所有实现者也实现了UsageTracked接口。 (在_中通过 JMX 为 example.)公开统计数据

@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 的 before 建议中,service beans 可以直接用作UsageTracked接口的 implementation。如果以编程方式访问 bean,您将编写以下内容:

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

11.2.6 Aspect instantiation models

(这是一个高级的 topic,所以如果你刚开始使用 AOP,你可以安全地跳过它直到 later.)

默认情况下,application context 中的每个 aspect 都有一个实例。 AspectJ calls singleton instantiation model。可以使用备用生命周期定义方面: - Spring 支持 AspectJ 的perthispertarget实例化模型(当前不支持percflow, percflowbelow,pertypewithin)。

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

@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-clauses 的更多信息,请参阅 AspectJ 编程指南。

'pertarget' instantiation model 的工作方式与 perthis 完全相同,但在匹配的连接点为每个唯一目标 object 创建一个 aspect 实例。

11.2.7 示例

既然你已经看到了所有组成部分的工作方式,那就让我们把它们放在一起做一些有用的事情吧!

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

因为我们想要重试操作,所以我们需要使用 around 建议,以便我们可以多次调用 proceed。以下是基本 aspect implementation 的外观:

@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()s。我们试图继续,如果我们失败PessimisticLockingFailureException我们只是再试一次,除非我们已经用尽所有的重试尝试。

相应的 Spring configuration 是:

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

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

并使用 annotation 注释服务操作的 implementation。对 aspect 的更改仅重试幂等操作只涉及改进切入点表达式,以便只有@Idempotent操作 match:

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

11.3 Schema-based AOP 支持

如果您更喜欢 XML-based 格式,那么 Spring 还支持使用新的“aop”命名空间标记定义方面。使用 @AspectJ 样式时支持完全相同的切入点表达式和建议种类,因此在本节中我们将重点介绍新语法,并将 reader 引用到上一节(第 11.2 节,“@AspectJ 支持”)中的讨论,以了解编写切入点表达式和建议参数的 binding。

要使用本节中描述的 aop 命名空间标记,您需要按第 41 章,XML Schema-based configuration中的描述 import spring-aop schema。有关如何 import aop命名空间中的标记,请参阅第 41.2.7 节,“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样式。

11.3.1 声明 aspect

使用 schema 支持,aspect 只是在 Spring application context 中定义为 bean 的常规 Java object。 state 和行为在 object 的字段和方法中捕获,切入点和建议信息在 XML 中捕获。

使用< hh:// +306+ 3 >元素声明 aspect,使用ref属性引用支持 bean:

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

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

支持 aspect 的 bean(在本例中为"aBean")当然可以配置和依赖注入,就像任何其他 Spring bean 一样。

11.3.2 声明一个切入点

可以在< hh:// +311+ .h >元素内声明命名切入点,从而使切入点定义能够跨多个方面和顾问程序共享。

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

<aop:config>

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

</aop:config>

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

<aop:config>

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

</aop:config>

假设你有一个SystemArchitecture aspect,如名为“共享 common 切入点定义”的部分所述。

在 aspect 中声明切入点与声明 top-level 切入点非常相似:

<aop:config>

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

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

        ...

    </aop:aspect>

</aop:config>

在 @AspectJ aspect 中,使用基于 schema 的定义样式声明的切入点可能会收集连接点 context。对于 example,以下切入点将'this'object 收集为连接点 context 并将其传递给建议:

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

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

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

当组合切入点 sub-expressions 时,&&在 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 引用,不能用作命名切入点来形成复合切入点。因此,基于 schema 的定义样式中的命名切入点支持比 @AspectJ 样式提供的更有限。

11.3.3 宣布建议

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

在建议之前

在匹配的方法执行之前运行建议之前。它使用< hh:// +337+ 3 >元素在<aop:aspect>内声明。

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

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

    ...

</aop:aspect>

这里dataAccessOperation是在顶部(<aop:config>)level 定义的切入点的 id。要改为内联切入点,请将pointcut-ref属性替换为pointcut属性:

<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>中声明。例如:

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

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

    ...

</aop:aspect>

就像在 @AspectJ 样式中一样,可以在建议体内获得 return value。使用 returns 属性指定 return value 应传递到的参数的 name:

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

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

    ...

</aop:aspect>

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

public void doAccessCheck(Object retVal) {...

投掷建议后

抛出建议执行时,匹配的方法执行通过抛出 exception 退出。它使用 after-throwing 元素在<aop:aspect>内声明:

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

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

    ...

</aop:aspect>

就像在 @AspectJ 样式中一样,可以在建议体内获取抛出的 exception。使用 throwing 属性指定 exception 应传递到的参数的 name:

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

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

    ...

</aop:aspect>

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

public void doRecoveryActions(DataAccessException dataAccessEx) {...

(最后)建议之后

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

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

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

    ...

</aop:aspect>

周围的建议

最后一种建议是建议。周围的建议围绕匹配的方法执行运行。它有机会在方法执行之前和之后完成工作,并确定方法实际上何时,如何,甚至是否实际执行。如果您需要在方法执行之前和之后以 thread-safe 方式共享 state(启动和停止 example 的计时器),则经常使用 around 建议。始终使用符合您要求的最不强大的建议形式;如果简单,建议之前不要使用周围的建议。

使用aop:around元素声明 around 建议。 advice 方法的第一个参数必须是ProceedingJoinPoint类型。在通知体内,在ProceedingJoinPoint上调用proceed()会导致执行基础方法。 proceed方法也可能正在调用传递Object[] - array 中的值将在进行时用作方法执行的 arguments。有关调用的注意事项,请参阅名为“围绕建议”的部分继续Object[]

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

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

    ...

</aop:aspect>

doBasicProfiling建议的 implementation 与 @AspectJ example(当然减去 annotation)完全相同:

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

建议参数

基于 schema 的声明样式支持完全类型化的建议,方法与 @AspectJ 支持描述的方式相同 - 通过 name 匹配建议方法参数的切入点参数。有关详细信息,请参阅名为“建议参数”的部分。如果您希望显式指定通知方法的参数名称(不依赖于前面描述的检测策略),那么这是使用 advice 元素的arg-names属性完成的,该属性的处理方式与“argNames”属性相同。建议 annotation 如“确定参数名称”一节中所述。例如:

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

arg-names属性接受 comma-delimited 参数名称列表。

下面是一个稍微更复杂的 XSD-based 方法的示例,它说明了与一些强类型参数一起使用的一些建议。

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

接下来是 aspect。请注意profile(..)方法接受多个 strongly-typed 参数这一事实,第一个参数恰好是用于继续进行方法调用的连接点:此参数的存在表明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 configuration:

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

建议 ordering

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

11.3.4 介绍

简介(在 AspectJ 中称为 inter-type 声明)使 aspect 能够声明建议的 objects 实现给定的接口,并代表那些 objects 提供该接口的 implementation。

使用aop:aspect中的aop:declare-parents元素进行介绍。此元素用于声明匹配类型具有新的 parent(因此为 name)。对于 example,给定一个接口UsageTracked,以及该接口DefaultUsageTracked的 implementation,以下 aspect 声明服务接口的所有实现者也实现了UsageTracked接口。 (在中通过 JMX 为 example.)公开统计数据

<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 将包含以下方法:

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

要实现的接口由implement-interface属性确定。 types-matching属性的 value 是一个 AspectJ 类型 pattern: - 匹配类型的任何 bean 将实现UsageTracked接口。请注意,在上述 example 的 before 建议中,service beans 可以直接用作UsageTracked接口的 implementation。如果以编程方式访问 bean,您将编写以下内容:

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

11.3.5 Aspect instantiation models

schema-defined 方面唯一支持的实例化 model 是 singleton model。未来的版本可能支持其他实例化模型。

11.3.6 顾问

“顾问”的概念是从 Spring 中定义的 AOP 支持中提出的,并且在 AspectJ 中没有直接的等价物。顾问就像一个小的 self-contained aspect,有一条建议。建议本身由 bean 表示,并且必须实现第 12.3.2 节,“Spring 中的建议类型”中描述的建议接口之一。顾问可以利用 AspectJ 切入点表达式。

Spring 支持带有<aop:advisor>元素的顾问程序概念。您最常见的是它与 transactional advice 一起使用,它在 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>

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

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

11.3.7 示例

让我们看看当使用 schema 支持重写时,并发锁定失败如何从第 11.2.7 节,“示例”重试 example。

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

因为我们想要重试操作,所以我们需要使用 around 建议,以便我们可以多次调用 proceed。以下是基本 aspect implementation 的外观(它只是使用 schema 支持的常规 Java class):

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 建议(我们希望每次 time 时都重新执行 transaction)。 maxRetriesorder properties 都将由 Spring 配置。主要操作发生在doConcurrentOperation around advice 方法中。我们试图继续,如果我们失败PessimisticLockingFailureException我们只是再试一次,除非我们已经用尽所有的重试尝试。

此 class 与 @AspectJ example 中使用的 class 相同,但删除了 annotations。

相应的 Spring configuration 是:

<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 来优化 aspect,使其仅重试真正的幂等操作:

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

并使用 annotation 注释服务操作的 implementation。对 aspect 进行更改以仅重试幂等操作只需要改进切入点表达式,以便只有@Idempotent操作 match:

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

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

一旦确定 aspect 是实现给定需求的最佳方法,您如何决定使用 Spring AOP 或 AspectJ,以及 Aspect 语言(code)样式, @AspectJ annotation 样式还是 Spring XML 样式?这些决策受到许多因素的影响,包括应用要求,开发工具和团队对 AOP 的熟悉程度。

11.4.1 Spring AOP 还是完整的 AspectJ?

使用最简单的方法。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要将 AspectJ 编译器/ weaver 引入您的开发和 build 进程。如果您只需要建议在 Spring beans 上执行操作,那么 Spring AOP 是正确的选择。如果您需要建议不由 Spring 容器管理的 objects(通常是域 objects),那么您将需要使用 AspectJ。如果您希望建议除简单方法执行之外的连接点(对于 example,字段获取或设置连接点等),您还需要使用 AspectJ。

使用 AspectJ 时,您可以选择 AspectJ 语言语法(也称为“code 样式”)或 @AspectJ annotation 样式。显然,如果您不使用 Java 5,那么已经为您做出选择...使用 code 样式。如果方面在您的设计中扮演重要角色,并且您能够使用_E的AspectJ 开发工具(AJDT)插件,那么 AspectJ 语言语法是首选选项:它更清晰,更简单,因为该语言是专门为编写方面而设计的。如果你没有使用 Eclipse,或只有几个方面在你的 application 中没有起主要作用,那么你可能要考虑使用 @AspectJ 样式并在 IDE 中坚持使用常规 Java 编译,并添加 aspect 编织阶段到你的 build 脚本。

11.4.2 @AspectJ 或 Spring AOP 的 XML?

如果您选择使用 Spring AOP,那么您可以选择 @AspectJ 或 XML 样式。需要考虑各种权衡。

XML 样式对于现有的 Spring 用户来说是最熟悉的,它由真正的 POJO 支持。当使用 AOP 作为配置企业服务的工具时,XML 可能是一个不错的选择(一个好的测试是你是否认为切入点表达式是你可能想要独立改变的 configuration 的一部分)。使用 XML 风格可以说,从 configuration 中可以更清楚地了解系统中存在哪些方面。

XML 风格有两个缺点。首先,它没有完全封装它在一个地方解决的要求的实现。 DRY 原则规定,系统中的任何知识都应该有单一,明确,权威的表示。使用 XML 样式时,有关如何实现需求的知识将分支到支持 bean class 的声明和 configuration 文件中的 XML。使用 @AspectJ 样式时,有一个模块--aspect - 用于封装此信息。其次,XML 样式在它表达的内容方面比 @AspectJ 样式稍微有限:只支持“singleton”aspect instantiation model,并且不可能组合在 XML 中声明的命名切入点。对于 example,在 @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 样式支持额外的实例化模型和更丰富的切入点组合。它具有将 aspect 保持为模块化单元的优点。它还具有 @AspectJ 方面可以被 Spring AOP 和 AspectJ 理解(并因此消耗)的优点 - 因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,那么很容易迁移到 AspectJ-based 方法。总的来说,只要你的方面不仅仅是简单的“configuration”企业服务,Spring 团队更喜欢 @AspectJ 风格。

11.5 混合 aspect 类型

完全可以使用自动代理支持,schema-defined <aop:aspect>方面,<aop:advisor>声明的顾问程序以及甚至在同一 configuration 中使用 Spring 1.2 样式定义的代理和拦截器来混合 @AspectJ 样式方面。所有这些都是使用相同的底层支持机制实现的,并且 co-exist 没有任何困难。

11.6 代理机制

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:

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

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

<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 代理来处理所有这三个元素。

11.6.1 了解 AOP 代理

Spring AOP 是 proxy-based。在编写自己的方面或使用 Spring Framework 提供的任何 Spring AOP-based 方面之前,掌握最后一个语句实际意味着什么的语义是非常重要的。

首先考虑具有 plain-vanilla,un-proxied,nothing-special-about-it,直 object reference 的场景,如下面的 code 片段所示。

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

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 代理电话

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

这里要理解的 key 是Main class 的main(..)中的 client code 对代理有一个 reference。这意味着 object reference 上的方法 calls 将在代理上为 calls,因此代理将能够委托给与该特定方法调用相关的所有拦截器(通知)。但是,一旦调用最终到达目标 object,在这种情况下SimplePojo reference,它可以对其自身进行的任何方法 calls,例如this.bar()this.foo(),将针对此 reference 而不是代理调用。这具有重要意义。这意味着 self-invocation 不会导致与方法调用相关的建议有机会执行。

好的,那么该怎么办呢?最好的方法(这里松散地使用最好的术语)是重构你的 code,以便 self-invocation 不会发生。当然,这确实需要你做一些工作,但这是最好的,least-invasive 方法。接下来的方法是绝对可怕的,我几乎要谨慎地指出它,因为它是如此可怕。你可以(扼流!)通过这样做完全将 class 中的逻辑绑定到 Spring AOP:

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:

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。

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

除了使用<aop:config><aop:aspectj-autoproxy>在 configuration 中声明方面之外,还可以通过编程方式创建建议目标 objects 的代理。有关 Spring 的 AOP API 的完整详细信息,请参阅下一章。在这里,我们希望关注使用 @AspectJ 方面自动创建代理的能力。

class org.springframework.aop.aspectj.annotation.AspectJProxyFactory可用于为一个或多个 @AspectJ 方面建议的目标 object 创建代理。这个 class 的基本用法非常简单,如下图所示。有关完整信息,请参阅 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 applications 一起使用

到目前为止,我们在本章中介绍的所有内容都是纯粹的 Spring AOP。在本节中,我们将了解如何使用 AspectJ compiler/weaver 代替 Spring AOP,或者除了 Spring AOP 之外,如果您的需求超出了 Spring AOP 提供的设施。

Spring 附带一个小的 AspectJ aspect library,它在您的发行版中可单独使用spring-aspects.jar;你需要将它添加到 order 中的 classpath 以使用其中的方面。 第 11.8.1 节,“使用 AspectJ 依赖 inject 域 objects 与 Spring”第 11.8.2 节,“AspectJ 的其他 Spring 方面”讨论了这个 library 的内容以及如何使用它。 第 11.8.3 节,“使用 Spring IoC 配置 AspectJ 方面”讨论了如何依赖 inject 使用 AspectJ 编译器编织的 AspectJ 方面。最后,第 11.8.4 节,“在 Spring Framework 中使用 AspectJ 编织 Load-time”提供了使用 AspectJ 对 Spring applications 进行 load-time 编织的介绍。

11.8.1 使用 AspectJ 依赖 inject 域 objects 与 Spring

Spring 容器实例化并配置 application context 中定义的 beans。如果给定__ definition 定义包含要应用的 configuration 的 name,也可以要求 bean 工厂配置 pre-existing object。 spring-aspects.jar包含 annotation-driven aspect,它利用此功能允许依赖注入任何 object。该支持旨在用于在任何容器控制之外创建的 objects。域 objects 通常属于此类别,因为它们通常使用new operator 以编程方式创建,或者由数据库查询以 ORM 工具创建。

@Configurable annotation 标记 class 符合 Spring-driven configuration 的条件。在最简单的情况下,它可以用作标记 annotation:

package com.xyz.myapp.domain;

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

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

当以这种方式用作标记接口时,Spring 将使用 bean 定义(通常为 prototype-scoped)和 fully-qualified 类型 name(com.xyz.myapp.domain.Account)具有相同的 name 来配置带注释类型的新实例(在本例中为Account)。由于 bean 的默认 name 是其类型的 fully-qualified name,因此声明原型定义的简便方法是省略id属性:

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

如果要显式指定要使用的原型 bean 定义的 name,可以直接在 annotation 中执行:

package com.xyz.myapp.domain;

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

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

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

您还可以使用自动装配来避免必须指定专用的 bean 定义。要让 Spring 应用自动装配,请使用@Configurable annotation 的autowire property:分别按类型或 name 指定@Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME进行自动装配。作为替代,从 Spring 2.5 开始,最好通过在字段或方法 level 上使用@Autowired@Inject@Configurable beans 指定显式的 annotation-driven 依赖注入(有关详细信息,请参阅第 7.9 节,“Annotation-based 容器配置”)。

最后,您可以使用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(e.g. ,objects 用new operator 实例化)以及正在进行反序列化的Serializable objects(e.g. ,来自readResolve())。

上段中的一个 key 短语是“本质上”。对于大多数情况,'从新的 object 初始化返回之后'的确切语义将很好......在此 context 中,'初始化'后意味着将在构造 object 之后注入依赖项 - 这意味着依赖项将无法在 class 的构造函数体中使用。如果您希望在构造函数体执行之前注入依赖项,从而可以在构造函数体中使用,那么您需要在@Configurable声明中定义它,如下所示:

@Configurable(preConstruction=true)

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

为了使其工作,带注释的类型必须与 AspectJ 编织器编织 - 您可以使用 build-time Ant 或 Maven 任务来执行此操作(请参阅示例AspectJ 开发环境指南)或 load-time 编织(请参阅第 11.8.4 节,“在 Spring Framework 中使用 AspectJ 编织 Load-time”)。 AnnotationBeanConfigurerAspect本身需要通过 Spring 进行配置(在 order 中获取_参考 bean 工厂,用于配置新的 objects)。如果您使用的是基于 Java 的 configuration,只需将@EnableSpringConfigured添加到任何@Configuration class。

@Configuration
@EnableSpringConfigured
public class AppConfig {

}

如果您更喜欢基于 XML 的 configuration,Spring context 命名空间定义了一个方便的context:spring-configured元素:

<context:spring-configured/>

在配置 aspect 之前创建的@Configurable objects 实例将导致向 debug log 发出消息,并且不会发生 object 的 configuration 配置。 example 可能是 Spring configuration 中的 bean,它在 Spring 初始化时创建域 objects。在这种情况下,您可以使用“depends-on”bean 属性手动指定 bean 取决于 configuration aspect。

<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 时看到一条警告消息,您构造一个@Configurable object,指示它尚未由 Spring 配置。

使用多个 application 上下文

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

考虑一个典型的 Spring web-app configuration,其中包含定义 common 业务服务的共享 parent application context 以及支持它们所需的一切,以及每个 servlet 包含一个 child application context,其中包含特定于该 servlet 的定义。所有这些上下文都将在同一个类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能对其中一个进行 reference。在这种情况下,我们建议在 shared(parent)application context 中定义@EnableSpringConfigured bean:这将定义您可能想要 inject 到域 objects 的服务。结果是你无法使用 @Configurable 机制(可能不是你想要做的事情!)来使用 child 在 child(servlet-specific)上下文中定义_beferences 来配置域 objects。

在同一容器中部署多个 web-apps 时,请确保每个 web-application 使用自己的类加载器加载spring-aspects.jar中的类型(对于 example,将spring-aspects.jar放在'WEB-INF/lib'中)。如果spring-aspects.jar仅添加到容器范围的 classpath(并因此由共享的 parent 类加载器加载),则所有 web applications 将共享相同的 aspect 实例,这可能不是您想要的。

11.8.2 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 中的 and/or 方法),而不是 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 的所有实例,使用原型 bean 定义匹配 fully-qualified class 名称:

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

当使用带有 Spring applications 的 AspectJ 方面时,很自然地希望并期望能够使用 Spring 配置这些方面。 AspectJ 运行时本身负责 aspect 创建,并且通过 Spring 配置 AspectJ 创建方面的方法取决于 aspect 使用的 AspectJ 实例化 model(per-xxx子句)。

AspectJ 的大多数方面都是 singleton 方面。 这些方面的配置非常简单:只需创建一个引用 aspect 类型的 bean 定义,并包含 bean 属性'factory-method="aspectOf"'。这可以确保 Spring 通过询问 AspectJ 来获取 aspect 实例,而不是尝试自己创建实例。例如:

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

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

Non-singleton 方面更难配置:但是可以通过 creating 原型 bean 定义并使用spring-aspects.jar@Configurable支持来配置 aspect 实例,一旦它们具有由 AspectJ 运行时创建的 bean,就可以这样做。

如果你有一些想要用 AspectJ 编织的 @AspectJ 方面(例如,使用 load-time 编织用于域 model 类型)和你想用 Spring AOP 的其他 @AspectJ 方面,并且这些方面都是使用 Spring 配置的,那么你将需要告诉 Spring AOP @AspectJ autoproxying 支持 configuration 中定义的 @AspectJ 方面的确切子集应该用于自动代理。您可以通过在<aop:aspectj-autoproxy/>声明中使用一个或多个<include/>元素来完成此操作。每个<include/>元素指定一个 name pattern,只有名称与至少一个模式匹配的 beans 才会用于 Spring AOP autoproxy configuration:

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

不要被<aop:aspectj-autoproxy/>元素的 name 误导:使用它将导致创建 Spring AOP 代理。这里只使用 @AspectJ 样式的 aspect 声明,但不涉及 AspectJ 运行时。

11.8.4 Load-time 在 Spring Framework 中使用 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-add 正在对编织 process 进行大量 finer-grained 控制。 “Vanilla”AspectJ LTW 使用 Java(5)代理实现,该代理在启动 JVM 时通过指定 VM 参数来启用。因此它是一个 JVM-wide 设置,在某些情况下可能会很好,但通常有点过于粗糙。 Spring-enabled LTW 使您能够在 per-ClassLoader 的基础上打开 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 编织,而不是依赖于通常负责部署配置的管理员,例如启动脚本。

现在销售宣传已经结束了,让我们首先使用 Spring 快速浏览 AspectJ LTW,然后详细介绍以下 example 中介绍的元素。有关完整示例,请参阅Petclinic sample application

第一个 example

让我们假设您是一名 application 开发人员,负责诊断系统中某些 performance 问题的原因。我们要做的是开启一个简单的分析 aspect,而不是 break 出分析工具,这将使我们能够非常快速地获得一些 performance metrics,以便我们可以在之后立即将 finer-grained 分析工具应用于该特定区域。

此处提供的 example 使用 XML 样式 configuration,也可以使用Java Configuration配置和使用 @AspectJ 。具体来说,@EnableLoadTimeWeaving annotation 可以用作<context:load-time-weaver/>的替代(详见下面)。

这是剖析 aspect。使用@AspectJ-style aspect 声明只是一个 quick-and-dirty time-based 探查器。

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。

<!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,如下所示(您可以指定更多选项,但稍后会详细介绍)。

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

现在所有必需的 artifact 都已到位--aspect,META-INF/aop.xml文件和 Spring configuration - 让我们创建一个带有main(..)方法的简单驱动程序 class 来演示 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,这是 true。但是,仅为此示例,我们将使用 Java 代理(随 Spring 提供)来打开 LTW。这是我们将用于运行上述Main class 的命令 line:

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程序的输出将如下所示。 (我在calculateEntitlement() implementation 中引入了Thread.sleep(..)语句,以便探查器实际捕获 0 毫秒以外的东西 - 01234毫秒不是 AOP 引入的开销:))

Calculating entitlement

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

由于 LTW 是使用 full-blown AspectJ 实现的,因此我们不仅限于建议 Spring beans; Main程序的以下微小变化将 yield 相同的结果。

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的新实例......分析建议仍然被编织进去。

example 无疑是简单的...但是 Spring 中 LTW 支持的基础知识都已经在上面的例子中引入了,本节的内容将详细解释 configuration 和用法的每一位背后的“原因”。

此 example 中使用的ProfilingAspect可能是基本的,但它非常有用。这是开发人员在开发过程中可以使用的 development-time aspect 的一个很好的例子,当然,然后很容易从部署到 UAT 或 production 的 application 的构建中排除。

方面

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

'META-INF/aop.xml'

AspectJ LTW 基础结构使用一个或多个META-INF/aop.xml files 配置,它们位于 Java classpath 上(直接或更常见于 jar files)。

该文件的结构和内容在主要的 AspectJ reference 文档中有详细介绍,感兴趣的 reader 是提到那个资源。 (我很欣赏这一部分是简短的,但aop.xml文件是 100%AspectJ - 没有适用于它的 Spring-specific 信息或语义,因此没有额外的 value 我可以作为结果贡献),所以而不是看看 AspectJ 开发人员编写的相当令人满意的部分,我只是在指导你 there.)

必需 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 configuration

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 来完成。

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {

}

或者,如果您更喜欢基于 XML 的 configuration,请使用<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>

上面的 configuration 将自动为您定义和注册一些 LTW-specific infrastructure beans,例如LoadTimeWeaverAspectJWeavingEnabler。默认的LoadTimeWeaverDefaultContextLoadTimeWeaver class,它试图修饰一个自动检测到的LoadTimeWeaver:将被“自动检测”的LoadTimeWeaver的确切类型取决于您的运行时环境(在下面的 table 中总结)。

表格 1_.DefaultContextLoadTimeWeaver LoadTimeWeavers

运行环境LoadTimeWeaver 实现
在 Oracle 的WebLogic中运行WebLogicLoadTimeWeaver
在 Oracle 的GlassFish中运行GlassFishLoadTimeWeaver
Apache Tomcat运行TomcatLoadTimeWeaver
Running in Red Hat 的JBoss ASWildFlyJBossLoadTimeWeaver
在 IBM 的WebSphere运行WebSphereLoadTimeWeaver
JVM 以 Spring InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar)开头InstrumentationLoadTimeWeaver
后备,期望底层的 ClassLoader 遵循 common 约定(e.g. 适用于TomcatInstrumentableClassLoader树脂)ReflectiveLoadTimeWeaver

请注意,这些只是在使用DefaultContextLoadTimeWeaver时自动检测的LoadTimeWeavers:当然可以准确指定您希望使用哪个LoadTimeWeaver implementation。

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

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

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

如果使用基于 XML 的 configuration,则可以将 fully-qualified classname 指定为<context:load-time-weaver/>元素上weaver-class属性的 value:

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

_conf可以稍后使用 well-known name loadTimeWeaver从 Spring 容器中检索由 configuration 定义和注册的LoadTimeWeaver。请记住,LoadTimeWeaver只是作为 Spring 的 LTW 基础设施添加一个或多个ClassFileTransformers的机制而存在的。执行 LTW 的实际ClassFileTransformerClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime包)class。有关详细信息,请参阅ClassPreProcessorAgentAdapter class 的 class-level javadocs,因为编织实际如何实现的细节超出了本节的范围。

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

表格 1_.AspectJ 编织属性值

Annotation ValueXML Value说明
ENABLEDonAspectJ 编织打开,方面将在 load-time 处编织。
DISABLEDoffLTW 已关闭......没有 aspect 将在 load-time 编织。
AUTODETECTautodetect如果 Spring LTW 基础结构可以找到至少一个META-INF/aop.xml文件,则 AspectJ 编织开启,否则关闭。这是默认值。

Environment-specific configuration

最后一节包含在 application servers 和 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 加载器(而不是默认值):

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

Apache Tomcat(6.0)支持多个 context 位置:

  • server configuration 文件 - $CATALINA_HOME/conf/server.xml

  • default context configuration - $CATALINA_HOME/conf/context.xml - 影响所有已部署的 web applications

  • per-web application configuration 可以部署在 server-side $CATALINA_HOME/conf/ [761]/[762]/[763] -context.xml 上,也可以嵌入 web-app 档案中 META-INF/context.xml

为了提高效率,建议使用嵌入式 per-web-app configuration 样式,因为它只会影响使用自定义 class 加载程序的 applications,并且不需要对服务器 configuration 进行任何更改。有关可用 context 位置的更多详细信息,请参阅 Tomcat 6.0.x 文件

或者,考虑使用 Spring-provided 泛型 VM 代理,在 Tomcat 的启动脚本中指定(见上文)。这将使所有已部署的 web applications 都可以使用检测,无论它们发生在哪个 ClassLoader 上。

WebLogic,WebSphere,Resin,GlassFish,JBoss

最新版本的 WebLogic Server(version 10 及更高版本),IBM WebSphere Application Server(version 7 及更高版本),Resin(3.1 及更高版本)和 JBoss(6.x 或更高版本)提供了一个能够进行本地检测的 ClassLoader。 Spring 的原生 LTW 利用这种 ClassLoader 来实现 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的文件:

<scanning xmlns="urn:jboss:scanning:1.0"/>
Generic Java applications

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

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

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

请注意,这需要修改 VM 启动脚本,这可能会阻止您在 application Server 环境中使用它(取决于您的操作 policies)。此外,JDK 代理将检测整个 VM,这可能证明是昂贵的。

出于性能原因,仅当目标环境(例如码头)没有(或不支持)专用 LTW 时,才建议使用此 configuration。

11.9 更多资源

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

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

强烈推荐 Ramnivas Laddad(Manning,2009)出版的“AspectJ in Action”第二版。本书的重点是 AspectJ,但很多一般的 AOP 主题都在探索中(在某种程度上)。

Updated at: 7 months ago
10.5. 示例中使用的类Table of content12. Spring AOP API