Spring 核心技术 - 使用 Spring 进行面向切面编程(AOP)

Spring 学习指南大全
Spring 核心技术

官方文档版本 Version 5.2.22.RELEASE

使用 Spring 进行面向切面编程(AOP)

面向切面编程 (AOP) 通过提供另一种思考程序结构的方式来补充面向对象编程 (OOP)。 OOP 中模块化的关键单元是类,而 AOP 中模块化的单元是切面。 切面支持跨多种类型和对象的关注点(例如事务管理)的模块化。 (这种关注点在 AOP 文献中通常被称为“横切”关注点。)

Spring 的关键组件之一是 AOP 框架。 虽然 Spring IoC 容器不依赖 AOP(这意味着如果您不想使用 AOP,则不需要使用 AOP),AOP 补充了 Spring IoC 以提供非常强大的中间件解决方案。

带有 AspectJ 切入点的 Spring AOP

Spring 通过使用【基于模式的方法】或【@AspectJ 注解样式】提供了编写自定义方面的简单而强大的方法。 这两种风格都提供了完全类型化的建议和使用 AspectJ 切入点语言,同时仍然使用 Spring AOP 进行编织。

本章讨论【基于模式】和【@AspectJ 的 AOP 支持】。 较低级别的 AOP 支持将在下一章中讨论【Spring AOP APIs】。

AOP 在 Spring Framework 中用于:

  • 提供声明式企业服务。 最重要的此类服务是【后续篇幅 - 声明式事务管理】。
  • 让用户实现自定义切面,用 AOP 补充他们对 OOP 的使用。

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

AOP 概念

让我们从定义一些核心 AOP 概念和术语开始。 这些术语不是 Spring 特定的。 不幸的是,AOP 术语并不是特别直观。 但是,如果 Spring 使用它自己的术语,那就更令人困惑了。

  • Aspect:跨多个类的关注点的模块化。 事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。 在 Spring AOP 中,切面是通过使用常规类(基于模式的方法)或使用 @Aspect 注解(@AspectJ 样式)注解的常规类来实现的。
  • 切入点:程序执行过程中的一个点,例如方法的执行或异常的处理。 在 Spring AOP 中,一个切入点总是代表一个方法执行。
  • Advice(建议):切面在特定切入点采取的行动。 不同类型的建议包括“around(周围)”、“before(之前)”和“after(之后)” 建议。 (通知类型将在后面讨论。)包括 Spring 在内的许多 AOP 框架将通知建模为拦截器,并在切入点周围维护一个拦截器链。
  • Pointcut(切入点):匹配切入点的谓词。 Advice 与 Pointcut(切入点)切入点表达式相关联,并在与切入点匹配的任何切入点处运行(例如,执行具有特定名称的方法)。 切入点表达式匹配的切入点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言。
  • Introduction(介绍):代表类型声明附加方法或字段。Spring AOP 允许您向任何被通知的对象引入新的接口(以及相应的实现)。例如,您可以使用 Introduction(介绍)使 bean 实现 IsModified 接口,以简化缓存。(在 AspectJ 社区中,介绍被称为内部类型声明。)
  • 目标对象:一个或多个方面建议的对象。 也称为“建议对象”。 由于 Spring AOP 是使用运行时代理实现的,因此该对象始终是代理对象。
  • AOP 代理:由 AOP 框架创建的对象,用于实现切面协定(建议方法执行等)。 在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
  • 编织,织入:将方面与其他应用程序类型或对象链接以创建建议对象。 这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。 Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。

Spring AOP 包括以下类型的通知:

  • Before advice(通知前):在切入点之前运行但不能阻止执行流继续到切入点的通知(除非它抛出异常)。
  • After returning advice(返回通知后):在切入点正常完成后运行的通知(例如,如果方法返回而没有引发异常)。
  • After throwing advice(抛出建议后):如果方法因抛出异常而退出,则运行建议。
  • After (finally) advice(在(最终)通知之后):无论切入点以何种方式退出(正常或异常返回),都将运行建议。
  • Around advice(围绕建议):围绕切入点的建议,例如方法调用。 这是最有力的建议。 环绕通知可以在方法调用之前和之后执行自定义行为。 它还负责选择是继续到切入点还是通过返回自己的返回值或抛出异常来缩短建议的方法执行。

环绕建议是最一般的建议。 由于 Spring AOP 与 AspectJ 一样,提供了全方位的通知类型,因此我们建议您使用可以实现所需行为的最不强大的通知类型。 例如,如果您只需要使用方法的返回值来更新缓存,那么您最好实现一个后返回通知而不是一个环绕通知,尽管一个环绕通知可以完成同样的事情。 使用最具体的通知类型提供了一个更简单的编程模型,并且出错的可能性更小。 例如,您不需要在用于环绕通知的 JoinPoint 上调用proceed() 方法,因此您不能调用它。

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

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

Spring AOP 的能力和目标

Spring AOP 是用纯 Java 实现的。 不需要特殊的编译过程。 Spring AOP 不需要控制类加载器层次结构,因此适用于 servlet 容器或 application server(应用程序服务器)。

Spring AOP 当前仅支持方法执行切入点(建议在 Spring bean 上执行方法)。 没有实现字段拦截,尽管可以在不破坏核心 Spring AOP API 的情况下添加对字段拦截的支持。 如果您需要建议字段访问和更新切入点,请考虑使用 AspectJ 等语言。

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

因此,例如,Spring 框架的 AOP 功能通常与 Spring IoC 容器一起使用。 切面是通过使用普通的 bean definition (定义)语法来配置的(尽管这允许强大的 “自动代理” 功能)。 这是与其他 AOP 实现的关键区别。 您无法使用 Spring AOP 轻松或高效地做一些事情,例如建议非常细粒度的对象(通常是 domain(域)对象)。 在这种情况下,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 的讨论,请参见下一章。

Spring 框架的核心原则之一是非侵入性。 这就是不应该强迫您将特定于框架的类和接口引入您的业务或领域模型的想法。 但是,在某些地方,Spring Framework 确实为您提供了将 Spring Framework 特定的依赖项引入代码库的选项。 为您提供此类选项的理由是,在某些情况下,以这种方式阅读或编写某些特定功能可能更容易。 但是,Spring 框架(几乎)总是为您提供选择:您可以自由地就哪个选项最适合您的特定用例或场景做出明智的决定。

与本章相关的一个这样的选择是选择哪种 AOP 框架(以及哪种 AOP 风格)。 您可以选择 AspectJ、Spring AOP 或两者兼而有之。 您还可以选择 @AspectJ 注解样式方法或 Spring XML 配置样式方法。 本章选择首先介绍 @AspectJ 风格的方法这一事实不应被视为 Spring 团队更喜欢 @AspectJ 注解风格的方法而不是 Spring XML 配置风格的迹象。

有关每种样式的 “原因和原因” 的更完整讨论,请参阅【选择使用哪种 AOP 声明样式】。

AOP 代理

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

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

重要的是要掌握 Spring AOP 是基于代理的这一事实。 请参阅【了解 AOP 代理】,以全面了解此实现细节的实际含义。

@AspectJ 支持

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

使用 AspectJ 编译器和编织器可以使用完整的 AspectJ 语言,并在【将 AspectJ 与 Spring 应用程序一起使用】中进行了讨论。

启用 @AspectJ 支持

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

可以使用 XML 或 Java 样式的配置启用 @AspectJ 支持。 无论哪种情况,您还需要确保 AspectJ 的 aspectjweaver.jar 库位于应用程序的类路径中(1.8 或更高版本)。 该库在 AspectJ 发行版的 lib 目录中或从 Maven 中央存储库中可用。

使用 Java 配置启用 @AspectJ 支持

要使用 Java @Configuration 启用 @AspectJ 支持,请添加 @EnableAspectJAutoProxy 注解,如以下示例所示:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
通过 XML 配置启用 @AspectJ 支持

要使用基于 XML 的配置启用 @AspectJ 支持,请使用 aop:aspectj-autoproxy 元素,如以下示例所示:

<aop:aspectj-autoproxy/>

这假定您使用【基于 XML 模式的配置】中描述的模式支持。 有关如何在 aop 命名空间中导入标签的信息,请参阅【 AOP 模式】。

声明一个 Aspect(切面)

启用 @AspectJ 支持后,在应用程序上下文中定义的具有 @AspectJ 方面(具有@Aspect 注解)的类的任何 bean 都会被 Spring 自动检测并用于配置 Spring AOP。 接下来的两个示例显示了一个不太有用的方面所需的最小定义。

这两个示例中的第一个显示了 application context(应用程序上下文)中的常规 bean definition (定义),该定义指向具有 @Aspect 注解的 bean 类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- 在此配置切面的属性 -->
</bean>

两个示例中的第二个显示了 NotVeryUsefulAspect 类定义,它使用 org.aspectj.lang.annotation.Aspect 注解进行注解;

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

@Aspect
public class NotVeryUsefulAspect {

}

方面(用 @Aspect 注解的类)可以具有方法和字段,与任何其他类相同。 它们还可以包含 pointcut(切入点)、advice(建议)和 introduction (介绍)(类型间)声明。

通过组件扫描自动检测切面

您可以在 Spring XML 配置中将方面类注册为常规 bean,或者通过类路径扫描自动检测它们 — 与任何其他 Spring 管理的 bean 相同。 但是,请注意 @Aspect 注解不足以在类路径中进行自动检测。 为此,您需要添加一个单独的 @Component 注解(或者,根据 Spring 组件扫描器的规则,一个符合条件的自定义构造型注解)。

建议切面与其他切面?

在 Spring AOP 中,切面本身不能成为其他切面通知的目标。 类上的 @Aspect 注解将其标记为切面,因此将其排除在自动代理之外。

声明切入点

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

一个示例可能有助于明确切入点签名和切入点表达式之间的区别。 以下示例定义了一个名为 anyOldTransfer 的切入点,它与任何名为 transfer 的方法的执行相匹配:

@Pointcut("execution(* transfer(..))") // 切入点表达式
private void anyOldTransfer() {} // 切入点签名

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

支持的切入点指示符

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

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

其他切入点类型

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

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

由于 Spring AOP 将匹配限制为仅方法执行切入点,因此前面对切入点指示符的讨论给出了比您在 AspectJ 编程指南中找到的更窄的定义。 此外,AspectJ 本身具有基于类型的语义,并且在执行切入点处,this 和 target 都引用同一个对象:执行该方法的对象。 Spring AOP 是一个基于代理的系统,它区分代理对象本身(绑定到 this)和代理背后的目标对象(绑定到 target)。

由于 Spring 的 AOP 框架基于代理的特性,根据定义,目标对象内的调用不会被拦截。 对于 JDK 代理,只能拦截代理上的公共接口方法调用。 使用 CGLIB,代理上的公共和受保护的方法调用被拦截(如果需要,甚至包可见的方法)。 但是,通过代理的常见交互应始终通过公共签名进行设计。

请注意,切入点定义通常与任何拦截的方法匹配。 如果切入点严格来说是只 public(公开) 的,即使在 CGLIB 代理场景中,通过代理进行潜在的非公开交互,也需要相应地定义它。

如果您的拦截需求包括目标类中的方法调用甚至构造函数,请考虑使用 Spring 驱动的【原生 AspectJ 织入】,而不是 Spring 的基于代理的 AOP 框架。 这就构成了具有不同特点的不同AOP使用模式,所以在做决定之前一定要让自己熟悉织入。

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

bean(idOrNameOfBean)

idOrNameOfBean 标记可以是任何 Spring bean 的名称。 提供了使用 * 字符的有限通配符支持,因此,如果您为 Spring bean 建立一些命名约定,则可以编写一个 bean PCD 表达式来选择它们。 与其他切入点指示符的情况一样,bean PCD 可以与 && (and),|| (or),和 ! (运算符) 也是如此。

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

bean PCD 在实例级别(基于 Spring bean 名称概念构建)而不是仅在类型级别(基于织入的 AOP 受限)运行。 基于实例的切入点指示符是 Spring 的基于代理的 AOP 框架的一种特殊功能,它与 Spring bean factory 紧密集成,通过名称识别特定 bean 是自然而直接的。

组合切入点表达式

您可以使用 &&、|| 和 !组合切入点表达式 。 您还可以按名称引用切入点表达式。 以下示例显示了三个切入点表达式:

// 如果方法执行切入点表示执行,则 anyPublicOperation 匹配任何公共方法。
@Pointcut("execution(public * *(..))")	
private void anyPublicOperation() {} 

// 如果方法执行在 trading(交易) 模块中,则 inTrading 匹配。
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} 

// 如果方法执行代表任何公共方法,则 tradingOperation 匹配 trading(交易) 模块。
@Pointcut("anyPublicOperation() && inTrading()")	//把上面两个切入点组合使用
private void tradingOperation() {} 

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

共享通用切入点定义

在使用企业应用程序时,开发人员通常希望从多个切面引用应用程序的模块和特定的操作集。 我们建议为此目的定义一个捕获公共切入点表达式的 CommonPointcuts 切面。 这样的切面通常类似于以下示例:

package com.xyz.myapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect	// 切面类
public class CommonPointcuts {

    /**
     * 如果方法是在 com.xyz.myapp.web 包或该包下的任何子包中的类型中定义的,则切入点位于 Web 层中。
     */
    @Pointcut("within(com.xyz.myapp.web..*)")	// 切入点
    public void inWebLayer() {}

    /**
     * 如果方法在 com.xyz.myapp.service 包或该包下的任何子包中的类型中定义,则切入点位于服务层中。
     */
    @Pointcut("within(com.xyz.myapp.service..*)")
    public void inServiceLayer() {}

    /**
     * 如果方法在 com.xyz.myapp.dao 包或该包下的任何子包中的类型中定义,则切入点位于数据访问层中。
     */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * 业务服务是定义在服务接口上的任何方法的执行。 这个定义假设接口放在 “service” 包中,实现类型放在子包中。
     *
     * 如果按功能区域对服务接口进行分组(例如,在包 com.xyz.myapp.abc.service 和 com.xyz.myapp.def.service 中),
     * 那么切入点表达式 “execution(* com.xyz.myapp..service .*.*(..))" 可以代替使用。
     *
     * 或者,您可以使用 “bean”PCD 编写表达式,例如 “bean(*Service)”。 
     *(这假设您以一致的方式命名了 Spring Service bean。)
     */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
    public void businessService() {}

    /**
     * 数据访问操作是定义在 dao 接口上的任何方法的执行。 这个定义假定接口放在“dao”包中,实现类型放在子包中。
     */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

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

<aop:config>
    <aop:advisor
        pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

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

<aop:config><aop:advisor> 元素在【基于模式的 AOP 支持】中讨论。 事务元素在【事务管理】中讨论。

例子

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

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)
                
execution(修饰符模式? 返回类型模式 声明类型模式?名称模式(参数模式) 抛出模式?)

除了返回类型模式(前面代码片段中的 ret-type-pattern)、名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么才能匹配切入点。 * 最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,完全限定的类型名称才匹配。名称模式与方法名称匹配。您可以将 * 通配符用作名称模式的全部或一部分。如果您指定声明类型模式,请包含尾随 .将其加入名称模式组件。参数模式稍微复杂一些:() 匹配不带参数的方法,而 (…) 匹配任意数量(零个或多个)参数。 () 模式匹配采用任何类型的一个参数的方法。 (,String) 匹配带有两个参数的方法。第一个可以是任何类型,而第二个必须是字符串。有关更多信息,请参阅【 AspectJ 编程指南的语言语义】部分。

以下示例显示了一些常见的切入点表达式:

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

‘this’ 更常用于绑定形式。 有关如何使代理对象在建议正文中可用的信息,请参阅【声明建议】部分。

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

‘target’ 更常用于绑定形式。 有关如何使目标对象在建议正文中可用的信息,请参阅【声明建议】部分。

  • 任何接受单个参数并且在运行时传递的参数是可序列化的切入点(仅在 Spring AOP 中执行方法):
args(java.io.Serializable)

‘args’ 更常用于绑定形式。 请参阅【声明建议】部分,了解如何使方法参数在建议正文中可用。

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

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

您还可以在绑定表单中使用 “@target”。 有关如何使注释对象在建议正文中可用的信息,请参阅【声明建议】部分。

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

您还可以在绑定表单中使用 “@within”。 有关如何使注解对象在建议正文中可用的信息,请参阅【声明建议】部分。

  • 执行方法具有 @Transactional 注解的任何切入点(仅在 Spring AOP 中执行方法):
@annotation(org.springframework.transaction.annotation.Transactional)

您还可以在绑定表单中使用 “@annotation”。 有关如何使注解对象在建议正文中可用的信息,请参阅【声明建议】部分。

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

您还可以在绑定表单中使用 “@args”。 请参阅【声明建议】部分如何使注解对象在建议正文中可用。

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

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

然而,AspectJ 只能使用它被告知的内容。 为了获得最佳匹配性能,您应该考虑他们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。 现有的指示符自然属于以下三组之一:kinded(种类)、scoping(范围) 和 contextual(上下文):

  • Kinded(种类)指示符选择一种特定类型的切入点:execution(执行)、get(获取)、set(设置)、call(调用)和 handler(处理程序。)
  • Scoping(范围)指示符选择一组切入点(可能有多种):代码内和代码内
  • contextual(上下文)指示符根据上下文匹配(并且可以选择绑定):this、target 和 @annotation

一个写得很好的切入点应该至少包括前两种类型(种类和范围)。 您可以包含上下文指示符以根据切入点上下文进行匹配,或绑定该上下文以在建议中使用。 由于额外的处理和分析,只提供一个 kinded 指示符或只提供一个上下文指示符是可行的,但可能会影响编织性能(使用的时间和内存)。 范围指示符的匹配速度非常快,使用它们意味着 AspectJ 可以非常快速地消除不应进一步处理的切入点组。 如果可能,一个好的切入点应始终包含一个切入点。

声明通知

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

执行之前的通知

您可以使用 @Before 注解在切面声明之前的通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}

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

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
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.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}

你可以有多个通知声明(以及其他成员),都在同一个切面。 我们在这些示例中只展示了一个通知声明,以集中每个通知的效果。

有时,您需要在通知正文中访问返回的实际值。 您可以使用绑定返回值的 @AfterReturning 形式来获得该访问权限,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}

返回属性中使用的名称必须与通知方法中的参数名称相对应。 当方法执行返回时,返回值作为相应的参数值传递给通知方法。 Returning 子句还将匹配限制为仅返回指定类型的值的那些方法执行(在本例中为 Object,它匹配任何返回值)。

请注意,在返回后通知使用时,不可能返回完全不同的参考。

抛出后的通知

当匹配的方法执行通过抛出异常退出时,抛出后运行通知。 您可以使用 @AfterThrowing 注解来声明它,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }
}

通常,您希望通知仅在引发给定类型的异常时运行,并且您还经常需要访问通知正文中引发的异常。 您可以使用 throwing 属性来限制匹配(如果需要 — 否则使用 Throwable 作为异常类型)并将抛出的异常绑定到通知参数。 以下示例显示了如何执行此操作:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}

throwing 属性中使用的名称必须与通知方法中的参数名称相对应。 当通过抛出异常退出方法执行时,异常将作为相应的参数值传递给通知方法。 throwing 子句还将匹配限制为仅抛出指定类型的异常(在本例中为 DataAccessException)的那些方法执行。

请注意,@AfterThrowing 并不表示一般的异常处理回调。 具体来说,@AfterThrowing 通知方法只应该从切入点(用户声明的目标方法)本身接收异常,而不是从伴随的 @After/@AfterReturning 方法接收异常。

(最终)通知之后

当匹配的方法执行退出时(最终)通知运行。 它是通过使用 @After 注解声明的。 After 通知必须准备好处理正常和异常返回条件。 它通常用于释放资源和类似目的。 下面的例子展示了如何使用 after 最终通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}

请注意,AspectJ 中的 @After 建议被定义为 “在 finally 建议之后”,类似于 try-catch 语句中的 finally 块。 它将被调用用于从连接点(用户声明的目标方法)抛出的任何结果、正常返回或异常,而 @AfterReturning 仅适用于成功的正常返回。

环绕通知

最后一种通知是环绕通知。 环绕通知 “环绕” 匹配方法的执行。 它有机会在方法运行之前和之后进行工作,并确定该方法何时、如何以及是否真正开始运行。 如果您需要以线程安全的方式(例如启动和停止计时器)在方法执行之前和之后共享状态,则通常使用环绕通知。 始终使用满足您要求的最不强大的通知形式(也就是说,如果之前的通知可以使用,请不要使用环绕通知)。

使用 @Around 注解声明环绕通知。 通知方法的第一个参数必须是 ProceedingJoinPoint 类型。 在通知正文中,对 ProceedingJoinPoint 调用 proceed() 会导致底层方法运行。 proceed 方法也可以传入一个 Object[]。 数组中的值用作方法执行时的参数。

使用 Object[] 调用时,proceed 的行为与 AspectJ 编译器编译的环绕通知的继续行为略有不同。对于使用传统 AspectJ 语言编写的环绕通知,传递给继续执行的参数数量必须与传递给环绕通知的参数数量相匹配(而不是底层切入点采用的参数数量),并且传递给继续执行的值给定的参数位置替换值绑定到的实体的切入点处的原始值(如果现在没有意义,请不要担心)。 Spring 采用的方法更简单,更符合其基于代理的、仅执行的语义。如果您编译为 Spring 编写的 @AspectJ 切面并使用 AspectJ 编译器和注入器的参数进行处理,您只需要注意这种差异。有一种方法可以编写跨 Spring AOP 和 AspectJ 100% 兼容的切面,这将在下一节有关通知参数的部分中讨论。

下面的例子展示了如何使用 around 通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // 开始秒表
        Object retVal = pjp.proceed();
        // 停止秒表
        return retVal;
    }
}

around 通知返回的值是方法调用者看到的返回值。 例如,一个简单的缓存切面可以从缓存中返回一个值,如果它有一个值,如果没有,则调用 proceed()。 请注意,在 around 通知的主体内,可能会调用一次、多次或根本不调用 proceed。 所有这些都是合法的。

通知参数

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

访问当前的 JoinPoint(切入点)

任何通知方法都可以声明一个 org.aspectj.lang.JoinPoint 类型的参数作为它的第一个参数(注意,围绕通知需要声明一个 ProceedingJoinPoint 类型的第一个参数,它是 JoinPoint 的子类。JoinPoint 接口提供了许多有用的方法:

  • getArgs():返回方法参数。
  • getThis():返回代理对象。
  • getTarget():返回目标对象。
  • getSignature():返回对所通知方法的描述。
  • toString():打印对所通知方法的有用描述。

有关更多详细信息,请参阅 javadoc

将参数传递给 Advice(通知)

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

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

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

另一种写法是声明一个切入点,当它与切入点匹配时 “提供” Account 对象值,然后从通知中引用命名的切入点。 这将如下所示:

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

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

有关详细信息,请参阅 AspectJ 编程指南。

代理对象 (this)、目标对象 (target) 和注解(@within、@target、@annotation 和 @args)都可以以类似的方式绑定。 接下来的两个示例展示了如何匹配使用 @Auditable 注解注解的方法的执行并提取审计代码:

这两个示例中的第一个显示了 @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);
}

您可以通过将通知参数键入要拦截方法的参数类型来将方法类型的拦截限制为某些参数类型:

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

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

确定参数名称

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

  • 如果用户已明确指定参数名称,则使用指定的参数名称。 建议和切入点注解都有一个可选的 argNames 属性,您可以使用它来指定带注解的方法的参数名称。 这些参数名称在运行时可用。 以下示例显示了如何使用 argNames 属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... 使用代码和 bean
}

如果第一个参数属于 JoinPoint、ProceedingJoinPoint 或 JoinPoint.StaticPart 类型,则可以在 argNames 属性的值中省略参数名称。 例如,如果您修改前面的通知以接收切入点对象,则 argNames 属性不需要包含它:

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

对 JoinPoint、ProceedingJoinPoint 和 JoinPoint.StaticPart 类型的第一个参数的特殊处理对于不收集任何其他切入点上下文的通知实例特别方便。 在这种情况下,您可以省略 argNames 属性。 例如,以下建议不需要声明 argNames 属性:

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

如果 AspectJ 编译器 (ajc) 已经编译了 @AspectJ 切面,即使没有调试信息,您也不需要添加 argNames 属性,因为编译器会保留所需的信息。

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

继续争论

我们之前提到过,我们将描述如何编写一个带有在 Spring AOP 和 AspectJ 中一致工作的参数的继续调用。 解决方案是确保通知签名按顺序绑定每个方法参数。 以下示例显示了如何执行此操作:

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}

在许多情况下,无论如何都要执行此绑定(如前面的示例中所示)。

通知顺序

当多条通知都想在同一个切入点运行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知执行的顺序。 最高优先级的通知首先“在进入的路上”运行(因此,给定两条之前的通知,优先级最高的一条首先运行)。 从切入点 “退出” 时,优先级最高的通知最后运行(因此,给定两条后通知,具有最高优先级的一条将运行第二个)。

当在不同切面定义的两条通知都需要在同一个切入点运行时,除非您另外指定,否则执行顺序是未定义的。 您可以通过指定优先级来控制执行顺序。 这可以通过在切面类中实现 org.springframework.core.Ordered 接口或使用 @Order 注解以正常的 Spring 方式完成。 给定两个方面,从 Ordered.getOrder()(或注解值)返回较低值的方面具有较高的优先级。

特定切面的每个不同的通知类型在概念上意味着直接应用于切入点。 因此,@AfterThrowing 通知方法不应该从伴随的 @After/@AfterReturning 方法接收异常。

从 Spring Framework 5.2.7 开始,需要在同一切入点运行的同一 @Aspect 类中定义的通知方法根据其通知类型按以下顺序分配优先级,从最高到最低优先级:@Around、@Before 、@After、@AfterReturning、@AfterThrowing。 但是请注意,@After 通知方法将在同一方面中的任何 @AfterReturning 或 @AfterThrowing 通知方法之后有效地调用,遵循 AspectJ 的 @After 的“最终通知之后”语义。

当同一个 @Aspect 类中定义的两条相同类型的 advice(例如两个 @Afteradvice 方法)都需要在同一个切入点运行时,排序是不确定的(因为没有办法检索源 通过 javac 编译类的反射的代码声明顺序)。 考虑在每个 @Aspect 类中将此类通知方法折叠为每个切入点的一个通知方法,或者将通知片段重构为单独的 @Aspect 类,您可以通过 Ordered 或 @Order 在方面级别对这些类进行排序。

引入

引入(在 AspectJ 中称为类型间声明)使切面能够声明通知对象实现给定接口,并代表这些对象提供该接口的实现。

您可以使用 @DeclareParents 注解进行引入。 此注解用于声明匹配类型有一个新的父级(因此得名)。 例如,给定一个名为 UsageTracked 的接口和一个名为 DefaultUsageTracked 的接口的实现,以下方面声明服务接口的所有实现者也实现 UsageTracked 接口(例如,通过 JMX 进行统计):

@Aspect
public class UsageTracking {

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

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

}

要实现的接口由注解字段的类型决定。 @DeclareParents 注释的 value 属性是 AspectJ 类型模式。 任何匹配类型的 bean 都实现 UsageTracked 接口。 请注意,在前面示例的之前通知中,服务 bean 可以直接用作 UsageTracked 接口的实现。 如果以编程方式访问 bean,您将编写以下内容:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
切面实例化模型

这是一个高级话题。 如果您刚开始使用 AOP,您可以放心地跳过它,直到以后。

默认情况下,应用程序上下文中的每个切面都有一个实例。 AspectJ 将此称为单例实例化模型。 可以定义具有备用生命周期的切面。 Spring 支持 AspectJ 的 perthis 和 pertarget 实例化模型; 当前不支持 percflow、percflowbelow 和 pertypewithin。

您可以通过在 @Aspect 注解中指定 perthis 子句来声明 perthis 切面。 考虑以下示例:

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

    private int someState;

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

在前面的示例中,perthis 子句的效果是为每个执行业务服务的唯一服务对象(每个唯一对象在与切入点表达式匹配的切入点绑定到 this)创建一个切面实例。 切面实例是在第一次在服务对象上调用方法时创建的。 当服务对象超出范围时,切面超出范围。 在创建切面实例之前,其中的任何通知都不会运行。 一旦创建了切面实例,其中声明的通知就会在匹配的切入点处运行,但仅当服务对象是与此切面关联的对象时。 有关 per 子句的更多信息,请参阅 AspectJ 编程指南。

pertarget 实例化模型的工作方式与 perthis 完全相同,但它为匹配的切入点处的每个唯一目标对象创建一个切面实例。

AOP 示例

现在您已经了解了所有组成部分的工作原理,我们可以将它们组合在一起做一些有用的事情。

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

因为我们要重试操作,所以我们需要使用 around 通知,这样我们就可以多次调用 proceed。 以下清单显示了基本切面的实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

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

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

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

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

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }
}

请注意,切面实现了 Ordered 接口,因此我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新事务)。 maxRetries 和 order 属性都由 Spring 配置。 主要动作发生在围绕建议的 doConcurrentOperation 中。 请注意,目前,我们将重试逻辑应用于每个 businessService()。 我们尝试继续,如果我们因 PessimisticLockingFailureException 而失败,我们会再试一次,除非我们已经用尽了所有重试尝试。

对应的Spring配置如下:

<aop:aspectj-autoproxy/>

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

为了细化切面以使其仅重试幂等操作,我们可以定义以下幂等注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // 标记注解
}

然后我们可以使用注解来注解服务操作的实现。 对仅重试幂等操作切面的更改涉及改进切入点表达式,以便仅匹配 @Idempotent 操作,如下所示:

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

所以 Spring 基于 @AspectJ 实现的面向切面编程总体分为以下步骤:

  • 使用 @Aspect 注解开启切面类
  • 使用通知注解如 @Around 等来使标记的方法为通知执行方法
  • 使用通知注解里的表达式来指定通知匹配的切入点
  • 最后在 Spring 中开启 @AspectJ 切面注解支持,并把此切面类交由 Spring 容器管理

XML 使用此元素开启 aspectj 自动代理支持:<aop:aspectj-autoproxy/>

切面类使用XML配置或者注解扫描交由 Spring 容器都行

基于模式的 AOP 支持

如果您更喜欢基于 XML 的格式,Spring 还支持使用 aop 命名空间标签定义方面。 支持与使用 @AspectJ 风格时完全相同的切入点表达式和通知类型。 因此,在本节中,我们将重点放在该语法上,并请读者参考上一节中的讨论(@AspectJ 支持),以了解编写切入点表达式和通知参数的绑定。

要使用本节中描述的 aop 命名空间标签,您需要导入 spring-aop 模式,如【基于 XML 模式的配置】中所述。 有关如何在 aop 命名空间中导入标签的信息,请参阅 【AOP 模式】。

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

<aop:config> 风格的配置大量使用了 Spring 的自动代理机制。 如果您已经通过使用 BeanNameAutoProxyCreator 或类似的东西使用显式自动代理,这可能会导致问题(例如未织入通知)。 推荐的使用模式是仅使用 <aop:config> 风格或仅使用 AutoProxyCreator 样式,并且永远不要混合使用它们。

声明一个切面

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

您可以使用 <aop:aspect> 元素声明一个切面,并使用 ref 属性引用支持 bean,如以下示例所示:

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

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

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

声明切入点

您可以在 <aop:config> 元素内声明一个命名切入点,让切入点定义在多个切面和通知之间共享。

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

<aop:config>

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

</aop:config>

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

<aop:config>

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

</aop:config>

假设您有一个 CommonPointcuts 切面,如【共享通用切入点定义】中所述。

然后在切面内声明切入点与声明顶级切入点非常相似,如以下示例所示:

<aop:config>

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

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

        ...
    </aop:aspect>

</aop:config>

与@AspectJ 切面非常相似,使用基于模式的定义样式声明的切入点可以收集切入点上下文。 例如,以下切入点收集 this 对象作为切入点上下文并将其传递给通知:

<aop:config>

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

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

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

        ...
    </aop:aspect>

</aop:config>

必须通过包含匹配名称的参数来声明通知以接收收集的切入点上下文,如下所示:

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

组合切入点子表达式时,&& 在 XML 文档中很尴尬,因此您可以使用 and、or 和 not 关键字分别代替 &&、|| 和 !。 例如,前面的切入点可以更好地写成如下:

<aop:config>

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

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

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

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

请注意,以这种方式定义的切入点由它们的 XML id 引用,不能用作命名切入点来形成复合切入点。 因此,基于模式的定义风格中的命名切入点支持比 @AspectJ 风格提供的更有限。

声明通知

基于模式的 AOP 支持使用与 @AspectJ 样式相同的五种通知,并且它们具有完全相同的语义。

通知前

Before 通知在匹配的方法执行之前运行。 它通过使用 <aop:before> 元素在 <aop:aspect> 内声明,如以下示例所示:

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

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

    ...

</aop:aspect>

在这里,dataAccessOperation 是定义在顶层 (<aop:config>) 级别的切入点的 id。 要改为内联定义切入点,请将切入点引用属性替换为切入点属性,如下所示:

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

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

    ...
</aop:aspect>

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

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

返回后的通知

当匹配的方法执行正常完成时,返回通知运行后。 它在 <aop:aspect> 中以与之前通知相同的方式声明。 以下示例显示了如何声明它:

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

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

    ...
</aop:aspect>

与 @AspectJ 样式一样,您可以在通知正文中获取返回值。 为此,请使用返回属性来指定应将返回值传递到的参数的名称,如以下示例所示:

<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>
环绕通知

最后一种通知是环绕通知。 环绕通知 “环绕” 匹配的方法执行。 它有机会在方法运行之前和之后进行工作,并确定该方法何时、如何以及是否真正开始运行。 环绕通知通常用于以线程安全的方式(例如启动和停止计时器)在方法执行之前和之后共享状态。 始终使用满足您要求的最不强大的通知形式。 如果之前的通知可以完成这项工作,请不要使用环绕的通知。

您可以使用 aop:around 元素声明环绕通知。 通知方法的第一个参数必须是 ProceedingJoinPoint 类型。 在通知正文中,对 ProceedingJoinPoint 调用 proceed() 会导致底层方法运行。 也可以使用 Object[] 调用 proceed 方法。 数组中的值用作方法执行时的参数。 有关使用 Object[] 调用继续的注解,请参阅环绕通知。 以下示例展示了如何在 XML 中声明环绕通知:

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

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

    ...
</aop:aspect>

doBasicProfiling 通知的实现可以与 @AspectJ 示例中的完全相同(当然,要减去注解),如下例所示:

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // 启动秒表
    Object retVal = pjp.proceed();
    // 停止秒表
    return retVal;
}
通知参数

基于模式的声明风格以与 @AspectJ 支持中描述的相同方式支持完全类型化的通知 - 通过按名称匹配切入点参数与通知方法参数。 有关详细信息,请参阅【通知参数】。 如果您希望为通知方法显式指定参数名称(不依赖于前面描述的检测策略),您可以使用通知元素的 arg-names 属性来实现,该属性的处理方式与 argNames 属性相同 在通知注解中(如确定参数名称中所述)。 以下示例显示如何在 XML 中指定参数名称:

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

    Person getPerson(String personName, int age);
}

public class DefaultPersonService implements PersonService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}

接下来是切面。 请注意 profile(…) 方法接受许多强类型参数的事实,其中第一个恰好是用于继续进行方法调用的切入点。 此参数的存在表明 profile(…) 将用作环绕通知,如以下示例所示:

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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 这是将由 Spring 的 AOP 基础设施代理的对象 -->
    <bean id="personService" class="x.y.service.DefaultPersonService"/>

    <!-- 这是实际的通知本身 -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config> <!-- AOP配置 -->
        <aop:aspect ref="profiler"><!-- 切面 -->
			<!-- 切入点 -->
            <aop:pointcut id="theExecutionOfSomePersonServiceMethod"
                expression="execution(* x.y.service.PersonService.getPerson(String,int))
                and args(name, age)"/>
			<!-- 通知 -->
            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
                method="profile"/>

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

</beans>

考虑以下驱动程序脚本:

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

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        PersonService person = (PersonService) ctx.getBean("personService");
        person.getPerson("Pengo", 12);
    }
}

使用这样的 Boot 类,我们将在标准输出中获得类似于以下内容的输出:

StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)
通知排序

当多条通知需要在同一个切入点(执行方法)运行时,排序规则如 【通知排序】中所述。 切面之间的优先级是通过 <aop:aspect> 元素中的 order 属性确定的,或者通过将 @Order 注解添加到支持方面的 bean 或通过让 bean 实现 Ordered 接口来确定。

与定义在同一个 @Aspect 类中的通知方法的优先级规则相反,当同一个 <aop:aspect> 元素中定义的两条通知都需要在同一个切入点运行时,优先级由其中通知元素在封闭的 <aop:aspect> 元素中声明,从最高到最低优先级。

例如,给定在同一个 <aop:aspect> 元素中定义的适用于同一切入点的环绕通知和前通知,为了确保环绕通知的优先级高于前通知,<aop:around> 元素 必须在 <aop:before> 元素之前声明。

作为一般经验法则,如果您发现在同一个 <aop:aspect> 元素中定义了多条适用于同一切入点的通知,请考虑将这些通知方法折叠为每个切入点中的一个通知方法 < aop:aspect> 元素或将通知片段重构为单独的 <aop:aspect> 元素,您可以在切面级别对其进行排序。

引入

引入(在 AspectJ 中称为类型间声明)让切面声明通知对象实现给定接口并代表这些对象提供该接口的实现。

您可以使用 aop:aspect 中的 aop:declare-parents 元素进行介绍。 您可以使用 aop:declare-parents 元素来声明匹配类型有一个新的父级(因此得名)。 例如,给定一个名为 UsageTracked 的接口和一个名为 DefaultUsageTracked 的接口的实现,以下方面声明服务接口的所有实现者也实现 UsageTracked 接口。 (例如,为了通过 JMX 公开统计信息。)

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

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

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

</aop:aspect>

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

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

要实现的接口由 implement-interface 属性决定。 types-matching 属性的值是 AspectJ 类型模式。 任何匹配类型的 bean 都实现 UsageTracked 接口。 请注意,在前面示例的之前通知中,服务 bean 可以直接用作 UsageTracked 接口的实现。 要以编程方式访问 bean,您可以编写以下代码:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
切面实例化模型

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

顾问

“顾问” 的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接的等价物。 顾问就像一个独立的小切面,只有一条建议。 通知本身由 bean 表示,并且必须实现 Spring 中的 Advice Types 中描述的通知接口之一。 顾问可以利用 AspectJ 切入点表达式。

Spring 通过 <aop:advisor> 元素支持顾问概念。 您最常看到它与事务通知一起使用,后者在 Spring 中也有自己的命名空间支持。 以下示例显示了一个顾问:

<aop:config>

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

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

</aop:config>

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

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

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

AOP 模式示例

本节展示了 AOP 示例中的并发锁定失败重试示例在使用模式支持重写后的样子。

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

因为我们要重试操作,所以我们需要使用 around 通知,这样我们就可以多次调用 proceed。 下面的清单显示了基本的切面实现(这是一个使用模式支持的常规 Java 类):

public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

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

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

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

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

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

请注意,切面实现了 Ordered 接口,因此我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新事务)。 maxRetries 和 order 属性都由 Spring 配置。 主要动作发生在 doConcurrentOperation around advice 方法中。 我们尝试继续。 如果我们因 PessimisticLockingFailureException 而失败,我们会再试一次,除非我们已经用尽了所有的重试尝试。

此类与 @AspectJ 示例中使用的类相同,但删除了注解。

对应的Spring配置如下:

<aop:config>

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

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

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

    </aop:aspect>

</aop:config>

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

请注意,目前我们假设所有业务服务都是幂等的。 如果不是这种情况,我们可以通过引入 Idempotent 注解并使用注解来注解服务操作的实现来改进切面,使其仅重试真正的幂等操作,如下例所示:

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

对仅重试幂等操作切面的更改涉及改进切入点表达式,以便仅匹配 @Idempotent 操作,如下所示:

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>
选择要使用的 AOP 声明风格

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

Spring AOP 还是 Full AspectJ?

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

当您使用 AspectJ 时,您可以选择 AspectJ 语言语法(也称为“代码样式”)或 @AspectJ 注解样式。 显然,如果您不使用 Java 5+,那么已经为您做出了选择:使用代码风格。 如果方面在您的设计中扮演重要角色,并且您能够使用 Eclipse 的 AspectJ 开发工具 (AJDT) 插件,那么 AspectJ 语言语法是首选选项。 它更简洁,因为该语言是专门为编写方面而设计的。 如果您不使用 Eclipse 或只有几个切面在您的应用程序中没有发挥主要作用,您可能需要考虑使用 @AspectJ 样式,在您的 IDE 中坚持常规 Java 编译,并添加一个切面编织阶段 你的构建脚本。

用于 Spring AOP 的 @AspectJ 或 XML?

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

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

XML 样式有两个缺点。 首先,它没有将它所解决的需求的实现完全封装在一个地方。 DRY 原则说,系统内的任何知识都应该有一个单一的、明确的、权威的表示。 使用 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 风格的自定义切面,而不是简单的企业服务配置。

混合切面类型

通过使用自动代理支持、模式定义的 <aop:aspect> 切面、<aop:advisor> 声明的顾问,甚至在同一配置中其他样式的代理和拦截器,完全可以混合 @AspectJ 风格切面。 所有这些都是通过使用相同的底层支持机制来实现的,并且可以毫无困难地共存。

代理机制

Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的目标对象创建代理。 JDK 动态代理内置在 JDK 中,而 CGLIB 是一个通用的开源类定义库(重新打包到 spring-core 中)。

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

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

  • 使用 CGLIB,不能建议 final (最终) 方法,因为它们不能在运行时生成的子类中被覆盖。
  • 从 Spring 4.0 开始,代理对象的构造函数不再被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。 仅当您的 JVM 不允许绕过构造函数时,您可能会看到来自 Spring 的 AOP 支持的双重调用和相应的调试日志条目。

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

<aop:config proxy-target-class="true">
    <!-- 此处定义的其他beans... -->
</aop:config>

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

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

多个 <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 代理 其中。

了解 AOP 代理

Spring AOP 是基于代理的。 在编写自己的切面或使用 Spring Framework 提供的任何基于 Spring AOP 的切面之前,掌握最后一条语句的实际含义是非常重要的。

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

public class SimplePojo implements Pojo {

    public void foo() {
        // 下一个方法调用是对 “this” 引用的直接调用
        this.bar();
    }

    public void bar() {
        // 一些逻辑...
    }
}

如果您在对象引用上调用方法,则直接在该对象引用上调用该方法,如下所示:

public class Main {

    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // 这是对“pojo”引用的直接方法调用
        pojo.foo();
    }
}

当客户端代码的引用是代理时,情况会发生轻微变化。 考虑下面的代码片段:

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // 这是对代理的方法调用!
        pojo.foo();
    }
}

这里要理解的关键是 Main 类的 main(…) 方法中的客户端代码有对代理的引用。 这意味着对该对象引用的方法调用是对代理的调用。 因此,代理可以委托给与该特定方法调用相关的所有拦截器(建议)。 然而,一旦调用最终到达目标对象(在本例中为 SimplePojo 引用),它可能对自身进行的任何方法调用,例如 this.bar() 或 this.foo(),都将被调用 这个参考,而不是代理。 这具有重要意义。 这意味着自调用不会导致与方法调用相关的通知有机会运行。

好的,那该怎么办呢? 最好的方法(术语“最好”在这里被松散地使用)是重构你的代码,这样自调用就不会发生。 这确实需要您做一些工作,但它是最好的、侵入性最小的方法。 下一种方法绝对可怕,我们不愿指出,正是因为它太可怕了。 您可以(对我们来说很痛苦)将您的类中的逻辑完全绑定到 Spring AOP,如以下示例所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // 这行得通,但是...啊!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // 一些逻辑...
    }
}

这完全将您的代码与 Spring AOP 耦合在一起,并且它使类本身意识到它是在 AOP 上下文中使用的,而 AOP 上下文与 AOP 相悖。 在创建代理时还需要一些额外的配置,如以下示例所示:

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();
        // 这是对代理的方法调用!
        pojo.foo();
    }
}

最后需要注意的是,AspectJ 不存在这个自调用问题,因为它不是基于代理的 AOP 框架。

@AspectJ 代理的程序化创建

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

您可以使用 org.springframework.aop.aspectj.annotation.AspectJProxyFactory 类为一个或多个 @AspectJ 切面通知的目标对象创建代理。 这个类的基本用法很简单,如下例所示:

// 创建一个可以为给定目标对象生成代理的工厂
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// 添加一个切面,该类必须是一个 @AspectJ 切面,您可以根据需要使用不同的切面多次调用它
factory.addAspect(SecurityManager.class);

// 您还可以添加现有切面实例,提供的对象类型必须是 @AspectJ 切面
factory.addAspect(usageTracker);

// 现在获取代理对象...
MyInterfaceType proxy = factory.getProxy();

有关更多信息,请参阅 javadoc

在 Spring 应用程序中使用 AspectJ

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

Spring 附带了一个小的 AspectJ 方面库,它在您的发行版中作为 spring-aspects.jar 独立提供。 您需要将其添加到您的类路径中才能使用其中的方面。使用 AspectJ 将域对象与 Spring 以及 AspectJ 的其他 Spring 特性进行依赖注入,讨论此库的内容以及如何使用它。

使用 Spring IoC 配置 AspectJ 方面讨论了如何依赖注入使用 AspectJ 编译器编织的 AspectJ 方面。 最后,在 Spring Framework 中使用 AspectJ 进行加载时编织介绍了使用 AspectJ 的 Spring 应用程序的加载时编织。

它包含以下内容:

  • 使用 AspectJ 通过 Spring 依赖注入域对象
    • 单元测试@Configurable 对象
    • 使用多个应用程序上下文
  • AspectJ 的其他 Spring 切面
  • 使用 Spring IoC 配置 AspectJ 切面
  • 在 Spring 框架中使用 AspectJ 进行加载时织入
    • 第一个例子
    • 切面
    • 所需的库 (JARS)
    • 特定环境的配置

详细篇幅-更多细节-感兴趣可点击这里,此处将不作介绍。一般情况下 Spring AOP 已经够用了,如果需要使用 AspectJ 可看这篇整合文章

更多资源

有关 AspectJ 的更多信息,请访问 AspectJ 网站

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值