书接上文
接下来到 Spring framework core 的第五大块 —— AOP
Spring 面向切面的编程
面向切面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。OOP 中模块化的关键单元是类,而在 AOP 中模块化是切面。切面使关注点(例如事务管理)的模块化可以跨越多种类型和对象。(这种关注在 AOP 文献中通常被称为“跨领域”关注。)
Spring 的关键组件之一是 AOP 框架。 尽管 Spring IoC 容器不依赖于 AOP(这意味着您不需要的话就不需要使用 AOP),但 AOP 是对 Spring IoC 的补充,以提供功能非常强大的中间件解决方案。
具有 AspectJ 切入点的 Spring AOP
Spring 提供了使用基于模式的方法或
@AspectJ
批注样式来编写自定义切面的简单而强大的方法。这两种样式都提供了完全类型化的 advice ,并使用了 AspectJ 切入点语言,同时仍使用 Spring AOP 进行编织。本章讨论基于架构和基于
@AspectJ
的 AOP 支持。 下一章将讨论较低级别的 AOP 支持。
AOP 在 Spring 框架中用于:
提供声明式企业服务。此类服务中最重要的是声明式事务管理。
让用户实现自定义切面,并用 AOP 补充其对 OOP 的使用。
如果您只对通用声明性服务或其他预包装的声明性中间件服务(例如池)感兴趣,则无需直接使用 Spring AOP,并且可以跳过本章的大部分内容。
AOP 概念
让我们首先定义一些主要的 AOP 概念和术语。这些术语不是特定于 Spring 的。不幸的是,AOP 术语并不是特别直观。但是,如果使用 Spring 自己的术语,将会更加令人困惑。
- Aspect 切面:涉及多个类别的关注点的模块化。事务管理是企业 Java 应用程序中横切关注的一个很好的例子。在 Spring AOP 中,切面是通过使用常规类(基于模式的方法)或使用@Aspect 注释(@AspectJ 样式)注释的常规类来实现的。
- join point 连接点:在程序执行过程中的一点,例如方法的执行或异常的处理。在 Spring AOP 中,连接点始终代表方法的执行。
- Advice:切面在特定的连接点处采取的操作。不同类型的 advice 包括“周围”,“之前”和“之后” advice 。(advice 类型将在后面讨论。)包括 Spring 在内的许多 AOP 框架都将 advice 建模为拦截器,并在连接点周围维护一系列拦截器。
- Pointcut 切入点:与连接点匹配的谓词。advice 与切入点表达式关联,并在与该切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。切入点表达式匹配的连接点的概念是 AOP 的核心,默认情况下,Spring 使用 AspectJ 切入点表达语言。
- Introduction 简介:代表类型声明其他方法或字段。 Spring AOP 允许您向任何 advice 对象引入新的接口(和相应的实现)。例如,您可以使用简介使 Bean 实现 IsModified 接口,以简化缓存。 (在 AspectJ 社区中,介绍被称为类型间声明。)
- Target object 目标对象:一个或多个切面 advice 的对象。也称为“ advice 对象”。由于 Spring AOP 是使用运行时代理实现的,因此该对象始终是代理对象。
- AOP 代理:由 AOP 框架创建的一个对象,用于实现切面合同( advice 方法执行等)。在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
- Weaving 编织:将切面与其他应用程序类型或对象链接以创建 advice 的对象。这可以在编译时(例如,使用 AspectJ 编译器),加载时或在运行时完成。像其他纯 Java AOP 框架一样,Spring AOP 在运行时执行编织。
Spring AOP 包括以下类型的 advice :
- Before advice:在连接点之前运行的 advice ,但是它不能阻止执行流程继续进行到连接点(除非它引发异常)。
- After returning advice:在连接点正常完成后要运行的 advice (例如,如果方法返回而没有引发异常)。
- After throwing advice:如果方法因抛出异常而退出,则执行 advice 。
- After (finally) advice:无论连接点退出的方式如何(正常或特殊返回),均应执行 advice 。
- Around advice:around 连接点的 advice ,例如方法调用。这是最有力的 advice 。周围 advice 可以在方法调用之前和之后执行自定义行为。它还负责选择是返回连接点还是通过返回其自身的返回值或引发异常来捷径 advice 的方法执行。
Around advice 是最通用的 advice 。由于 Spring AOP 与 AspectJ 一样,提供了各种 advice 类型,因此我们 advice 您使用功能最弱的 advice 类型,以实现所需的行为。例如,如果您只需要使用方法的返回值更新缓存,则最好使用 after returning advice 而不是 around advice ,尽管 around advice 可以完成相同的事情。使用最具体的 advice 类型可提供更简单的编程模型,并减少出错的可能性。例如,您不需要在用于 around advice 的 JoinPoint
上调用 proceed()
方法,因此,您不会失败。
所有 advice 参数都是静态类型的,因此您可以使用适当类型(例如,从方法执行返回的值的类型)而不是 Object
数组的 advice 参数。
切入点匹配的连接点的概念是 AOP 的关键,它与仅提供拦截功能的旧技术有所不同。切入点使 advice 的目标独立于面向对象的层次结构。例如,您可以将提供声明性事务管理的 around advice 应用于跨越多个对象(例如服务层中的所有业务操作)的一组方法。
Spring AOP 能力和目标
Spring AOP 是用纯 Java 实现的。不需要特殊的编译过程。 Spring AOP 不需要控制类加载器的层次结构,因此适合在 Servlet 容器或应用程序服务器中使用。
Spring AOP 当前仅支持方法执行连接点( advice 在 Spring Bean 执行方法上)。尽管可以在不破坏核心 Spring AOP API 的情况下添加对字段拦截的支持,但并未实现字段拦截。如果需要 advice 字段访问和更新连接点,请考虑使用诸如 AspectJ 之类的语言。
Spring AOP 的 AOP 方法不同于大多数其他 AOP 框架。目的不是提供最完整的 AOP 实现(尽管 Spring AOP 相当强大)。相反,其目的是在 AOP 实现和 Spring IoC 之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。
因此,例如,通常将 Spring Framework 的 AOP 功能与 Spring IoC 容器结合使用。通过使用常规 bean 定义语法来配置切面(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现的关键区别。使用 Spring AOP 不能轻松或高效地完成某些事情,例如 advice 非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ 是最佳选择。但是,我们的经验是,Spring AOP 为 AOP 可以解决的企业 Java 应用程序中的大多数问题提供了出色的解决方案。
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 团队比 Spring XML 配置风格更喜欢 @AspectJ 注释风格的方法。
有关每种样式的“whys and wherefores”的更完整讨论,请参见[选择要使用的 AOP 声明样式](https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/core.html#aop-choosing]。
AOP 代理
Spring AOP 默认将标准 JDK 动态代理用于 AOP 代理。这使得可以代理任何接口(或一组接口)。
Spring AOP 也可以使用 CGLIB 代理。 这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在那些需要 advice 在接口上未声明的方法或需要将代理对象作为具体类型传递给方法的情况下(在极少数情况下),可以强制使用 CGLIB。
掌握 Spring AOP 是基于代理的这一事实很重要。 请参阅了解 AOP 代理以全面了解此实现细节的实际含义。
@AspectJ 支持
@AspectJ 是一种将切面声明为带有注释的常规 Java 类的样式。@AspectJ 样式是 AspectJ 项目在 AspectJ 5 版本中引入的。 Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ 5 相同的注释。 但是,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。
使用 AspectJ 编译器和 weaver 可以使用完整的 AspectJ 语言,有关内容在 在 Spring Applications 中使用 AspectJ 进行了讨论。
启用 @AspectJ 支持
要在 Spring 配置中使用 @AspectJ 切面,您需要启用 Spring 支持以基于 @AspectJ
切面配置 Spring AOP,并基于这些切面是否 advice 对 Bean 进行自动代理。 通过自动代理,我们的意思是,如果 Spring 确定一个或多个切面 advice 一个 bean,它会自动为该 bean 生成一个代理来拦截方法调用并确保按需执行 advice 。
可以使用 XML 或 Java 样式的配置来启用 @AspectJ 支持。 无论哪种情况,您都需要确保 AspectJ 的 Aspectjweaver.jar
库位于应用程序的类路径(版本 1.8 或更高版本)上。 该库在 AspectJ 发行版的 lib 目录中或从 Maven Central 存储库中可用。
通过 Java 配置启用 @AspectJ 支持
要通过 Java @Configuration
启用 @AspectJ
支持,请添加 @EnableAspectJAutoProxy
批注,如以下示例所示:
1 |
|
通过 XML 配置启用 @AspectJ 支持
要通过基于 XML 的配置启用 @AspectJ 支持,请使用 aop:aspectj-autoproxy
元素,如以下示例所示:
1 | <aop:aspectj-autoproxy/> |
假定您使用基于 XML Schema 的配置中所述的架构支持。 有关如何在 aop 名称空间中导入标签的信息,请参见 AOP 模式。
声明一个切面
启用 @AspectJ 支持后,Spring 会自动检测在应用程序上下文中使用 @AspectJ 切面(具有 @Aspect 批注)的类定义的任何 bean,并用于配置 Spring AOP。 接下来的两个示例显示了一个不太有用的切面所需的最小定义。
两个示例中的第一个示例显示了应用程序上下文中的常规 bean 定义,该定义指向具有 @Aspect 批注的 bean 类:
1 | <bean id="myAspect" class="org.xyz.NotVeryUsefulAspect"> |
这两个示例中的第二个示例显示了 NotVeryUsefulAspect
类定义,该类定义使用 org.aspectj.lang.annotation.Aspect
注释进行注释;
1 | package org.xyz; |
切面(使用 @Aspect 注释的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点, advice 和介绍(类型间)声明。
通过组件扫描自动检测切面
您可以将切面类注册为 Spring XML 配置中的常规 bean,也可以通过类路径扫描自动检测它们——与其他任何 Spring 管理的 bean 一样。 但是,请注意,@Aspect 注释不足以在类路径中进行自动检测。为此,您需要添加一个单独的 @Component 批注(或者,或者,按照 Spring 的组件扫描程序的规则,有条件的自定义构造型批注)。
向其他切面提供 advice ?
在 Spring AOP 中,切面本身不能成为其他切面的 advice 目标。 类上的 @Aspect 注释将其标记为一个切面,因此将其从自动代理中排除。
声明切入点
切入点确定了感兴趣的连接点,从而使我们能够控制何时执行 advice 。 Spring AOP 仅支持 Spring Bean 的方法执行连接点,因此您可以将切入点视为与 Spring Bean 上的方法执行匹配。 切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该切入点表达式精确确定我们感兴趣的方法执行。在 AOP 的@AspectJ 批注样式中,常规方法定义提供了切入点签名。 ,并使用 @Pointcut
批注指示切入点表达式(用作切入点签名的方法必须具有 void
返回类型)。
一个示例可能有助于使切入点签名和切入点表达式之间的区别变得清晰。 下面的示例定义一个名为 anyOldTransfer
的切入点,该切入点与任何名为 transfer
的方法的执行相匹配:
1 | "execution(* transfer(..))") // the pointcut expression ( |
形成 @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 本身具有基于类型的语义,并且在执行连接点处,此对象和目标都引用同一个对象:执行该方法的对象。 Spring AOP 是基于代理的系统,可区分代理对象本身(绑定到此对象)和代理后面的目标对象(绑定到目标)。
由于 Spring 的 AOP 框架基于代理的性质,因此根据定义,不会拦截目标对象内的调用。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,将拦截代理上的公共方法和受保护的方法调用(必要时甚至包可见的方法)。但是,通常应通过公共签名设计通过代理进行的常见交互。
请注意,切入点定义通常与任何拦截方法匹配。如果严格地将切入点设置为仅公开使用,即使在 CGLIB 代理方案中通过代理可能存在非公开交互,也需要相应地进行定义。
如果您的拦截需要在目标类中包括方法调用甚至构造函数,请考虑使用 Spring 驱动的本机 AspectJ 编织,而不是 Spring 的基于代理的 AOP 框架。这构成了具有不同特征的 AOP 使用模式,因此请确保在做出决定之前先熟悉编织。
Spring AOP 还支持其他名为 bean
的 PCD。使用 PCD,可以将连接点的匹配限制为特定的命名 Spring Bean 或一组命名 Spring Bean(使用通配符时)。bean
PCD 具有以下形式:
1 | bean(idOrNameOfBean) |
idOrNameOfBean
令牌可以是任何 Spring bean 的名称。提供了使用 *
字符的有限通配符支持,因此,如果为 Spring bean 建立了一些命名约定,则可以编写 bean PCD 表达式来选择它们。与其他切入点指示符一样,bean PCD 可以和与或非运算符一起使用。
bean
PCD 仅在 Spring AOP 中受支持,而在本机 AspectJ 编织中不受支持。它是 AspectJ 定义的标准 PCD 的特定于 Spring 的扩展,因此不适用于@Aspect
模型中声明的切面。
bean
PCD 在实例级别(基于 Spring bean 名称概念构建)上运行,而不是仅在类型级别(基于编织的 AOP 受其限制)上运行。基于实例的切入点指示符是 Spring 基于代理的 AOP 框架及其与 Spring bean 工厂的紧密集成的一种特殊功能,可以自然而直接地识别特定对象。
组合切入点表达式
您可以使用&&
、||
、!
组合切入点表达式,您也可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
1 | "execution(public * *(..))") ( |
如果方法执行连接点表示任何公共方法的执行,则 anyPublicOperation 匹配。
如果交易模块中有方法执行,则 inTrading 匹配。
如果方法执行代表交易模块中的任何公共方法,则 tradingOperation 匹配。
最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如先前所示。按名称引用切入点时,将应用常规的 Java 可见性规则(您可以看到相同类型的私有切入点,层次结构中受保护的切入点,任何位置的公共切入点,等等)。可见性不影响切入点匹配。
共享通用切入点定义
在使用企业应用程序时,开发人员通常希望从多个切面引用应用程序的模块和特定的操作集。我们推荐为此定义一个 “SystemArchitecture” 切面,以捕获常见的切入点表达式。这样的切面通常类似于以下示例:
1 | package com.xyz.someapp; |
您可以在需要切入点表达式的任何地方引用在此切面定义的切入点。 例如,要使服务层具有事务性,您可以编写以下内容:
1 | <aop:config> |
在基于模式的 AOP 支持中讨论了 <aop:config>
和 <aop:advisor>
元素。事务管理中讨论了事务元素。
例子
Spring AOP 用户可能最常使用执行切入点指示符。执行表达式的格式如下:
1 | execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) |
除了返回类型模式(前面的代码片段中的 ret-type-pattern
),名称模式和参数模式以外的所有部分都是可选的。返回类型模式确定该方法的返回类型必须是什么才能使连接点匹配。 *
最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,标准类型名称才匹配。名称模式与方法名称匹配。您可以将 *
通配符用作名称模式的全部或一部分。如果指定了声明类型模式,请在其后加上 .
将其连接到名称模式组件。参数模式稍微复杂一些:()
匹配不带参数的方法,而 (..)
匹配任意数量(零个或多个)的参数。 (*)
模式与采用任何类型的一个参数的方法匹配。(*,String)
与采用两个参数的方法匹配。第一个可以是任何类型,而第二个必须是 String
。有关更多信息,请查阅 AspectJ 编程指南的语言语义部分。
以下示例显示了一些常用的切入点表达式:
- 任何公共方法的执行:
1 | execution(public * *(..)) |
- 名称以 set 开头的任何方法的执行:
1 | execution(* set*(..)) |
- AccountService 接口定义的任何方法的执行:
1 | execution(* com.xyz.service.AccountService.*(..)) |
- 服务包中定义的任何方法的执行:
1 | execution(* com.xyz.service.*.*(..)) |
- 服务包或其子包之一中定义的任何方法的执行:
1 | execution(* com.xyz.service..*.*(..)) |
- 服务包中的任何连接点(仅在 Spring AOP 中执行方法):
1 | within(com.xyz.service.*) |
- 服务包或其子包之一中的任何连接点(仅在 Spring AOP 中执行方法):
1 | within(com.xyz.service..*) |
- 代理实现
AccountService
接口的任何连接点(仅在 Spring AOP 中执行方法):
1 | this(com.xyz.service.AccountService) |
this
通常以绑定形式使用。 有关如何在 advice 正文中使代理对象可用的信息,请参阅声明 advice部分。
- 目标对象实现 AccountService 接口的任何连接点(仅在 Spring AOP 中执行方法):
1 | target(com.xyz.service.AccountService) |
target
通常以绑定形式使用。有关如何使目标对象在 advice 正文中可用的信息,请参见声明 advice部分。
- 任何采用单个参数并且在运行时传递的参数为
Serializable
的连接点(仅在 Spring AOP 中执行方法):
1 | args(java.io.Serializable) |
args
通常以绑定形式使用。 有关如何使方法参数在 advice 正文中可用的信息,请参见声明 advice部分。
请注意,此示例中给出的切入点与 execution(* *(java.io.Serializable))
不同。如果在运行时传递的参数为 Serializable
,则 args
版本匹配;如果方法签名声明单个类型为 Serializable
的参数,则 execution
版本匹配。
- 目标对象具有
@Transactional
批注的任何连接点(仅在 Spring AOP 中是方法执行):
1 | @target(org.springframework.transaction.annotation.Transactional) |
您也可以在绑定形式中使用
@target
。有关如何使注释对象在 advice 正文中可用的信息,请参见声明 advice部分。
- 目标对象的声明类型具有
@Transactional
批注的任何连接点(仅在 Spring AOP 中是方法执行)
1 | @within(org.springframework.transaction.annotation.Transactional) |
您也可以在绑定形式中使用
@within
。有关如何使注释对象在 advice 正文中可用的信息,请参见声明 advice部分。
- 任何执行方法带有
@Transactional
批注的联接点(仅在 Spring AOP 中是方法执行):
1 | @annotation(org.springframework.transaction.annotation.Transactional) |
您也可以在绑定形式中使用
@annotation
。有关如何使注释对象在 advice 正文中可用的信息,请参见声明 advice部分。
- 任何采用单个参数的联接点(仅在 Spring AOP 中是方法执行),并且传递的参数的运行时类型具有@Classified 批注:
1 | @args(com.xyz.security.Classified) |
您也可以在绑定形式中使用“ @args”。 请参阅“声明 advice”部分,如何使 advice 对象中的注释对象可用。
- 名为
tradeService
的 Spring bean 上的任何连接点(仅在 Spring AOP 中执行方法):
1 | bean(tradeService) |
Spring Bean 上具有与通配符表达式 *Service
匹配的名称的任何连接点(仅在 Spring AOP 中才执行方法):
1 | bean(*Service) |
写好切入点
在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个昂贵的过程。 (动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中进行了测试以确定在运行代码时是否存在实际匹配)。首次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点以 DNF(析取范式)重写,并且对切入点的组件进行排序,以便首先检查那些较便宜的组件。这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。
但是,AspectJ 只能使用所告诉的内容。为了获得最佳的匹配性能,您应该考虑他们试图达到的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三类之一:同类,作用域和上下文:
友好的指示者选择一种特殊的连接点:execution
, get
, set
, call
, 和 handler
作用域指定者选择一组感兴趣的连接点(可能是多种):within
和 withincode
上下文指示符根据上下文匹配(并可选地绑定):this
, target
, 和 @annotation
编写正确的切入点至少应包括前两种类型(种类和作用域)。您可以包括上下文指示符以根据连接点上下文进行匹配,也可以绑定该上下文以在 advice 中使用。仅提供同类的标识符或仅提供上下文的标识符是可行的,但是由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指定符的匹配非常快,使用它们的使用意味着 AspectJ 可以非常迅速地消除不应进一步处理的连接点组。一个好的切入点应尽可能包括一个切入点。
声明 advice
advice 与切入点表达式关联,并且在切入点匹配的方法执行之前,之后或周围运行。 切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。
Before advice
您可以使用 @Before
批注在一个切面中声明先 advice :
1 | import org.aspectj.lang.annotation.Aspect; |
如果使用就地切入点表达式,则可以将前面的示例重写为以下示例:
1 | import org.aspectj.lang.annotation.Aspect; |
After Returning advice
当匹配的方法执行正常返回时,After returning advice 运行。 您可以使用 @AfterReturning
批注进行声明:
1 | import org.aspectj.lang.annotation.Aspect; |
您可以在同一切面内拥有多个 advice 声明(以及其他成员)。在这些示例中,我们仅显示单个 advice 声明,以集中每个 advice 的效果。
有时,您需要在 advice 正文中访问返回的实际值。 您可以使用 @AfterReturning
的形式绑定返回值以获取该访问,如以下示例所示:
1 | import org.aspectj.lang.annotation.Aspect; |
returning
属性中使用的名称必须与 advice 方法中的参数名称相对应。当方法执行返回时,返回值将作为相应的参数值传递到 advice 方法。返回子句也将匹配限制为仅返回指定类型值的方法执行(在这种情况下为 Object
,它匹配任何返回值)。
请注意,使用 after returning advice 时,不可能返回完全不同的引用。
After Throwing Advice
当匹配的方法执行通过抛出异常退出时 after throwing advice 运行。 您可以使用 @AfterThrowing
批注进行声明,如以下示例所示:
1 | import org.aspectj.lang.annotation.Aspect; |
通常,您希望 advice 仅在引发给定类型的异常时才运行,并且您通常还需要访问 advice 正文中的引发异常。 您可以使用 throwing
属性来限制匹配(如果需要)(否则,请使用 Throwable
作为异常类型),并将抛出的异常绑定到 advice 参数。 以下示例显示了如何执行此操作:
1 | import org.aspectj.lang.annotation.Aspect; |
throwing
属性中使用的名称必须与 advice 方法中的参数名称相对应。 当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给 advice 方法。 throwing 子句还将匹配仅限制为抛出指定类型的异常(在这种情况下为 DataAccessException
)的方法执行。
After (Finally) Advice
当匹配的方法执行退出时,After (Finally) Advice 运行。 通过使用 @After
注释声明它。 之后必须准备处理正常和异常返回条件的 advice。它通常用于释放资源和类似目的。 以下示例显示了最终 advice 后的用法:
1 | import org.aspectj.lang.annotation.Aspect; |
Around Advice
最后一种 advice 是 around advice。around advice 在匹配方法的执行过程中“around”运行。它有机会在方法执行之前和之后进行工作,并确定何时,如何以及什至根本不执行该方法。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用绕行 advice。始终使用最不可行的 advice 形式来满足您的要求(也就是说,在 advice 可以使用之前,请勿在 advice 周围使用)。
通过使用 @Around
批注来声明周围 advice。咨询方法的第一个参数必须是 ProceedingJoinPoint
类型。在 advice 的正文中,在 ProceedingJoinPoint
上调用 proceed()
会使基础方法执行。proceed
方法也可以传入 Object[]
。数组中的值用作方法执行时的参数。
当用
Object[]
调用proceed
时, 行为与 AspectJ 编译器所编译的 around advice 的行为略有不同。对于使用传统 AspectJ 语言编写的 around advice,传递给proceed
的参数数量必须与传递给 around advice 的参数数量(而不是基础连接点采用的参数数量)相匹配,并且传递给给定的参数位置会取代该值绑定到的实体的连接点处的原始值(不要担心,如果这现在没有意义)。 Spring 采取的方法更简单,并且更适合其基于代理的,仅执行的语义。如果您编译为 Spring 编写的@AspectJ 切面,并在 AspectJ 编译器和 weaver 中使用参数进行处理,则只需要意识到这种区别。有一种方法可以在 Spring AOP 和 AspectJ 之间 100%兼容,并且在下面有关 advice 参数的部分中对此进行了讨论。
以下示例显示了如何使用周围 advice:
1 | import org.aspectj.lang.annotation.Aspect; |
around advice 返回的值是该方法的调用者看到的返回值。例如,如果一个简单的缓存切面有一个值,则它可以从缓存中返回一个值,如果没有,则调用 proceed()
。请注意,在 around advice 的正文中,proceed
可能被调用一次,多次或完全不被调用。所有这些都是合法的。
Advice 参数
Spring 提供了完全类型化的 advice,这意味着您可以在 advice 签名中声明所需的参数(如我们先前在返回和抛出示例中所看到的),而不是一直使用 Object[]
数组。 我们将在本节的后面部分介绍如何使参数和其他上下文值可用于 advice 主体。首先,我们看一下如何编写通用 advice,以了解该 advice 当前 advice 的方法。
访问当前的 JoinPoint
任何 advice 方法都可以将 org.aspectj.lang.JoinPoint
类型的参数声明为它的第一个参数(请注意,需要 around advice 以声明 ProceedingJoinPoint
类型的第一个参数,该类型是 JoinPoint 的子类。JoinPoint 接口提供了一个 几种有用的方法:
getArgs()
:返回方法参数。
getThis()
:返回代理对象。
getTarget()
:返回目标对象。
getSignature()
:返回所 advice 方法的描述。
toString()
:打印有关所 advice 方法的有用描述。
有关更多详细信息,请参见 javadoc。
将参数传递给 advice
我们已经看到了如何绑定返回的值或异常值(在返回之后和引发 advice 之后使用)。要使参数值可用于 advice 正文,可以使用 args
的绑定形式。如果在 args
表达式中使用参数名称代替类型名称,则在调用 advice 时会将相应参数的值作为参数值传递。一个例子应该使这一点更清楚。假设您要 advice 以 Account
对象作为第一个参数的 DAO 操作的执行,并且您需要在 advice 正文中访问该帐户。您可以编写以下内容:
1 | "com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)") ( |
切入点表达式的 args(account,..)
部分有两个用途。首先,它将匹配限制为仅方法采用至少一个参数且传递给该参数的参数为 Account
实例的那些方法执行。其次,它通过 account
参数使 advice 的实际 Account
对象可用。
编写此代码的另一种方法是声明一个切入点,当切入点与匹配点匹配时“提供” Account
对象值,然后从通知中引用命名切入点。如下所示:
1 | "com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)") ( |
有关更多详细信息,请参见 AspectJ 编程指南。
代理对象(this
),目标对象(target
)和注释(@within
,@target
,@annotation
和 @args
)都可以以类似的方式绑定。接下来的两个示例显示如何匹配使用 @Auditable
注释注释的方法的执行并提取审计代码:
这两个示例中的第一个显示了 @Auditable
批注的定义:
1 | (RetentionPolicy.RUNTIME) |
这两个示例中的第二个示例显示了与 @Auditable
方法的执行相匹配的 advice:
1 | "com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") ( |
advice 参数和泛型
Spring AOP 可以处理类声明和方法参数中使用的泛型。假设您具有如下通用类型:
1 | public interface Sample<T> { |
您可以通过在要拦截方法的参数类型中键入 advice 参数,将方法类型的拦截限制为某些参数类型:
1 | "execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") ( |
这种方法不适用于通用集合。因此,您不能按以下方式定义切入点:
1 | "execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") ( |
为了使这项工作有效,我们将不得不检查集合的每个元素,这是不合理的,因为我们也无法决定通常如何处理空值。要实现类似的目的,您必须定义参数 Collection<?>
并手动检查元素的类型。
确定参数名称
通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。通过 Java 反射无法获得参数名称,因此 Spring AOP 使用以下策略来确定参数名称:
- 如果用户已明确指定参数名称,则使用指定的参数名称。advice 和切入点注释均具有可选的
argNames
属性,您可以使用该属性来指定带注释的方法的参数名称。这些参数名称在运行时可用。以下示例显示如何使用argNames
属性:
1 | "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", (value= |
如果第一个参数是 JoinPoint
,ProceedingJoinPoint
或 JoinPoint.StaticPart
类型,则可以从 argNames
属性的值中忽略该参数的名称。例如,如果您修改前面的 advice 以接收连接点对象,则 argNames
属性不需要包括它:
1 | "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", (value= |
对 JoinPoint
,ProceedingJoinPoint
和 JoinPoint.StaticPart
类型的第一个参数给予的特殊处理对于不收集任何其他联接点上下文的 advice 实例特别方便。 在这种情况下,您可以省略 argNames
属性。 例如,以下 advice 无需声明 argNames
属性:
1 | "com.xyz.lib.Pointcuts.anyPublicMethod()") ( |
- 使用
'argNames'
属性有点笨拙,因此,如果未指定'argNames'
属性,Spring AOP 将查看该类的调试信息,并尝试从局部变量表中确定参数名称。只要已使用调试信息(至少是'-g:vars'
)编译了类,此信息就会存在。启用此标志时进行编译的结果是:(1)您的代码更易于理解(反向工程),(2)类文件的大小略大(通常无关紧要),(3)删除未使用的本地代码的优化变量不适用于您的编译器。换句话说,通过启用该标志,您应该不会遇到任何困难。
如果即使没有调试信息,AspectJ 编译器(ajc)都已编译 @AspectJ 切面,则无需添加
argNames
属性,因为编译器会保留所需的信息。
如果在没有必要调试信息的情况下编译了代码,Spring AOP 将尝试推断绑定变量与参数的配对(例如,如果切入点表达式中仅绑定了一个变量,并且 advice 方法仅接受一个参数,则配对很明显)。如果在给定可用信息的情况下变量的绑定不明确,则抛出
AmbiguousBindingException
。如果以上所有策略均失败,则抛出
IllegalArgumentException
。
带参数执行
前面我们提到过,我们将描述如何编写一个在 Spring AOP 和 AspectJ 中始终有效的参数的 proceed
调用。 解决方案是确保 advice 签名按顺序绑定每个方法参数。 以下示例显示了如何执行此操作:
1 | "execution(List<Account> find*(..)) && " + ( |
在许多情况下,无论如何都要进行此绑定(如上例所示)。
advice 顺序
当多条 advice 都希望在同一连接点上运行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定 advice 执行的顺序。优先级最高的 advice 首先“在途中”运行(因此,给定两条优先 advice,则优先级最高的 advice 首先运行)。从连接点“出路”时,优先级最高的 advice 将最后运行(因此,给定两条后置通知,优先级最高的 advice 将第二次运行)。
当在不同切面定义的两条 advice 都需要在同一连接点上运行时,除非另行指定,否则执行顺序是不确定的。您可以通过指定优先级来控制执行顺序。通过在切面类中实现 org.springframework.core.Ordered
接口或使用 Order 批注对其进行注释,可以通过常规的 Spring 方法来完成。给定两个切面,从 Ordered.getValue()
(或注释值)返回较低值的切面具有较高的优先级。
当在相同切面定义的两条 advice 都需要在同一连接点上运行时,其顺序是未定义的(因为无法通过反射为 javac 编译的类检索声明顺序)。考虑将这些 advice 方法折叠为每个切面类中每个连接点的一个 advice 方法,或将 advice 重构为单独的切面类,您可以在切面级别进行订购。
简介 Introductions
简介(在 AspectJ 中称为类型间声明)使切面可以声明 advice 对象实现给定的接口,并代表那些对象提供该接口的实现。
您可以使用 @DeclareParents 批注进行介绍。 此批注用于声明匹配类型具有新的父代(因此具有名称)。 例如,给定一个名为 UsageTracked 的接口和该接口名为 DefaultUsageTracked 的实现,以下切面声明服务接口的所有实现者也都实现了 UsageTracked 接口(例如,通过 JMX 公开统计信息):
1 |
|
要实现的接口由带注释的字段的类型确定。 @DeclareParents 批注的 value 属性是 AspectJ 类型的模式。 匹配类型的任何 bean 都实现 UsageTracked 接口。 请注意,在前面示例的之前 advice 中,服务 Bean 可以直接用作 UsageTracked 接口的实现。 如果以编程方式访问 bean,则应编写以下内容:
1 | UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); |
切面实例化模型
这是一个高级主题。如果您刚开始使用 AOP,则可以放心地跳过它,直到以后。
默认情况下,应用程序上下文中每个切面都有一个实例。 AspectJ 将此称为单例实例化模型。 可以使用备用生命周期来定义切面。 Spring 支持 AspectJ 的 perthis
和 pertarget
实例化模型(当前不支持 percflow
,percflowbelow
和 pertypewithin
)。
您可以通过在 @Aspect
批注中指定 perthis
子句来声明 perthis
切面。 考虑以下示例:
1 | "perthis(com.xyz.myapp.SystemArchitecture.businessService())") ( |
在前面的示例中,'perthis'
子句的作用是为每个执行业务服务的唯一服务对象(每个与切入点表达式匹配的联接点绑定到 “this” 的唯一对象)创建一个切面实例。切面实例是在服务对象上首次调用方法时创建的。 当服务对象超出范围时,切面将超出范围。在创建切面实例之前,其中的任何 advice 都不会执行。创建切面实例后,在其中声明的 advice 将在匹配的连接点处执行,但是仅当服务对象是与此切面相关联的对象时才执行。有关 per
子句的更多信息,请参见 AspectJ 编程指南。
pertarget
实例化模型的工作方式与 perthis
完全相同,但是它在匹配的连接点为每个唯一目标对象创建一个切面实例。
AOP 示例
既然您已经了解了所有组成部分是如何工作的,那么我们可以将它们放在一起做一些有用的事情。
有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException
。这项要求明确地跨越了服务层中的多个服务,因此非常适合通过一个切面实施。
因为我们想重试该操作,所以我们需要使用周围 advice,以便可以多次调用 proceed。 以下清单显示了基本切面的实现:
1 |
|
请注意,切面实现了 Ordered
接口,因此我们可以将切面的优先级设置为高于事务 advice(每次重试时都希望有新的事务)。 maxRetries 和 order 属性均由 Spring 配置。 advice 的主要动作发生在 doConcurrentOperation
中。 请注意,目前,我们将重试逻辑应用于每个 businessService()
。 我们尝试继续,如果失败并出现 PessimisticLockingFailureException
,则我们将再次尝试,除非我们用尽了所有重试尝试。
相应的 Spring 配置如下:
1 | <aop:aspectj-autoproxy/> |
为了完善切面,使其仅重试幂等运算,我们可以定义以下幂等注解:
1 | (RetentionPolicy.RUNTIME) |
然后,我们可以使用注释来注释服务操作的实现。切面更改为仅重试幂等操作涉及更改切入点表达式,以便仅 @Idempotent
操作匹配,如下所示:
1 | "com.xyz.myapp.SystemArchitecture.businessService() && " + ( |
基于 Schema 的 AOP 支持
如果您更喜欢基于 XML 的格式,Spring 还提供了使用新的 aop 名称空间标签定义切面的支持。支持与使用 @AspectJ
样式时完全相同的切入点表达式和 advice 类型。因此,在本节中,我们将重点放在新语法上,并使读者参考上一节中的讨论(@AspectJ
支持),以了解编写切入点表达式和 advice 参数的绑定。
要使用本节中描述的 aop 名称空间标签,您需要导入 spring-aop 模式,如基于 XML Schema 的配置中所述。有关如何在 aop 名称空间中导入标签的信息,请参见 AOP 模式。
在您的 Spring 配置中,所有切面和顾问程序元素都必须放在 <aop:config>
元素内(在应用程序上下文配置中可以有多个 <aop:config>
元素)。<aop:config>
元素可以包含切入点,顾问程序和 aspect 元素(请注意,必须按此顺序声明它们)。
<aop:config>
的配置样式大量使用了 Spring 的自动代理机制。如果您已经通过使用BeanNameAutoProxyCreator
或类似方法使用显式自动代理,则可能会导致问题(例如,未编制 advice)。推荐的用法模式是仅使用<aop:config>
样式或仅使用AutoProxyCreator
样式,并且不要混合使用。
因为我不喜欢 XML 格式,此段暂时跳过,原文参考 https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/core.html#aop-schema
选择要使用的 AOP 声明样式
一旦确定切面是实现给定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ 以及在 Aspect 语言(代码)样式,@ AspectJ 批注样式或 Spring XML 样式之间做出选择?这些决定受许多因素影响,包括应用程序需求,开发工具和团队对 AOP 的熟悉程度。
Spring AOP 还是 Full AspectJ?
使用最简单的方法即可。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/编织器。如果您只需要 advice 在 Spring bean 上执行操作,则 Spring AOP 是正确的选择。如果您需要 advice 不受 Spring 容器管理的对象(通常是域对象),则需要使用 AspectJ。如果您希望 advice 除简单方法执行之外的连接点(例如,字段 get 或设置连接点等),则还需要使用 AspectJ。
使用 AspectJ 时,可以选择 AspectJ 语言语法(也称为“代码样式”)或@AspectJ 注释样式。显然,如果您不使用 Java 5+,则已经为您做出了选择:使用代码样式。如果切面在您的设计中起着重要作用,并且您能够使用用于 Eclipse 的 AspectJ 开发工具(AJDT)插件,则 AspectJ 语言语法是首选。它更干净,更简单,因为该语言是专为编写切面而设计的。如果您不使用 Eclipse 或只有少数几个切面在您的应用程序中不起作用,那么您可能要考虑使用@AspectJ 样式,在 IDE 中坚持常规 Java 编译,并向其中添加切面编织阶段您的构建脚本。
@AspectJ 或 Spring AOP 的 XML?
如果您选择使用 Spring AOP,则可以选择 @AspectJ 或 XML 样式。有各种折衷考虑。
XML 样式可能是现有 Spring 用户最熟悉的,并且得到了真正的 POJO 的支持。当使用 AOP 作为配置企业服务的工具时,XML 是一个不错的选择(一个很好的测试是您是否将切入点表达式视为您可能希望独立更改的配置的一部分)。使用 XML 样式,可以说从您的配置中可以更清楚地了解系统中存在哪些切面。
XML 样式有两个缺点。首先,它没有完全将要解决的需求的实现封装在一个地方。 DRY 原则说,系统中的任何知识都应该有一个单一,明确,权威的表示形式。使用 XML 样式时,关于如何实现需求的知识会在配置文件中的后备 bean 类的声明和 XML 中分散。当您使用@AspectJ 样式时,此信息将封装在一个模块中:切面。其次,与@AspectJ 样式相比,XML 样式在表达能力上有更多限制:仅支持“单例”切面实例化模型,并且无法组合以 XML 声明的命名切入点。例如,使用@AspectJ 样式,您可以编写如下内容:
1 | "execution(* get*())") ( |
在 XML 样式中,您可以声明前两个切入点:
1 | <aop:pointcut id="propertyAccess" |
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,不能 advice final 方法,因为不能在运行时生成的子类中覆盖它们。
- 从 Spring 4.0 开始,由于 CGLIB 代理实例是通过 Objenesis 创建的,因此不再调用代理对象的构造函数两次。只有在您的 JVM 不允许绕过构造函数的情况下,您才可能从 Spring 的 AOP 支持中看到两次调用和相应的调试日志条目。
要强制使用 CGLIB 代理,请将 <aop:config>
元素的 proxy-target-class
属性的值设置为 true,如下所示:
1 | <aop:config proxy-target-class="true"> |
要在使用 @AspectJ
自动代理支持时强制 CGLIB 代理,请将 <aop:aspectj-autoproxy>
元素的 proxy-target-class
属性设置为 true,如下所示:
1 | <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 的切面之前,掌握最后一条语句实际含义的语义至关重要。
首先考虑以下情况:您有一个普通的,未经代理的,无特殊要求的直接对象引用,如以下代码片段所示:
1 | public class SimplePojo implements Pojo { |
如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图所示:
1 | public class Main { |
当客户端代码具有的引用是代理时,情况会稍有变化。考虑以下图表和代码片段:
1 | public class Main { |
此处要理解的关键是,Main 类的 main(..)
方法内部的客户端代码具有对代理的引用。 这意味着该对象引用上的方法调用是代理上的调用。结果,代理可以委派给与该特定方法调用相关的所有拦截器(advice)。 但是,一旦调用最终到达目标对象(在此示例中为 SimplePojo
,则为引用),它可能对其自身进行的任何方法调用(例如 this.bar()
或 this.foo()
)都是针对 this 引用的调用,而不是代理。这具有重要意义。这意味着自调用不会导致与方法调用相关的 advice 得到执行的机会。
好吧,那么该怎么办? 最佳方法(在这里宽松地使用术语“最佳”)是重构代码,以免发生自调用。这确实需要您做一些工作,但这是最好的,侵入性最小的方法。下一种方法绝对可怕,我们正要指出这一点,恰恰是因为它是如此可怕。您可以(对我们来说是痛苦的)完全将类中的逻辑与 Spring AOP 绑定在一起,如以下示例所示:
1 | public class SimplePojo implements Pojo { |
这将您的代码完全耦合到 Spring AOP,并且使类本身意识到在 AOP 上下文中使用它的事实,而 AOP 上下文却是这样。创建代理时,还需要一些其他配置,如以下示例所示:
1 | public class Main { |
最后,必须注意,AspectJ 没有此自调用问题,因为它不是基于代理的 AOP 框架。
以编程方式创建 @AspectJ 代理
除了通过使用 <aop:config>
或 <aop:aspectj-autoproxy>
声明配置中的各个切面外,还可以通过编程方式创建建议目标对象的代理。有关 Spring 的 AOP API 的完整详细信息,请参阅下一章。在这里,我们要重点介绍使用 @AspectJ 切面自动创建代理的功能。
您可以使用 org.springframework.aop.aspectj.annotation.AspectJProxyFactory
类为一个或多个 @AspectJ 切面建议的目标对象创建代理。此类的基本用法非常简单,如以下示例所示:
1 | // create a factory that can generate a proxy for the given target object |
有关更多信息,请参见 javadoc。
在 Spring 应用程序中使用 AspectJ
到目前为止,本章介绍的所有内容都是纯 Spring AOP。 在本节中,我们将研究如果您的需求超出了 Spring AOP 所提供的功能,那么如何使用 AspectJ 编译器或 weaver 代替 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 依赖注入域对象
Spring 容器实例化并配置在您的应用程序上下文中定义的 bean。 给定包含要应用的配置的 Bean 定义的名称,也可以要求 Bean 工厂配置预先存在的对象。 spring-aspects.jar
包含注释驱动的切面,该切面利用此功能允许依赖项注入任何对象。 该支撑旨在用于在任何容器的控制范围之外创建的对象。 域对象通常属于此类,因为它们通常是通过数据库查询的结果由 new 运算符或 ORM 工具以编程方式创建的。
@Configurable
批注将一个类标记为符合 Spring 驱动的配置。 在最简单的情况下,您可以将其纯粹用作标记注释,如以下示例所示:
1 | package com.xyz.myapp.domain; |
当以这种方式用作标记接口时,Spring 通过使用具有与完全限定类型名称(com.xyz.myapp.domain.Account
)相同名称的 bean 定义(通常为原型作用域)来配置带注释类型的新实例(在这种情况下为 Account)。由于 bean 的默认名称是其类型的全限定名,因此声明原型定义的便捷方法是省略 id 属性,如以下示例所示:
1 | <bean class="com.xyz.myapp.domain.Account" scope="prototype"> |
如果要显式指定要使用的原型 bean 定义的名称,则可以直接在批注中这样做,如以下示例所示:
1 | package com.xyz.myapp.domain; |
Spring 现在查找名为 account 的 bean 定义,并将其用作配置新 Account 实例的定义。
您也可以使用自动装配来避免完全指定专用的 bean 定义。要让 Spring 应用自动装配,请使用 @Configurable
批注的 autowire
属性。您可以指定 @Configurable(autowire=Autowire.BY_TYPE)
或 @Configurable(autowire=Autowire.BY_NAME)
分别按类型或名称进行自动装配。作为替代方案,最好为您的对象指定显式的,注释驱动的依赖项注入。通过 @Autowired
或 @Inject
在字段或方法级别上使用 @Configurable
bean(有关更多详细信息,请参见基于注释的容器配置)。
最后,您可以使用 dependencyCheck
属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)
)为新创建和配置的对象中的对象引用启用 Spring 依赖检查。如果此属性设置为 true,则 Spring 在配置后验证是否已设置所有属性(不是基本类型或集合)。
请注意,单独使用注释不会执行任何操作。spring-aspects.jar
中的 AnnotationBeanConfigurerAspect
会对注释的存在起作用。从本质上讲,切面说:“在从带有 @Configurable
注释的类型的新对象的初始化返回之后,使用 Spring 根据注释的属性配置新创建的对象”。在这种情况下,“初始化”是指新实例化的对象(例如,用 new 运算符实例化的对象)以及正在进行反序列化(例如,通过 readResolve()
的可序列化的对象)。
上段中的关键短语之一是“本质上”。在大多数情况下,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”是指在构造对象之后注入依赖项。这意味着该依赖项不可在类的构造函数体中使用。如果您希望在构造函数主体执行之前注入依赖项,从而可以在构造函数主体中使用这些依赖项,则需要在
@Configurable
声明中对此进行定义,如下所示:
1
2 > true) (preConstruction =
>
您可以在AspectJ 编程指南的此附录中找到有关各种切入点类型的语言语义的更多信息。
为此,必须将带注释的类型与 AspectJ 编织器编织在一起。您可以使用构建时的 Ant 或 Maven 任务来执行此操作(例如,参见《 AspectJ 开发环境指南》),也可以使用加载时编织(请参见 Spring Framework 中的使用 AspectJ 进行加载时编织)。 Spring 需要配置 AnnotationBeanConfigurerAspect
自身(以便获得对将用于配置新对象的 Bean 工厂的引用)。如果使用基于 Java 的配置,则可以将 @EnableSpringConfigured
添加到任何 @Configuration
类中,如下所示:
1 |
|
如果您更喜欢基于 XML 的配置,则 Spring 上下文名称空间定义了一个方便的 context:spring-configured
元素,您可以按以下方式使用它:
1 | <context:spring-configured/> |
除非您真的想在运行时依赖它的语义,否则不要通过 bean configurer 切面激活
@Configurable
处理。特别是,请确保不要在通过容器注册为常规 Spring bean 的 bean 类上使用@Configurable
。这样做会导致两次初始化,一次是通过容器,一次是通过切面。
单元测试@Configurable 对象
@Configurable
支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果 AspectJ 尚未编织 @Configurable
类型,则注释在单元测试期间不起作用。您可以在被测对象中设置模拟或存根属性引用,然后照常进行。如果 AspectJ 编织了 @Configurable
类型,您仍然可以像往常一样在容器外部进行单元测试,但是每次构造 @Configurable
对象时,您都会看到一条警告消息,指示该对象尚未由 Spring 配置。
使用多个应用程序上下文
用于实现 @Configurable
支持的 AnnotationBeanConfigurerAspect
是 AspectJ 单例切面。单例切面的范围与静态成员的范围相同:每个类加载器都有一个切面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在何处定义@EnableSpringConfigured bean,以及在哪里将 spring-aspects.jar 放置在类路径上。
考虑一个典型的 Spring Web 应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用的业务服务,支持那些服务所需的一切,以及每个 Servlet 的一个子应用程序上下文(其中包含该 Servlet 的特定定义)。所有这些上下文共存于同一类加载器层次结构中,因此 AnnotationBeanConfigurerAspect
只能保存对其中一个的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured
bean。这定义了您可能想注入域对象的服务。结果是,您无法使用@Configurable 机制来配置域对象,该域对象引用的是在子(特定于 servlet 的)上下文中定义的 Bean 的引用(无论如何,这可能不是您想要做的)。
在同一容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序通过使用其自己的类加载器(例如,将 spring-aspects.jar
放置在 'WEB-INF/lib'
中)将其类型加载到 spring-aspects.jar
中。如果将 spring-aspects.jar
仅添加到容器级的类路径中(并因此由共享的父类加载器加载),则所有 Web 应用程序都共享相同的切面实例(可能不是您想要的)。
AspectJ 的其他 Spring 切面
除了 @Configurable
切面之外,spring-aspects.jar
还包含一个 AspectJ 切面,您可以使用该切面来驱动 Spring 的事务管理,以使用 @Transactional
批注来批注类型和方法。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。
解释 @Transactional
批注的切面是 AnnotationTransactionAspect
。使用此切面时,必须注释实现类(或该类中的方法或两者),而不是注释该类所实现的接口(如果有)。AspectJ 遵循 Java 的规则,即不继承接口上的注释。
类上的 @Transactional
批注指定用于执行该类中任何公共操作的默认事务语义。
类中方法上的 @Transactional
注释将覆盖类注释(如果存在)给出的默认事务语义。可以注释任何可见性的方法,包括私有方法。直接注释非公共方法是执行此类方法而获得事务划分的唯一方法。
从 Spring Framework 4.2 开始,spring-aspects 提供了一个相似的切面,为标准
javax.transaction.Transactional
注释提供了完全相同的功能。检查JtaAnnotationTransactionAspect
了解更多详细信息。
对于希望使用 Spring 配置和事务管理支持但又不想(或不能)使用注释的 AspectJ 程序员,spring-aspects.jar
也包含抽象切面,您可以扩展它们以提供自己的切入点定义。有关更多信息,请参见 AbstractBeanConfigurerAspect
和 AbstractTransactionAspect
切面的资源。作为示例,以下摘录显示了如何编写切面来使用与完全限定的类名匹配的原型 bean 定义来配置域模型中定义的对象的所有实例:
1 | public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect { |
使用 Spring IoC 配置 AspectJ Aspects
当您将 AspectJ 切面与 Spring 应用程序一起使用时,既自然又希望能够使用 Spring 配置这些切面。 AspectJ 运行时本身负责切面的创建,并且通过 Spring 配置 AspectJ 创建的切面的方法取决于切面所使用的 AspectJ 实例化模型(per-xxx 子句)。
AspectJ 的大多数切面都是单例切面。这些切面的配置很容易。您可以创建一个 bean 定义,该 bean 定义按常规引用切面类型,并包括 factory-method =“ aspectOf” bean 属性。这样可以确保 Spring 通过向 AspectJ 索要长宽比实例,而不是尝试自己创建实例来获得长宽比实例。以下示例显示如何使用 factory-method =“ aspectOf”属性:
1 | <bean id="profiler" class="com.xyz.profiler.Profiler" |
注意 factory-method =“ aspectOf”属性
非单一切面很难配置。但是,可以通过创建原型 Bean 定义并使用 spring-aspects.jar 中的@Configurable 支持来实现,一旦它们由 AspectJ 运行时创建了 Bean,就可以配置切面实例。
如果您有一些要与 AspectJ 编织的@AspectJ 切面(例如,对域模型类型使用加载时编织)以及要与 Spring AOP 一起使用的其他@AspectJ 切面,那么这些切面都已在 Spring 中配置,您需要告诉 Spring AOP @AspectJ 自动代理支持,应使用配置中定义的@AspectJ 切面的确切子集进行自动代理。您可以通过在<aop:aspectj-autoproxy />
声明中使用一个或多个<include />
元素来做到这一点。每个<include />
元素都指定一个名称模式,只有名称与至少一个模式匹配的 bean 才可用于 Spring AOP 自动代理配置。以下示例显示了如何使用<include />
元素:
1 | <aop:aspectj-autoproxy> |
不要被
<aop:aspectj-autoproxy/>
元素的名称所迷惑。使用它可以创建 Spring AOP 代理。 这里使用的是 @AspectJ 样式的切面声明,但不涉及 AspectJ 运行时。
在 Spring Framework 中使用 AspectJ 进行加载时编织
AspectJ 切面加载到应用程序的类文件中时将其编织到 Java 虚拟机(JVM)中的过程。本节的重点是在 Spring 框架的特定上下文中配置和使用 LTW。本节不是 LTW 的一般介绍。有关 LTW 的详细信息以及仅使用 AspectJ 配置 LTW(完全不涉及 Spring)的详细信息,请参阅《AspectJ 开发环境指南》的 LTW 部分。
Spring 框架为 AspectJ LTW 带来的价值在于能够对编织过程进行更精细的控制。 “Vanilla” AspectJ LTW 通过使用 Java(5+)代理来实现,该代理在启动 JVM 时通过指定 VM 参数来打开。因此,它是一个 JVM 范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用 Spring 的 LTW 可让您基于每个 ClassLoader 开启 LTW,它的粒度更细,并且在“单个 JVM-多个应用程序”环境(例如在典型的应用程序服务器中发现)中更有意义。环境)。
此外,在某些环境中,此支持无需在添加-javaagent:path/to/aspectjweaver.jar
或(如本节稍后所述)-javaagent:path/to/spring-instrument.jar
所需的应用程序服务器的启动脚本进行任何修改的情况下即可进行加载时编织。开发人员将应用程序上下文配置为启用加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。
现在,销售工作已经结束,让我们首先浏览一个使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中引入的元素。有关完整的示例,请参见 Petclinic 示例应用程序。
此处过于深入,暂时跳过。
更多资源
可以在 AspectJ 网站上找到有关 AspectJ 的更多信息。
Eclipse AspectJ,作者:Adrian Colyer 等。(Addison-Wesley,2005 年)为 AspectJ 语言提供了全面的介绍和参考。
强烈推荐 Ramnivas Laddad 撰写的《AspectJ in Action》第二版(Manning,2009 年)。本书的重点是 AspectJ,但是(在一定程度上)探讨了许多通用的 AOP 主题。